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 001/105] 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 002/105] 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 003/105] 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 004/105] 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 005/105] 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 006/105] 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 007/105] 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 008/105] 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 009/105] 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 010/105] 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 011/105] 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 012/105] 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 013/105] 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 014/105] 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 #} From b21b5cceb88419ee9c0ae5764f90f0b78fd2bd84 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 25 Sep 2022 18:00:22 +0200 Subject: [PATCH 015/105] Doc, SoE: Logic mixin: no underscore for public members (#1049) * Doc: logic mixin, drop underscore, clarify conventionally, we added a leading underscore to logic mixins' function names. This is noisy in the warning section of IDEs. Leading underscores should only be used for private/protected functions. In addition, the use of self.world and/or requirement to (no) pass in stuff was not made clear earlier. * SoE: fix _ warnings for logic mixin --- docs/world api.md | 13 ++++++++----- worlds/soe/Logic.py | 4 ++-- worlds/soe/__init__.py | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 0471cf1b68..cf26cfd967 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -617,8 +617,10 @@ the name of the implementing world. This is due to sharing a namespace with all other logic mixins. Typical uses are defining methods that are used instead of `state.has` -in lambdas, e.g.`state._mygame_has(custom, world, player)` or recurring checks -like `state._mygame_can_do_something(world, player)` to simplify lambdas. +in lambdas, e.g.`state.mygame_has(custom, player)` or recurring checks +like `state.mygame_can_do_something(player)` to simplify lambdas. +Private members, only accessible from mixins, should start with `_mygame_`, +public members with `mygame_`. More advanced uses could be to add additional variables to the state object, override `World.collect(self, state, item)` and `remove(self, state, item)` @@ -633,9 +635,10 @@ Please do this with caution and only when neccessary. from worlds.AutoWorld import LogicMixin class MyGameLogic(LogicMixin): - def _mygame_has_key(self, world: MultiWorld, player: int): + def mygame_has_key(self, player: int): # Arguments above are free to choose - # it may make sense to use World as argument instead of MultiWorld + # MultiWorld can be accessed through self.world, explicitly passing in + # MyGameWorld instance for easy options access is also a valid approach return self.has("key", player) # or whatever ``` ```python @@ -648,7 +651,7 @@ class MyGameWorld(World): # ... def set_rules(self): set_rule(self.world.get_location("A Door", self.player), - lamda state: state._mygame_has_key(self.world, self.player)) + lamda state: state.mygame_has_key(self.player)) ``` ### Generate Output diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index d08e6a3e96..97c73a1bd1 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -35,7 +35,7 @@ class SecretOfEvermoreLogic(LogicMixin): if pvd[1] == progress and pvd[0] > 0: has = True for req in rule.requires: - if not self._soe_has(req[1], world, player, req[0]): + if not self.soe_has(req[1], world, player, req[0]): has = False break if has: @@ -44,7 +44,7 @@ class SecretOfEvermoreLogic(LogicMixin): return n return n - def _soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: + def soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: """ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE """ diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index f86fc48e93..a0dc41c3ce 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -283,7 +283,7 @@ class SoEWorld(World): self.world.completion_condition[self.player] = lambda state: state.has('Victory', self.player) # set Done from goal option once we have multiple goals set_rule(self.world.get_location('Done', self.player), - lambda state: state._soe_has(pyevermizer.P_FINAL_BOSS, self.world, self.player)) + lambda state: state.soe_has(pyevermizer.P_FINAL_BOSS, self.world, self.player)) set_rule(self.world.get_entrance('New Game', self.player), lambda state: True) for loc in _locations: location = self.world.get_location(loc.name, self.player) @@ -292,7 +292,7 @@ class SoEWorld(World): def make_rule(self, requires: typing.List[typing.Tuple[int]]) -> typing.Callable[[typing.Any], bool]: def rule(state) -> bool: for count, progress in requires: - if not state._soe_has(progress, self.world, self.player, count): + if not state.soe_has(progress, self.world, self.player, count): return False return True From b4b9ff5d828ad269ff390fe07d6d8c51aba16d52 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Tue, 27 Sep 2022 07:26:33 -0400 Subject: [PATCH 016/105] Docs: Update snes9x Links (#1048) --- worlds/alttp/docs/multiworld_de.md | 4 ++-- worlds/alttp/docs/multiworld_en.md | 2 +- worlds/alttp/docs/multiworld_es.md | 6 +++--- worlds/alttp/docs/multiworld_fr.md | 6 +++--- worlds/dkc3/docs/setup_en.md | 5 ++--- worlds/sm/docs/multiworld_en.md | 5 ++--- worlds/smz3/docs/multiworld_en.md | 5 ++--- 7 files changed, 15 insertions(+), 18 deletions(-) diff --git a/worlds/alttp/docs/multiworld_de.md b/worlds/alttp/docs/multiworld_de.md index 877f4abe83..417bb8acff 100644 --- a/worlds/alttp/docs/multiworld_de.md +++ b/worlds/alttp/docs/multiworld_de.md @@ -6,7 +6,7 @@ - [SNI](https://github.com/alttpo/sni/releases) (Integriert in Archipelago) - Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien fähig zu einer Internetverbindung - Ein Emulator, der mit SNI verbinden kann - ([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + ([snes9x rr](https://github.com/gocha/snes9x-rr/releases), [BizHawk](http://tasvideos.org/BizHawk.html)) - Ein SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), oder andere kompatible Hardware - Die Japanische Zelda 1.0 ROM-Datei, mit folgendem Namen: `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc` @@ -93,7 +93,7 @@ Wenn der client den Emulator automatisch gestartet hat, wird SNI ebenfalls im Hi Mal ist, wird möglicherweise ein Fenster angezeigt, wo man bestätigen muss, dass das Programm durch die Windows Firewall kommunizieren darf. -##### snes9x Multitroid +##### snes9x-rr 1. Lade die Entsprechende ROM-Datei, wenn sie nicht schon automatisch geladen wurde. 2. Klicke auf den Reiter "File" oben im Menü und wähle **Lua Scripting** diff --git a/worlds/alttp/docs/multiworld_en.md b/worlds/alttp/docs/multiworld_en.md index 1e6b2d1044..e758edc22c 100644 --- a/worlds/alttp/docs/multiworld_en.md +++ b/worlds/alttp/docs/multiworld_en.md @@ -75,7 +75,7 @@ client, and will also create your ROM in the same place as your patch file. When the client launched automatically, SNI should have also automatically launched in the background. If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. -##### snes9x Multitroid +##### snes9x-rr 1. Load your ROM file if it hasn't already been loaded. 2. Click on the File menu and hover on **Lua Scripting** diff --git a/worlds/alttp/docs/multiworld_es.md b/worlds/alttp/docs/multiworld_es.md index da0b9d99bf..ca33f796c2 100644 --- a/worlds/alttp/docs/multiworld_es.md +++ b/worlds/alttp/docs/multiworld_es.md @@ -12,7 +12,7 @@ - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities) - Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES - Un emulador capaz de ejecutar scripts Lua - ([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + ([snes9x rr](https://github.com/gocha/snes9x-rr/releases), [BizHawk](http://tasvideos.org/BizHawk.html), o [RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). O, - Un flashcart SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), o otro hardware compatible @@ -111,13 +111,13 @@ automáticamente el cliente, y ademas creara la rom en el mismo directorio donde Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también. Si es la primera vez que lo ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación. -##### snes9x Multitroid +##### snes9x-rr 1. Carga tu fichero de ROM, si no lo has hecho ya 2. Abre el menu "File" y situa el raton en **Lua Scripting** 3. Haz click en **New Lua Script Window...** 4. En la nueva ventana, haz click en **Browse...** -5. Navega hacia el directorio donde este situado snes9x Multitroid, entra en el directorio `lua`, y +5. Navega hacia el directorio donde este situado snes9x-rr, entra en el directorio `lua`, y escoge `multibridge.lua` 6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo nombre en la esquina superior izquierda. diff --git a/worlds/alttp/docs/multiworld_fr.md b/worlds/alttp/docs/multiworld_fr.md index 4a8ca5902e..380a010232 100644 --- a/worlds/alttp/docs/multiworld_fr.md +++ b/worlds/alttp/docs/multiworld_fr.md @@ -12,7 +12,7 @@ - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents) - Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES - Un émulateur capable d'éxécuter des scripts Lua - ([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + ([snes9x rr](https://github.com/gocha/snes9x-rr/releases), [BizHawk](http://tasvideos.org/BizHawk.html)) - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle compatible @@ -112,13 +112,13 @@ Quand le client se lance automatiquement, QUsb2Snes devrait se lancer automatiqu c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu Windows. -##### snes9x Multitroid +##### snes9x-rr 1. Chargez votre ROM si ce n'est pas déjà fait. 2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting** 3. Cliquez alors sur **New Lua Script Window...** 4. Dans la nouvelle fenêtre, sélectionnez **Browse...** -5. Dirigez vous vers le dossier où vous avez extrait snes9x Multitroid, allez dans le dossier `lua`, puis +5. Dirigez vous vers le dossier où vous avez extrait snes9x-rr, allez dans le dossier `lua`, puis choisissez `multibridge.lua` 6. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom dans le coin en haut à gauche. diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index 471248deb8..0ebe189a1e 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -7,8 +7,7 @@ - Hardware or software capable of loading and playing SNES ROM files - An emulator capable of connecting to SNI such as: - - snes9x Multitroid - from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other @@ -81,7 +80,7 @@ client, and will also create your ROM in the same place as your patch file. When the client launched automatically, SNI should have also automatically launched in the background. If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. -##### snes9x Multitroid +##### snes9x-rr 1. Load your ROM file if it hasn't already been loaded. 2. Click on the File menu and hover on **Lua Scripting** diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md index 27e1d4a4ad..826be60188 100644 --- a/worlds/sm/docs/multiworld_en.md +++ b/worlds/sm/docs/multiworld_en.md @@ -7,8 +7,7 @@ - Hardware or software capable of loading and playing SNES ROM files - An emulator capable of connecting to SNI such as: - - snes9x Multitroid - from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other @@ -81,7 +80,7 @@ client, and will also create your ROM in the same place as your patch file. When the client launched automatically, SNI should have also automatically launched in the background. If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. -##### snes9x Multitroid +##### snes9x-rr 1. Load your ROM file if it hasn't already been loaded. 2. Click on the File menu and hover on **Lua Scripting** diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md index 7457d6d0a7..735be9d519 100644 --- a/worlds/smz3/docs/multiworld_en.md +++ b/worlds/smz3/docs/multiworld_en.md @@ -8,8 +8,7 @@ `SNI Client - A Link to the Past Patch Setup` - Hardware or software capable of loading and playing SNES ROM files - An emulator capable of connecting to SNI such as: - - snes9x Multitroid - from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html), or - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other @@ -79,7 +78,7 @@ client, and will also create your ROM in the same place as your patch file. When the client launched automatically, SNI should have also automatically launched in the background. If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. -##### snes9x Multitroid +##### snes9x-rr 1. Load your ROM file if it hasn't already been loaded. 2. Click on the File menu and hover on **Lua Scripting** From 2033f1738d4953a12c379ca7a24a592f9e9877c0 Mon Sep 17 00:00:00 2001 From: Marechal-l Date: Tue, 27 Sep 2022 16:09:52 +0200 Subject: [PATCH 017/105] DS3: Increment data_version --- worlds/dark_souls_3/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index fc3f77dbc7..7b0106d910 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -51,7 +51,7 @@ class DarkSouls3World(World): remote_items: bool = False remote_start_inventory: bool = False web = DarkSouls3Web() - data_version = 2 + data_version = 3 base_id = 100000 item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)} location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)} From daf11510b2d1a851229dcb504505a7ffe494e4a3 Mon Sep 17 00:00:00 2001 From: Marechal-l Date: Tue, 27 Sep 2022 16:25:41 +0200 Subject: [PATCH 018/105] DS3: Fix item name in rule --- worlds/dark_souls_3/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 7b0106d910..8d58de1221 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -210,7 +210,7 @@ class DarkSouls3World(World): set_rule(self.world.get_location("ID: Covetous Gold Serpent Ring", self.player), lambda state: state.has("Old Cell Key", self.player)) set_rule(self.world.get_location("ID: Karla's Ashes", self.player), - lambda state: state.has("Jailers Key Ring", self.player)) + lambda state: state.has("Jailer's Key Ring", self.player)) black_hand_gotthard_corpse_rule = lambda state: \ (state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player)) From 8bc8b412a384f9ac0f661e9cb746691b4c439dc2 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 28 Sep 2022 16:02:42 -0500 Subject: [PATCH 019/105] Core: fix unweighted options for meta files (#1053) --- Generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index f048e54383..57d060d4d4 100644 --- a/Generate.py +++ b/Generate.py @@ -377,7 +377,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: if option_key in options: if options[option_key].supports_weighting: return get_choice(option_key, category_dict) - return options[option_key] + return category_dict[option_key] if game == "A Link to the Past": # TODO wow i hate this if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode", "triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra", From c96b6d7b957a393e247b5b0804528629018dfb81 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 28 Sep 2022 14:54:10 -0700 Subject: [PATCH 020/105] Core: some typing and docs in various parts of the interface (#1060) * some typing and docs in various parts of the interface * fix whitespace in docstring Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * suggested changes from discussion * remove redundant import * adjust type for json messages * for options module detection: module.lower().endswith("options") Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- BaseClasses.py | 6 ++- CommonClient.py | 28 +++++----- NetUtils.py | 2 +- Options.py | 5 +- Patch.py | 89 +++++++++++++++----------------- Utils.py | 16 +++--- WebHostLib/options.py | 5 +- WebHostLib/upload.py | 2 +- docs/running from source.md | 4 ++ test/general/TestReachability.py | 2 +- worlds/AutoWorld.py | 15 +++--- worlds/generic/Rules.py | 6 +-- 12 files changed, 96 insertions(+), 84 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index df8ac02071..478dd1cf9c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -40,6 +40,7 @@ class MultiWorld(): plando_connections: List worlds: Dict[int, auto_world] groups: Dict[int, Group] + regions: List[Region] itempool: List[Item] is_race: bool = False precollected_items: Dict[int, List[Item]] @@ -50,6 +51,7 @@ class MultiWorld(): non_local_items: Dict[int, Options.NonLocalItems] progression_balancing: Dict[int, Options.ProgressionBalancing] completion_condition: Dict[int, Callable[[CollectionState], bool]] + exclude_locations: Dict[int, Options.ExcludeLocations] class AttributeProxy(): def __init__(self, rule): @@ -993,7 +995,7 @@ class Entrance: return False - def connect(self, region: Region, addresses=None, target=None): + def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None: self.connected_region = region self.target = target self.addresses = addresses @@ -1081,7 +1083,7 @@ class Location: show_in_spoiler: bool = True progress_type: LocationProgressType = LocationProgressType.DEFAULT always_allow = staticmethod(lambda item, state: False) - access_rule = staticmethod(lambda state: True) + access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) item_rule = staticmethod(lambda item: True) item: Optional[Item] = None diff --git a/CommonClient.py b/CommonClient.py index 94d4359dd1..2940ceed31 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -132,12 +132,12 @@ class CommonContext: # defaults starting_reconnect_delay: int = 5 current_reconnect_delay: int = starting_reconnect_delay - command_processor: type(CommandProcessor) = ClientCommandProcessor + command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor ui = None - ui_task: typing.Optional[asyncio.Task] = None - input_task: typing.Optional[asyncio.Task] = None - keep_alive_task: typing.Optional[asyncio.Task] = None - server_task: typing.Optional[asyncio.Task] = None + ui_task: typing.Optional["asyncio.Task[None]"] = None + input_task: typing.Optional["asyncio.Task[None]"] = None + keep_alive_task: typing.Optional["asyncio.Task[None]"] = None + server_task: typing.Optional["asyncio.Task[None]"] = None server: typing.Optional[Endpoint] = None server_version: Version = Version(0, 0, 0) current_energy_link_value: int = 0 # to display in UI, gets set by server @@ -146,7 +146,7 @@ class CommonContext: # remaining type info slot_info: typing.Dict[int, NetworkSlot] - server_address: str + server_address: typing.Optional[str] password: typing.Optional[str] hint_cost: typing.Optional[int] player_names: typing.Dict[int, str] @@ -154,6 +154,7 @@ class CommonContext: # locations locations_checked: typing.Set[int] # local state locations_scouted: typing.Set[int] + items_received: typing.List[NetworkItem] 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 @@ -163,7 +164,7 @@ class CommonContext: # current message box through kvui _messagebox = None - def __init__(self, server_address, password): + def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: # server state self.server_address = server_address self.username = None @@ -243,7 +244,8 @@ class CommonContext: if self.server_task is not None: await self.server_task - async def send_msgs(self, msgs): + async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: + """ `msgs` JSON serializable """ if not self.server or not self.server.socket.open or self.server.socket.closed: return await self.server.socket.send(encode(msgs)) @@ -271,7 +273,7 @@ class CommonContext: logger.info('Enter slot name:') self.auth = await self.console_input() - async def send_connect(self, **kwargs): + async def send_connect(self, **kwargs: typing.Any) -> None: payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -282,7 +284,7 @@ class CommonContext: payload.update(kwargs) await self.send_msgs([payload]) - async def console_input(self): + async def console_input(self) -> str: self.input_requests += 1 return await self.input_queue.get() @@ -390,7 +392,7 @@ class CommonContext: # DeathLink hooks - def on_deathlink(self, data: dict): + def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: """Gets dispatched when a new DeathLink is triggered by another linked player.""" self.last_death_link = max(data["time"], self.last_death_link) text = data.get("cause", "") @@ -477,7 +479,7 @@ async def keep_alive(ctx: CommonContext, seconds_between_checks=100): seconds_elapsed = 0 -async def server_loop(ctx: CommonContext, address=None): +async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) -> None: if ctx.server and ctx.server.socket: logger.error('Already connected') return @@ -722,7 +724,7 @@ async def console_loop(ctx: CommonContext): logger.exception(e) -def get_base_parser(description=None): +def get_base_parser(description: typing.Optional[str] = None): import argparse parser = argparse.ArgumentParser(description=description) parser.add_argument('--connect', default=None, help='Address of the multiworld host.') diff --git a/NetUtils.py b/NetUtils.py index 1e7d66d824..513ab074fc 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -100,7 +100,7 @@ _encode = JSONEncoder( ).encode -def encode(obj): +def encode(obj: typing.Any) -> str: return _encode(_scan_for_TypedTuples(obj)) diff --git a/Options.py b/Options.py index 49f044d8cd..567ac8dbc6 100644 --- a/Options.py +++ b/Options.py @@ -165,6 +165,7 @@ class FreeText(Option): class NumericOption(Option[int], numbers.Integral): + default = 0 # note: some of the `typing.Any`` here is a result of unresolved issue in python standards # `int` is not a `numbers.Integral` according to the official typestubs # (even though isinstance(5, numbers.Integral) == True) @@ -628,7 +629,7 @@ class VerifyKeys: class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys): - default = {} + default: typing.Dict[str, typing.Any] = {} supports_weighting = False def __init__(self, value: typing.Dict[str, typing.Any]): @@ -659,7 +660,7 @@ class ItemDict(OptionDict): class OptionList(Option[typing.List[typing.Any]], VerifyKeys): - default = [] + default: typing.List[typing.Any] = [] supports_weighting = False def __init__(self, value: typing.List[typing.Any]): diff --git a/Patch.py b/Patch.py index aaa4fc2404..9b2c88a6b6 100644 --- a/Patch.py +++ b/Patch.py @@ -2,7 +2,7 @@ from __future__ import annotations import shutil import json -import bsdiff4 +import bsdiff4 # type: ignore import yaml import os import lzma @@ -10,7 +10,7 @@ import threading import concurrent.futures import zipfile import sys -from typing import Tuple, Optional, Dict, Any, Union, BinaryIO +from typing import ClassVar, List, Tuple, Optional, Dict, Any, Union, BinaryIO import ModuleUpdate ModuleUpdate.update() @@ -21,10 +21,10 @@ current_patch_version = 5 class AutoPatchRegister(type): - patch_types: Dict[str, APDeltaPatch] = {} - file_endings: Dict[str, APDeltaPatch] = {} + patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {} + file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {} - def __new__(cls, name: str, bases, dct: Dict[str, Any]): + def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchRegister: # construct class new_class = super().__new__(cls, name, bases, dct) if "game" in dct: @@ -35,10 +35,11 @@ class AutoPatchRegister(type): return new_class @staticmethod - def get_handler(file: str) -> Optional[type(APDeltaPatch)]: + def get_handler(file: str) -> Optional[AutoPatchRegister]: for file_ending, handler in AutoPatchRegister.file_endings.items(): if file.endswith(file_ending): return handler + return None class APContainer: @@ -61,34 +62,36 @@ class APContainer: self.player_name = player_name self.server = server - def write(self, file: Optional[Union[str, BinaryIO]] = None): - if not self.path and not file: + def write(self, file: Optional[Union[str, BinaryIO]] = None) -> None: + zip_file = file if file else self.path + if not zip_file: raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.") - with zipfile.ZipFile(file if file else self.path, "w", self.compression_method, True, self.compression_level) \ + with zipfile.ZipFile(zip_file, "w", self.compression_method, True, self.compression_level) \ as zf: if file: self.path = zf.filename self.write_contents(zf) - def write_contents(self, opened_zipfile: zipfile.ZipFile): + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: manifest = self.get_manifest() try: - manifest = json.dumps(manifest) + manifest_str = json.dumps(manifest) except Exception as e: raise Exception(f"Manifest {manifest} did not convert to json.") from e else: - opened_zipfile.writestr("archipelago.json", manifest) + opened_zipfile.writestr("archipelago.json", manifest_str) - def read(self, file: Optional[Union[str, BinaryIO]] = None): + def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None: """Read data into patch object. file can be file-like, such as an outer zip file's stream.""" - if not self.path and not file: + zip_file = file if file else self.path + if not zip_file: raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.") - with zipfile.ZipFile(file if file else self.path, "r") as zf: + with zipfile.ZipFile(zip_file, "r") as zf: if file: self.path = zf.filename self.read_contents(zf) - def read_contents(self, opened_zipfile: zipfile.ZipFile): + def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None: with opened_zipfile.open("archipelago.json", "r") as f: manifest = json.load(f) if manifest["compatible_version"] > self.version: @@ -98,7 +101,7 @@ class APContainer: self.server = manifest["server"] self.player_name = manifest["player_name"] - def get_manifest(self) -> dict: + def get_manifest(self) -> Dict[str, Any]: return { "server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise "player": self.player, @@ -114,17 +117,17 @@ class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): """An APContainer that additionally has delta.bsdiff4 containing a delta patch to get the desired file, often a rom.""" - hash = Optional[str] # base checksum of source file + hash: Optional[str] # base checksum of source file patch_file_ending: str = "" delta: Optional[bytes] = None result_file_ending: str = ".sfc" source_data: bytes - def __init__(self, *args, patched_path: str = "", **kwargs): + def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: self.patched_path = patched_path super(APDeltaPatch, self).__init__(*args, **kwargs) - def get_manifest(self) -> dict: + def get_manifest(self) -> Dict[str, Any]: manifest = super(APDeltaPatch, self).get_manifest() manifest["base_checksum"] = self.hash manifest["result_file_ending"] = self.result_file_ending @@ -205,15 +208,19 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAM return patch.encode(encoding="utf-8-sig") -def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: +def generate_patch(rom: bytes, metadata: Optional[Dict[str, Any]] = None, game: str = GAME_ALTTP) -> bytes: if metadata is None: metadata = {} patch = bsdiff4.diff(get_base_rom_data(game), rom) return generate_yaml(patch, metadata, game) -def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None, - player: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> str: +def create_patch_file(rom_file_to_patch: str, + server: str = "", + destination: Optional[str] = None, + player: int = 0, + player_name: str = "", + game: str = GAME_ALTTP) -> str: meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise "player_id": player, "player_name": player_name} @@ -229,19 +236,19 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str return target -def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]: +def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[Dict[str, Any], str, bytearray]: data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) game_name = data["game"] if not ignore_version and data["compatible_version"] > current_patch_version: raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") - patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"]) + patched_data: bytearray = bsdiff4.patch(get_base_rom_data(game_name), data["patch"]) rom_hash = patched_data[int(0x7FC0):int(0x7FD5)] data["meta"]["hash"] = "".join(chr(x) for x in rom_hash) target = os.path.splitext(patch_file)[0] + ".sfc" return data["meta"], target, patched_data -def get_base_rom_data(game: str): +def get_base_rom_data(game: str) -> bytes: if game == GAME_ALTTP: from worlds.alttp.Rom import get_base_rom_bytes elif game == "alttp": # old version for A Link to the Past @@ -260,7 +267,7 @@ def get_base_rom_data(game: str): return get_base_rom_bytes() -def create_rom_file(patch_file: str) -> Tuple[dict, str]: +def create_rom_file(patch_file: str) -> Tuple[Dict[str, Any], str]: auto_handler = AutoPatchRegister.get_handler(patch_file) if auto_handler: handler: APDeltaPatch = auto_handler(patch_file) @@ -293,7 +300,7 @@ def write_lzma(data: bytes, path: str): f.write(data) -def read_rom(stream, strip_header=True) -> bytearray: +def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray: """Reads rom into bytearray and optionally strips off any smc header""" buffer = bytearray(stream.read()) if strip_header and len(buffer) % 0x400 == 0x200: @@ -321,7 +328,7 @@ if __name__ == "__main__": elif rom.endswith(".apbp"): print(f"Applying patch {rom}") data, target = create_rom_file(rom) - #romfile, adjusted = Utils.get_adjuster_settings(target) + # romfile, adjusted = Utils.get_adjuster_settings(target) adjuster_settings = Utils.get_adjuster_settings(GAME_ALTTP) adjusted = False if adjuster_settings: @@ -385,21 +392,9 @@ if __name__ == "__main__": if 'server' in data: Utils.persistent_store("servers", data['hash'], data['server']) print(f"Host is {data['server']}") - elif rom.endswith(".apm3"): - print(f"Applying patch {rom}") - data, target = create_rom_file(rom) - print(f"Created rom {target}.") - if 'server' in data: - Utils.persistent_store("servers", data['hash'], data['server']) - print(f"Host is {data['server']}") - elif rom.endswith(".apsmz"): - print(f"Applying patch {rom}") - data, target = create_rom_file(rom) - print(f"Created rom {target}.") - if 'server' in data: - Utils.persistent_store("servers", data['hash'], data['server']) - print(f"Host is {data['server']}") - elif rom.endswith(".apdkc3"): + elif rom.endswith(".apm3") \ + or rom.endswith(".apsmz") \ + or rom.endswith(".apdkc3"): print(f"Applying patch {rom}") data, target = create_rom_file(rom) print(f"Created rom {target}.") @@ -410,8 +405,7 @@ if __name__ == "__main__": elif rom.endswith(".zip"): print(f"Updating host in patch files contained in {rom}") - - def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str): + def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str) -> str: data = zfr.read(zfinfo) if zfinfo.filename.endswith(".apbp") or \ zfinfo.filename.endswith(".apm3") or \ @@ -421,8 +415,7 @@ if __name__ == "__main__": zfw.writestr(zfinfo, data) return zfinfo.filename - - futures = [] + futures: List[concurrent.futures.Future[str]] = [] with zipfile.ZipFile(rom, "r") as zfr: updated_zip = os.path.splitext(rom)[0] + "_updated.zip" with zipfile.ZipFile(updated_zip, "w", compression=zipfile.ZIP_DEFLATED, diff --git a/Utils.py b/Utils.py index c362131d75..93627ffd89 100644 --- a/Utils.py +++ b/Utils.py @@ -217,8 +217,11 @@ def get_public_ipv6() -> str: return ip +OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]] + + @cache_argsless -def get_default_options() -> dict: +def get_default_options() -> OptionsType: # Refer to host.yaml for comments as to what all these options mean. options = { "general_options": { @@ -290,7 +293,7 @@ def get_default_options() -> dict: return options -def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict: +def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType: for key, value in src.items(): new_keys = keys.copy() new_keys.append(key) @@ -310,9 +313,9 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict: @cache_argsless -def get_options() -> dict: +def get_options() -> OptionsType: filenames = ("options.yaml", "host.yaml") - locations = [] + locations: typing.List[str] = [] if os.path.join(os.getcwd()) != local_path(): locations += filenames # use files from cwd only if it's not the local_path locations += [user_path(filename) for filename in filenames] @@ -353,7 +356,7 @@ def persistent_load() -> typing.Dict[str, dict]: return storage -def get_adjuster_settings(game_name: str): +def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]: adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {}) return adjuster_settings @@ -392,7 +395,8 @@ class RestrictedUnpickler(pickle.Unpickler): # Options and Plando are unpickled by WebHost -> Generate if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: return getattr(self.generic_properties_module, name) - if module.endswith("Options"): + # pep 8 specifies that modules should have "all-lowercase names" (options, not Options) + if module.lower().endswith("options"): if module == "Options": mod = self.options_module else: diff --git a/WebHostLib/options.py b/WebHostLib/options.py index daa742d90e..6807d54689 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -64,7 +64,10 @@ def create(): for game_name, world in AutoWorldRegister.world_types.items(): - all_options = {**Options.per_game_common_options, **world.option_definitions} + all_options: typing.Dict[str, Options.AssembleOptions] = { + **Options.per_game_common_options, + **world.option_definitions + } with open(local_path("WebHostLib", "templates", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 00825df47b..6907bb2acd 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -22,7 +22,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if not owner: owner = session["_id"] infolist = zfile.infolist() - slots = set() + slots: typing.Set[Slot] = set() spoiler = "" multidata = None for file in infolist: diff --git a/docs/running from source.md b/docs/running from source.md index 39addd0a28..24486146f8 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -16,6 +16,10 @@ Then run any of the starting point scripts, like Generate.py, and the included M required modules and after pressing enter proceed to install everything automatically. After this, you should be able to run the programs. + * With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive. + * `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally. + * `--log_network` is a command line parameter useful for debugging. + ## Windows diff --git a/test/general/TestReachability.py b/test/general/TestReachability.py index 2cadf9d29a..d638b56e8d 100644 --- a/test/general/TestReachability.py +++ b/test/general/TestReachability.py @@ -20,7 +20,7 @@ class TestBase(unittest.TestCase): for location in world.get_locations(): if location.name not in excluded: with self.subTest("Location should be reached", location=location): - self.assertTrue(location.can_reach(state)) + self.assertTrue(location.can_reach(state), f"{location.name} unreachable") with self.subTest("Completion Condition"): self.assertTrue(world.can_beat_game(state)) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index db72ca6a95..5dea03481f 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging import sys import pathlib -from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING +from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING -from Options import Option +from Options import AssembleOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -13,7 +13,7 @@ if TYPE_CHECKING: class AutoWorldRegister(type): - world_types: Dict[str, type(World)] = {} + world_types: Dict[str, Type[World]] = {} def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister: if "web" in dct: @@ -120,7 +120,7 @@ class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. A Game should have its own subclass of World in which it defines the required data structures.""" - option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping + option_definitions: Dict[str, AssembleOptions] = {} # link your Options mapping game: str # name the game topology_present: bool = False # indicate if world type has any meaningful layout/pathing @@ -229,7 +229,8 @@ class World(metaclass=AutoWorldRegister): pass def post_fill(self) -> None: - """Optional Method that is called after regular fill. Can be used to do adjustments before output generation.""" + """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. + This happens before progression balancing, so the items may not be in their final locations yet.""" def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use world.random here. @@ -237,7 +238,9 @@ class World(metaclass=AutoWorldRegister): pass def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot - """Fill in the slot_data field in the Connected network package.""" + """Fill in the `slot_data` field in the `Connected` network package. + This is a way the generator can give custom data to the client. + The client will receive this as JSON in the `Connected` response.""" return {} def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index f53c417e1c..9b338e4d70 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,6 +1,6 @@ import typing -from BaseClasses import LocationProgressType +from BaseClasses import LocationProgressType, MultiWorld if typing.TYPE_CHECKING: import BaseClasses @@ -37,14 +37,14 @@ def locality_rules(world, player: int): forbid_items_for_player(location, world.non_local_items[player].value, player) -def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]): +def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: for loc_name in exclude_locations: try: location = world.get_location(loc_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location if loc_name not in world.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e - else: + else: add_item_rule(location, lambda i: not (i.advancement or i.useful)) location.progress_type = LocationProgressType.EXCLUDED From e6a4925f0c197e33cb2a16dce98deb52eb4adf31 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 29 Sep 2022 00:09:04 +0200 Subject: [PATCH 021/105] Doc: update apclientpp to header-only (#1054) --- docs/network protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 3315ddec2d..d5c56a62b4 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -21,7 +21,7 @@ There are also a number of community-supported libraries available that implemen | | [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 | +| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | 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) | | From 885c8d3fccb831c00f610fa4f07807b71ea115da Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 29 Sep 2022 13:59:57 -0400 Subject: [PATCH 022/105] Fix minimal accessibility failures (#726) --- BaseClasses.py | 4 ++-- Fill.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 478dd1cf9c..634c2a833f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -531,7 +531,7 @@ class MultiWorld(): beatable_fulfilled = False - def location_conditition(location: Location): + def location_condition(location: Location): """Determine if this location has to be accessible, location is already filtered by location_relevant""" if location.player in players["minimal"]: return False @@ -548,7 +548,7 @@ class MultiWorld(): def all_done(): """Check if all access rules are fulfilled""" if beatable_fulfilled: - if any(location_conditition(location) for location in locations): + if any(location_condition(location) for location in locations): return False # still locations required to be collected return True diff --git a/Fill.py b/Fill.py index c62eaabde8..4b095eb108 100644 --- a/Fill.py +++ b/Fill.py @@ -4,9 +4,10 @@ import collections import itertools from collections import Counter, deque -from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item +from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item, ItemClassification from worlds.AutoWorld import call_all +from worlds.generic.Rules import add_item_rule class FillError(RuntimeError): @@ -209,6 +210,34 @@ def fast_fill(world: MultiWorld, return item_pool[placing:], fill_locations[placing:] +def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]): + maximum_exploration_state = sweep_from_pool(state, pool) + minimal_players = {player for player in world.player_ids if world.accessibility[player] == "minimal"} + unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and + not location.can_reach(maximum_exploration_state)] + for location in unreachable_locations: + if (location.item is not None and location.item.advancement and location.address is not None and not + location.locked and location.item.player not in minimal_players): + pool.append(location.item) + state.remove(location.item) + location.item = None + location.event = False + if location in state.events: + state.events.remove(location) + locations.append(location) + + if pool: + fill_restrictive(world, state, locations, pool) + + +def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): + maximum_exploration_state = sweep_from_pool(state, []) + unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] + for location in unreachable_locations: + add_item_rule(location, lambda item: not ((item.classification & 0b0011) and + world.accessibility[item.player] != 'minimal')) + + def distribute_items_restrictive(world: MultiWorld) -> None: fill_locations = sorted(world.get_unfilled_locations()) world.random.shuffle(fill_locations) @@ -239,7 +268,15 @@ def distribute_items_restrictive(world: MultiWorld) -> None: defaultlocations = locations[LocationProgressType.DEFAULT] excludedlocations = locations[LocationProgressType.EXCLUDED] - fill_restrictive(world, world.state, prioritylocations, progitempool, lock=True) + prioritylocations_lock = prioritylocations.copy() + + fill_restrictive(world, world.state, prioritylocations, progitempool) + accessibility_corrections(world, world.state, prioritylocations, progitempool) + + for location in prioritylocations_lock: + if location.item: + location.locked = True + if prioritylocations: defaultlocations = prioritylocations + defaultlocations @@ -248,6 +285,9 @@ def distribute_items_restrictive(world: MultiWorld) -> None: if progitempool: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') + accessibility_corrections(world, world.state, defaultlocations) + + inaccessible_location_rules(world, world.state, defaultlocations) remaining_fill(world, excludedlocations, filleritempool) if excludedlocations: From 13edfa60be199e0b35f7c879a224907ed6a247da Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu, 29 Sep 2022 14:16:59 -0400 Subject: [PATCH 023/105] Super Mario World: Implement New Game (#1045) --- Launcher.py | 2 +- Patch.py | 1 + README.md | 1 + SNIClient.py | 12 +- Utils.py | 5 + host.yaml | 9 + inno_setup.iss | 35 + worlds/smw/Aesthetics.py | 202 ++++ worlds/smw/Client.py | 455 +++++++++ worlds/smw/Items.py | 69 ++ worlds/smw/Levels.py | 579 +++++++++++ worlds/smw/Locations.py | 233 +++++ worlds/smw/Names/ItemName.py | 32 + worlds/smw/Names/LiteratureTrap.py | 52 + worlds/smw/Names/LocationName.py | 364 +++++++ worlds/smw/Names/TextBox.py | 140 +++ worlds/smw/Options.py | 236 +++++ worlds/smw/Regions.py | 1187 +++++++++++++++++++++++ worlds/smw/Rom.py | 846 ++++++++++++++++ worlds/smw/Rules.py | 20 + worlds/smw/__init__.py | 253 +++++ worlds/smw/docs/en_Super Mario World.md | 43 + worlds/smw/docs/setup_en.md | 149 +++ 23 files changed, 4923 insertions(+), 2 deletions(-) create mode 100644 worlds/smw/Aesthetics.py create mode 100644 worlds/smw/Client.py create mode 100644 worlds/smw/Items.py create mode 100644 worlds/smw/Levels.py create mode 100644 worlds/smw/Locations.py create mode 100644 worlds/smw/Names/ItemName.py create mode 100644 worlds/smw/Names/LiteratureTrap.py create mode 100644 worlds/smw/Names/LocationName.py create mode 100644 worlds/smw/Names/TextBox.py create mode 100644 worlds/smw/Options.py create mode 100644 worlds/smw/Regions.py create mode 100644 worlds/smw/Rom.py create mode 100644 worlds/smw/Rules.py create mode 100644 worlds/smw/__init__.py create mode 100644 worlds/smw/docs/en_Super Mario World.md create mode 100644 worlds/smw/docs/setup_en.md diff --git a/Launcher.py b/Launcher.py index 8a3d53f866..9f9aaa4fb2 100644 --- a/Launcher.py +++ b/Launcher.py @@ -132,7 +132,7 @@ components: Iterable[Component] = ( Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), # SNI Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')), + file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')), Component('LttP Adjuster', 'LttPAdjuster'), # Factorio Component('Factorio Client', 'FactorioClient'), diff --git a/Patch.py b/Patch.py index 9b2c88a6b6..6ac75dc9dd 100644 --- a/Patch.py +++ b/Patch.py @@ -171,6 +171,7 @@ GAME_SM = "Super Metroid" GAME_SOE = "Secret of Evermore" GAME_SMZ3 = "SMZ3" GAME_DKC3 = "Donkey Kong Country 3" +GAME_SMW = "Super Mario World" supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"} preferred_endings = { diff --git a/README.md b/README.md index c8362dddd0..a82282037b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Currently, the following games are supported: * Starcraft 2: Wings of Liberty * Donkey Kong Country 3 * Dark Souls 3 +* Super Mario World For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index 477cde86a2..9170c845e3 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -30,7 +30,7 @@ from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT from worlds.smz3.Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT import Utils from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser -from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3 +from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3, GAME_SMW snes_logger = logging.getLogger("SNES") @@ -236,6 +236,10 @@ async def deathlink_kill_player(ctx: Context): snes_buffered_write(ctx, WRAM_START + 0x0A50, bytes([255])) # deal 255 of damage at next opportunity if not ctx.death_link_allow_survive: snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0 + elif ctx.game == GAME_SMW: + from worlds.smw.Client import deathlink_kill_player as smw_deathlink_kill_player + await smw_deathlink_kill_player(ctx) + await snes_flush_writes(ctx) await asyncio.sleep(1) @@ -1041,6 +1045,9 @@ async def game_watcher(ctx: Context): from worlds.dkc3.Client import dkc3_rom_init init_handled = await dkc3_rom_init(ctx) + if not init_handled: + from worlds.smw.Client import smw_rom_init + init_handled = await smw_rom_init(ctx) if not init_handled: game_name = await snes_read(ctx, SM_ROMNAME_START, 5) if game_name is None: @@ -1299,6 +1306,9 @@ async def game_watcher(ctx: Context): elif ctx.game == GAME_DKC3: from worlds.dkc3.Client import dkc3_game_watcher await dkc3_game_watcher(ctx) + elif ctx.game == GAME_SMW: + from worlds.smw.Client import smw_game_watcher + await smw_game_watcher(ctx) async def run_game(romfile): diff --git a/Utils.py b/Utils.py index 93627ffd89..c5fc00035a 100644 --- a/Utils.py +++ b/Utils.py @@ -288,6 +288,11 @@ def get_default_options() -> OptionsType: "sni": "SNI", "rom_start": True, }, + "smw_options": { + "rom_file": "Super Mario World (USA).sfc", + "sni": "SNI", + "rom_start": True, + }, } return options diff --git a/host.yaml b/host.yaml index 901e6cd727..b114135520 100644 --- a/host.yaml +++ b/host.yaml @@ -138,3 +138,12 @@ dkc3_options: # True for operating system default program # Alternatively, a path to a program to open the .sfc file with rom_start: true +smw_options: + # File name of the SMW US rom + rom_file: "Super Mario World (USA).sfc" + # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found + sni: "SNI" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program + # Alternatively, a path to a program to open the .sfc file with + rom_start: true diff --git a/inno_setup.iss b/inno_setup.iss index cfdfec7ba8..9a2a40444e 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -55,6 +55,7 @@ Name: "core"; Description: "Core Files"; Types: full hosting playing Name: "generator"; Description: "Generator"; Types: full hosting Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning +Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning @@ -64,6 +65,7 @@ Name: "client/sni"; Description: "SNI Client"; Types: full playing Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing @@ -79,6 +81,7 @@ NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-mod Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3 +Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs @@ -151,6 +154,11 @@ Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Arc Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni + Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni @@ -217,6 +225,9 @@ var SMRomFilePage: TInputFileWizardPage; var dkc3rom: string; var DKC3RomFilePage: TInputFileWizardPage; +var smwrom: string; +var SMWRomFilePage: TInputFileWizardPage; + var soerom: string; var SoERomFilePage: TInputFileWizardPage; @@ -308,6 +319,8 @@ begin Result := not (SMROMFilePage.Values[0] = '') else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then Result := not (DKC3ROMFilePage.Values[0] = '') + else if (assigned(SMWROMFilePage)) and (CurPageID = SMWROMFilePage.ID) then + Result := not (SMWROMFilePage.Values[0] = '') else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then Result := not (SoEROMFilePage.Values[0] = '') else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then @@ -364,6 +377,22 @@ begin Result := ''; end; +function GetSMWROMPath(Param: string): string; +begin + if Length(smwrom) > 0 then + Result := smwrom + else if Assigned(SMWRomFilePage) then + begin + R := CompareStr(GetSNESMD5OfFile(SMWROMFilePage.Values[0]), 'cdd3c8c37322978ca8669b34bc89c804') + if R <> 0 then + MsgBox('Super Mario World ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := SMWROMFilePage.Values[0] + end + else + Result := ''; + end; + function GetSoEROMPath(Param: string): string; begin if Length(soerom) > 0 then @@ -412,6 +441,10 @@ begin if Length(dkc3rom) = 0 then DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc'); + smwrom := CheckRom('Super Mario World (USA).sfc', 'cdd3c8c37322978ca8669b34bc89c804'); + if Length(smwrom) = 0 then + SMWRomFilePage:= AddRomPage('Super Mario World (USA).sfc'); + soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); @@ -427,6 +460,8 @@ begin Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm')); if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3')); + if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw')); if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then diff --git a/worlds/smw/Aesthetics.py b/worlds/smw/Aesthetics.py new file mode 100644 index 0000000000..624440c55f --- /dev/null +++ b/worlds/smw/Aesthetics.py @@ -0,0 +1,202 @@ + +mario_palettes = [ + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x39, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0xB6, 0x30, 0xDF, 0x35, 0xFF, 0x03], # Mario + [0x3F, 0x4F, 0x1D, 0x58, 0x40, 0x11, 0xE0, 0x3F, 0x07, 0x3C, 0xAE, 0x7C, 0xB3, 0x7D, 0x00, 0x2F, 0x5F, 0x16, 0xFF, 0x03], # Luigi + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x03, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x16, 0x02, 0xDF, 0x35, 0xFF, 0x03], # Wario + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x12, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x0D, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Waluigi + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x00, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x00, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Geno + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x16, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Princess + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0xE0, 0x00, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x80, 0x00, 0xDF, 0x35, 0xFF, 0x03], # Dark + [0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0xFF, 0x01, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x5F, 0x01, 0xDF, 0x35, 0xFF, 0x03], # Sponge +] + +fire_mario_palettes = [ + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x17, 0x00, 0x1F, 0x00, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Mario + [0x1F, 0x3B, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x40, 0x11, 0xE0, 0x01, 0xE0, 0x02, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Luigi + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x16, 0x02, 0x1F, 0x03, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Wario + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x0D, 0x58, 0x12, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Waluigi + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x00, 0x58, 0x00, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Geno + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x16, 0x58, 0x1F, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Princess + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Dark + [0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x5F, 0x01, 0xFF, 0x01, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Sponge +] + +ow_mario_palettes = [ + [0x16, 0x00, 0x1F, 0x00], # Mario + [0x80, 0x02, 0xE0, 0x03], # Luigi + [0x16, 0x02, 0x1F, 0x03], # Wario + [0x0D, 0x58, 0x12, 0x7C], # Waluigi + [0x00, 0x58, 0x00, 0x7C], # Geno + [0x16, 0x58, 0x1F, 0x7C], # Princess + [0x80, 0x00, 0xE0, 0x00], # Dark + [0x5F, 0x01, 0xFF, 0x01], # Sponge +] + +level_music_address_data = [ + 0x284DB, + 0x284DC, + 0x284DD, + 0x284DE, + 0x284DF, + 0x284E0, + 0x284E1, + 0x284E2, +] + +level_music_value_data = [ + 0x02, + 0x06, + 0x01, + 0x08, + 0x07, + 0x03, + 0x05, + 0x12, +] + +ow_music_address_data = [ + [0x25BC8, 0x20D8A], + [0x25BC9, 0x20D8B], + [0x25BCA, 0x20D8C], + [0x25BCB, 0x20D8D], + [0x25BCC, 0x20D8E], + [0x25BCD, 0x20D8F], + [0x25BCE, 0x20D90], + [0x16C7] +] + +ow_music_value_data = [ + 0x02, + 0x03, + 0x04, + 0x06, + 0x07, + 0x09, + 0x05, + 0x01, +] + +valid_foreground_palettes = { + 0x00: [0x00, 0x01, 0x03, 0x04, 0x05, 0x07], # Normal 1 + 0x01: [0x03, 0x04, 0x05, 0x07], # Castle 1 + 0x02: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 1 + 0x03: [0x02, 0x03, 0x04, 0x05, 0x07], # Underground 1 + 0x04: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Switch Palace 1 + 0x05: [0x04, 0x05], # Ghost House 1 + 0x06: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 2 + 0x07: [0x00, 0x01, 0x03, 0x04, 0x05, 0x07], # Normal 2 + 0x08: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 3 + 0x09: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Underground 2 + 0x0A: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Switch Palace 2 + 0x0B: [0x03, 0x04, 0x05, 0x07], # Castle 2 + #0x0C: [], # Cloud/Forest + 0x0D: [0x04, 0x05], # Ghost House 2 + 0x0E: [0x02, 0x03, 0x04, 0x05, 0x07], # Underground 3 +} + +valid_background_palettes = { + 0x06861B: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Ghost House Exit + 0xFFD900: [0x01], # P. Hills + 0xFFDAB9: [0x04], # Water + 0xFFDC71: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Hills & Clouds + 0xFFDD44: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Clouds + 0xFFDE54: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Small Hills + 0xFFDF59: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Mountains & Clouds + 0xFFE103: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Castle Pillars + 0xFFE472: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Big Hills + 0xFFE674: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Bonus + 0xFFE684: [0x01, 0x03, 0x05, 0x06], # Stars + 0xFFE7C0: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Mountains + 0xFFE8EE: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Empty/Layer 2 + 0xFFE8FE: [0x01, 0x06], # Cave + 0xFFEC82: [0x00, 0x02, 0x03, 0x05, 0x06, 0x07], # Bushes + 0xFFEF80: [0x01, 0x03, 0x05, 0x06], # Ghost House + 0xFFF175: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06], # Ghost Ship + 0xFFF45A: [0x01, 0x03, 0x06], # Castle +} + +valid_background_colors = { + 0x06861B: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Ghost House Exit + 0xFFD900: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # P. Hills + 0xFFDAB9: [0x02, 0x03, 0x05], # Water + 0xFFDC71: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Hills & Clouds + 0xFFDD44: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Clouds + 0xFFDE54: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Small Hills + 0xFFDF59: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Mountains & Clouds + 0xFFE103: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Castle Pillars + 0xFFE472: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Big Hills + 0xFFE674: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Bonus + 0xFFE684: [0x02, 0x03, 0x04, 0x05], # Stars + 0xFFE7C0: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Mountains + 0xFFE8EE: [0x03, 0x05], # Empty/Layer 2 + 0xFFE8FE: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Cave + 0xFFEC82: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Bushes + 0xFFEF80: [0x03, 0x04], # Ghost House + 0xFFF175: [0x02, 0x03, 0x04, 0x05], # Ghost Ship + 0xFFF45A: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Castle +} + +def generate_shuffled_level_music(world, player): + shuffled_level_music = level_music_value_data.copy() + + if world.music_shuffle[player] == "consistent": + world.random.shuffle(shuffled_level_music) + elif world.music_shuffle[player] == "singularity": + single_song = world.random.choice(shuffled_level_music) + shuffled_level_music = [single_song for i in range(len(shuffled_level_music))] + + return shuffled_level_music + +def generate_shuffled_ow_music(world, player): + shuffled_ow_music = ow_music_value_data.copy() + + if world.music_shuffle[player] == "consistent" or world.music_shuffle[player] == "full": + world.random.shuffle(shuffled_ow_music) + elif world.music_shuffle[player] == "singularity": + single_song = world.random.choice(shuffled_ow_music) + shuffled_ow_music = [single_song for i in range(len(shuffled_ow_music))] + + return shuffled_ow_music + +def generate_shuffled_header_data(rom, world, player): + if world.music_shuffle[player] != "full" and not world.foreground_palette_shuffle[player] and not world.background_palette_shuffle[player]: + return + + for level_id in range(0, 0x200): + layer1_ptr_list = list(rom.read_bytes(0x2E000 + level_id * 3, 3)) + layer1_ptr = (layer1_ptr_list[2] << 16 | layer1_ptr_list[1] << 8 | layer1_ptr_list[0]) + + if layer1_ptr == 0x68000: + # Unused Levels + continue + + if layer1_ptr >= 0x70000: + layer1_ptr -= 0x8000 + + layer1_ptr -= 0x38000 + + level_header = list(rom.read_bytes(layer1_ptr, 5)) + + tileset = level_header[4] & 0x0F + + if world.music_shuffle[player] == "full": + level_header[2] &= 0x8F + level_header[2] |= (world.random.randint(0, 7) << 5) + + if (world.foreground_palette_shuffle[player] and tileset in valid_foreground_palettes): + level_header[3] &= 0xF8 + level_header[3] |= world.random.choice(valid_foreground_palettes[tileset]) + + if world.background_palette_shuffle[player]: + layer2_ptr_list = list(rom.read_bytes(0x2E600 + level_id * 3, 3)) + layer2_ptr = (layer2_ptr_list[2] << 16 | layer2_ptr_list[1] << 8 | layer2_ptr_list[0]) + + if layer2_ptr in valid_background_palettes: + level_header[0] &= 0x1F + level_header[0] |= (world.random.choice(valid_background_palettes[layer2_ptr]) << 5) + + if layer2_ptr in valid_background_colors: + level_header[1] &= 0x1F + level_header[1] |= (world.random.choice(valid_background_colors[layer2_ptr]) << 5) + + rom.write_bytes(layer1_ptr, bytes(level_header)) diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py new file mode 100644 index 0000000000..6ddd4e1073 --- /dev/null +++ b/worlds/smw/Client.py @@ -0,0 +1,455 @@ +import logging +import asyncio +import time + +from NetUtils import ClientStatus, color +from worlds import AutoWorldRegister +from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read +from .Names.TextBox import generate_received_text +from Patch import GAME_SMW + +snes_logger = logging.getLogger("SNES") + +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +SAVEDATA_START = WRAM_START + 0xF000 +SAVEDATA_SIZE = 0x500 + +SMW_ROMHASH_START = 0x7FC0 +ROMHASH_SIZE = 0x15 + +SMW_PROGRESS_DATA = WRAM_START + 0x1F02 +SMW_DRAGON_COINS_DATA = WRAM_START + 0x1F2F +SMW_PATH_DATA = WRAM_START + 0x1EA2 +SMW_EVENT_ROM_DATA = ROM_START + 0x2D608 +SMW_ACTIVE_LEVEL_DATA = ROM_START + 0x37F70 + +SMW_GOAL_DATA = ROM_START + 0x01BFA0 +SMW_REQUIRED_BOSSES_DATA = ROM_START + 0x01BFA1 +SMW_REQUIRED_EGGS_DATA = ROM_START + 0x01BFA2 +SMW_SEND_MSG_DATA = ROM_START + 0x01BFA3 +SMW_RECEIVE_MSG_DATA = ROM_START + 0x01BFA4 +SMW_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x01BFA5 +SMW_DRAGON_COINS_ACTIVE_ADDR = ROM_START + 0x01BFA6 +SMW_SWAMP_DONUT_GH_ADDR = ROM_START + 0x01BFA7 + +SMW_GAME_STATE_ADDR = WRAM_START + 0x100 +SMW_MARIO_STATE_ADDR = WRAM_START + 0x71 +SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B +SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC +SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF +SMW_MESSAGE_BOX_ADDR = WRAM_START + 0x1426 +SMW_BONUS_STAR_ADDR = WRAM_START + 0xF48 +SMW_EGG_COUNT_ADDR = WRAM_START + 0x1F24 +SMW_BOSS_COUNT_ADDR = WRAM_START + 0x1F26 +SMW_NUM_EVENTS_ADDR = WRAM_START + 0x1F2E +SMW_SFX_ADDR = WRAM_START + 0x1DFC +SMW_PAUSE_ADDR = WRAM_START + 0x13D4 +SMW_MESSAGE_QUEUE_ADDR = WRAM_START + 0xC391 + +SMW_RECV_PROGRESS_ADDR = WRAM_START + 0x1F2B + +SMW_GOAL_LEVELS = [0x28, 0x31, 0x32] +SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D] +SMW_BAD_TEXT_BOX_LEVELS = [0x26, 0x02, 0x4B] +SMW_BOSS_STATES = [0x80, 0xC0, 0xC1] +SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32] + +async def deathlink_kill_player(ctx: Context): + if ctx.game == GAME_SMW: + game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + if game_state[0] != 0x14: + return + + mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) + if mario_state[0] != 0x00: + return + + message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) + if message_box[0] != 0x00: + return + + pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1) + if pause_state[0] != 0x00: + return + + snes_buffered_write(ctx, WRAM_START + 0x9D, bytes([0x30])) # Freeze Gameplay + snes_buffered_write(ctx, WRAM_START + 0x1DFB, bytes([0x09])) # Death Music + snes_buffered_write(ctx, WRAM_START + 0x0DDA, bytes([0xFF])) # Flush Music Buffer + snes_buffered_write(ctx, WRAM_START + 0x1407, bytes([0x00])) # Flush Cape Fly Phase + snes_buffered_write(ctx, WRAM_START + 0x140D, bytes([0x00])) # Flush Spin Jump Flag + snes_buffered_write(ctx, WRAM_START + 0x188A, bytes([0x00])) # Flush Empty Byte because the game does it + snes_buffered_write(ctx, WRAM_START + 0x7D, bytes([0x90])) # Mario Y Speed + snes_buffered_write(ctx, WRAM_START + 0x1496, bytes([0x30])) # Death Timer + snes_buffered_write(ctx, SMW_MARIO_STATE_ADDR, bytes([0x09])) # Mario State -> Dead + + await snes_flush_writes(ctx) + + from SNIClient import DeathState + ctx.death_state = DeathState.dead + ctx.last_death_link = time.time() + + return + + +async def smw_rom_init(ctx: Context): + if not ctx.rom: + ctx.finished_game = False + ctx.death_link_allow_survive = False + game_hash = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE) + if game_hash is None or game_hash == bytes([0] * ROMHASH_SIZE) or game_hash[:3] != b"SMW": + return False + else: + ctx.game = GAME_SMW + ctx.items_handling = 0b111 # remote items + + ctx.rom = game_hash + + receive_option = await snes_read(ctx, SMW_RECEIVE_MSG_DATA, 0x1) + send_option = await snes_read(ctx, SMW_SEND_MSG_DATA, 0x1) + + ctx.receive_option = receive_option[0] + ctx.send_option = send_option[0] + + ctx.message_queue = [] + + ctx.allow_collect = True + + death_link = await snes_read(ctx, SMW_DEATH_LINK_ACTIVE_ADDR, 1) + if death_link: + await ctx.update_death_link(bool(death_link[0] & 0b1)) + return True + + +def add_message_to_queue(ctx: Context, new_message): + + if not hasattr(ctx, "message_queue"): + ctx.message_queue = [] + + ctx.message_queue.append(new_message) + + return + + +async def handle_message_queue(ctx: Context): + + game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + if game_state[0] != 0x14: + return + + mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) + if mario_state[0] != 0x00: + return + + message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) + if message_box[0] != 0x00: + return + + pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1) + if pause_state[0] != 0x00: + return + + current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) + if current_level[0] in SMW_BAD_TEXT_BOX_LEVELS: + return + + boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1) + if boss_state[0] in SMW_BOSS_STATES: + return + + active_boss = await snes_read(ctx, SMW_ACTIVE_BOSS_ADDR, 0x1) + if active_boss[0] != 0x00: + return + + if not hasattr(ctx, "message_queue") or len(ctx.message_queue) == 0: + return + + next_message = ctx.message_queue.pop(0) + + snes_buffered_write(ctx, SMW_MESSAGE_QUEUE_ADDR, bytes(next_message)) + snes_buffered_write(ctx, SMW_MESSAGE_BOX_ADDR, bytes([0x03])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x22])) + + await snes_flush_writes(ctx) + + return + + +async def smw_game_watcher(ctx: Context): + if ctx.game == GAME_SMW: + # SMW_TODO: Handle Deathlink + game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) + if game_state is None: + # We're not properly connected + return + elif game_state[0] >= 0x18: + if not ctx.finished_game: + current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) + + if current_level[0] in SMW_GOAL_LEVELS: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + return + elif game_state[0] < 0x0B: + # We haven't loaded a save file + ctx.message_queue = [] + return + elif mario_state[0] in SMW_INVALID_MARIO_STATES: + # Mario can't come to the phone right now + return + + if "DeathLink" in ctx.tags and game_state[0] == 0x14 and ctx.last_death_link + 1 < time.time(): + currently_dead = mario_state[0] == 0x09 + await ctx.handle_deathlink_state(currently_dead) + + # Check for Egg Hunt ending + goal = await snes_read(ctx, SMW_GOAL_DATA, 0x1) + if game_state[0] == 0x14 and goal[0] == 1: + current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) + message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) + egg_count = await snes_read(ctx, SMW_EGG_COUNT_ADDR, 0x1) + required_egg_count = await snes_read(ctx, SMW_REQUIRED_EGGS_DATA, 0x1) + + if current_level[0] == 0x28 and message_box[0] == 0x01 and egg_count[0] >= required_egg_count[0]: + snes_buffered_write(ctx, WRAM_START + 0x13C6, bytes([0x08])) + snes_buffered_write(ctx, WRAM_START + 0x13CE, bytes([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x1DE9, bytes([0x01])) + snes_buffered_write(ctx, SMW_GAME_STATE_ADDR, bytes([0x18])) + + await snes_flush_writes(ctx) + return + + egg_count = await snes_read(ctx, SMW_EGG_COUNT_ADDR, 0x1) + boss_count = await snes_read(ctx, SMW_BOSS_COUNT_ADDR, 0x1) + display_count = await snes_read(ctx, SMW_BONUS_STAR_ADDR, 0x1) + + if goal[0] == 0 and boss_count[0] > display_count[0]: + snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([boss_count[0]])) + await snes_flush_writes(ctx) + elif goal[0] == 1 and egg_count[0] > display_count[0]: + snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([egg_count[0]])) + await snes_flush_writes(ctx) + + await handle_message_queue(ctx) + + new_checks = [] + event_data = await snes_read(ctx, SMW_EVENT_ROM_DATA, 0x60) + progress_data = bytearray(await snes_read(ctx, SMW_PROGRESS_DATA, 0x0F)) + dragon_coins_data = bytearray(await snes_read(ctx, SMW_DRAGON_COINS_DATA, 0x0C)) + dragon_coins_active = await snes_read(ctx, SMW_DRAGON_COINS_ACTIVE_ADDR, 0x1) + from worlds.smw.Rom import item_rom_data, ability_rom_data + from worlds.smw.Levels import location_id_to_level_id, level_info_dict + for loc_name, level_data in location_id_to_level_id.items(): + loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] + if loc_id not in ctx.locations_checked: + + event_id = event_data[level_data[0]] + + if level_data[1] == 2: + # Dragon Coins Check + if not dragon_coins_active or dragon_coins_active[0] == 0: + continue + + progress_byte = (level_data[0] // 8) + progress_bit = 7 - (level_data[0] % 8) + + data = dragon_coins_data[progress_byte] + masked_data = data & (1 << progress_bit) + bit_set = (masked_data != 0) + + if bit_set: + # SMW_TODO: Handle non-included checks + new_checks.append(loc_id) + else: + event_id_value = event_id + level_data[1] + + progress_byte = (event_id_value // 8) + progress_bit = 7 - (event_id_value % 8) + + data = progress_data[progress_byte] + masked_data = data & (1 << progress_bit) + bit_set = (masked_data != 0) + + if bit_set: + # SMW_TODO: Handle non-included checks + new_checks.append(loc_id) + + verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + if verify_game_state is None or verify_game_state[0] < 0x0B or verify_game_state[0] > 0x29: + # We have somehow exited the save file (or worse) + print("Exit Save File") + return + + rom = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + print("Exit ROM") + # We have somehow loaded a different ROM + return + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names[new_check_id] + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) + + if game_state[0] != 0x14: + # Don't receive items or collect locations outside of in-level mode + return + + recv_count = await snes_read(ctx, SMW_RECV_PROGRESS_ADDR, 1) + recv_index = recv_count[0] + + if recv_index < len(ctx.items_received): + item = ctx.items_received[recv_index] + recv_index += 1 + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names[item.location], recv_index, len(ctx.items_received))) + + if ctx.receive_option == 1 or (ctx.receive_option == 2 and ((item.flags & 1) != 0)): + if item.item != 0xBC0012: + # Don't send messages for Boss Tokens + item_name = ctx.item_names[item.item] + player_name = ctx.player_names[item.player] + + receive_message = generate_received_text(item_name, player_name) + add_message_to_queue(ctx, receive_message) + + snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index])) + if item.item in item_rom_data: + item_count = await snes_read(ctx, WRAM_START + item_rom_data[item.item][0], 0x1) + increment = item_rom_data[item.item][1] + + new_item_count = item_count[0] + if increment > 1: + new_item_count = increment + else: + new_item_count += increment + + if verify_game_state[0] == 0x14 and len(item_rom_data[item.item]) > 2: + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([item_rom_data[item.item][2]])) + + snes_buffered_write(ctx, WRAM_START + item_rom_data[item.item][0], bytes([new_item_count])) + elif item.item in ability_rom_data: + # Handle Upgrades + for rom_data in ability_rom_data[item.item]: + data = await snes_read(ctx, WRAM_START + rom_data[0], 1) + masked_data = data[0] | (1 << rom_data[1]) + snes_buffered_write(ctx, WRAM_START + rom_data[0], bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E])) # SMW_TODO: Custom sounds for each + elif item.item == 0xBC000A: + # Handle Progressive Powerup + data = await snes_read(ctx, WRAM_START + 0x1F2D, 1) + mushroom_data = data[0] & (1 << 0) + fire_flower_data = data[0] & (1 << 1) + cape_data = data[0] & (1 << 2) + if mushroom_data == 0: + masked_data = data[0] | (1 << 0) + snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E])) + elif fire_flower_data == 0: + masked_data = data[0] | (1 << 1) + snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E])) + elif cape_data == 0: + masked_data = data[0] | (1 << 2) + snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x41])) + else: + # Extra Powerup? + pass + elif item.item == 0xBC0015: + # Handle Literature Trap + from .Names.LiteratureTrap import lit_trap_text_list + import random + rand_trap = random.choice(lit_trap_text_list) + + for message in rand_trap: + add_message_to_queue(ctx, message) + + await snes_flush_writes(ctx) + + # Handle Collected Locations + new_events = 0 + path_data = bytearray(await snes_read(ctx, SMW_PATH_DATA, 0x60)) + donut_gh_swapped = await snes_read(ctx, SMW_SWAMP_DONUT_GH_ADDR, 0x1) + new_dragon_coin = False + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked: + ctx.locations_checked.add(loc_id) + loc_name = ctx.location_names[loc_id] + + if loc_name not in location_id_to_level_id: + continue + + level_data = location_id_to_level_id[loc_name] + + if level_data[1] == 2: + # Dragon Coins Check + + progress_byte = (level_data[0] // 8) + progress_bit = 7 - (level_data[0] % 8) + + data = dragon_coins_data[progress_byte] + new_data = data | (1 << progress_bit) + dragon_coins_data[progress_byte] = new_data + + new_dragon_coin = True + else: + if level_data[0] in SMW_UNCOLLECTABLE_LEVELS: + continue + + event_id = event_data[level_data[0]] + event_id_value = event_id + level_data[1] + + progress_byte = (event_id_value // 8) + progress_bit = 7 - (event_id_value % 8) + + data = progress_data[progress_byte] + masked_data = data & (1 << progress_bit) + bit_set = (masked_data != 0) + + if bit_set: + continue + + new_events += 1 + new_data = data | (1 << progress_bit) + progress_data[progress_byte] = new_data + + tile_id = await snes_read(ctx, SMW_ACTIVE_LEVEL_DATA + level_data[0], 0x1) + + level_info = level_info_dict[tile_id[0]] + + path = level_info.exit1Path if level_data[1] == 0 else level_info.exit2Path + + if donut_gh_swapped[0] != 0 and tile_id[0] == 0x04: + # Handle Swapped Donut GH Exits + path = level_info.exit2Path if level_data[1] == 0 else level_info.exit1Path + + if not path: + continue + + this_end_path = path_data[tile_id[0]] + new_data = this_end_path | path.thisEndDirection + path_data[tile_id[0]] = new_data + + other_end_path = path_data[path.otherLevelID] + new_data = other_end_path | path.otherEndDirection + path_data[path.otherLevelID] = new_data + + if new_dragon_coin: + snes_buffered_write(ctx, SMW_DRAGON_COINS_DATA, bytes(dragon_coins_data)) + if new_events > 0: + snes_buffered_write(ctx, SMW_PROGRESS_DATA, bytes(progress_data)) + snes_buffered_write(ctx, SMW_PATH_DATA, bytes(path_data)) + old_events = await snes_read(ctx, SMW_NUM_EVENTS_ADDR, 0x1) + snes_buffered_write(ctx, SMW_NUM_EVENTS_ADDR, bytes([old_events[0] + new_events])) + + await snes_flush_writes(ctx) diff --git a/worlds/smw/Items.py b/worlds/smw/Items.py new file mode 100644 index 0000000000..e650aef4a5 --- /dev/null +++ b/worlds/smw/Items.py @@ -0,0 +1,69 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemName + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + progression: bool + trap: bool = False + quantity: int = 1 + event: bool = False + + +class SMWItem(Item): + game: str = "Super Mario World" + + +# Separate tables for each type of item. +junk_table = { + ItemName.one_up_mushroom: ItemData(0xBC0001, False), +} + +collectable_table = { + ItemName.yoshi_egg: ItemData(0xBC0002, True), +} + +upgrade_table = { + ItemName.mario_run: ItemData(0xBC0003, True), + ItemName.mario_carry: ItemData(0xBC0004, True), + ItemName.mario_swim: ItemData(0xBC0005, True), + ItemName.mario_spin_jump: ItemData(0xBC0006, True), + ItemName.mario_climb: ItemData(0xBC0007, True), + ItemName.yoshi_activate: ItemData(0xBC0008, True), + ItemName.p_switch: ItemData(0xBC0009, True), + ItemName.progressive_powerup: ItemData(0xBC000A, True), + ItemName.p_balloon: ItemData(0xBC000B, True), + ItemName.super_star_active: ItemData(0xBC000D, True), +} + +switch_palace_table = { + ItemName.yellow_switch_palace: ItemData(0xBC000E, True), + ItemName.green_switch_palace: ItemData(0xBC000F, True), + ItemName.red_switch_palace: ItemData(0xBC0010, True), + ItemName.blue_switch_palace: ItemData(0xBC0011, True), +} + +trap_table = { + ItemName.ice_trap: ItemData(0xBC0013, False, True), + ItemName.stun_trap: ItemData(0xBC0014, False, True), + ItemName.literature_trap: ItemData(0xBC0015, False, True), +} + +event_table = { + ItemName.victory: ItemData(0xBC0000, True), + ItemName.koopaling: ItemData(0xBC0012, True), +} + +# Complete item table. +item_table = { + **junk_table, + **collectable_table, + **upgrade_table, + **switch_palace_table, + **trap_table, + **event_table, +} + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} diff --git a/worlds/smw/Levels.py b/worlds/smw/Levels.py new file mode 100644 index 0000000000..0783ebfd39 --- /dev/null +++ b/worlds/smw/Levels.py @@ -0,0 +1,579 @@ + +from .Names import LocationName + +class SMWPath(): + thisEndDirection: int + otherLevelID: int + otherEndDirection: int + + def __init__(self, thisEndDirection: int, otherLevelID: int, otherEndDirection: int): + self.thisEndDirection = thisEndDirection + self.otherLevelID = otherLevelID + self.otherEndDirection = otherEndDirection + + +class SMWLevel(): + levelName: str + levelIDAddress: int + #eventIDAddress: int + eventIDValue: int + #progressByte: int + #progressBit: int + exit1Path: SMWPath + exit2Path: SMWPath + + def __init__(self, levelName: str, levelIDAddress: int, eventIDValue: int, exit1Path: SMWPath = None, exit2Path: SMWPath = None): + self.levelName = levelName + self.levelIDAddress = levelIDAddress + #self.eventIDAddress = eventIDAddress # Inferred from: LevelIDValue (Dict Key): $2D608 + LevelIDValue + self.eventIDValue = eventIDValue + #self.progressByte = progressByte # Inferred from EventIDValue: (ID / 8) + $1F02 + #self.progressBit = progressBit # Inferred from EventIDValue: 1 << (7 - (ID % 8)) + self.exit1Path = exit1Path + self.exit2Path = exit2Path + + +level_info_dict = { + 0x28: SMWLevel(LocationName.yoshis_house, 0x37A76, 0x00), + 0x29: SMWLevel(LocationName.yoshis_island_1_region, 0x37A83, 0x01, SMWPath(0x08, 0x14, 0x04)), + 0x14: SMWLevel(LocationName.yellow_switch_palace, 0x37812, 0x02), + 0x2A: SMWLevel(LocationName.yoshis_island_2_region, 0x37A89, 0x03, SMWPath(0x08, 0x27, 0x04)), + 0x27: SMWLevel(LocationName.yoshis_island_3_region, 0x37A69, 0x04, SMWPath(0x01, 0x26, 0x04)), + 0x26: SMWLevel(LocationName.yoshis_island_4_region, 0x37A4B, 0x05, SMWPath(0x08, 0x25, 0x01)), + 0x25: SMWLevel(LocationName.yoshis_island_castle_region, 0x37A29, 0x06, SMWPath(0x08, 0x15, 0x04)), + + 0x15: SMWLevel(LocationName.donut_plains_1_region, 0x37815, 0x07, SMWPath(0x02, 0x09, 0x04), SMWPath(0x08, 0x0A, 0x04)), + 0x09: SMWLevel(LocationName.donut_plains_2_region, 0x376D3, 0x09, SMWPath(0x08, 0x04, 0x02), SMWPath(0x02, 0x08, 0x01)), + 0x0A: SMWLevel(LocationName.donut_secret_1_region, 0x376E5, 0x10, SMWPath(0x08, 0x04, 0x04), SMWPath(0x01, 0x13, 0x08)), + 0x08: SMWLevel(LocationName.green_switch_palace, 0x376D1, 0x28), + 0x04: SMWLevel(LocationName.donut_ghost_house_region, 0x376A5, 0x0B, SMWPath(0x08, 0x03, 0x04), SMWPath(0x01, 0x05, 0x02)), + 0x13: SMWLevel(LocationName.donut_secret_house_region, 0x37807, 0x12, SMWPath(0x01, 0x2F, 0x04), SMWPath(0x04, 0x16, 0x08)), # SMW_TODO: Check this wrt pipe behavior + 0x05: SMWLevel(LocationName.donut_plains_3_region, 0x376A9, 0x0D, SMWPath(0x01, 0x06, 0x08)), + 0x06: SMWLevel(LocationName.donut_plains_4_region, 0x376CB, 0x0E, SMWPath(0x01, 0x07, 0x02)), + 0x2F: SMWLevel(LocationName.donut_secret_2_region, 0x37B10, 0x14, SMWPath(0x01, 0x05, 0x04)), + 0x07: SMWLevel(LocationName.donut_plains_castle_region, 0x376CD, 0x0F, SMWPath(0x08, 0x3E, 0x04)), + 0x03: SMWLevel(LocationName.donut_plains_top_secret, 0x37685, 0xFF), + 0x16: SMWLevel(LocationName.donut_plains_star_road, 0x37827, 0xFF), + + 0x3E: SMWLevel(LocationName.vanilla_dome_1_region, 0x37C25, 0x15, SMWPath(0x01, 0x3C, 0x04), SMWPath(0x02, 0x2D, 0x04)), + 0x3C: SMWLevel(LocationName.vanilla_dome_2_region, 0x37C08, 0x17, SMWPath(0x08, 0x2B, 0x04), SMWPath(0x01, 0x3F, 0x08)), + 0x2D: SMWLevel(LocationName.vanilla_secret_1_region, 0x37AE3, 0x1D, SMWPath(0x08, 0x01, 0x02), SMWPath(0x02, 0x2C, 0x01)), + 0x2B: SMWLevel(LocationName.vanilla_ghost_house_region, 0x37AC8, 0x19, SMWPath(0x01, 0x2E, 0x08)), + 0x2E: SMWLevel(LocationName.vanilla_dome_3_region, 0x37AEC, 0x1A, SMWPath(0x04, 0x3D, 0x08)), + 0x3D: SMWLevel(LocationName.vanilla_dome_4_region, 0x37C0C, 0x1B, SMWPath(0x04, 0x40, 0x08)), + 0x3F: SMWLevel(LocationName.red_switch_palace, 0x37C2A, 0x29), + 0x01: SMWLevel(LocationName.vanilla_secret_2_region, 0x3763C, 0x1F, SMWPath(0x01, 0x02, 0x02)), + 0x02: SMWLevel(LocationName.vanilla_secret_3_region, 0x3763E, 0x20, SMWPath(0x01, 0x0B, 0x02)), + 0x0B: SMWLevel(LocationName.vanilla_fortress_region, 0x37730, 0x21, SMWPath(0x01, 0x0C, 0x02)), + 0x40: SMWLevel(LocationName.vanilla_dome_castle_region, 0x37C2C, 0x1C, SMWPath(0x04, 0x0F, 0x02)), + 0x2C: SMWLevel(LocationName.vanilla_dome_star_road, 0x37AE0, 0xFF), + + 0x0C: SMWLevel(LocationName.butter_bridge_1_region, 0x37734, 0x22, SMWPath(0x01, 0x0D, 0x02)), + 0x0D: SMWLevel(LocationName.butter_bridge_2_region, 0x37736, 0x23, SMWPath(0x01, 0x0E, 0x02)), + 0x0F: SMWLevel(LocationName.cheese_bridge_region, 0x37754, 0x25, SMWPath(0x01, 0x10, 0x02), SMWPath(0x04, 0x11, 0x08)), + 0x11: SMWLevel(LocationName.soda_lake_region, 0x37784, 0x60, SMWPath(0x04, 0x12, 0x04)), + 0x10: SMWLevel(LocationName.cookie_mountain_region, 0x37757, 0x27, SMWPath(0x04, 0x0E, 0x04)), + 0x0E: SMWLevel(LocationName.twin_bridges_castle_region, 0x3773A, 0x24, SMWPath(0x01, 0x42, 0x08)), + 0x12: SMWLevel(LocationName.twin_bridges_star_road, 0x377F0, 0xFF), + + 0x42: SMWLevel(LocationName.forest_of_illusion_1_region, 0x37C78, 0x2A, SMWPath(0x01, 0x44, 0x08), SMWPath(0x02, 0x41, 0x01)), + 0x44: SMWLevel(LocationName.forest_of_illusion_2_region, 0x37CAA, 0x2C, SMWPath(0x04, 0x47, 0x08), SMWPath(0x01, 0x45, 0x02)), + 0x47: SMWLevel(LocationName.forest_of_illusion_3_region, 0x37CC8, 0x2E, SMWPath(0x02, 0x41, 0x04), SMWPath(0x04, 0x20, 0x01)), + 0x43: SMWLevel(LocationName.forest_of_illusion_4_region, 0x37CA4, 0x32, SMWPath(0x01, 0x44, 0x02), SMWPath(0x04, 0x46, 0x08)), + 0x41: SMWLevel(LocationName.forest_ghost_house_region, 0x37C76, 0x30, SMWPath(0x01, 0x42, 0x02), SMWPath(0x02, 0x43, 0x08)), + 0x46: SMWLevel(LocationName.forest_secret_region, 0x37CC4, 0x34, SMWPath(0x04, 0x1F, 0x01)), + 0x45: SMWLevel(LocationName.blue_switch_palace, 0x37CAC, 0x37), + 0x1F: SMWLevel(LocationName.forest_fortress_region, 0x37906, 0x35, SMWPath(0x02, 0x1E, 0x01)), + 0x20: SMWLevel(LocationName.forest_castle_region, 0x37928, 0x61, SMWPath(0x04, 0x22, 0x08)), + 0x1E: SMWLevel(LocationName.forest_star_road, 0x37904, 0x36), + + 0x22: SMWLevel(LocationName.chocolate_island_1_region, 0x37968, 0x62, SMWPath(0x02, 0x21, 0x01)), + 0x24: SMWLevel(LocationName.chocolate_island_2_region, 0x379B5, 0x46, SMWPath(0x02, 0x23, 0x01), SMWPath(0x04, 0x3B, 0x01)), + 0x23: SMWLevel(LocationName.chocolate_island_3_region, 0x379B3, 0x48, SMWPath(0x04, 0x23, 0x08), SMWPath(0x02, 0x1B, 0x01)), + 0x1D: SMWLevel(LocationName.chocolate_island_4_region, 0x378DF, 0x4B, SMWPath(0x02, 0x1C, 0x01)), + 0x1C: SMWLevel(LocationName.chocolate_island_5_region, 0x378DC, 0x4C, SMWPath(0x08, 0x1A, 0x04)), + 0x21: SMWLevel(LocationName.chocolate_ghost_house_region, 0x37965, 0x63, SMWPath(0x04, 0x24, 0x08)), + 0x1B: SMWLevel(LocationName.chocolate_fortress_region, 0x378BF, 0x4A, SMWPath(0x04, 0x1D, 0x08)), + 0x3B: SMWLevel(LocationName.chocolate_secret_region, 0x37B97, 0x4F, SMWPath(0x02, 0x1A, 0x02)), + 0x1A: SMWLevel(LocationName.chocolate_castle_region, 0x378BC, 0x4D, SMWPath(0x08, 0x18, 0x02)), + + 0x18: SMWLevel(LocationName.sunken_ghost_ship_region, 0x3787E, 0x4E, SMWPath(0x08, 0x3A, 0x01)), + 0x3A: SMWLevel(LocationName.valley_of_bowser_1_region, 0x37B7B, 0x38, SMWPath(0x02, 0x39, 0x01)), + 0x39: SMWLevel(LocationName.valley_of_bowser_2_region, 0x37B79, 0x39, SMWPath(0x02, 0x38, 0x01), SMWPath(0x08, 0x35, 0x04)), + 0x37: SMWLevel(LocationName.valley_of_bowser_3_region, 0x37B74, 0x3D, SMWPath(0x08, 0x33, 0x04)), + 0x33: SMWLevel(LocationName.valley_of_bowser_4_region, 0x37B54, 0x3E, SMWPath(0x01, 0x34, 0x02), SMWPath(0x08, 0x30, 0x04)), + 0x38: SMWLevel(LocationName.valley_ghost_house_region, 0x37B77, 0x3B, SMWPath(0x02, 0x37, 0x01), SMWPath(0x08, 0x34, 0x04)), + 0x35: SMWLevel(LocationName.valley_fortress_region, 0x37B59, 0x41, SMWPath(0x08, 0x32, 0x04)), + 0x34: SMWLevel(LocationName.valley_castle_region, 0x37B57, 0x40, SMWPath(0x08, 0x31, 0x04)), + 0x31: SMWLevel(LocationName.front_door, 0x37B37, 0x45), + 0x81: SMWLevel(LocationName.front_door, 0x37B37, 0x45), # Fake Extra Front Door + 0x32: SMWLevel(LocationName.back_door, 0x37B39, 0x42), + 0x82: SMWLevel(LocationName.back_door, 0x37B39, 0x42), # Fake Extra Back Door + 0x30: SMWLevel(LocationName.valley_star_road, 0x37B34, 0x44), + + 0x5B: SMWLevel(LocationName.star_road_donut, 0x37DD3, 0x50), + 0x58: SMWLevel(LocationName.star_road_1_region, 0x37DA4, 0x51, None, SMWPath(0x02, 0x53, 0x04)), + 0x53: SMWLevel(LocationName.star_road_vanilla, 0x37D82, 0x53), + 0x54: SMWLevel(LocationName.star_road_2_region, 0x37D85, 0x54, None, SMWPath(0x08, 0x52, 0x02)), + 0x52: SMWLevel(LocationName.star_road_twin_bridges, 0x37D67, 0x56), + 0x56: SMWLevel(LocationName.star_road_3_region, 0x37D89, 0x57, None, SMWPath(0x01, 0x57, 0x02)), + 0x57: SMWLevel(LocationName.star_road_forest, 0x37D8C, 0x59), + 0x59: SMWLevel(LocationName.star_road_4_region, 0x37DAA, 0x5A, None, SMWPath(0x04, 0x5C, 0x08)), + 0x5C: SMWLevel(LocationName.star_road_valley, 0x37DDC, 0x5C), + 0x5A: SMWLevel(LocationName.star_road_5_region, 0x37DB7, 0x5D, SMWPath(0x02, 0x5B, 0x01), SMWPath(0x08, 0x55, 0x04)), + 0x55: SMWLevel(LocationName.star_road_special, 0x37D87, 0x5F), + + 0x4D: SMWLevel(LocationName.special_star_road, 0x37D31, 0x64), + 0x4E: SMWLevel(LocationName.special_zone_1_region, 0x37D33, 0x65, SMWPath(0x01, 0x4F, 0x02)), + 0x4F: SMWLevel(LocationName.special_zone_2_region, 0x37D36, 0x66, SMWPath(0x01, 0x50, 0x02)), + 0x50: SMWLevel(LocationName.special_zone_3_region, 0x37D39, 0x67, SMWPath(0x01, 0x51, 0x02)), + 0x51: SMWLevel(LocationName.special_zone_4_region, 0x37D3C, 0x68, SMWPath(0x01, 0x4C, 0x01)), + 0x4C: SMWLevel(LocationName.special_zone_5_region, 0x37D1C, 0x69, SMWPath(0x02, 0x4B, 0x01)), + 0x4B: SMWLevel(LocationName.special_zone_6_region, 0x37D19, 0x6A, SMWPath(0x02, 0x4A, 0x01)), + 0x4A: SMWLevel(LocationName.special_zone_7_region, 0x37D16, 0x6B, SMWPath(0x02, 0x49, 0x01)), + 0x49: SMWLevel(LocationName.special_zone_8_region, 0x37D13, 0x6C, SMWPath(0x02, 0x48, 0x01)), + 0x48: SMWLevel(LocationName.special_complete, 0x37D11, 0x6D), +} + +full_level_list = [ + 0x28, 0x29, 0x14, 0x2A, 0x27, 0x26, 0x25, + 0x15, 0x09, 0x0A, 0x08, 0x04, 0x13, 0x05, 0x06, 0x2F, 0x07, 0x03, 0x16, + 0x3E, 0x3C, 0x2D, 0x2B, 0x2E, 0x3D, 0x3F, 0x01, 0x02, 0x0B, 0x40, 0x2C, + 0x0C, 0x0D, 0x0F, 0x11, 0x10, 0x0E, 0x12, + 0x42, 0x44, 0x47, 0x43, 0x41, 0x46, 0x45, 0x1F, 0x20, 0x1E, + 0x22, 0x24, 0x23, 0x1D, 0x1C, 0x21, 0x1B, 0x3B, 0x1A, + 0x18, 0x3A, 0x39, 0x37, 0x33, 0x38, 0x35, 0x34, 0x31, 0x32, 0x30, + 0x5B, 0x58, 0x53, 0x54, 0x52, 0x56, 0x57, 0x59, 0x5C, 0x5A, 0x55, + 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x4C, 0x4B, 0x4A, 0x49, 0x48, +] + +submap_level_list = [ + 0x28, 0x29, 0x2A, 0x27, 0x26, 0x25, + 0x2F, + 0x3E, 0x3C, 0x2D, 0x2B, 0x2E, 0x3D, 0x3F, 0x40, 0x2C, + 0x42, 0x44, 0x47, 0x43, 0x41, 0x46, 0x45, + 0x3B, + 0x3A, 0x39, 0x37, 0x33, 0x38, 0x35, 0x34, 0x31, 0x32, 0x30, + 0x5B, 0x58, 0x53, 0x54, 0x52, 0x56, 0x57, 0x59, 0x5C, 0x5A, 0x55, + 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x4C, 0x4B, 0x4A, 0x49, 0x48, +] + +easy_castle_fortress_levels = [ + 0x07, + 0x40, + 0x1F, + 0x20, + 0x1B, + 0x34, +] + +hard_castle_fortress_levels = [ + 0x25, + 0x0B, + 0x0E, + 0x1A, + 0x35, +] + +easy_single_levels = [ + 0x29, + 0x2A, + 0x27, + 0x26, + 0x05, + 0x06, + 0x2F, + 0x2E, + 0x3D, + 0x01, + 0x0C, + 0x0D, + 0x46, + 0x1D, +] + +hard_single_levels = [ + 0x2B, + 0x02, + 0x11, + 0x10, + 0x22, + 0x1C, + 0x21, + 0x3B, + 0x3A, + 0x37, + 0x4E, + 0x4F, + 0x50, + 0x51, + 0x4C, + 0x4B, + 0x4A, + 0x49, +] + +easy_double_levels = [ + 0x15, + 0x09, + 0x0F, + 0x42, + 0x43, + 0x24, + 0x38, + 0x58, + 0x54, + 0x56, +] + +hard_double_levels = [ + 0x0A, + 0x04, + 0x13, + 0x3E, + 0x3C, + 0x2D, + 0x44, + 0x47, + 0x41, + 0x23, + 0x33, + 0x39, + 0x59, + 0x5A, +] + +switch_palace_levels = [ + 0x14, + 0x08, + 0x3F, + 0x45, +] + +location_id_to_level_id = { + LocationName.yoshis_island_1_exit_1: [0x29, 0], + LocationName.yoshis_island_1_dragon: [0x29, 2], + LocationName.yoshis_island_2_exit_1: [0x2A, 0], + LocationName.yoshis_island_2_dragon: [0x2A, 2], + LocationName.yoshis_island_3_exit_1: [0x27, 0], + LocationName.yoshis_island_3_dragon: [0x27, 2], + LocationName.yoshis_island_4_exit_1: [0x26, 0], + LocationName.yoshis_island_4_dragon: [0x26, 2], + LocationName.yoshis_island_castle: [0x25, 0], + LocationName.yoshis_island_koopaling: [0x25, 0], + LocationName.yellow_switch_palace: [0x14, 0], + + LocationName.donut_plains_1_exit_1: [0x15, 0], + LocationName.donut_plains_1_exit_2: [0x15, 1], + LocationName.donut_plains_1_dragon: [0x15, 2], + LocationName.donut_plains_2_exit_1: [0x09, 0], + LocationName.donut_plains_2_exit_2: [0x09, 1], + LocationName.donut_plains_2_dragon: [0x09, 2], + LocationName.donut_plains_3_exit_1: [0x05, 0], + LocationName.donut_plains_3_dragon: [0x05, 2], + LocationName.donut_plains_4_exit_1: [0x06, 0], + LocationName.donut_plains_4_dragon: [0x06, 2], + LocationName.donut_secret_1_exit_1: [0x0A, 0], + LocationName.donut_secret_1_exit_2: [0x0A, 1], + LocationName.donut_secret_1_dragon: [0x0A, 2], + LocationName.donut_secret_2_exit_1: [0x2F, 0], + LocationName.donut_secret_2_dragon: [0x2F, 2], + LocationName.donut_ghost_house_exit_1: [0x04, 0], + LocationName.donut_ghost_house_exit_2: [0x04, 1], + LocationName.donut_secret_house_exit_1: [0x13, 0], + LocationName.donut_secret_house_exit_2: [0x13, 1], + LocationName.donut_plains_castle: [0x07, 0], + LocationName.donut_plains_koopaling: [0x07, 0], + LocationName.green_switch_palace: [0x08, 0], + + LocationName.vanilla_dome_1_exit_1: [0x3E, 0], + LocationName.vanilla_dome_1_exit_2: [0x3E, 1], + LocationName.vanilla_dome_1_dragon: [0x3E, 2], + LocationName.vanilla_dome_2_exit_1: [0x3C, 0], + LocationName.vanilla_dome_2_exit_2: [0x3C, 1], + LocationName.vanilla_dome_2_dragon: [0x3C, 2], + LocationName.vanilla_dome_3_exit_1: [0x2E, 0], + LocationName.vanilla_dome_3_dragon: [0x2E, 2], + LocationName.vanilla_dome_4_exit_1: [0x3D, 0], + LocationName.vanilla_dome_4_dragon: [0x3D, 2], + LocationName.vanilla_secret_1_exit_1: [0x2D, 0], + LocationName.vanilla_secret_1_exit_2: [0x2D, 1], + LocationName.vanilla_secret_1_dragon: [0x2D, 2], + LocationName.vanilla_secret_2_exit_1: [0x01, 0], + LocationName.vanilla_secret_2_dragon: [0x01, 2], + LocationName.vanilla_secret_3_exit_1: [0x02, 0], + LocationName.vanilla_secret_3_dragon: [0x02, 2], + LocationName.vanilla_ghost_house_exit_1: [0x2B, 0], + LocationName.vanilla_ghost_house_dragon: [0x2B, 2], + LocationName.vanilla_fortress: [0x0B, 0], + LocationName.vanilla_reznor: [0x0B, 0], + LocationName.vanilla_dome_castle: [0x40, 0], + LocationName.vanilla_dome_koopaling: [0x40, 0], + LocationName.red_switch_palace: [0x3F, 0], + + LocationName.butter_bridge_1_exit_1: [0x0C, 0], + LocationName.butter_bridge_1_dragon: [0x0C, 2], + LocationName.butter_bridge_2_exit_1: [0x0D, 0], + LocationName.butter_bridge_2_dragon: [0x0D, 2], + LocationName.cheese_bridge_exit_1: [0x0F, 0], + LocationName.cheese_bridge_exit_2: [0x0F, 1], + LocationName.cheese_bridge_dragon: [0x0F, 2], + LocationName.cookie_mountain_exit_1: [0x10, 0], + LocationName.cookie_mountain_dragon: [0x10, 2], + LocationName.soda_lake_exit_1: [0x11, 0], + LocationName.soda_lake_dragon: [0x11, 2], + LocationName.twin_bridges_castle: [0x0E, 0], + LocationName.twin_bridges_koopaling: [0x0E, 0], + + LocationName.forest_of_illusion_1_exit_1: [0x42, 0], + LocationName.forest_of_illusion_1_exit_2: [0x42, 1], + LocationName.forest_of_illusion_2_exit_1: [0x44, 0], + LocationName.forest_of_illusion_2_exit_2: [0x44, 1], + LocationName.forest_of_illusion_2_dragon: [0x44, 2], + LocationName.forest_of_illusion_3_exit_1: [0x47, 0], + LocationName.forest_of_illusion_3_exit_2: [0x47, 1], + LocationName.forest_of_illusion_3_dragon: [0x47, 2], + LocationName.forest_of_illusion_4_exit_1: [0x43, 0], + LocationName.forest_of_illusion_4_exit_2: [0x43, 1], + LocationName.forest_of_illusion_4_dragon: [0x43, 2], + LocationName.forest_ghost_house_exit_1: [0x41, 0], + LocationName.forest_ghost_house_exit_2: [0x41, 1], + LocationName.forest_ghost_house_dragon: [0x41, 2], + LocationName.forest_secret_exit_1: [0x46, 0], + LocationName.forest_secret_dragon: [0x46, 2], + LocationName.forest_fortress: [0x1F, 0], + LocationName.forest_reznor: [0x1F, 0], + LocationName.forest_castle: [0x20, 0], + LocationName.forest_koopaling: [0x20, 0], + LocationName.forest_castle_dragon: [0x20, 2], + LocationName.blue_switch_palace: [0x45, 0], + + LocationName.chocolate_island_1_exit_1: [0x22, 0], + LocationName.chocolate_island_1_dragon: [0x22, 2], + LocationName.chocolate_island_2_exit_1: [0x24, 0], + LocationName.chocolate_island_2_exit_2: [0x24, 1], + LocationName.chocolate_island_2_dragon: [0x24, 2], + LocationName.chocolate_island_3_exit_1: [0x23, 0], + LocationName.chocolate_island_3_exit_2: [0x23, 1], + LocationName.chocolate_island_3_dragon: [0x23, 2], + LocationName.chocolate_island_4_exit_1: [0x1D, 0], + LocationName.chocolate_island_4_dragon: [0x1D, 2], + LocationName.chocolate_island_5_exit_1: [0x1C, 0], + LocationName.chocolate_island_5_dragon: [0x1C, 2], + LocationName.chocolate_ghost_house_exit_1: [0x21, 0], + LocationName.chocolate_secret_exit_1: [0x3B, 0], + LocationName.chocolate_fortress: [0x1B, 0], + LocationName.chocolate_reznor: [0x1B, 0], + LocationName.chocolate_castle: [0x1A, 0], + LocationName.chocolate_koopaling: [0x1A, 0], + + LocationName.sunken_ghost_ship: [0x18, 0], + LocationName.sunken_ghost_ship_dragon: [0x18, 2], + + LocationName.valley_of_bowser_1_exit_1: [0x3A, 0], + LocationName.valley_of_bowser_1_dragon: [0x3A, 2], + LocationName.valley_of_bowser_2_exit_1: [0x39, 0], + LocationName.valley_of_bowser_2_exit_2: [0x39, 1], + LocationName.valley_of_bowser_2_dragon: [0x39, 2], + LocationName.valley_of_bowser_3_exit_1: [0x37, 0], + LocationName.valley_of_bowser_3_dragon: [0x37, 2], + LocationName.valley_of_bowser_4_exit_1: [0x33, 0], + LocationName.valley_of_bowser_4_exit_2: [0x33, 1], + LocationName.valley_ghost_house_exit_1: [0x38, 0], + LocationName.valley_ghost_house_exit_2: [0x38, 1], + LocationName.valley_ghost_house_dragon: [0x38, 2], + LocationName.valley_fortress: [0x35, 0], + LocationName.valley_reznor: [0x35, 0], + LocationName.valley_castle: [0x34, 0], + LocationName.valley_koopaling: [0x34, 0], + LocationName.valley_castle_dragon: [0x34, 2], + + LocationName.star_road_1_exit_1: [0x58, 0], + LocationName.star_road_1_exit_2: [0x58, 1], + LocationName.star_road_1_dragon: [0x58, 2], + LocationName.star_road_2_exit_1: [0x54, 0], + LocationName.star_road_2_exit_2: [0x54, 1], + LocationName.star_road_3_exit_1: [0x56, 0], + LocationName.star_road_3_exit_2: [0x56, 1], + LocationName.star_road_4_exit_1: [0x59, 0], + LocationName.star_road_4_exit_2: [0x59, 1], + LocationName.star_road_5_exit_1: [0x5A, 0], + LocationName.star_road_5_exit_2: [0x5A, 1], + + LocationName.special_zone_1_exit_1: [0x4E, 0], + LocationName.special_zone_1_dragon: [0x4E, 2], + LocationName.special_zone_2_exit_1: [0x4F, 0], + LocationName.special_zone_2_dragon: [0x4F, 2], + LocationName.special_zone_3_exit_1: [0x50, 0], + LocationName.special_zone_3_dragon: [0x50, 2], + LocationName.special_zone_4_exit_1: [0x51, 0], + LocationName.special_zone_4_dragon: [0x51, 2], + LocationName.special_zone_5_exit_1: [0x4C, 0], + LocationName.special_zone_5_dragon: [0x4C, 2], + LocationName.special_zone_6_exit_1: [0x4B, 0], + LocationName.special_zone_6_dragon: [0x4B, 2], + LocationName.special_zone_7_exit_1: [0x4A, 0], + LocationName.special_zone_7_dragon: [0x4A, 2], + LocationName.special_zone_8_exit_1: [0x49, 0], + LocationName.special_zone_8_dragon: [0x49, 2], +} + +def generate_level_list(world, player): + + if not world.level_shuffle[player]: + out_level_list = full_level_list.copy() + out_level_list[0x00] = 0x03 + out_level_list[0x11] = 0x28 + + if world.bowser_castle_doors[player] == "fast": + out_level_list[0x41] = 0x82 + out_level_list[0x42] = 0x32 + elif world.bowser_castle_doors[player] == "slow": + out_level_list[0x41] = 0x31 + out_level_list[0x42] = 0x81 + + return out_level_list + + shuffled_level_list = [] + easy_castle_fortress_levels_copy = easy_castle_fortress_levels.copy() + world.random.shuffle(easy_castle_fortress_levels_copy) + hard_castle_fortress_levels_copy = hard_castle_fortress_levels.copy() + world.random.shuffle(hard_castle_fortress_levels_copy) + easy_single_levels_copy = easy_single_levels.copy() + world.random.shuffle(easy_single_levels_copy) + hard_single_levels_copy = hard_single_levels.copy() + world.random.shuffle(hard_single_levels_copy) + easy_double_levels_copy = easy_double_levels.copy() + world.random.shuffle(easy_double_levels_copy) + hard_double_levels_copy = hard_double_levels.copy() + world.random.shuffle(hard_double_levels_copy) + switch_palace_levels_copy = switch_palace_levels.copy() + world.random.shuffle(switch_palace_levels_copy) + + # Yoshi's Island + shuffled_level_list.append(0x03) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(0x14) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_castle_fortress_levels_copy.pop(0)) + + # Donut Plains + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(0x08) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_double_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_single_levels_copy.pop(0)) + shuffled_level_list.append(easy_castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x28) + shuffled_level_list.append(0x16) + + single_levels_copy = (easy_single_levels_copy.copy() + hard_single_levels_copy.copy()) + world.random.shuffle(single_levels_copy) + + castle_fortress_levels_copy = (easy_castle_fortress_levels_copy.copy() + hard_castle_fortress_levels_copy.copy()) + world.random.shuffle(castle_fortress_levels_copy) + + double_levels_copy = (easy_double_levels_copy.copy() + hard_double_levels_copy.copy()) + world.random.shuffle(double_levels_copy) + + # Vanilla Dome + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(0x3F) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x2C) + + # Twin Bridges + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x12) + + # Forest of Illusion + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(0x45) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(0x1E) + + # Chocolate Island + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + + # Valley of Bowser + shuffled_level_list.append(0x18) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + shuffled_level_list.append(castle_fortress_levels_copy.pop(0)) + + # Front/Back Door + if world.bowser_castle_doors[player] == "fast": + shuffled_level_list.append(0x82) + shuffled_level_list.append(0x32) + elif world.bowser_castle_doors[player] == "slow": + shuffled_level_list.append(0x31) + shuffled_level_list.append(0x81) + else: + shuffled_level_list.append(0x31) + shuffled_level_list.append(0x32) + + shuffled_level_list.append(0x30) + + # Star Road + shuffled_level_list.append(0x5B) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x53) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x52) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x57) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x5C) + shuffled_level_list.append(double_levels_copy.pop(0)) + shuffled_level_list.append(0x55) + + # Special Zone + shuffled_level_list.append(0x4D) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(single_levels_copy.pop(0)) + shuffled_level_list.append(0x48) + + return shuffled_level_list diff --git a/worlds/smw/Locations.py b/worlds/smw/Locations.py new file mode 100644 index 0000000000..a997f92f65 --- /dev/null +++ b/worlds/smw/Locations.py @@ -0,0 +1,233 @@ +import typing + +from BaseClasses import Location +from .Names import LocationName + + +class SMWLocation(Location): + game: str = "Super Mario World" + + def __init__(self, player: int, name: str = '', address: int = None, parent=None, prog_byte: int = None, prog_bit: int = None): + super().__init__(player, name, address, parent) + self.progress_byte = prog_byte + self.progress_bit = prog_bit + + +level_location_table = { + LocationName.yoshis_island_1_exit_1: 0xBC0000, + LocationName.yoshis_island_2_exit_1: 0xBC0001, + LocationName.yoshis_island_3_exit_1: 0xBC0002, + LocationName.yoshis_island_4_exit_1: 0xBC0003, + LocationName.yoshis_island_castle: 0xBC0004, + LocationName.yoshis_island_koopaling: 0xBC00A0, + + LocationName.yellow_switch_palace: 0xBC0005, + + LocationName.donut_plains_1_exit_1: 0xBC0006, + LocationName.donut_plains_1_exit_2: 0xBC0007, + LocationName.donut_plains_2_exit_1: 0xBC0008, + LocationName.donut_plains_2_exit_2: 0xBC0009, + LocationName.donut_plains_3_exit_1: 0xBC000A, + LocationName.donut_plains_4_exit_1: 0xBC000B, + LocationName.donut_secret_1_exit_1: 0xBC000C, + LocationName.donut_secret_1_exit_2: 0xBC000D, + LocationName.donut_secret_2_exit_1: 0xBC0063, + LocationName.donut_ghost_house_exit_1: 0xBC000E, + LocationName.donut_ghost_house_exit_2: 0xBC000F, + LocationName.donut_secret_house_exit_1: 0xBC0010, + LocationName.donut_secret_house_exit_2: 0xBC0011, + LocationName.donut_plains_castle: 0xBC0012, + LocationName.donut_plains_koopaling: 0xBC00A1, + + LocationName.green_switch_palace: 0xBC0013, + + LocationName.vanilla_dome_1_exit_1: 0xBC0014, + LocationName.vanilla_dome_1_exit_2: 0xBC0015, + LocationName.vanilla_dome_2_exit_1: 0xBC0016, + LocationName.vanilla_dome_2_exit_2: 0xBC0017, + LocationName.vanilla_dome_3_exit_1: 0xBC0018, + LocationName.vanilla_dome_4_exit_1: 0xBC0019, + LocationName.vanilla_secret_1_exit_1: 0xBC001A, + LocationName.vanilla_secret_1_exit_2: 0xBC001B, + LocationName.vanilla_secret_2_exit_1: 0xBC001C, + LocationName.vanilla_secret_3_exit_1: 0xBC001D, + LocationName.vanilla_ghost_house_exit_1: 0xBC001E, + LocationName.vanilla_fortress: 0xBC0020, + LocationName.vanilla_reznor: 0xBC00B0, + LocationName.vanilla_dome_castle: 0xBC0021, + LocationName.vanilla_dome_koopaling: 0xBC00A2, + + LocationName.red_switch_palace: 0xBC0022, + + LocationName.butter_bridge_1_exit_1: 0xBC0023, + LocationName.butter_bridge_2_exit_1: 0xBC0024, + LocationName.cheese_bridge_exit_1: 0xBC0025, + LocationName.cheese_bridge_exit_2: 0xBC0026, + LocationName.cookie_mountain_exit_1: 0xBC0027, + LocationName.soda_lake_exit_1: 0xBC0028, + LocationName.twin_bridges_castle: 0xBC0029, + LocationName.twin_bridges_koopaling: 0xBC00A3, + + LocationName.forest_of_illusion_1_exit_1: 0xBC002A, + LocationName.forest_of_illusion_1_exit_2: 0xBC002B, + LocationName.forest_of_illusion_2_exit_1: 0xBC002C, + LocationName.forest_of_illusion_2_exit_2: 0xBC002D, + LocationName.forest_of_illusion_3_exit_1: 0xBC002E, + LocationName.forest_of_illusion_3_exit_2: 0xBC002F, + LocationName.forest_of_illusion_4_exit_1: 0xBC0030, + LocationName.forest_of_illusion_4_exit_2: 0xBC0031, + LocationName.forest_ghost_house_exit_1: 0xBC0032, + LocationName.forest_ghost_house_exit_2: 0xBC0033, + LocationName.forest_secret_exit_1: 0xBC0034, + LocationName.forest_fortress: 0xBC0035, + LocationName.forest_reznor: 0xBC00B1, + LocationName.forest_castle: 0xBC0036, + LocationName.forest_koopaling: 0xBC00A4, + + LocationName.blue_switch_palace: 0xBC0037, + + LocationName.chocolate_island_1_exit_1: 0xBC0038, + LocationName.chocolate_island_2_exit_1: 0xBC0039, + LocationName.chocolate_island_2_exit_2: 0xBC003A, + LocationName.chocolate_island_3_exit_1: 0xBC003B, + LocationName.chocolate_island_3_exit_2: 0xBC003C, + LocationName.chocolate_island_4_exit_1: 0xBC003D, + LocationName.chocolate_island_5_exit_1: 0xBC003E, + LocationName.chocolate_ghost_house_exit_1: 0xBC003F, + LocationName.chocolate_secret_exit_1: 0xBC0041, + LocationName.chocolate_fortress: 0xBC0042, + LocationName.chocolate_reznor: 0xBC00B2, + LocationName.chocolate_castle: 0xBC0043, + LocationName.chocolate_koopaling: 0xBC00A5, + + LocationName.sunken_ghost_ship: 0xBC0044, + + LocationName.valley_of_bowser_1_exit_1: 0xBC0045, + LocationName.valley_of_bowser_2_exit_1: 0xBC0046, + LocationName.valley_of_bowser_2_exit_2: 0xBC0047, + LocationName.valley_of_bowser_3_exit_1: 0xBC0048, + LocationName.valley_of_bowser_4_exit_1: 0xBC0049, + LocationName.valley_of_bowser_4_exit_2: 0xBC004A, + LocationName.valley_ghost_house_exit_1: 0xBC004B, + LocationName.valley_ghost_house_exit_2: 0xBC004C, + LocationName.valley_fortress: 0xBC004E, + LocationName.valley_reznor: 0xBC00B3, + LocationName.valley_castle: 0xBC004F, + LocationName.valley_koopaling: 0xBC00A6, + + LocationName.star_road_1_exit_1: 0xBC0051, + LocationName.star_road_1_exit_2: 0xBC0052, + LocationName.star_road_2_exit_1: 0xBC0053, + LocationName.star_road_2_exit_2: 0xBC0054, + LocationName.star_road_3_exit_1: 0xBC0055, + LocationName.star_road_3_exit_2: 0xBC0056, + LocationName.star_road_4_exit_1: 0xBC0057, + LocationName.star_road_4_exit_2: 0xBC0058, + LocationName.star_road_5_exit_1: 0xBC0059, + LocationName.star_road_5_exit_2: 0xBC005A, + + LocationName.special_zone_1_exit_1: 0xBC005B, + LocationName.special_zone_2_exit_1: 0xBC005C, + LocationName.special_zone_3_exit_1: 0xBC005D, + LocationName.special_zone_4_exit_1: 0xBC005E, + LocationName.special_zone_5_exit_1: 0xBC005F, + LocationName.special_zone_6_exit_1: 0xBC0060, + LocationName.special_zone_7_exit_1: 0xBC0061, + LocationName.special_zone_8_exit_1: 0xBC0062, +} + +dragon_coin_location_table = { + LocationName.yoshis_island_1_dragon: 0xBC0100, + LocationName.yoshis_island_2_dragon: 0xBC0101, + LocationName.yoshis_island_3_dragon: 0xBC0102, + LocationName.yoshis_island_4_dragon: 0xBC0103, + + LocationName.donut_plains_1_dragon: 0xBC0106, + LocationName.donut_plains_2_dragon: 0xBC0108, + LocationName.donut_plains_3_dragon: 0xBC010A, + LocationName.donut_plains_4_dragon: 0xBC010B, + LocationName.donut_secret_1_dragon: 0xBC010C, + LocationName.donut_secret_2_dragon: 0xBC010D, + + LocationName.vanilla_dome_1_dragon: 0xBC0114, + LocationName.vanilla_dome_2_dragon: 0xBC0116, + LocationName.vanilla_dome_3_dragon: 0xBC0118, + LocationName.vanilla_dome_4_dragon: 0xBC0119, + LocationName.vanilla_secret_1_dragon: 0xBC011A, + LocationName.vanilla_secret_2_dragon: 0xBC011C, + LocationName.vanilla_secret_3_dragon: 0xBC011D, + LocationName.vanilla_ghost_house_dragon: 0xBC011E, + + LocationName.butter_bridge_1_dragon: 0xBC0123, + LocationName.butter_bridge_2_dragon: 0xBC0124, + LocationName.cheese_bridge_dragon: 0xBC0125, + LocationName.cookie_mountain_dragon: 0xBC0127, + LocationName.soda_lake_dragon: 0xBC0128, + + LocationName.forest_of_illusion_2_dragon: 0xBC012C, + LocationName.forest_of_illusion_3_dragon: 0xBC012E, + LocationName.forest_of_illusion_4_dragon: 0xBC0130, + LocationName.forest_ghost_house_dragon: 0xBC0132, + LocationName.forest_secret_dragon: 0xBC0134, + LocationName.forest_castle_dragon: 0xBC0136, + + LocationName.chocolate_island_1_dragon: 0xBC0138, + LocationName.chocolate_island_2_dragon: 0xBC0139, + LocationName.chocolate_island_3_dragon: 0xBC013B, + LocationName.chocolate_island_4_dragon: 0xBC013D, + LocationName.chocolate_island_5_dragon: 0xBC013E, + + LocationName.sunken_ghost_ship_dragon: 0xBC0144, + + LocationName.valley_of_bowser_1_dragon: 0xBC0145, + LocationName.valley_of_bowser_2_dragon: 0xBC0146, + LocationName.valley_of_bowser_3_dragon: 0xBC0148, + LocationName.valley_ghost_house_dragon: 0xBC014B, + LocationName.valley_castle_dragon: 0xBC014F, + + LocationName.star_road_1_dragon: 0xBC0151, + + LocationName.special_zone_1_dragon: 0xBC015B, + LocationName.special_zone_2_dragon: 0xBC015C, + LocationName.special_zone_3_dragon: 0xBC015D, + LocationName.special_zone_4_dragon: 0xBC015E, + LocationName.special_zone_5_dragon: 0xBC015F, + LocationName.special_zone_6_dragon: 0xBC0160, + LocationName.special_zone_7_dragon: 0xBC0161, + LocationName.special_zone_8_dragon: 0xBC0162, +} + +bowser_location_table = { + LocationName.bowser: 0xBC0200, +} + +yoshi_house_location_table = { + LocationName.yoshis_house: 0xBC0201, +} + +all_locations = { + **level_location_table, + **dragon_coin_location_table, + **bowser_location_table, + **yoshi_house_location_table, +} + +location_table = {} + + +def setup_locations(world, player: int): + location_table = {**level_location_table} + + # Dragon Coins here + if world.dragon_coin_checks[player].value: + location_table.update({**dragon_coin_location_table}) + + if world.goal[player] == "yoshi_egg_hunt": + location_table.update({**yoshi_house_location_table}) + else: + location_table.update({**bowser_location_table}) + + return location_table + + +lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()} diff --git a/worlds/smw/Names/ItemName.py b/worlds/smw/Names/ItemName.py new file mode 100644 index 0000000000..72c984b016 --- /dev/null +++ b/worlds/smw/Names/ItemName.py @@ -0,0 +1,32 @@ +# Junk Definitions +one_up_mushroom = "1-Up Mushroom" + +# Collectable Definitions +yoshi_egg = "Yoshi Egg" + +# Upgrade Definitions +mario_run = "Run" +mario_carry = "Carry" +mario_swim = "Swim" +mario_spin_jump = "Spin Jump" +mario_climb = "Climb" +yoshi_activate = "Yoshi" +p_switch = "P-Switch" +p_balloon = "P-Balloon" +progressive_powerup = "Progressive Powerup" +super_star_active = "Super Star Activate" + +# Switch Palace Definitions +yellow_switch_palace = "Yellow Switch Palace" +green_switch_palace = "Green Switch Palace" +red_switch_palace = "Red Switch Palace" +blue_switch_palace = "Blue Switch Palace" + +# Trap Definitions +ice_trap = "Ice Trap" +stun_trap = "Stun Trap" +literature_trap = "Literature Trap" + +# Other Definitions +victory = "The Princess" +koopaling = "Boss Token" diff --git a/worlds/smw/Names/LiteratureTrap.py b/worlds/smw/Names/LiteratureTrap.py new file mode 100644 index 0000000000..94c038228d --- /dev/null +++ b/worlds/smw/Names/LiteratureTrap.py @@ -0,0 +1,52 @@ +lit_trap_text_list = [ +[[0x8, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x1b, 0x9f, 0x5, 0x44, 0x40, 0x51, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x48, 0x4d, 0x43, 0x1c, 0x4a, 0x48, 0x4b, 0x4b, 0x44, 0x51, 0x1b, 0x1f, 0x5, 0x44, 0x40, 0x51, 0x9f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1c, 0x43, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x41, 0x51, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x40, 0x41, 0x4e, 0x54, 0x53, 0x1f, 0x53, 0x4e, 0x53, 0x40, 0xcb, 0x4e, 0x41, 0x4b, 0x48, 0x53, 0x44, 0x51, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x1f, 0x8, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x45, 0x40, 0x42, 0x44, 0x1f, 0x4c, 0x58, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x9b, ], [0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4f, 0x44, 0x51, 0x4c, 0x48, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x53, 0x4e, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x4c, 0x44, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x1f, 0x4c, 0x44, 0x1b, 0x9f, 0x0, 0x4d, 0x43, 0x1f, 0x56, 0x47, 0x44, 0x4d, 0x1f, 0x48, 0x53, 0x1f, 0x47, 0x40, 0x52, 0x9f, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x4f, 0x40, 0x52, 0x53, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x53, 0x54, 0x51, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x48, 0x4d, 0x4d, 0x44, 0x51, 0x1f, 0x44, 0x58, 0xc4, 0x53, 0x4e, 0x1f, 0x52, 0x44, 0x44, 0x1f, 0x48, 0x53, 0x52, 0x1f, 0x4f, 0x40, 0x53, 0x47, 0x1b, 0x9f, 0x16, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x1f, 0x47, 0x40, 0xd2, ], [0x46, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x41, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0xe, 0x4d, 0x4b, 0x58, 0x1f, 0x88, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x1b, 0x9f, 0x1c, 0x7, 0x44, 0x51, 0x41, 0x44, 0x51, 0x53, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x0, 0x41, 0x4e, 0x54, 0x53, 0x1f, 0x53, 0x47, 0x51, 0x44, 0x44, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0xd2, 0x8, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x41, 0x52, 0x4e, 0x4b, 0x54, 0x53, 0x44, 0x4b, 0x58, 0x9f, 0x4f, 0x4e, 0x52, 0x48, 0x53, 0x48, 0x55, 0x44, 0x1b, 0x1f, 0x5, 0x48, 0x51, 0x52, 0x53, 0x1d, 0x9f, 0x4, 0x43, 0x56, 0x40, 0x51, 0x43, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x9f, 0x55, 0x40, 0x4c, 0x4f, 0x48, 0x51, 0x44, 0x1b, 0x1f, 0x12, 0x44, 0x42, 0x4e, 0x4d, 0x43, 0x1d, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x9f, 0x4e, 0x45, 0x1f, 0x47, 0x48, 0x4c, 0x1f, 0x1c, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x1f, 0x47, 0x4e, 0x56, 0x9f, ], [0x4f, 0x4e, 0x53, 0x44, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x9f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x41, 0x44, 0x1f, 0x1c, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x53, 0x47, 0x48, 0x51, 0x52, 0x53, 0x44, 0x43, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x4c, 0x58, 0x9f, 0x41, 0x4b, 0x4e, 0x4e, 0x43, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x48, 0x51, 0x43, 0x1d, 0x9f, 0x8, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x54, 0x4d, 0x42, 0x4e, 0x4d, 0x43, 0x48, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x4b, 0x4b, 0x58, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x48, 0x51, 0x51, 0x44, 0x55, 0x4e, 0x42, 0x40, 0x41, 0x4b, 0x58, 0x1f, 0x48, 0xcd, 0x4b, 0x4e, 0x55, 0x44, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x47, 0x48, 0x4c, 0x1b, 0x9f, ], [0x1c, 0xc, 0x44, 0x58, 0x44, 0x51, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x12, 0x40, 0x58, 0x1a, 0x1f, 0x8, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x46, 0x51, 0x44, 0x44, 0x4d, 0x9f, 0x44, 0x46, 0x46, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x4c, 0x1a, 0x1f, 0x8, 0x9f, 0x43, 0x4e, 0x1a, 0x1f, 0x8, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1d, 0x9f, 0x12, 0x40, 0x4c, 0x1c, 0x8, 0x1c, 0x40, 0x4c, 0x1a, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x4d, 0x9f, 0x40, 0x1f, 0x41, 0x4e, 0x40, 0x53, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x40, 0x53, 0x1b, 0x1b, 0x1b, 0x1f, 0x0, 0x4d, 0xc3, ], [0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0xcd, 0x53, 0x47, 0x44, 0x1f, 0x51, 0x40, 0x48, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x51, 0x4a, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x4e, 0x4d, 0x1f, 0xc0, 0x53, 0x51, 0x40, 0x48, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x42, 0x40, 0x51, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x53, 0x51, 0x44, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x52, 0x4e, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1d, 0x1f, 0x52, 0x4e, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1d, 0x1f, 0x58, 0x4e, 0xd4, 0x52, 0x44, 0x44, 0x1a, 0x1f, 0x12, 0x4e, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0xd3, ], [0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x41, 0x4e, 0x57, 0x1b, 0x1f, 0x0, 0x4d, 0xc3, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x1f, 0x45, 0x4e, 0x57, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x4d, 0x1f, 0xc0, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x9f, 0x4c, 0x4e, 0x54, 0x52, 0x44, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x9f, ], [0x53, 0x47, 0x44, 0x51, 0x44, 0x1b, 0x1f, 0x12, 0x40, 0x58, 0x1a, 0x1f, 0x8, 0x1f, 0x56, 0x48, 0x4b, 0xcb, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x4d, 0x58, 0x56, 0x47, 0x44, 0x51, 0x44, 0x9a, 0x8, 0x1f, 0x43, 0x4e, 0x1f, 0x52, 0x4e, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x46, 0x51, 0x44, 0x44, 0xcd, 0x44, 0x46, 0x46, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x4c, 0x1a, 0x9f, 0x13, 0x47, 0x40, 0x4d, 0x4a, 0x1f, 0x58, 0x4e, 0x54, 0x1a, 0x1f, 0x13, 0x47, 0x40, 0x4d, 0x4a, 0x9f, 0x58, 0x4e, 0x54, 0x1d, 0x1f, 0x12, 0x40, 0x4c, 0x1c, 0x8, 0x1c, 0x40, 0x4c, 0x1a, 0x9f, 0x1c, 0x12, 0x44, 0x54, 0x52, 0x52, 0x1f, 0x9f, 0x9f, ]], +[[0x1, 0x54, 0x53, 0x1d, 0x1f, 0x52, 0x4e, 0x45, 0x53, 0x1a, 0x1f, 0x56, 0x47, 0x40, 0x53, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x58, 0x4e, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x56, 0x48, 0x4d, 0x43, 0x4e, 0x56, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x52, 0x1e, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x44, 0x40, 0x52, 0x53, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x9, 0x54, 0x4b, 0x48, 0x44, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x54, 0x4d, 0x1b, 0x1f, 0x0, 0x51, 0x48, 0x52, 0x44, 0x9d, 0x45, 0x40, 0x48, 0x51, 0x1f, 0x52, 0x54, 0x4d, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4a, 0x48, 0x4b, 0xcb, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4d, 0x55, 0x48, 0x4e, 0x54, 0x52, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1d, 0x9f, ], [0x16, 0x47, 0x4e, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x4b, 0x51, 0x44, 0x40, 0x43, 0x58, 0x9f, 0x52, 0x48, 0x42, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x40, 0x4b, 0x44, 0x1f, 0x56, 0x48, 0x53, 0xc7, 0x46, 0x51, 0x48, 0x44, 0x45, 0x1d, 0x1f, 0x13, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x4e, 0x54, 0x9f, 0x47, 0x44, 0x51, 0x1f, 0x4c, 0x40, 0x48, 0x43, 0x1f, 0x40, 0x51, 0x53, 0x1f, 0x45, 0x40, 0x51, 0x9f, 0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x45, 0x40, 0x48, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x9f, 0x52, 0x47, 0x44, 0x1b, 0x1f, 0x1, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x47, 0x44, 0x51, 0x9f, 0x4c, 0x40, 0x48, 0x43, 0x1d, 0x1f, 0x52, 0x48, 0x4d, 0x42, 0x44, 0x1f, 0x52, 0x47, 0x44, 0x1f, 0x48, 0xd2, 0x44, 0x4d, 0x55, 0x48, 0x4e, 0x54, 0x52, 0x1b, 0x1f, 0x7, 0x44, 0x51, 0x9f, ], [0x55, 0x44, 0x52, 0x53, 0x40, 0x4b, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x48, 0x52, 0x9f, 0x41, 0x54, 0x53, 0x1f, 0x52, 0x48, 0x42, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x46, 0x51, 0x44, 0x44, 0xcd, 0x0, 0x4d, 0x43, 0x1f, 0x4d, 0x4e, 0x4d, 0x44, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x45, 0x4e, 0x4e, 0x4b, 0xd2, 0x43, 0x4e, 0x1f, 0x56, 0x44, 0x40, 0x51, 0x1f, 0x48, 0x53, 0x1b, 0x1f, 0x42, 0x40, 0x52, 0x53, 0x9f, 0x48, 0x53, 0x1f, 0x4e, 0x45, 0x45, 0x1b, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4c, 0x58, 0x9f, 0x4b, 0x40, 0x43, 0x58, 0x1d, 0x1f, 0xe, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4c, 0x58, 0x9f, 0x4b, 0x4e, 0x55, 0x44, 0x1a, 0x1f, 0xe, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x52, 0x47, 0x44, 0x9f, 0x4a, 0x4d, 0x44, 0x56, 0x1f, 0x52, 0x47, 0x44, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1a, 0x9f, ], [0x1c, 0x12, 0x47, 0x40, 0x4a, 0x44, 0x52, 0x4f, 0x44, 0x40, 0x51, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x41, 0x44, 0x52, 0x53, 0x1f, 0x4e, 0xc5, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x52, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1d, 0x1f, 0x48, 0xd3, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x40, 0x46, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x56, 0x48, 0x52, 0x43, 0x4e, 0x4c, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0xc4, 0x40, 0x46, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x45, 0x4e, 0x4e, 0x4b, 0x48, 0x52, 0x47, 0x4d, 0x44, 0x52, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4f, 0x4e, 0x42, 0x47, 0x1f, 0x4e, 0x45, 0x9f, ], [0x41, 0x44, 0x4b, 0x48, 0x44, 0x45, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0xc4, 0x44, 0x4f, 0x4e, 0x42, 0x47, 0x1f, 0x4e, 0x45, 0x9f, 0x48, 0x4d, 0x42, 0x51, 0x44, 0x43, 0x54, 0x4b, 0x48, 0x53, 0x58, 0x1d, 0x1f, 0x48, 0x53, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x44, 0x40, 0x52, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x44, 0x40, 0x52, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x9f, 0x43, 0x40, 0x51, 0x4a, 0x4d, 0x44, 0x52, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4f, 0x51, 0x48, 0x4d, 0x46, 0x1f, 0x4e, 0x45, 0x9f, ], [0x47, 0x4e, 0x4f, 0x44, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x48, 0x4d, 0x53, 0x44, 0x51, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x44, 0x52, 0x4f, 0x40, 0x48, 0x51, 0x9b, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x44, 0x4d, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xc, 0x40, 0x51, 0x4b, 0x44, 0x58, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x43, 0x44, 0x40, 0x43, 0x1d, 0x9f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x1f, 0x43, 0x4e, 0x54, 0x41, 0x53, 0x9f, 0x56, 0x47, 0x40, 0x53, 0x44, 0x55, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x41, 0x4e, 0x54, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x1f, 0x51, 0x44, 0x46, 0x48, 0x52, 0x53, 0x44, 0xd1, 0x4e, 0x45, 0x1f, 0x47, 0x48, 0x52, 0x1f, 0x41, 0x54, 0x51, 0x48, 0x40, 0x4b, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x52, 0x48, 0x46, 0x4d, 0x44, 0x43, 0x1f, 0x41, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x42, 0x4b, 0x44, 0x51, 0x46, 0x58, 0x4c, 0x40, 0x4d, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x42, 0x4b, 0x44, 0x51, 0x4a, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x53, 0x40, 0x4a, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x42, 0x47, 0x48, 0x44, 0x45, 0x1f, 0x4c, 0x4e, 0x54, 0x51, 0x4d, 0x44, 0x51, 0x9b, 0x12, 0x42, 0x51, 0x4e, 0x4e, 0x46, 0x44, 0x1f, 0x52, 0x48, 0x46, 0x4d, 0x44, 0x43, 0x1f, 0x48, 0x53, 0x9b, 0x40, 0x4d, 0x43, 0x1f, 0x12, 0x42, 0x51, 0x4e, 0x4e, 0x46, 0x44, 0x5d, 0x52, 0x1f, 0x4d, 0x40, 0x4c, 0xc4, 0x56, 0x40, 0x52, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x54, 0x4f, 0x4e, 0x4d, 0x9f, 0x5d, 0x42, 0x47, 0x40, 0x4d, 0x46, 0x44, 0x1d, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x40, 0x4d, 0x58, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x47, 0x44, 0x1f, 0x42, 0x47, 0x4e, 0x52, 0x44, 0x9f, ], [0x53, 0x4e, 0x1f, 0x4f, 0x54, 0x53, 0x1f, 0x47, 0x48, 0x52, 0x1f, 0x47, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x4e, 0x1b, 0x1f, 0xe, 0x4b, 0x43, 0x1f, 0xc, 0x40, 0x51, 0x4b, 0x44, 0x58, 0x1f, 0x56, 0x40, 0xd2, 0x40, 0x52, 0x1f, 0x43, 0x44, 0x40, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x40, 0x9f, 0x43, 0x4e, 0x4e, 0x51, 0x1c, 0x4d, 0x40, 0x48, 0x4b, 0x1b, 0x9f, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x44, 0x4d, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xc, 0x40, 0x4d, 0x58, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x9f, 0x43, 0x44, 0x52, 0x44, 0x51, 0x55, 0x44, 0x1f, 0x43, 0x44, 0x40, 0x53, 0x47, 0x1b, 0x1f, 0x0, 0x4d, 0xc3, 0x52, 0x4e, 0x4c, 0x44, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x43, 0x48, 0x44, 0x9f, 0x43, 0x44, 0x52, 0x44, 0x51, 0x55, 0x44, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1b, 0x1f, 0x2, 0x40, 0x4d, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x1f, 0x48, 0x53, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1e, 0x1f, 0x13, 0x47, 0x44, 0x4d, 0x1f, 0x43, 0x4e, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x41, 0x44, 0x1f, 0x53, 0x4e, 0x4e, 0x1f, 0x44, 0x40, 0x46, 0x44, 0x51, 0x1f, 0x53, 0x4e, 0x9f, 0x43, 0x44, 0x40, 0x4b, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x43, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x48, 0x4d, 0x9f, ], [0x49, 0x54, 0x43, 0x46, 0x44, 0x4c, 0x44, 0x4d, 0x53, 0x1b, 0x1f, 0x5, 0x4e, 0x51, 0x9f, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x56, 0x48, 0x52, 0xc4, 0x42, 0x40, 0x4d, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x44, 0x44, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x44, 0x4d, 0x43, 0x52, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x1f, 0x53, 0x51, 0x54, 0x53, 0x47, 0x9f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x40, 0x4b, 0x4b, 0x58, 0x9f, 0x40, 0x42, 0x4a, 0x4d, 0x4e, 0x56, 0x4b, 0x44, 0x43, 0x46, 0x44, 0x43, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0xd3, 0x40, 0x1f, 0x52, 0x48, 0x4d, 0x46, 0x4b, 0x44, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x48, 0x4d, 0x9f, 0x4f, 0x4e, 0x52, 0x52, 0x44, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x45, 0x4e, 0x51, 0x53, 0x54, 0x4d, 0x44, 0x1d, 0x1f, 0x4c, 0x54, 0x52, 0xd3, 0x41, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x56, 0x48, 0x45, 0x44, 0x1b, 0x1f, 0x1c, 0x0, 0x54, 0x52, 0x53, 0x44, 0x4d, 0x1f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x54, 0x53, 0x47, 0x1f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x9f, 0x42, 0x40, 0x51, 0x51, 0x48, 0x44, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x40, 0x4c, 0x41, 0x48, 0x46, 0x54, 0x48, 0x53, 0x58, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x43, 0x52, 0x1f, 0x54, 0x52, 0x44, 0x43, 0x1f, 0x53, 0x4e, 0x9f, 0x44, 0x57, 0x4f, 0x51, 0x44, 0x52, 0x52, 0x1f, 0x48, 0x53, 0x1b, 0x9f, 0x1c, 0x7, 0x44, 0x51, 0x41, 0x44, 0x51, 0x53, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0x8, 0x1f, 0x43, 0x40, 0x51, 0x44, 0x52, 0x40, 0x58, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x4d, 0x5d, 0x53, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x4c, 0x54, 0x42, 0x47, 0x9f, 0x4f, 0x51, 0x40, 0x42, 0x53, 0x48, 0x42, 0x44, 0x1d, 0x5d, 0x1f, 0x52, 0x40, 0x48, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x10, 0x54, 0x44, 0x44, 0x4d, 0x1b, 0x1f, 0x5d, 0x16, 0x47, 0x44, 0x4d, 0x1f, 0x88, 0x56, 0x40, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x40, 0x46, 0x44, 0x1d, 0x1f, 0x8, 0x9f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x1f, 0x43, 0x48, 0x43, 0x1f, 0x48, 0x53, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x47, 0x40, 0x4b, 0x45, 0x1c, 0x40, 0x4d, 0x1c, 0x47, 0x4e, 0x54, 0x51, 0x1f, 0x40, 0x9f, 0x43, 0x40, 0x58, 0x1b, 0x1f, 0x16, 0x47, 0x58, 0x1d, 0x9f, ], [0x52, 0x4e, 0x4c, 0x44, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1f, 0x8, 0x5d, 0x55, 0x44, 0x9f, 0x41, 0x44, 0x4b, 0x48, 0x44, 0x55, 0x44, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x4c, 0x40, 0x4d, 0x58, 0x9f, 0x40, 0x52, 0x1f, 0x52, 0x48, 0x57, 0x1f, 0x48, 0x4c, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x4b, 0x44, 0x9f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x41, 0x44, 0x45, 0x4e, 0x51, 0x44, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x45, 0x40, 0x52, 0x53, 0x1b, 0x5d, 0x9f, 0x1c, 0x2, 0x40, 0x51, 0x51, 0x4e, 0x4b, 0x4b, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x51, 0x54, 0x4b, 0x44, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x4e, 0x41, 0x4b, 0x48, 0x46, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x48, 0x53, 0x52, 0x1f, 0x45, 0x51, 0x44, 0x44, 0x43, 0x4e, 0x4c, 0x52, 0x1d, 0x1f, 0x48, 0x52, 0x9f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x40, 0x1f, 0x52, 0x4e, 0x4d, 0x4d, 0x44, 0x53, 0x1b, 0x9f, 0x18, 0x4e, 0x54, 0x5d, 0x51, 0x44, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x45, 0x4e, 0x51, 0x4c, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x47, 0x40, 0x55, 0xc4, 0x53, 0x4e, 0x1f, 0x56, 0x51, 0x48, 0x53, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x52, 0x4e, 0x4d, 0x4d, 0x44, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x45, 0x1b, 0x9f, 0x1c, 0xb, 0x5d, 0x4, 0x4d, 0x46, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x48, 0x4a, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1f, 0x4e, 0x55, 0x44, 0xd1, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x58, 0x1f, 0x4c, 0x58, 0x1f, 0x46, 0x44, 0x4d, 0x48, 0x54, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x41, 0x51, 0x40, 0x56, 0x4d, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x4b, 0x4e, 0x52, 0xd3, 0x4e, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x52, 0x44, 0x1f, 0x45, 0x4e, 0x4e, 0x4b, 0x52, 0x9f, 0x1c, 0x7, 0x40, 0x48, 0x4a, 0x54, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x4d, 0x1f, 0x40, 0x1f, 0x47, 0x4e, 0x4b, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x46, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0xc3, 0x40, 0x1f, 0x47, 0x4e, 0x41, 0x41, 0x48, 0x53, 0x1b, 0x1f, 0xd, 0x4e, 0x53, 0x1f, 0x40, 0x9f, 0x4d, 0x40, 0x52, 0x53, 0x58, 0x1d, 0x1f, 0x43, 0x48, 0x51, 0x53, 0x58, 0x1d, 0x1f, 0x56, 0x44, 0x53, 0x9f, 0x47, 0x4e, 0x4b, 0x44, 0x1d, 0x1f, 0x45, 0x48, 0x4b, 0x4b, 0x44, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4d, 0x43, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x56, 0x4e, 0x51, 0x4c, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x4d, 0x1f, 0x4e, 0x4e, 0x59, 0x58, 0x1f, 0x52, 0x4c, 0x44, 0x4b, 0x4b, 0x9d, 0x4d, 0x4e, 0x51, 0x1f, 0x58, 0x44, 0x53, 0x1f, 0x40, 0x1f, 0x43, 0x51, 0x58, 0x1d, 0x9f, ], [0x41, 0x40, 0x51, 0x44, 0x1d, 0x1f, 0x52, 0x40, 0x4d, 0x43, 0x58, 0x1f, 0x47, 0x4e, 0x4b, 0x44, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x48, 0x4d, 0x1f, 0x48, 0xd3, 0x53, 0x4e, 0x1f, 0x52, 0x48, 0x53, 0x1f, 0x43, 0x4e, 0x56, 0x4d, 0x1f, 0x4e, 0x4d, 0x1f, 0x4e, 0x51, 0x9f, 0x53, 0x4e, 0x1f, 0x44, 0x40, 0x53, 0x1b, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x9f, 0x47, 0x4e, 0x41, 0x41, 0x48, 0x53, 0x1c, 0x47, 0x4e, 0x4b, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4c, 0x44, 0x40, 0x4d, 0x52, 0x9f, 0x42, 0x4e, 0x4c, 0x45, 0x4e, 0x51, 0x53, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, ]], +[[0x5d, 0x6, 0x4e, 0x4e, 0x43, 0x1f, 0xc, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1a, 0x5d, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x1, 0x48, 0x4b, 0x41, 0x4e, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0xc4, 0x4c, 0x44, 0x40, 0x4d, 0x53, 0x1f, 0x48, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x1f, 0x52, 0x54, 0x4d, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x52, 0x47, 0x48, 0x4d, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x51, 0x40, 0x52, 0x52, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x55, 0x44, 0x51, 0xd8, 0x46, 0x51, 0x44, 0x44, 0x4d, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x6, 0x40, 0x4d, 0x43, 0x40, 0x4b, 0xc5, 0x4b, 0x4e, 0x4e, 0x4a, 0x44, 0x43, 0x1f, 0x40, 0x53, 0x1f, 0x47, 0x48, 0x4c, 0x1f, 0x45, 0x51, 0x4e, 0xcc, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x1f, 0x41, 0x54, 0x52, 0x47, 0x58, 0x9f, ], [0x44, 0x58, 0x44, 0x41, 0x51, 0x4e, 0x56, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x52, 0x53, 0x54, 0x42, 0x4a, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x45, 0x54, 0x51, 0x53, 0x47, 0x44, 0x51, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x41, 0x51, 0x48, 0x4c, 0x1f, 0x4e, 0x45, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x47, 0x40, 0x43, 0x58, 0x1f, 0x47, 0x40, 0x53, 0x1b, 0x9f, 0x5d, 0x16, 0x47, 0x40, 0x53, 0x1f, 0x43, 0x4e, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4c, 0x44, 0x40, 0x4d, 0x1e, 0x5d, 0x1f, 0x47, 0x44, 0x1f, 0x52, 0x40, 0x48, 0x43, 0x1b, 0x9f, 0x5d, 0x3, 0x4e, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x48, 0x52, 0x47, 0x1f, 0x4c, 0x44, 0x1f, 0x40, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x4e, 0x51, 0x9f, ], [0x4c, 0x44, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x9f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x9f, 0x56, 0x47, 0x44, 0x53, 0x47, 0x44, 0x51, 0x1f, 0x8, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x4e, 0x51, 0x1f, 0x4d, 0x4e, 0x53, 0x1d, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x45, 0x44, 0x44, 0x4b, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x53, 0x47, 0x48, 0xd2, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0xce, 0x41, 0x44, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4e, 0x4d, 0x1e, 0x5d, 0x1f, 0x5d, 0x0, 0x4b, 0x4b, 0x9f, ], [0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x53, 0x1f, 0x4e, 0x4d, 0x42, 0x44, 0x1d, 0x5d, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x1, 0x48, 0x4b, 0x41, 0x4e, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0x6, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1a, 0x5d, 0x1f, 0x47, 0xc4, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x40, 0x53, 0x1f, 0x4b, 0x40, 0x52, 0x53, 0x1b, 0x1f, 0x5d, 0x16, 0x44, 0x9f, 0x43, 0x4e, 0x4d, 0x5d, 0x53, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x9f, 0x40, 0x43, 0x55, 0x44, 0x4d, 0x53, 0x54, 0x51, 0x44, 0x52, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1d, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x4a, 0x1f, 0x58, 0x4e, 0x54, 0x1a, 0x1f, 0x18, 0x4e, 0x54, 0x9f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x53, 0x51, 0x58, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x13, 0x47, 0xc4, 0x7, 0x48, 0x4b, 0x4b, 0x1f, 0x4e, 0x51, 0x1f, 0x40, 0x42, 0x51, 0x4e, 0x52, 0x52, 0x1f, 0x13, 0x47, 0xc4, 0x16, 0x40, 0x53, 0x44, 0x51, 0x1b, 0x5d, 0x1f, 0x1, 0x58, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x47, 0xc4, ], [0x4c, 0x44, 0x40, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x42, 0x4e, 0x4d, 0x55, 0x44, 0x51, 0x52, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x40, 0x53, 0x1f, 0x40, 0x4d, 0x1f, 0x44, 0x4d, 0x43, 0x1b, 0x1f, 0x5d, 0x16, 0x47, 0x40, 0x53, 0x1f, 0xc0, 0x4b, 0x4e, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x43, 0x4e, 0x1f, 0x54, 0x52, 0x44, 0x1f, 0x6, 0x4e, 0x4e, 0x43, 0x9f, 0x4c, 0x4e, 0x51, 0x4d, 0x48, 0x4d, 0x46, 0x1f, 0x45, 0x4e, 0x51, 0x1a, 0x5d, 0x1f, 0x52, 0x40, 0x48, 0xc3, 0x6, 0x40, 0x4d, 0x43, 0x40, 0x4b, 0x45, 0x1b, 0x1f, 0x5d, 0xd, 0x4e, 0x56, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4c, 0x44, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x40, 0x4d, 0xd3, ], [0x53, 0x4e, 0x1f, 0x46, 0x44, 0x53, 0x1f, 0x51, 0x48, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x44, 0x1d, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x4e, 0x4d, 0x5d, 0x53, 0x9f, 0x41, 0x44, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x8, 0x9f, 0x4c, 0x4e, 0x55, 0x44, 0x1f, 0x4e, 0x45, 0x45, 0x1b, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x9f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x48, 0x45, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x45, 0x48, 0x4d, 0x43, 0x9f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x9f, 0x42, 0x44, 0x51, 0x53, 0x40, 0x48, 0x4d, 0x4b, 0x58, 0x1f, 0x54, 0x52, 0x54, 0x40, 0x4b, 0x4b, 0x58, 0x9f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x48, 0xc5, 0x58, 0x4e, 0x54, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x9f, ], [0x50, 0x54, 0x48, 0x53, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x56, 0x44, 0x51, 0xc4, 0x40, 0x45, 0x53, 0x44, 0x51, 0x1b, 0x5d, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x3, 0x51, 0x54, 0x4c, 0x4c, 0x44, 0x51, 0x1d, 0x1f, 0x41, 0x44, 0x40, 0x53, 0x1d, 0x1f, 0x40, 0x4d, 0xc3, 0x4f, 0x48, 0x4f, 0x44, 0x51, 0x1d, 0x1f, 0x41, 0x4b, 0x4e, 0x56, 0x1b, 0x9f, 0x7, 0x40, 0x51, 0x4f, 0x44, 0x51, 0x1d, 0x1f, 0x52, 0x53, 0x51, 0x48, 0x4a, 0x44, 0x1d, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x4e, 0x4b, 0x43, 0x48, 0x44, 0x51, 0x1d, 0x1f, 0x46, 0x4e, 0x1b, 0x9f, 0x5, 0x51, 0x44, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x4b, 0x40, 0x4c, 0x44, 0x1f, 0x40, 0x4d, 0xc3, 0x52, 0x44, 0x40, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x51, 0x40, 0x52, 0x52, 0x44, 0x52, 0x1d, 0x9f, 0x13, 0x48, 0x4b, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x56, 0x4d, 0x48, 0x4d, 0x46, 0x9f, 0x11, 0x44, 0x43, 0x1f, 0x12, 0x53, 0x40, 0x51, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x44, 0x52, 0x1b, 0x9f, ], [0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x53, 0x44, 0x40, 0x51, 0x52, 0x1f, 0x8, 0x1f, 0x45, 0x44, 0x44, 0x4b, 0x9f, 0x53, 0x4e, 0x43, 0x40, 0x58, 0x1f, 0x8, 0x5d, 0x4b, 0x4b, 0x1f, 0x56, 0x40, 0x48, 0x53, 0x1f, 0x53, 0xce, 0x52, 0x47, 0x44, 0x43, 0x1f, 0x53, 0x4e, 0x4c, 0x4e, 0x51, 0x51, 0x4e, 0x56, 0x1b, 0x9f, 0x13, 0x47, 0x4e, 0x54, 0x46, 0x47, 0x1f, 0x8, 0x5d, 0x4b, 0x4b, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x52, 0x4b, 0x44, 0x44, 0x4f, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x4d, 0x48, 0x46, 0x47, 0x53, 0x9f, 0xd, 0x4e, 0x51, 0x1f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x52, 0x54, 0x51, 0x42, 0x44, 0x40, 0x52, 0x44, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x52, 0x4e, 0x51, 0x51, 0x4e, 0x56, 0x1b, 0x1f, 0xc, 0x58, 0x9f, 0x44, 0x58, 0x44, 0x52, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x4a, 0x44, 0x44, 0x4f, 0x9f, ], [0x53, 0x47, 0x44, 0x48, 0x51, 0x1f, 0x52, 0x48, 0x46, 0x47, 0x53, 0x1b, 0x1f, 0x8, 0x9f, 0x43, 0x40, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x41, 0x44, 0x9f, 0x53, 0x44, 0x40, 0x51, 0x1c, 0x41, 0x4b, 0x48, 0x4d, 0x43, 0x44, 0x43, 0x1b, 0x1f, 0x8, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x41, 0x44, 0x1f, 0x45, 0x51, 0x44, 0x44, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x40, 0x4b, 0x4a, 0x1f, 0xd, 0x4e, 0x53, 0x1f, 0x42, 0x47, 0x4e, 0x4a, 0x44, 0x43, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x46, 0x51, 0x48, 0x44, 0x45, 0x1d, 0x9f, 0x42, 0x4b, 0x44, 0x40, 0x51, 0x1c, 0x4c, 0x48, 0x4d, 0x43, 0x44, 0x43, 0x1b, 0x1f, 0xc, 0x58, 0x9f, 0x4c, 0x4e, 0x54, 0x53, 0x47, 0x1f, 0x42, 0x40, 0x4d, 0x4d, 0x4e, 0x53, 0x9f, ], [0x41, 0x44, 0x53, 0x51, 0x40, 0x58, 0x1f, 0x13, 0x47, 0x44, 0x1f, 0x40, 0x4d, 0x46, 0x54, 0x48, 0x52, 0xc7, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x8, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x1b, 0x1f, 0x18, 0x44, 0x52, 0x1d, 0x9f, 0x8, 0x5d, 0x4b, 0x4b, 0x1f, 0x4a, 0x44, 0x44, 0x4f, 0x1f, 0x4c, 0x58, 0x1f, 0x53, 0x44, 0x40, 0x51, 0xd2, 0x53, 0x48, 0x4b, 0x1f, 0x4b, 0x40, 0x53, 0x44, 0x51, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x4c, 0x58, 0x9f, 0x46, 0x51, 0x48, 0x44, 0x45, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x46, 0x4e, 0x1b, 0x1f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x16, 0x47, 0x4e, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x52, 0x1d, 0x1f, 0x2, 0x40, 0x4d, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x53, 0x51, 0x48, 0x44, 0x52, 0x1d, 0x1f, 0x3, 0x4e, 0x44, 0x52, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x4b, 0x4e, 0x55, 0x44, 0x52, 0x1d, 0x1f, 0xb, 0x48, 0x55, 0x44, 0x52, 0x1b, 0x9f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1f, 0x50, 0x54, 0x44, 0x44, 0x4d, 0x9f, 0x40, 0x4b, 0x4b, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x44, 0x4d, 0x1f, 0x5, 0x4b, 0x44, 0x56, 0x9f, 0x47, 0x48, 0x52, 0x52, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x44, 0x40, 0x1b, 0x1f, 0x13, 0x4e, 0x1f, 0x52, 0x53, 0x4e, 0x4f, 0x1f, 0x44, 0x40, 0x42, 0x47, 0x9f, 0x56, 0x40, 0x55, 0x44, 0x1f, 0x7, 0x44, 0x51, 0x1f, 0x42, 0x4b, 0x54, 0x53, 0x42, 0x47, 0x1f, 0x53, 0xce, 0x52, 0x40, 0x55, 0x44, 0x1f, 0x12, 0x47, 0x44, 0x1f, 0x55, 0x44, 0x4d, 0x53, 0x54, 0x51, 0x44, 0x43, 0x9f, 0x41, 0x51, 0x40, 0x55, 0x44, 0x4b, 0x58, 0x1b, 0x1f, 0x0, 0x52, 0x1f, 0x52, 0x47, 0x44, 0x9f, 0x40, 0x53, 0x53, 0x40, 0x42, 0x4a, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x44, 0x40, 0x9f, ], [0x48, 0x4d, 0x1f, 0x51, 0x40, 0x46, 0x44, 0x1f, 0x0, 0x9f, 0x47, 0x4e, 0x4b, 0x43, 0x44, 0x51, 0x4c, 0x40, 0x4d, 0x1f, 0x42, 0x40, 0x4c, 0x44, 0x9f, 0x4d, 0x48, 0x46, 0x47, 0x1f, 0x0, 0x4b, 0x4e, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x40, 0x4d, 0x43, 0x1f, 0x5, 0x48, 0x52, 0x47, 0x4d, 0x44, 0x53, 0x1f, 0x48, 0x4d, 0x9f, 0x47, 0x40, 0x4d, 0x43, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x52, 0x40, 0x56, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x50, 0x54, 0x44, 0x44, 0x4d, 0x1f, 0x4c, 0x48, 0x43, 0x52, 0x4a, 0x58, 0x1b, 0x1f, 0x7, 0x44, 0x9f, 0x52, 0x53, 0x40, 0x51, 0x44, 0x43, 0x1f, 0x40, 0x53, 0x1f, 0x47, 0x44, 0x51, 0x1f, 0x48, 0x4d, 0x9f, 0x56, 0x4e, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x5, 0x4e, 0x51, 0x1f, 0x4e, 0x45, 0x53, 0x44, 0x4d, 0x9f, ], [0x47, 0x44, 0x5d, 0x43, 0x1f, 0x41, 0x44, 0x44, 0x4d, 0x1f, 0x53, 0x4e, 0x4b, 0x43, 0x9f, 0x13, 0x47, 0x40, 0x53, 0x1f, 0x52, 0x54, 0x42, 0x47, 0x1f, 0x40, 0x52, 0x1f, 0x52, 0x47, 0x44, 0x9f, 0x2, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x41, 0x44, 0x1f, 0x16, 0x47, 0xce, 0x47, 0x4e, 0x55, 0x44, 0x51, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1d, 0x9f, 0x41, 0x51, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x1b, 0x1f, 0x7, 0x44, 0x9f, 0x52, 0x40, 0x56, 0x1f, 0x47, 0x44, 0x51, 0x1f, 0x4f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x40, 0x4d, 0xc3, 0x50, 0x54, 0x48, 0x42, 0x4a, 0x4b, 0x58, 0x1f, 0x7, 0x44, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x44, 0x43, 0x9f, 0x54, 0x4f, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x42, 0x4b, 0x48, 0x45, 0x45, 0x1f, 0x47, 0x44, 0x9f, ], [0x45, 0x40, 0x42, 0x44, 0x43, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x52, 0x40, 0x56, 0x1f, 0x40, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x1f, 0x0, 0x41, 0x4e, 0x55, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x40, 0x55, 0x44, 0x1f, 0x8, 0x4d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x47, 0x44, 0x51, 0x9f, 0x44, 0x46, 0x46, 0x52, 0x1f, 0x47, 0x44, 0x1f, 0x4f, 0x4b, 0x40, 0x42, 0x44, 0x43, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1f, 0x50, 0x54, 0x44, 0x44, 0x4d, 0x9f, 0x40, 0x4b, 0x4b, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x44, 0x4d, 0x1f, 0x14, 0x4f, 0x4e, 0x4d, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0x43, 0x44, 0x51, 0x1f, 0x52, 0x53, 0x4e, 0x4e, 0xc3, 0x7, 0x44, 0x51, 0x1f, 0x44, 0x58, 0x44, 0x52, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x4b, 0x54, 0x44, 0x9f, ], [0x6, 0x4b, 0x4e, 0x56, 0x44, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x47, 0x44, 0x51, 0x1f, 0x53, 0x51, 0x54, 0xc4, 0x14, 0x4d, 0x43, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x46, 0x51, 0x40, 0x53, 0x48, 0x53, 0x54, 0x43, 0x44, 0x9b, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x7, 0x40, 0x51, 0x4f, 0x44, 0x51, 0x1d, 0x1f, 0x53, 0x51, 0x44, 0x40, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0xd1, 0x56, 0x4e, 0x51, 0x43, 0x52, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x42, 0x40, 0x51, 0x44, 0x9f, 0x5, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x4c, 0x40, 0x58, 0x1f, 0x42, 0x40, 0x54, 0x52, 0xc4, 0x49, 0x4e, 0x58, 0x1f, 0x4e, 0x51, 0x1f, 0x43, 0x44, 0x52, 0x4f, 0x40, 0x48, 0x51, 0x9f, 0x12, 0x48, 0x4d, 0x46, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x52, 0x4e, 0x4d, 0x46, 0x52, 0x1f, 0x4e, 0xc5, 0x47, 0x44, 0x40, 0x4b, 0x53, 0x47, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4b, 0x4e, 0x55, 0x44, 0x1f, 0xe, 0xc5, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0x52, 0x1f, 0x45, 0x4b, 0x40, 0x4c, 0x48, 0x4d, 0x46, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x40, 0x41, 0x4e, 0x55, 0x44, 0x1b, 0x9f, ], [0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x4e, 0x4d, 0x4b, 0x58, 0x1f, 0x4e, 0x4d, 0xc4, 0x42, 0x40, 0x53, 0x42, 0x47, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0xd2, 0x2, 0x40, 0x53, 0x42, 0x47, 0x1c, 0x24, 0x24, 0x1d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x9f, 0x52, 0x4f, 0x44, 0x42, 0x48, 0x45, 0x48, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x40, 0x9f, 0x42, 0x4e, 0x4d, 0x42, 0x44, 0x51, 0x4d, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x4e, 0x4d, 0x44, 0x5d, 0x52, 0x9f, 0x52, 0x40, 0x45, 0x44, 0x53, 0x58, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x40, 0x42, 0xc4, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x4d, 0x46, 0x44, 0x51, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x51, 0x44, 0x40, 0x4b, 0x1f, 0x40, 0x4d, 0x43, 0x9f, ], [0x48, 0x4c, 0x4c, 0x44, 0x43, 0x48, 0x40, 0x53, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x51, 0x4e, 0x42, 0x44, 0x52, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x51, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x4b, 0x1f, 0x4c, 0x48, 0x4d, 0x43, 0x1b, 0x1f, 0xe, 0x51, 0xd1, 0x56, 0x40, 0x52, 0x1f, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x41, 0x44, 0x1f, 0x46, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x44, 0x43, 0x9b, 0x0, 0x4b, 0x4b, 0x1f, 0x47, 0x44, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x53, 0x4e, 0x1f, 0x43, 0x4e, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x40, 0x52, 0x4a, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x52, 0x9f, 0x52, 0x4e, 0x4e, 0x4d, 0x1f, 0x40, 0x52, 0x1f, 0x47, 0x44, 0x1f, 0x43, 0x48, 0x43, 0x1d, 0x1f, 0x47, 0xc4, ], [0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x4d, 0x4e, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x44, 0x51, 0x1f, 0x41, 0xc4, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x53, 0x4e, 0x1f, 0x45, 0x4b, 0x58, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x4c, 0x48, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x52, 0x1b, 0x1f, 0xe, 0x51, 0x51, 0x9f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x41, 0x44, 0x1f, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x53, 0x4e, 0x9f, 0x45, 0x4b, 0x58, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x40, 0x4d, 0x44, 0x1f, 0x48, 0x45, 0x1f, 0x47, 0x44, 0x9f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x45, 0x1f, 0x47, 0x44, 0x9f, ], [0x56, 0x40, 0x52, 0x1f, 0x52, 0x40, 0x4d, 0x44, 0x1f, 0x47, 0x44, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x53, 0xce, 0x45, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1b, 0x1f, 0x8, 0x45, 0x1f, 0x47, 0x44, 0x9f, 0x45, 0x4b, 0x44, 0x56, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x47, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x42, 0x51, 0x40, 0x59, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x53, 0x4e, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x45, 0x1f, 0x47, 0xc4, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1f, 0x56, 0x40, 0x4d, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x47, 0x44, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x52, 0x40, 0x4d, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x43, 0x9f, 0x53, 0x4e, 0x1b, 0x1f, 0x1c, 0x7, 0x44, 0x4b, 0x4b, 0x44, 0x51, 0x1f, 0x9f, ]], +[[0x5d, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x51, 0x44, 0x1f, 0x53, 0x51, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0xce, 0x4a, 0x48, 0x4b, 0x4b, 0x1f, 0x4c, 0x44, 0x1d, 0x5d, 0x9f, 0x18, 0x4e, 0x52, 0x52, 0x40, 0x51, 0x48, 0x40, 0x4d, 0x1f, 0x53, 0x4e, 0x4b, 0x43, 0x1f, 0x47, 0x48, 0xcc, 0x42, 0x40, 0x4b, 0x4c, 0x4b, 0x58, 0x1b, 0x1f, 0x5d, 0xd, 0x4e, 0x1f, 0x4e, 0x4d, 0x44, 0x5d, 0x52, 0x9f, 0x53, 0x51, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x4a, 0x48, 0x4b, 0x4b, 0x9f, 0x58, 0x4e, 0x54, 0x1d, 0x5d, 0x1f, 0x2, 0x4b, 0x44, 0x55, 0x48, 0x4d, 0x46, 0x44, 0x51, 0x9f, 0x42, 0x51, 0x48, 0x44, 0x43, 0x1b, 0x1f, 0x5d, 0x13, 0x47, 0x44, 0x4d, 0x1f, 0x56, 0x47, 0x58, 0x9f, 0x40, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x52, 0x47, 0x4e, 0x4e, 0x53, 0x48, 0x4d, 0x46, 0x9f, ], [0x40, 0x53, 0x1f, 0x4c, 0x44, 0x1e, 0x5d, 0x1f, 0x18, 0x4e, 0x52, 0x52, 0x40, 0x51, 0x48, 0x40, 0x4d, 0x9f, 0x40, 0x52, 0x4a, 0x44, 0x43, 0x1b, 0x1f, 0x5d, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x51, 0x44, 0x9f, 0x52, 0x47, 0x4e, 0x4e, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0x53, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x4e, 0x4d, 0x44, 0x1d, 0x5d, 0x9f, 0x2, 0x4b, 0x44, 0x55, 0x48, 0x4d, 0x46, 0x44, 0x51, 0x9f, 0x40, 0x4d, 0x52, 0x56, 0x44, 0x51, 0x44, 0x43, 0x1b, 0x1f, 0x5d, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x51, 0xc4, 0x53, 0x51, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x4a, 0x48, 0x4b, 0x4b, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x4e, 0x4d, 0x44, 0x1b, 0x5d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, ], [0x56, 0x47, 0x40, 0x53, 0x1f, 0x43, 0x48, 0x45, 0x45, 0x44, 0x51, 0x44, 0x4d, 0x42, 0x44, 0x9f, 0x43, 0x4e, 0x44, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4c, 0x40, 0x4a, 0x44, 0x1e, 0x9f, 0x1c, 0x7, 0x44, 0x4b, 0x4b, 0x44, 0x51, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x18, 0x4e, 0x54, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x41, 0x51, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x48, 0xcd, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x47, 0x44, 0x40, 0x43, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x45, 0x44, 0x44, 0x53, 0x1f, 0x48, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x9f, 0x52, 0x47, 0x4e, 0x44, 0x52, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x1f, 0x42, 0x40, 0x4d, 0x9f, 0x52, 0x53, 0x44, 0x44, 0x51, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x45, 0x1f, 0x40, 0x4d, 0xd8, 0x43, 0x48, 0x51, 0x44, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x42, 0x47, 0x4e, 0x4e, 0x52, 0x44, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x5d, 0x51, 0x44, 0x1f, 0x4e, 0x4d, 0x9f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x4e, 0x56, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x58, 0x4e, 0x54, 0x9f, ], [0x4a, 0x4d, 0x4e, 0x56, 0x1f, 0x56, 0x47, 0x40, 0x53, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4a, 0x4d, 0x4e, 0x56, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x18, 0xe, 0x14, 0x1f, 0x40, 0x51, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4e, 0x4d, 0x44, 0x1f, 0x56, 0x47, 0x4e, 0x5d, 0x4b, 0x4b, 0x9f, 0x43, 0x44, 0x42, 0x48, 0x43, 0x44, 0x1f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x4e, 0x9f, 0x46, 0x4e, 0x1b, 0x1b, 0x1b, 0x1f, 0x1c, 0x12, 0x54, 0x44, 0x52, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x16, 0x47, 0x44, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x44, 0x4b, 0x48, 0x4c, 0x48, 0x4d, 0x40, 0x53, 0x44, 0x43, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x48, 0x52, 0x9f, 0x48, 0x4c, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x4b, 0x44, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x4d, 0x9f, 0x56, 0x47, 0x40, 0x53, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x52, 0x1d, 0x9f, 0x47, 0x4e, 0x56, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x48, 0x4c, 0x4f, 0x51, 0x4e, 0x41, 0x40, 0x41, 0x4b, 0x44, 0x1d, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x9f, 0x41, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x54, 0x53, 0x47, 0x1b, 0x9f, ], [0x1c, 0x3, 0x4e, 0x58, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5d, 0xc, 0x58, 0x1f, 0x4c, 0x48, 0x4d, 0x43, 0x1d, 0x5d, 0x1f, 0x47, 0x44, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1d, 0x1f, 0x5d, 0x51, 0x44, 0x41, 0x44, 0x4b, 0x52, 0x1f, 0x40, 0x53, 0x9f, 0x52, 0x53, 0x40, 0x46, 0x4d, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x1f, 0x6, 0x48, 0x55, 0x44, 0x9f, 0x4c, 0x44, 0x1f, 0x4f, 0x51, 0x4e, 0x41, 0x4b, 0x44, 0x4c, 0x52, 0x1d, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x9f, 0x4c, 0x44, 0x1f, 0x56, 0x4e, 0x51, 0x4a, 0x1d, 0x1f, 0x46, 0x48, 0x55, 0x44, 0x1f, 0x4c, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x52, 0x53, 0x1f, 0x40, 0x41, 0x52, 0x53, 0x51, 0x54, 0x52, 0x44, 0x9f, 0x42, 0x51, 0x58, 0x4f, 0x53, 0x4e, 0x46, 0x51, 0x40, 0x4c, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x52, 0x53, 0x1f, 0x48, 0x4d, 0x53, 0x51, 0x48, 0x42, 0x40, 0x53, 0x44, 0x9f, ], [0x40, 0x4d, 0x40, 0x4b, 0x58, 0x52, 0x48, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x8, 0x1f, 0x40, 0xcc, 0x48, 0x4d, 0x1f, 0x4c, 0x58, 0x1f, 0x4e, 0x56, 0x4d, 0x1f, 0x4f, 0x51, 0x4e, 0x4f, 0x44, 0x51, 0x9f, 0x40, 0x53, 0x4c, 0x4e, 0x52, 0x4f, 0x47, 0x44, 0x51, 0x44, 0x1b, 0x1f, 0x8, 0x1f, 0x42, 0x40, 0x4d, 0x9f, 0x43, 0x48, 0x52, 0x4f, 0x44, 0x4d, 0x52, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x4d, 0x1f, 0x56, 0x48, 0x53, 0xc7, 0x40, 0x51, 0x53, 0x48, 0x45, 0x48, 0x42, 0x48, 0x40, 0x4b, 0x9f, 0x52, 0x53, 0x48, 0x4c, 0x54, 0x4b, 0x40, 0x4d, 0x53, 0x52, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x8, 0x9f, 0x40, 0x41, 0x47, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x54, 0x4b, 0x4b, 0x9f, 0x51, 0x4e, 0x54, 0x53, 0x48, 0x4d, 0x44, 0x1f, 0x4e, 0x45, 0x9f, ], [0x44, 0x57, 0x48, 0x52, 0x53, 0x44, 0x4d, 0x42, 0x44, 0x1b, 0x1f, 0x8, 0x1f, 0x42, 0x51, 0x40, 0x55, 0xc4, 0x45, 0x4e, 0x51, 0x1f, 0x4c, 0x44, 0x4d, 0x53, 0x40, 0x4b, 0x9f, 0x44, 0x57, 0x40, 0x4b, 0x53, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x1f, 0x13, 0x47, 0x40, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x56, 0x47, 0x58, 0x1f, 0x8, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x42, 0x47, 0x4e, 0x52, 0x44, 0x4d, 0x1f, 0x4c, 0x58, 0x1f, 0x4e, 0x56, 0x4d, 0x9f, 0x4f, 0x40, 0x51, 0x53, 0x48, 0x42, 0x54, 0x4b, 0x40, 0x51, 0x9f, 0x4f, 0x51, 0x4e, 0x45, 0x44, 0x52, 0x52, 0x48, 0x4e, 0x4d, 0x1d, 0x1f, 0x4e, 0x51, 0x9f, 0x51, 0x40, 0x53, 0x47, 0x44, 0x51, 0x1f, 0x42, 0x51, 0x44, 0x40, 0x53, 0x44, 0x43, 0x1f, 0x48, 0x53, 0x9d, ], [0x45, 0x4e, 0x51, 0x1f, 0x8, 0x1f, 0x40, 0x4c, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4e, 0x4d, 0x4b, 0x58, 0x9f, 0x4e, 0x4d, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1b, 0xdd, 0x1c, 0x3, 0x4e, 0x58, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x48, 0x45, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x4b, 0xd8, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x9f, 0x40, 0x4d, 0x58, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x53, 0x47, 0xc4, 0x4c, 0x48, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x48, 0x4d, 0x55, 0x44, 0x4d, 0x53, 0x1b, 0x1f, 0x16, 0x44, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x43, 0x40, 0x51, 0x44, 0x1f, 0x53, 0x4e, 0x9f, 0x42, 0x4e, 0x4d, 0x42, 0x44, 0x48, 0x55, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x40, 0x51, 0x44, 0x9f, ], [0x51, 0x44, 0x40, 0x4b, 0x4b, 0x58, 0x1f, 0x4c, 0x44, 0x51, 0x44, 0x9f, 0x42, 0x4e, 0x4c, 0x4c, 0x4e, 0x4d, 0x4f, 0x4b, 0x40, 0x42, 0x44, 0x52, 0x1f, 0x4e, 0x45, 0x9f, 0x44, 0x57, 0x48, 0x52, 0x53, 0x44, 0x4d, 0x42, 0x44, 0x1b, 0x1f, 0x8, 0x45, 0x1f, 0x56, 0x44, 0x9f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x1f, 0x45, 0x4b, 0x58, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x4e, 0x45, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x48, 0x4d, 0x43, 0x4e, 0x56, 0x1f, 0x47, 0x40, 0x4d, 0x43, 0x9f, 0x48, 0x4d, 0x1f, 0x47, 0x40, 0x4d, 0x43, 0x1d, 0x1f, 0x47, 0x4e, 0x55, 0x44, 0x51, 0x9f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x46, 0x51, 0x44, 0x40, 0x53, 0x9f, 0x42, 0x48, 0x53, 0x58, 0x1d, 0x1f, 0x46, 0x44, 0x4d, 0x53, 0x4b, 0x58, 0x9f, ], [0x51, 0x44, 0x4c, 0x4e, 0x55, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x51, 0x4e, 0x4e, 0x45, 0x52, 0x1d, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x44, 0x44, 0x4f, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0xd3, 0x53, 0x47, 0x44, 0x1f, 0x50, 0x54, 0x44, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x46, 0x4e, 0x48, 0x4d, 0x46, 0x9f, 0x4e, 0x4d, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x9f, 0x42, 0x4e, 0x48, 0x4d, 0x42, 0x48, 0x43, 0x44, 0x4d, 0x42, 0x44, 0x52, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x4b, 0x40, 0x4d, 0x4d, 0x48, 0x4d, 0x46, 0x52, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x42, 0x51, 0x4e, 0x52, 0x52, 0x1c, 0x4f, 0x54, 0x51, 0x4f, 0x4e, 0x52, 0x44, 0x52, 0x1d, 0x9f, ], [0x53, 0x47, 0x44, 0x1f, 0x56, 0x4e, 0x4d, 0x43, 0x44, 0x51, 0x45, 0x54, 0x4b, 0x9f, 0x42, 0x47, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x44, 0x55, 0x44, 0x4d, 0x53, 0x52, 0x1d, 0x9f, 0x56, 0x4e, 0x51, 0x4a, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x46, 0x44, 0x4d, 0x44, 0x51, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4b, 0x44, 0x40, 0x43, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x52, 0x53, 0x1f, 0x4e, 0x54, 0x53, 0x51, 0x44, 0x9f, 0x51, 0x44, 0x52, 0x54, 0x4b, 0x53, 0x52, 0x1d, 0x1f, 0x48, 0x53, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x4c, 0x40, 0x4a, 0x44, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x45, 0x48, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x9f, ], [0x56, 0x48, 0x53, 0x47, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x42, 0x4e, 0x4d, 0x55, 0x44, 0x4d, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x4b, 0x48, 0x53, 0x48, 0x44, 0x52, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x45, 0x4e, 0x51, 0x44, 0x52, 0x44, 0x44, 0x4d, 0x9f, 0x42, 0x4e, 0x4d, 0x42, 0x4b, 0x54, 0x52, 0x48, 0x4e, 0x4d, 0x52, 0x1f, 0x4c, 0x4e, 0x52, 0x53, 0x9f, 0x52, 0x53, 0x40, 0x4b, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x54, 0x4d, 0x4f, 0x51, 0x4e, 0x45, 0x48, 0x53, 0x40, 0x41, 0x4b, 0x44, 0x1b, 0x9f, 0x1c, 0x3, 0x4e, 0x58, 0x4b, 0x44, 0x1f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x52, 0x53, 0x4e, 0x51, 0x58, 0x1f, 0x52, 0x4e, 0x1f, 0x45, 0x40, 0x51, 0x1b, 0x9f, 0x8, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x4d, 0x48, 0x4d, 0x46, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x14, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x9f, 0x42, 0x51, 0x44, 0x40, 0x53, 0x44, 0x43, 0x1b, 0x1f, 0x13, 0x47, 0x48, 0x52, 0x1f, 0x47, 0x40, 0x52, 0x9f, 0x4c, 0x40, 0x43, 0x44, 0x1f, 0x40, 0x1f, 0x4b, 0x4e, 0x53, 0x1f, 0x4e, 0x45, 0x9f, 0x4f, 0x44, 0x4e, 0x4f, 0x4b, 0x44, 0x1f, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x40, 0x4d, 0x46, 0x51, 0x58, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x41, 0x44, 0x44, 0x4d, 0x1f, 0x56, 0x48, 0x43, 0x44, 0x4b, 0x58, 0x9f, 0x51, 0x44, 0x46, 0x40, 0x51, 0x43, 0x44, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x40, 0x1f, 0x41, 0x40, 0x43, 0x9f, ], [0x4c, 0x4e, 0x55, 0x44, 0x1b, 0x1f, 0x1c, 0x0, 0x43, 0x40, 0x4c, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5, 0x4e, 0x51, 0x1f, 0x48, 0x4d, 0x52, 0x53, 0x40, 0x4d, 0x42, 0x44, 0x1d, 0x1f, 0x4e, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4f, 0x4b, 0x40, 0x4d, 0x44, 0x53, 0x1f, 0x4, 0x40, 0x51, 0x53, 0x47, 0x1d, 0x9f, 0x4c, 0x40, 0x4d, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x9f, 0x40, 0x52, 0x52, 0x54, 0x4c, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x47, 0x44, 0x9f, 0x56, 0x40, 0x52, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x48, 0x4d, 0x53, 0x44, 0x4b, 0x4b, 0x48, 0x46, 0x44, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x9f, 0x43, 0x4e, 0x4b, 0x4f, 0x47, 0x48, 0x4d, 0x52, 0x1f, 0x41, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x9f, 0x47, 0x44, 0x1f, 0x47, 0x40, 0x43, 0x1f, 0x40, 0x42, 0x47, 0x48, 0x44, 0x55, 0x44, 0x43, 0x1f, 0x52, 0xce, ], [0x4c, 0x54, 0x42, 0x47, 0x1c, 0x53, 0x47, 0x44, 0x1f, 0x56, 0x47, 0x44, 0x44, 0x4b, 0x1d, 0x9f, 0xd, 0x44, 0x56, 0x1f, 0x18, 0x4e, 0x51, 0x4a, 0x1d, 0x1f, 0x56, 0x40, 0x51, 0x52, 0x1f, 0x40, 0x4d, 0xc3, 0x52, 0x4e, 0x1f, 0x4e, 0x4d, 0x1c, 0x56, 0x47, 0x48, 0x4b, 0x52, 0x53, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x4e, 0x4b, 0x4f, 0x47, 0x48, 0x4d, 0x52, 0x1f, 0x47, 0x40, 0x43, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x43, 0x4e, 0x4d, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x4c, 0x54, 0x42, 0xca, 0x40, 0x41, 0x4e, 0x54, 0x53, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x56, 0x40, 0x53, 0x44, 0xd1, 0x47, 0x40, 0x55, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x9f, 0x53, 0x48, 0x4c, 0x44, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x9f, ], [0x42, 0x4e, 0x4d, 0x55, 0x44, 0x51, 0x52, 0x44, 0x4b, 0x58, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x43, 0x4e, 0x4b, 0x4f, 0x47, 0x48, 0x4d, 0x52, 0x1f, 0x47, 0x40, 0x43, 0x9f, 0x40, 0x4b, 0x56, 0x40, 0x58, 0x52, 0x1f, 0x41, 0x44, 0x4b, 0x48, 0x44, 0x55, 0x44, 0x43, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x45, 0x40, 0xd1, 0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x48, 0x4d, 0x53, 0x44, 0x4b, 0x4b, 0x48, 0x46, 0x44, 0x4d, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x4c, 0x40, 0x4d, 0x1b, 0x45, 0x4e, 0x51, 0x9f, 0x4f, 0x51, 0x44, 0x42, 0x48, 0x52, 0x44, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x40, 0x4c, 0xc4, 0x51, 0x44, 0x40, 0x52, 0x4e, 0x4d, 0x52, 0x1b, 0x1f, 0x1c, 0x0, 0x43, 0x40, 0x4c, 0x52, 0x1f, 0x9f, ]], +[[0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0x9f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x1f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0x51, 0x1f, 0x4e, 0xc5, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x52, 0x1d, 0x1f, 0x52, 0x48, 0x4c, 0x4f, 0x4b, 0x58, 0x9f, 0x41, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x9f, 0x40, 0x4d, 0x1f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x1f, 0x40, 0x4c, 0x4e, 0x54, 0x4d, 0xd3, 0x4e, 0x45, 0x1f, 0x52, 0x4f, 0x40, 0x42, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x1f, 0x48, 0x4d, 0x1b, 0x1f, 0x7, 0x4e, 0x56, 0x44, 0x55, 0x44, 0x51, 0x9d, ], [0x4d, 0x4e, 0x53, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x4e, 0x4d, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x48, 0x52, 0x1f, 0x48, 0x4d, 0x47, 0x40, 0x41, 0x48, 0x53, 0x44, 0x43, 0x9b, 0x13, 0x47, 0x44, 0x51, 0x44, 0x45, 0x4e, 0x51, 0x44, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x41, 0x44, 0x1f, 0x40, 0x1f, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x9f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0x51, 0x1f, 0x4e, 0x45, 0x9f, 0x48, 0x4d, 0x47, 0x40, 0x41, 0x48, 0x53, 0x44, 0x43, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x52, 0x1b, 0x9f, 0x0, 0x4d, 0x58, 0x1f, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x1f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0x51, 0x9f, 0x43, 0x48, 0x55, 0x48, 0x43, 0x44, 0x43, 0x1f, 0x41, 0x58, 0x9f, ], [0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x58, 0x1f, 0x48, 0x52, 0x1f, 0x40, 0x52, 0x9f, 0x4d, 0x44, 0x40, 0x51, 0x1f, 0x53, 0x4e, 0x1f, 0x4d, 0x4e, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x40, 0xd2, 0x4c, 0x40, 0x4a, 0x44, 0x52, 0x1f, 0x4d, 0x4e, 0x1f, 0x4e, 0x43, 0x43, 0x52, 0x1d, 0x1f, 0x52, 0x4e, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x40, 0x55, 0x44, 0x51, 0x40, 0x46, 0x44, 0x9f, 0x4f, 0x4e, 0x4f, 0x54, 0x4b, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4f, 0x4b, 0x40, 0x4d, 0x44, 0x53, 0x52, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0xc4, 0x14, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x42, 0x40, 0x4d, 0x1f, 0x41, 0x44, 0x9f, 0x52, 0x40, 0x48, 0x43, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x1f, 0x59, 0x44, 0x51, 0x4e, 0x1b, 0x9f, ], [0x5, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x48, 0x53, 0x9f, 0x45, 0x4e, 0x4b, 0x4b, 0x4e, 0x56, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x4e, 0x4f, 0x54, 0x4b, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x47, 0x4e, 0x4b, 0x44, 0x1f, 0x14, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x48, 0x52, 0x9f, 0x40, 0x4b, 0x52, 0x4e, 0x1f, 0x59, 0x44, 0x51, 0x4e, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4f, 0x44, 0x4e, 0x4f, 0x4b, 0x44, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x4c, 0x40, 0x58, 0x1f, 0x4c, 0x44, 0x44, 0x53, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x9f, 0x53, 0x48, 0x4c, 0x44, 0x1f, 0x53, 0x4e, 0x1f, 0x53, 0x48, 0x4c, 0x44, 0x1f, 0x40, 0x51, 0x44, 0x9f, ], [0x4c, 0x44, 0x51, 0x44, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4f, 0x51, 0x4e, 0x43, 0x54, 0x42, 0x53, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x43, 0x44, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x43, 0x9f, 0x48, 0x4c, 0x40, 0x46, 0x48, 0x4d, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1b, 0x9f, 0x1c, 0x0, 0x43, 0x40, 0x4c, 0x52, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5, 0x40, 0x51, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x53, 0xd8, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x42, 0x4e, 0x4b, 0x43, 0x1f, 0x13, 0x4e, 0x9f, 0x43, 0x54, 0x4d, 0x46, 0x44, 0x4e, 0x4d, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x51, 0x4d, 0x52, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x16, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x44, 0x51, 0x44, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x13, 0x4e, 0x9f, 0x52, 0x44, 0x44, 0x4a, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4f, 0x40, 0x4b, 0x44, 0x9f, 0x44, 0x4d, 0x42, 0x47, 0x40, 0x4d, 0x53, 0x44, 0x43, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x1b, 0x9f, ], [0x13, 0x47, 0x44, 0x1f, 0x43, 0x56, 0x40, 0x51, 0x55, 0x44, 0x52, 0x1f, 0x4e, 0x45, 0x9f, 0x58, 0x4e, 0x51, 0x44, 0x1f, 0x4c, 0x40, 0x43, 0x44, 0x1f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x58, 0x9f, 0x52, 0x4f, 0x44, 0x4b, 0x4b, 0x52, 0x1d, 0x1f, 0x16, 0x47, 0x48, 0x4b, 0x44, 0x9f, 0x47, 0x40, 0x4c, 0x4c, 0x44, 0x51, 0x52, 0x1f, 0x45, 0x44, 0x4b, 0x4b, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x9f, 0x51, 0x48, 0x4d, 0x46, 0x48, 0x4d, 0x46, 0x1f, 0x41, 0x44, 0x4b, 0x4b, 0x52, 0x1f, 0x8, 0x4d, 0x9f, 0x4f, 0x4b, 0x40, 0x42, 0x44, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1d, 0x1f, 0x56, 0x47, 0x44, 0x51, 0xc4, 0x43, 0x40, 0x51, 0x4a, 0x1f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x52, 0x4b, 0x44, 0x44, 0x4f, 0x9d, 0x8, 0x4d, 0x1f, 0x47, 0x4e, 0x4b, 0x4b, 0x4e, 0x56, 0x1f, 0x47, 0x40, 0x4b, 0x4b, 0x52, 0x9f, ], [0x41, 0x44, 0x4d, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x45, 0x44, 0x4b, 0x4b, 0x52, 0x9b, 0x5, 0x4e, 0x51, 0x1f, 0x40, 0x4d, 0x42, 0x48, 0x44, 0x4d, 0x53, 0x1f, 0x4a, 0x48, 0x4d, 0x46, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x44, 0x4b, 0x55, 0x48, 0x52, 0x47, 0x1f, 0x4b, 0x4e, 0x51, 0x43, 0x9f, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4c, 0x40, 0x4d, 0x58, 0x1f, 0x40, 0x9f, 0x46, 0x4b, 0x44, 0x40, 0x4c, 0x48, 0x4d, 0x46, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x44, 0x4d, 0x9f, 0x47, 0x4e, 0x40, 0x51, 0x43, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x1f, 0x52, 0x47, 0x40, 0x4f, 0x44, 0x43, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x53, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x42, 0x40, 0x54, 0x46, 0x47, 0x53, 0x9f, ], [0x13, 0x4e, 0x1f, 0x47, 0x48, 0x43, 0x44, 0x1f, 0x48, 0x4d, 0x1f, 0x46, 0x44, 0x4c, 0x52, 0x1f, 0x4e, 0xcd, 0x47, 0x48, 0x4b, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x52, 0x56, 0x4e, 0x51, 0x43, 0x1b, 0x1f, 0xe, 0x4d, 0x9f, 0x52, 0x48, 0x4b, 0x55, 0x44, 0x51, 0x1f, 0x4d, 0x44, 0x42, 0x4a, 0x4b, 0x40, 0x42, 0x44, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x52, 0x53, 0x51, 0x54, 0x4d, 0x46, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x45, 0x4b, 0x4e, 0x56, 0x44, 0x51, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x53, 0x40, 0x51, 0x52, 0x1d, 0x9f, 0x4e, 0x4d, 0x1f, 0x42, 0x51, 0x4e, 0x56, 0x4d, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x9f, 0x47, 0x54, 0x4d, 0x46, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0x1c, 0x45, 0x48, 0x51, 0x44, 0x1d, 0x1f, 0x48, 0x4d, 0x9f, ], [0x53, 0x56, 0x48, 0x52, 0x53, 0x44, 0x43, 0x1f, 0x56, 0x48, 0x51, 0x44, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x9f, 0x4c, 0x44, 0x52, 0x47, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x9f, 0x4e, 0x45, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x54, 0x4d, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x5, 0x40, 0x51, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x53, 0xd8, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x42, 0x4e, 0x4b, 0x43, 0x1f, 0x13, 0x4e, 0x9f, 0x43, 0x54, 0x4d, 0x46, 0x44, 0x4e, 0x4d, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x51, 0x4d, 0x52, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x16, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x44, 0x51, 0x44, 0x9f, 0x41, 0x51, 0x44, 0x40, 0x4a, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x13, 0x4e, 0x9f, 0x42, 0x4b, 0x40, 0x48, 0x4c, 0x1f, 0x4e, 0x54, 0x51, 0x9f, 0x4b, 0x4e, 0x4d, 0x46, 0x1c, 0x45, 0x4e, 0x51, 0x46, 0x4e, 0x53, 0x53, 0x44, 0x4d, 0x9f, ], [0x46, 0x4e, 0x4b, 0x43, 0x1b, 0x1f, 0x6, 0x4e, 0x41, 0x4b, 0x44, 0x53, 0x52, 0x1f, 0x53, 0x47, 0x44, 0xd8, 0x42, 0x40, 0x51, 0x55, 0x44, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x47, 0x40, 0x51, 0x4f, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x1d, 0x9f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x1f, 0x4c, 0x40, 0x4d, 0x9f, 0x43, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x13, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4b, 0x40, 0x58, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4c, 0x40, 0x4d, 0x58, 0x1f, 0x40, 0x1f, 0x52, 0x4e, 0x4d, 0x46, 0x1f, 0x16, 0x40, 0x52, 0x9f, ], [0x52, 0x54, 0x4d, 0x46, 0x1f, 0x54, 0x4d, 0x47, 0x44, 0x40, 0x51, 0x43, 0x1f, 0x41, 0x58, 0x9f, 0x4c, 0x44, 0x4d, 0x1f, 0x4e, 0x51, 0x1f, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x4f, 0x48, 0x4d, 0x44, 0x52, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x51, 0x4e, 0x40, 0x51, 0x48, 0x4d, 0xc6, 0x4e, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x47, 0x44, 0x48, 0x46, 0x47, 0x53, 0x1d, 0x1f, 0x13, 0x47, 0xc4, 0x56, 0x48, 0x4d, 0x43, 0x52, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x4c, 0x4e, 0x40, 0x4d, 0x48, 0x4d, 0xc6, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4d, 0x48, 0x46, 0x47, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x45, 0x48, 0x51, 0x44, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x51, 0x44, 0x43, 0x1d, 0x1f, 0x48, 0x53, 0x9f, 0x45, 0x4b, 0x40, 0x4c, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x4f, 0x51, 0x44, 0x40, 0x43, 0x1b, 0x9f, ], [0x13, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x44, 0x44, 0x52, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x9f, 0x53, 0x4e, 0x51, 0x42, 0x47, 0x44, 0x52, 0x1f, 0x41, 0x4b, 0x40, 0x59, 0x44, 0x43, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x44, 0x1f, 0x41, 0x44, 0x4b, 0x4b, 0x52, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x9f, 0x51, 0x48, 0x4d, 0x46, 0x48, 0x4d, 0x46, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x43, 0x40, 0x4b, 0x44, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x4c, 0x44, 0x4d, 0x9f, 0x4b, 0x4e, 0x4e, 0x4a, 0x44, 0x43, 0x1f, 0x54, 0x4f, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x45, 0x40, 0x42, 0x44, 0x52, 0x1f, 0x4f, 0x40, 0x4b, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x4d, 0x9f, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0x5d, 0x52, 0x1f, 0x48, 0x51, 0x44, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x45, 0x48, 0x44, 0x51, 0x42, 0x44, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x45, 0x48, 0x51, 0x44, 0x9f, 0xb, 0x40, 0x48, 0x43, 0x1f, 0x4b, 0x4e, 0x56, 0x1f, 0x53, 0x47, 0x44, 0x48, 0x51, 0x9f, ], [0x53, 0x4e, 0x56, 0x44, 0x51, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x52, 0x9f, 0x45, 0x51, 0x40, 0x48, 0x4b, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x1f, 0x52, 0x4c, 0x4e, 0x4a, 0x44, 0x43, 0x9f, 0x41, 0x44, 0x4d, 0x44, 0x40, 0x53, 0x47, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x1f, 0x43, 0x56, 0x40, 0x51, 0x55, 0x44, 0x52, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x9f, 0x47, 0x44, 0x40, 0x51, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x53, 0x51, 0x40, 0x4c, 0x4f, 0x1f, 0x4e, 0xc5, 0x43, 0x4e, 0x4e, 0x4c, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x1f, 0x45, 0x4b, 0x44, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x48, 0x51, 0x1f, 0x47, 0x40, 0x4b, 0x4b, 0x1d, 0x1f, 0x53, 0x4e, 0x9f, ], [0x43, 0x58, 0x48, 0x4d, 0x46, 0x1f, 0x45, 0x40, 0x4b, 0x4b, 0x1f, 0x1, 0x44, 0x4d, 0x44, 0x40, 0x53, 0xc7, 0x47, 0x48, 0x52, 0x1f, 0x45, 0x44, 0x44, 0x53, 0x1d, 0x1f, 0x41, 0x44, 0x4d, 0x44, 0x40, 0x53, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1b, 0x1f, 0x5, 0x40, 0x51, 0x1f, 0x4e, 0x55, 0x44, 0xd1, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x48, 0x52, 0x53, 0x58, 0x9f, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x46, 0x51, 0x48, 0x4c, 0x1f, 0x13, 0x4e, 0x9f, 0x43, 0x54, 0x4d, 0x46, 0x44, 0x4e, 0x4d, 0x52, 0x1f, 0x43, 0x44, 0x44, 0x4f, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x51, 0x4d, 0x52, 0x1f, 0x43, 0x48, 0x4c, 0x1f, 0x16, 0x44, 0x9f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x44, 0x51, 0x44, 0x9f, ], [0x41, 0x51, 0x44, 0x40, 0x4a, 0x1f, 0x4e, 0x45, 0x1f, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x13, 0x4e, 0x9f, 0x56, 0x48, 0x4d, 0x1f, 0x4e, 0x54, 0x51, 0x1f, 0x47, 0x40, 0x51, 0x4f, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x46, 0x4e, 0x4b, 0x43, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x47, 0x48, 0x4c, 0x1a, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x11, 0x4e, 0x40, 0x43, 0x52, 0x1f, 0x46, 0x4e, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x44, 0x55, 0x44, 0xd1, 0x4e, 0x4d, 0x1d, 0x1f, 0xe, 0x55, 0x44, 0x51, 0x1f, 0x51, 0x4e, 0x42, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x53, 0x51, 0x44, 0x44, 0x1d, 0x1f, 0x1, 0x58, 0x9f, 0x42, 0x40, 0x55, 0x44, 0x52, 0x1f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x52, 0x54, 0x4d, 0x1f, 0x47, 0x40, 0x52, 0x1f, 0x52, 0x47, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x1, 0x58, 0x9f, 0x52, 0x53, 0x51, 0x44, 0x40, 0x4c, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0xd1, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x44, 0x40, 0x1b, 0x1f, 0xe, 0x55, 0x44, 0xd1, 0x52, 0x4d, 0x4e, 0x56, 0x1f, 0x41, 0x58, 0x1f, 0x56, 0x48, 0x4d, 0x53, 0x44, 0x51, 0x9f, ], [0x52, 0x4e, 0x56, 0x4d, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4c, 0x44, 0x51, 0x51, 0x58, 0x1f, 0x45, 0x4b, 0x4e, 0x56, 0x44, 0x51, 0x52, 0x9f, 0x4e, 0x45, 0x1f, 0x9, 0x54, 0x4d, 0x44, 0x1d, 0x1f, 0xe, 0x55, 0x44, 0x51, 0x9f, 0x46, 0x51, 0x40, 0x52, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4e, 0x55, 0x44, 0x51, 0x9f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x9f, 0x4c, 0x4e, 0x54, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4c, 0x4e, 0x4e, 0x4d, 0x1b, 0x1f, 0x11, 0x4e, 0x40, 0x43, 0x52, 0x1f, 0x46, 0x4e, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x4e, 0x4d, 0x1f, 0x14, 0x4d, 0x43, 0x44, 0xd1, ], [0x42, 0x4b, 0x4e, 0x54, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x9f, 0x52, 0x53, 0x40, 0x51, 0x1d, 0x1f, 0x18, 0x44, 0x53, 0x1f, 0x45, 0x44, 0x44, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0x4d, 0x43, 0x44, 0x51, 0x48, 0x4d, 0x46, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x13, 0x54, 0x51, 0x4d, 0x1f, 0x40, 0x53, 0x9f, 0x4b, 0x40, 0x52, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x47, 0x4e, 0x4c, 0x44, 0x1f, 0x40, 0x45, 0x40, 0x51, 0x9b, 0x4, 0x58, 0x44, 0x52, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x45, 0x48, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0xc3, 0x52, 0x56, 0x4e, 0x51, 0x43, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x52, 0x44, 0x44, 0x4d, 0x9f, 0x0, 0x4d, 0x43, 0x1f, 0x47, 0x4e, 0x51, 0x51, 0x4e, 0x51, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x47, 0x40, 0x4b, 0x4b, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x9f, 0xb, 0x4e, 0x4e, 0x4a, 0x1f, 0x40, 0x53, 0x1f, 0x4b, 0x40, 0x52, 0x53, 0x1f, 0x4e, 0x4d, 0x9f, 0x4c, 0x44, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x46, 0x51, 0x44, 0x44, 0x4d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x53, 0x51, 0x44, 0x44, 0x52, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x48, 0x4b, 0x4b, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x4b, 0x4e, 0x4d, 0x46, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x4a, 0x4d, 0x4e, 0x56, 0x4d, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x51, 0x44, 0x44, 0x1f, 0x11, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4, 0x4b, 0x55, 0x44, 0x4d, 0x1c, 0x4a, 0x48, 0x4d, 0x46, 0x52, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4a, 0x58, 0x1d, 0x9f, 0x12, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x3, 0x56, 0x40, 0x51, 0x45, 0x1c, 0x4b, 0x4e, 0x51, 0x43, 0x52, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x48, 0x51, 0x1f, 0x47, 0x40, 0x4b, 0x4b, 0x52, 0x1f, 0x4e, 0x45, 0x9f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0xd, 0x48, 0x4d, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0xc, 0x4e, 0x51, 0x53, 0x40, 0x4b, 0x1f, 0xc, 0x44, 0x4d, 0x1f, 0x43, 0x4e, 0x4e, 0x4c, 0x44, 0x43, 0x9f, ], [0x53, 0x4e, 0x1f, 0x43, 0x48, 0x44, 0x1d, 0x1f, 0xe, 0x4d, 0x44, 0x1f, 0x45, 0x4e, 0x51, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x3, 0x40, 0x51, 0x4a, 0x1f, 0xb, 0x4e, 0x51, 0x43, 0x1f, 0x4e, 0x4d, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x43, 0x40, 0x51, 0x4a, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x4d, 0x44, 0x1f, 0x8, 0xcd, 0x53, 0x47, 0x44, 0x1f, 0xb, 0x40, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0xc, 0x4e, 0x51, 0x43, 0x4e, 0xd1, 0x56, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x12, 0x47, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x9f, 0x4b, 0x48, 0x44, 0x1b, 0x1f, 0xe, 0x4d, 0x44, 0x1f, 0x11, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x9f, 0x51, 0x54, 0x4b, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x4b, 0x4b, 0x1d, 0x1f, 0xe, 0x4d, 0xc4, 0x11, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x4c, 0x9d, ], [0xe, 0x4d, 0x44, 0x1f, 0x11, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x51, 0x48, 0x4d, 0x46, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1f, 0x40, 0x4b, 0x4b, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x43, 0x40, 0x51, 0x4a, 0x4d, 0x44, 0x52, 0x52, 0x1f, 0x41, 0x48, 0x4d, 0x43, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x1d, 0x1f, 0x8, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0xb, 0x40, 0x4d, 0x43, 0x9f, 0x4e, 0x45, 0x1f, 0xc, 0x4e, 0x51, 0x43, 0x4e, 0x51, 0x1f, 0x56, 0x47, 0x44, 0x51, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x12, 0x47, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x4b, 0x48, 0x44, 0x1b, 0x9f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, ]], +[[0x0, 0x4b, 0x4b, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x46, 0x4e, 0x4b, 0x43, 0x9f, 0x43, 0x4e, 0x44, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x46, 0x4b, 0x48, 0x53, 0x53, 0x44, 0x51, 0x1d, 0x9f, 0xd, 0x4e, 0x53, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x53, 0x47, 0x4e, 0x52, 0x44, 0x1f, 0x56, 0x47, 0x4e, 0x9f, 0x56, 0x40, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x40, 0x51, 0x44, 0x1f, 0x4b, 0x4e, 0x52, 0x53, 0x1b, 0x9f, 0x13, 0x47, 0x44, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x52, 0x9f, 0x52, 0x53, 0x51, 0x4e, 0x4d, 0x46, 0x1f, 0x43, 0x4e, 0x44, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x44, 0x51, 0x1d, 0x1f, 0x3, 0x44, 0x44, 0x4f, 0x1f, 0x51, 0x4e, 0x4e, 0x53, 0xd2, 0x40, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x51, 0x44, 0x40, 0x42, 0x47, 0x44, 0x43, 0x1f, 0x41, 0xd8, ], [0x53, 0x47, 0x44, 0x1f, 0x45, 0x51, 0x4e, 0x52, 0x53, 0x1b, 0x1f, 0x5, 0x51, 0x4e, 0x4c, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x40, 0x52, 0x47, 0x44, 0x52, 0x1f, 0x40, 0x1f, 0x45, 0x48, 0x51, 0x44, 0x9f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x1f, 0x56, 0x4e, 0x4a, 0x44, 0x4d, 0x1d, 0x1f, 0x0, 0x9f, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x52, 0x47, 0x40, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x9f, 0x52, 0x4f, 0x51, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x11, 0x44, 0x4d, 0x44, 0x56, 0x44, 0x43, 0x9f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x1f, 0x41, 0x4b, 0x40, 0x43, 0x44, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x41, 0x51, 0x4e, 0x4a, 0x44, 0x4d, 0x1d, 0x9f, ], [0x13, 0x47, 0x44, 0x1f, 0x42, 0x51, 0x4e, 0x56, 0x4d, 0x4b, 0x44, 0x52, 0x52, 0x9f, 0x40, 0x46, 0x40, 0x48, 0x4d, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x9f, 0x4a, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x1c, 0x13, 0x4e, 0x4b, 0x4a, 0x48, 0x44, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x16, 0x47, 0x4e, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x52, 0x1d, 0x1f, 0x2, 0x40, 0x4d, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x53, 0x51, 0x48, 0x44, 0x52, 0x1d, 0x1f, 0x3, 0x4e, 0x44, 0x52, 0x1b, 0x9f, 0x16, 0x47, 0x4e, 0x1f, 0x4b, 0x4e, 0x55, 0x44, 0x52, 0x1d, 0x1f, 0xb, 0x48, 0x55, 0x44, 0x52, 0x1b, 0x9f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x6, 0x4e, 0x4d, 0x44, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x1d, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x9f, 0x40, 0x47, 0x44, 0x40, 0x43, 0x1d, 0x1f, 0x4, 0x42, 0x47, 0x4e, 0x44, 0x52, 0x1f, 0x51, 0x4e, 0x4b, 0xcb, 0x54, 0x4d, 0x40, 0x4d, 0x52, 0x56, 0x44, 0x51, 0x44, 0x43, 0x1b, 0x1f, 0x4, 0x4c, 0x4f, 0x53, 0x58, 0x9d, 0x4e, 0x4f, 0x44, 0x4d, 0x1d, 0x1f, 0x43, 0x54, 0x52, 0x53, 0x58, 0x1d, 0x1f, 0x43, 0x44, 0x40, 0x43, 0x9d, 0x16, 0x47, 0x58, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x16, 0x44, 0x58, 0x51, 0x45, 0x4e, 0x4b, 0x4a, 0x1f, 0x45, 0x4b, 0x44, 0x43, 0x1e, 0x9f, 0x16, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x43, 0x51, 0x40, 0x46, 0x4e, 0x4d, 0xd2, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x53, 0x4e, 0x46, 0x44, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x9f, ], [0xb, 0x44, 0x40, 0x55, 0x48, 0x4d, 0x46, 0x1f, 0x16, 0x44, 0x58, 0x51, 0x52, 0x1f, 0x53, 0x4e, 0x9f, 0x56, 0x48, 0x4d, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x44, 0x40, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x9f, 0x12, 0x44, 0x53, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x47, 0x44, 0x51, 0x43, 0x41, 0x44, 0x40, 0x52, 0x53, 0xd2, 0x45, 0x51, 0x44, 0x44, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x44, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x9f, 0x6, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x4e, 0x54, 0x51, 0x9f, 0x52, 0x40, 0x45, 0x44, 0x46, 0x54, 0x40, 0x51, 0x43, 0x52, 0x1d, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x1d, 0x9f, 0x41, 0x54, 0x53, 0x1f, 0x56, 0x47, 0x48, 0x53, 0x47, 0x44, 0x51, 0x1e, 0x1f, 0x7, 0x40, 0x55, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x45, 0x4b, 0x4e, 0x56, 0x4d, 0x1f, 0x53, 0x4e, 0x1f, 0x52, 0x4e, 0x4c, 0xc4, ], [0x4d, 0x44, 0x56, 0x1f, 0x16, 0x44, 0x58, 0x51, 0x1f, 0x16, 0x47, 0x44, 0x4d, 0x9f, 0x42, 0x51, 0x54, 0x44, 0x4b, 0x1f, 0x13, 0x47, 0x51, 0x44, 0x40, 0x43, 0x52, 0x1f, 0x52, 0x4e, 0x4c, 0xc4, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x52, 0x1f, 0x45, 0x44, 0x40, 0x51, 0x1e, 0x1f, 0x0, 0x51, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x52, 0x1f, 0x40, 0x56, 0x40, 0x58, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1e, 0x1f, 0x16, 0x47, 0x58, 0x1d, 0x9f, 0x4e, 0x47, 0x1d, 0x1f, 0x56, 0x47, 0x58, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4c, 0x4f, 0x53, 0xd8, 0x16, 0x44, 0x58, 0x51, 0x1e, 0x1f, 0x1c, 0xc, 0x42, 0x2, 0x40, 0x45, 0x45, 0x51, 0x44, 0x58, 0x1f, 0x9f, 0x9f, ]], +[[0xc, 0x51, 0x1b, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0xc, 0x51, 0x52, 0x1b, 0x9f, 0x3, 0x54, 0x51, 0x52, 0x4b, 0x44, 0x58, 0x1d, 0x1f, 0x4e, 0x45, 0x1f, 0x4d, 0x54, 0x4c, 0x41, 0x44, 0xd1, 0x45, 0x4e, 0x54, 0x51, 0x1d, 0x1f, 0xf, 0x51, 0x48, 0x55, 0x44, 0x53, 0x9f, 0x3, 0x51, 0x48, 0x55, 0x44, 0x1d, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x4f, 0x51, 0x4e, 0x54, 0x43, 0x9f, 0x53, 0x4e, 0x1f, 0x52, 0x40, 0x58, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x9f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x4f, 0x44, 0x51, 0x45, 0x44, 0x42, 0x53, 0x4b, 0x58, 0x9f, 0x4d, 0x4e, 0x51, 0x4c, 0x40, 0x4b, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0x4d, 0x4a, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x55, 0x44, 0x51, 0x58, 0x1f, 0x4c, 0x54, 0x42, 0x47, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x9f, ], [0x56, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x40, 0x52, 0x53, 0x9f, 0x4f, 0x44, 0x4e, 0x4f, 0x4b, 0x44, 0x1f, 0x58, 0x4e, 0x54, 0x5d, 0x43, 0x9f, 0x44, 0x57, 0x4f, 0x44, 0x42, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x9f, 0x48, 0x4d, 0x55, 0x4e, 0x4b, 0x55, 0x44, 0x43, 0x1f, 0x48, 0x4d, 0x9f, 0x40, 0x4d, 0x58, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x9f, 0x4e, 0x51, 0x1f, 0x4c, 0x58, 0x52, 0x53, 0x44, 0x51, 0x48, 0x4e, 0x54, 0x52, 0x1d, 0x9f, 0x41, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x58, 0x1f, 0x49, 0x54, 0x52, 0x53, 0x9f, 0x43, 0x48, 0x43, 0x4d, 0x5d, 0x53, 0x1f, 0x47, 0x4e, 0x4b, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, ], [0x52, 0x54, 0x42, 0x47, 0x1f, 0x4d, 0x4e, 0x4d, 0x52, 0x44, 0x4d, 0x52, 0x44, 0x1b, 0x9f, 0x1c, 0x11, 0x4e, 0x56, 0x4b, 0x48, 0x4d, 0x46, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0xb, 0x4e, 0x55, 0x44, 0x1d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x9f, 0x50, 0x54, 0x48, 0x42, 0x4a, 0x4b, 0x58, 0x1f, 0x40, 0x51, 0x51, 0x44, 0x52, 0x53, 0x52, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x44, 0x4d, 0x53, 0x4b, 0x44, 0x1f, 0x47, 0x44, 0x40, 0x51, 0x53, 0x1d, 0x9f, 0x12, 0x44, 0x48, 0x59, 0x44, 0x43, 0x1f, 0x47, 0x48, 0x4c, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4c, 0xd8, 0x41, 0x44, 0x40, 0x54, 0x53, 0x48, 0x45, 0x54, 0x4b, 0x1f, 0x45, 0x4e, 0x51, 0x4c, 0x9f, 0x13, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x40, 0x52, 0x1f, 0x53, 0x40, 0x4a, 0x44, 0x4d, 0x9f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x4c, 0x44, 0x1d, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x4c, 0x40, 0x4d, 0x4d, 0x44, 0x51, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x52, 0x53, 0x48, 0x4b, 0xcb, ], [0x46, 0x51, 0x48, 0x44, 0x55, 0x44, 0x52, 0x1f, 0x4c, 0x44, 0x1b, 0x1f, 0xb, 0x4e, 0x55, 0x44, 0x1d, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x4f, 0x40, 0x51, 0x43, 0x4e, 0x4d, 0x52, 0x1f, 0x4d, 0x4e, 0x9f, 0x41, 0x44, 0x4b, 0x4e, 0x55, 0x44, 0x43, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x9f, 0x4b, 0x4e, 0x55, 0x48, 0x4d, 0x46, 0x1d, 0x1f, 0x53, 0x4e, 0x4e, 0x4a, 0x1f, 0x4c, 0x44, 0x1f, 0x52, 0xce, 0x52, 0x53, 0x51, 0x4e, 0x4d, 0x46, 0x4b, 0x58, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x43, 0x44, 0x4b, 0x48, 0x46, 0x47, 0x53, 0x1f, 0x48, 0x4d, 0x1f, 0x47, 0x48, 0x4c, 0x9f, 0x13, 0x47, 0x40, 0x53, 0x1d, 0x1f, 0x40, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x52, 0x44, 0x44, 0x1d, 0x9f, 0x48, 0x53, 0x1f, 0x52, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x40, 0x41, 0x40, 0x4d, 0x43, 0x4e, 0x4d, 0x52, 0x9f, ], [0x4c, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1b, 0x1b, 0x1b, 0x9f, 0x1c, 0x0, 0x4b, 0x48, 0x46, 0x47, 0x48, 0x44, 0x51, 0x48, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x1f, 0x42, 0x40, 0x4d, 0x4d, 0x4e, 0x53, 0x1f, 0x44, 0x57, 0x4f, 0x51, 0x44, 0x52, 0x52, 0x9f, 0x48, 0x53, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x52, 0x54, 0x51, 0x44, 0x4b, 0x58, 0x1f, 0x58, 0x4e, 0xd4, 0x40, 0x4d, 0x43, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x41, 0x4e, 0x43, 0x58, 0x1f, 0x47, 0x40, 0x55, 0xc4, 0x40, 0x1f, 0x4d, 0x4e, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x4e, 0x51, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0xc3, 0x41, 0x44, 0x1f, 0x40, 0x4d, 0x1f, 0x44, 0x57, 0x48, 0x52, 0x53, 0x44, 0x4d, 0x42, 0x44, 0x1f, 0x4e, 0xc5, 0x58, 0x4e, 0x54, 0x51, 0x52, 0x1f, 0x41, 0x44, 0x58, 0x4e, 0x4d, 0x43, 0x1f, 0x58, 0x4e, 0x54, 0x1b, 0x9f, 0x16, 0x47, 0x40, 0x53, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x54, 0x52, 0x44, 0x9f, ], [0x4e, 0x45, 0x1f, 0x4c, 0x58, 0x1f, 0x42, 0x51, 0x44, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1d, 0x1f, 0x48, 0xc5, 0x8, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x1f, 0x44, 0x4d, 0x53, 0x48, 0x51, 0x44, 0x4b, 0x58, 0x9f, 0x42, 0x4e, 0x4d, 0x53, 0x40, 0x48, 0x4d, 0x44, 0x43, 0x1f, 0x47, 0x44, 0x51, 0x44, 0x1e, 0x1f, 0xc, 0xd8, 0x46, 0x51, 0x44, 0x40, 0x53, 0x1f, 0x4c, 0x48, 0x52, 0x44, 0x51, 0x48, 0x44, 0x52, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x9f, 0x41, 0x44, 0x44, 0x4d, 0x1f, 0x7, 0x44, 0x40, 0x53, 0x47, 0x42, 0x4b, 0x48, 0x45, 0x45, 0x5d, 0x52, 0x9f, 0x4c, 0x48, 0x52, 0x44, 0x51, 0x48, 0x44, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x8, 0x9f, 0x56, 0x40, 0x53, 0x42, 0x47, 0x44, 0x43, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x45, 0x44, 0x4b, 0x53, 0x9f, ], [0x44, 0x40, 0x42, 0x47, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x4d, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x4c, 0x58, 0x9f, 0x46, 0x51, 0x44, 0x40, 0x53, 0x1f, 0x53, 0x47, 0x4e, 0x54, 0x46, 0x47, 0x53, 0x1f, 0x48, 0x4d, 0x9f, 0x4b, 0x48, 0x55, 0x48, 0x4d, 0x46, 0x1f, 0x48, 0x52, 0x1f, 0x47, 0x48, 0x4c, 0x52, 0x44, 0x4b, 0x45, 0x9b, 0x8, 0x45, 0x1f, 0x40, 0x4b, 0x4b, 0x1f, 0x44, 0x4b, 0x52, 0x44, 0x9f, 0x4f, 0x44, 0x51, 0x48, 0x52, 0x47, 0x44, 0x43, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x44, 0x9f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x44, 0x43, 0x1d, 0x1f, 0x8, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0xc3, 0x52, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x42, 0x4e, 0x4d, 0x53, 0x48, 0x4d, 0x54, 0x44, 0x1f, 0x53, 0x4e, 0x9f, ], [0x41, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x48, 0x45, 0x1f, 0x40, 0x4b, 0x4b, 0x9f, 0x44, 0x4b, 0x52, 0x44, 0x1f, 0x51, 0x44, 0x4c, 0x40, 0x48, 0x4d, 0x44, 0x43, 0x1d, 0x1f, 0x40, 0x4d, 0xc3, 0x47, 0x44, 0x1f, 0x56, 0x44, 0x51, 0x44, 0x9f, 0x40, 0x4d, 0x4d, 0x48, 0x47, 0x48, 0x4b, 0x40, 0x53, 0x44, 0x43, 0x1d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x44, 0x1f, 0x56, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x53, 0x54, 0x51, 0x4d, 0x1f, 0x53, 0x4e, 0x1f, 0x40, 0x1f, 0x4c, 0x48, 0x46, 0x47, 0x53, 0x58, 0x9f, 0x52, 0x53, 0x51, 0x40, 0x4d, 0x46, 0x44, 0x51, 0x1b, 0x1f, 0x8, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0xc3, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x44, 0x44, 0x4c, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x1f, 0x4e, 0xc5, ], [0x48, 0x53, 0x1b, 0x1f, 0x1c, 0x1, 0x51, 0x4e, 0x4d, 0x53, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x43, 0x51, 0x44, 0x40, 0x4c, 0x53, 0x1f, 0x48, 0x4d, 0x9f, 0x4c, 0x58, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x43, 0x51, 0x44, 0x40, 0x4c, 0x52, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x52, 0x53, 0x40, 0x58, 0x44, 0x43, 0x9f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4c, 0x44, 0x1f, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x40, 0x45, 0x53, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x42, 0x47, 0x40, 0x4d, 0x46, 0x44, 0xc3, 0x4c, 0x58, 0x1f, 0x48, 0x43, 0x44, 0x40, 0x52, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x58, 0x9f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x46, 0x4e, 0x4d, 0x44, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x1f, 0x4c, 0x44, 0x1d, 0x9f, ], [0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x56, 0x48, 0x4d, 0x44, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, 0x56, 0x40, 0x53, 0x44, 0x51, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x4b, 0x53, 0x44, 0x51, 0x44, 0xc3, 0x53, 0x47, 0x44, 0x1f, 0x42, 0x4e, 0x4b, 0x4e, 0x51, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x58, 0x9f, 0x4c, 0x48, 0x4d, 0x43, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x48, 0x52, 0x9f, 0x4e, 0x4d, 0x44, 0x1b, 0x1f, 0x8, 0x5d, 0x4c, 0x1f, 0x46, 0x4e, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x44, 0x4b, 0x4b, 0x1f, 0x48, 0x53, 0x1f, 0x1c, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x53, 0x40, 0x4a, 0xc4, 0x42, 0x40, 0x51, 0x44, 0x1f, 0x4d, 0x4e, 0x53, 0x1f, 0x53, 0x4e, 0x1f, 0x52, 0x4c, 0x48, 0x4b, 0x44, 0x9f, 0x40, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4f, 0x40, 0x51, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x48, 0x53, 0x9b, ], [0x1c, 0x1, 0x51, 0x4e, 0x4d, 0x53, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x13, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x53, 0x4e, 0x51, 0x58, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x53, 0x44, 0x40, 0x42, 0xc7, 0x47, 0x48, 0x52, 0x1f, 0x52, 0x4e, 0x4d, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x2, 0x51, 0x48, 0x52, 0x4f, 0x48, 0x4d, 0x1f, 0x2, 0x51, 0x48, 0x52, 0x4f, 0x48, 0x40, 0x4d, 0x9f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x4d, 0x44, 0x5d, 0x44, 0x51, 0x1f, 0x46, 0x4e, 0x1f, 0x41, 0x58, 0x9d, 0x5, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0x48, 0x52, 0x1f, 0x43, 0x40, 0x58, 0x1f, 0x53, 0x4e, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x44, 0x4d, 0x43, 0x48, 0x4d, 0x46, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1d, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x56, 0x44, 0x1f, 0x48, 0x4d, 0x9f, ], [0x48, 0x53, 0x1f, 0x52, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x9f, 0x51, 0x44, 0x4c, 0x44, 0x4c, 0x41, 0x44, 0x51, 0x44, 0x43, 0x1b, 0x1f, 0x16, 0x44, 0x9f, 0x45, 0x44, 0x56, 0x1d, 0x1f, 0x56, 0x44, 0x1f, 0x47, 0x40, 0x4f, 0x4f, 0x58, 0x1f, 0x45, 0x44, 0x56, 0x9d, 0x56, 0x44, 0x1f, 0x41, 0x40, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x9f, 0x41, 0x51, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x52, 0x1b, 0x1f, 0x5, 0x4e, 0x51, 0x1f, 0x47, 0x44, 0x9f, 0x53, 0x4e, 0x1c, 0x43, 0x40, 0x58, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x52, 0x47, 0x44, 0x43, 0x52, 0x9f, 0x47, 0x48, 0x52, 0x1f, 0x41, 0x4b, 0x4e, 0x4e, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x4c, 0x44, 0x9f, 0x12, 0x47, 0x40, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x1f, 0x4c, 0x58, 0x9f, ], [0x41, 0x51, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x1b, 0x9f, 0x1c, 0x12, 0x47, 0x40, 0x4a, 0x44, 0x52, 0x4f, 0x44, 0x40, 0x51, 0x44, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x7, 0x4e, 0x56, 0x44, 0x55, 0x44, 0x51, 0x1f, 0x4c, 0x44, 0x40, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x9f, 0x4b, 0x48, 0x45, 0x44, 0x1f, 0x48, 0x52, 0x1d, 0x1f, 0x4c, 0x44, 0x44, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x1f, 0x48, 0x53, 0x1b, 0x1f, 0x3, 0x4e, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x47, 0x54, 0x4d, 0x1f, 0x48, 0x53, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x42, 0x40, 0x4b, 0x4b, 0x1f, 0x48, 0x53, 0x1f, 0x47, 0x40, 0x51, 0x43, 0x9f, 0x4d, 0x40, 0x4c, 0x44, 0x52, 0x1b, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x4d, 0x4e, 0x53, 0x9f, 0x52, 0x4e, 0x1f, 0x41, 0x40, 0x43, 0x1f, 0x40, 0x52, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x40, 0x51, 0x44, 0x9b, 0x8, 0x53, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x52, 0x1f, 0x4f, 0x4e, 0x4e, 0x51, 0x44, 0x52, 0x53, 0x9f, ], [0x56, 0x47, 0x44, 0x4d, 0x1f, 0x58, 0x4e, 0x54, 0x1f, 0x40, 0x51, 0x44, 0x9f, 0x51, 0x48, 0x42, 0x47, 0x44, 0x52, 0x53, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x45, 0x40, 0x54, 0x4b, 0x53, 0x1c, 0x45, 0x48, 0x4d, 0x43, 0x44, 0x51, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x9f, 0x45, 0x48, 0x4d, 0x43, 0x1f, 0x45, 0x40, 0x54, 0x4b, 0x53, 0x52, 0x1f, 0x44, 0x55, 0x44, 0x4d, 0x9f, 0x48, 0x4d, 0x1f, 0x4f, 0x40, 0x51, 0x40, 0x43, 0x48, 0x52, 0x44, 0x1b, 0x1f, 0xb, 0x4e, 0x55, 0x44, 0x9f, 0x58, 0x4e, 0x54, 0x51, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x4f, 0x4e, 0x4e, 0x51, 0x1f, 0x40, 0xd2, 0x48, 0x53, 0x1f, 0x48, 0x52, 0x1b, 0x1f, 0x18, 0x4e, 0x54, 0x1f, 0x4c, 0x40, 0x58, 0x9f, 0x4f, 0x44, 0x51, 0x47, 0x40, 0x4f, 0x52, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x52, 0x4e, 0x4c, 0x44, 0x9f, ], [0x4f, 0x4b, 0x44, 0x40, 0x52, 0x40, 0x4d, 0x53, 0x1d, 0x9f, 0x53, 0x47, 0x51, 0x48, 0x4b, 0x4b, 0x48, 0x4d, 0x46, 0x1d, 0x9f, 0x46, 0x4b, 0x4e, 0x51, 0x48, 0x4e, 0x54, 0x52, 0x1f, 0x47, 0x4e, 0x54, 0x51, 0x52, 0x1d, 0x9f, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x9f, 0x4f, 0x4e, 0x4e, 0x51, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x52, 0x44, 0x53, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x52, 0x54, 0x4d, 0x1f, 0x48, 0x52, 0x9f, 0x51, 0x44, 0x45, 0x4b, 0x44, 0x42, 0x53, 0x44, 0x43, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x1f, 0x53, 0x47, 0xc4, 0x56, 0x48, 0x4d, 0x43, 0x4e, 0x56, 0x52, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, ], [0x40, 0x4b, 0x4c, 0x52, 0x47, 0x4e, 0x54, 0x52, 0x44, 0x1f, 0x40, 0x52, 0x9f, 0x41, 0x51, 0x48, 0x46, 0x47, 0x53, 0x4b, 0x58, 0x1f, 0x40, 0x52, 0x1f, 0x45, 0x51, 0x4e, 0x4c, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x51, 0x48, 0x42, 0x47, 0x1f, 0x4c, 0x40, 0x4d, 0x5d, 0x52, 0x9f, 0x40, 0x41, 0x4e, 0x43, 0x44, 0x1b, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4d, 0x4e, 0x56, 0x9f, 0x4c, 0x44, 0x4b, 0x53, 0x52, 0x1f, 0x41, 0x44, 0x45, 0x4e, 0x51, 0x44, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x43, 0x4e, 0x4e, 0x51, 0x1f, 0x40, 0x52, 0x1f, 0x44, 0x40, 0x51, 0x4b, 0x58, 0x1f, 0x48, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x52, 0x4f, 0x51, 0x48, 0x4d, 0x46, 0x1b, 0x1f, 0x8, 0x1f, 0x43, 0x4e, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x44, 0x44, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x40, 0x9f, ], [0x50, 0x54, 0x48, 0x44, 0x53, 0x1f, 0x4c, 0x48, 0x4d, 0x43, 0x1f, 0x4c, 0x40, 0x58, 0x9f, 0x4b, 0x48, 0x55, 0x44, 0x1f, 0x40, 0x52, 0x9f, 0x42, 0x4e, 0x4d, 0x53, 0x44, 0x4d, 0x53, 0x44, 0x43, 0x4b, 0x58, 0x1f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x9d, 0x40, 0x4d, 0x43, 0x1f, 0x47, 0x40, 0x55, 0x44, 0x1f, 0x40, 0x52, 0x9f, 0x42, 0x47, 0x44, 0x44, 0x51, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x4e, 0x54, 0x46, 0x47, 0x53, 0x52, 0x9d, 0x40, 0x52, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x4b, 0x40, 0x42, 0x44, 0x1b, 0x9f, 0x1c, 0x13, 0x47, 0x4e, 0x51, 0x44, 0x40, 0x54, 0x1f, 0x9f, 0x9f, ]], +[[0x16, 0x44, 0x1f, 0x4c, 0x54, 0x52, 0x53, 0x1f, 0x4b, 0x44, 0x40, 0x51, 0x4d, 0x1f, 0x53, 0x4e, 0x9f, 0x51, 0x44, 0x40, 0x56, 0x40, 0x4a, 0x44, 0x4d, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4a, 0x44, 0x44, 0x4f, 0x9f, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x40, 0x56, 0x40, 0x4a, 0x44, 0x1d, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x41, 0x58, 0x1f, 0x4c, 0x44, 0x42, 0x47, 0x40, 0x4d, 0x48, 0x42, 0x40, 0x4b, 0x9f, 0x40, 0x48, 0x43, 0x52, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x41, 0x58, 0x1f, 0x40, 0x4d, 0x9f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x9f, 0x44, 0x57, 0x4f, 0x44, 0x42, 0x53, 0x40, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0xc4, 0x43, 0x40, 0x56, 0x4d, 0x1d, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x43, 0x4e, 0x44, 0x52, 0x9f, ], [0x4d, 0x4e, 0x53, 0x1f, 0x45, 0x4e, 0x51, 0x52, 0x40, 0x4a, 0x44, 0x1f, 0x54, 0x52, 0x9f, 0x44, 0x55, 0x44, 0x4d, 0x1f, 0x48, 0x4d, 0x1f, 0x4e, 0x54, 0x51, 0x9f, 0x52, 0x4e, 0x54, 0x4d, 0x43, 0x44, 0x52, 0x53, 0x1f, 0x52, 0x4b, 0x44, 0x44, 0x4f, 0x1b, 0x1f, 0x8, 0x9f, 0x4a, 0x4d, 0x4e, 0x56, 0x1f, 0x4e, 0x45, 0x1f, 0x4d, 0x4e, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x44, 0x4d, 0x42, 0x4e, 0x54, 0x51, 0x40, 0x46, 0x48, 0x4d, 0x46, 0x1f, 0x45, 0x40, 0x42, 0x53, 0x9f, 0x53, 0x47, 0x40, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x54, 0x4d, 0x50, 0x54, 0x44, 0x52, 0x53, 0x48, 0x4e, 0x4d, 0x40, 0x41, 0x4b, 0x44, 0x9f, 0x40, 0x41, 0x48, 0x4b, 0x48, 0x53, 0x58, 0x1f, 0x4e, 0x45, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x53, 0x4e, 0x9f, ], [0x44, 0x4b, 0x44, 0x55, 0x40, 0x53, 0x44, 0x1f, 0x47, 0x48, 0x52, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x9f, 0x41, 0x58, 0x1f, 0x40, 0x1f, 0x42, 0x4e, 0x4d, 0x52, 0x42, 0x48, 0x4e, 0x54, 0x52, 0x9f, 0x44, 0x4d, 0x43, 0x44, 0x40, 0x55, 0x4e, 0x54, 0x51, 0x1b, 0x1f, 0x8, 0x53, 0x1f, 0x48, 0x52, 0x9f, 0x52, 0x4e, 0x4c, 0x44, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x9f, 0x40, 0x41, 0x4b, 0x44, 0x1f, 0x53, 0x4e, 0x1f, 0x4f, 0x40, 0x48, 0x4d, 0x53, 0x1f, 0x40, 0x9f, 0x4f, 0x40, 0x51, 0x53, 0x48, 0x42, 0x54, 0x4b, 0x40, 0x51, 0x9f, 0x4f, 0x48, 0x42, 0x53, 0x54, 0x51, 0x44, 0x1d, 0x1f, 0x4e, 0x51, 0x1f, 0x53, 0x4e, 0x9f, 0x42, 0x40, 0x51, 0x55, 0x44, 0x1f, 0x40, 0x1f, 0x52, 0x53, 0x40, 0x53, 0x54, 0x44, 0x1d, 0x9f, ], [0x40, 0x4d, 0x43, 0x1f, 0x52, 0x4e, 0x1f, 0x53, 0x4e, 0x1f, 0x4c, 0x40, 0x4a, 0x44, 0x1f, 0x40, 0x9f, 0x45, 0x44, 0x56, 0x1f, 0x4e, 0x41, 0x49, 0x44, 0x42, 0x53, 0x52, 0x9f, 0x41, 0x44, 0x40, 0x54, 0x53, 0x48, 0x45, 0x54, 0x4b, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x1f, 0x48, 0x53, 0x9f, 0x48, 0x52, 0x1f, 0x45, 0x40, 0x51, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x46, 0x4b, 0x4e, 0x51, 0x48, 0x4e, 0x54, 0x52, 0x1f, 0x53, 0x4e, 0x1f, 0x42, 0x40, 0x51, 0x55, 0x44, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x40, 0x48, 0x4d, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x55, 0x44, 0x51, 0xd8, 0x40, 0x53, 0x4c, 0x4e, 0x52, 0x4f, 0x47, 0x44, 0x51, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x4c, 0x44, 0x43, 0x48, 0x54, 0x4c, 0x1f, 0x53, 0x47, 0x51, 0x4e, 0x54, 0x46, 0x47, 0x9f, ], [0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x56, 0x44, 0x1f, 0x4b, 0x4e, 0x4e, 0x4a, 0x1d, 0x9f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x4c, 0x4e, 0x51, 0x40, 0x4b, 0x4b, 0x58, 0x1f, 0x56, 0x44, 0x9f, 0x42, 0x40, 0x4d, 0x1f, 0x43, 0x4e, 0x1b, 0x1f, 0x13, 0x4e, 0x1f, 0x40, 0x45, 0x45, 0x44, 0x42, 0x53, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x50, 0x54, 0x40, 0x4b, 0x48, 0x53, 0x58, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0xc4, 0x43, 0x40, 0x58, 0x1d, 0x1f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x47, 0x48, 0x46, 0x47, 0x44, 0x52, 0x53, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x51, 0x53, 0x52, 0x1b, 0x9f, 0x1c, 0x13, 0x47, 0x4e, 0x51, 0x44, 0x40, 0x54, 0x1f, 0x9f, 0x9f, ]], +[[0x8, 0x45, 0x1f, 0x4e, 0x4d, 0x44, 0x1f, 0x40, 0x43, 0x55, 0x40, 0x4d, 0x42, 0x44, 0x52, 0x9f, 0x42, 0x4e, 0x4d, 0x45, 0x48, 0x43, 0x44, 0x4d, 0x53, 0x4b, 0x58, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0xc4, 0x43, 0x48, 0x51, 0x44, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x4e, 0x45, 0x1f, 0x47, 0x48, 0x52, 0x9f, 0x43, 0x51, 0x44, 0x40, 0x4c, 0x52, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x44, 0x4d, 0x43, 0x44, 0x40, 0x55, 0x4e, 0x51, 0x52, 0x1f, 0x53, 0x4e, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1f, 0x56, 0x47, 0x48, 0x42, 0x47, 0x1f, 0x47, 0x44, 0x9f, 0x47, 0x40, 0x52, 0x1f, 0x48, 0x4c, 0x40, 0x46, 0x48, 0x4d, 0x44, 0x43, 0x1d, 0x1f, 0x47, 0x44, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4c, 0x44, 0x44, 0x53, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x1f, 0x40, 0x9f, ], [0x52, 0x54, 0x42, 0x42, 0x44, 0x52, 0x52, 0x1f, 0x54, 0x4d, 0x44, 0x57, 0x4f, 0x44, 0x42, 0x53, 0x44, 0xc3, 0x48, 0x4d, 0x1f, 0x42, 0x4e, 0x4c, 0x4c, 0x4e, 0x4d, 0x1f, 0x47, 0x4e, 0x54, 0x51, 0x52, 0x1b, 0x9f, 0x7, 0x44, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4f, 0x54, 0x53, 0x1f, 0x52, 0x4e, 0x4c, 0x44, 0x9f, 0x53, 0x47, 0x48, 0x4d, 0x46, 0x52, 0x1f, 0x41, 0x44, 0x47, 0x48, 0x4d, 0x43, 0x1d, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x1f, 0x40, 0x4d, 0x9f, 0x48, 0x4d, 0x55, 0x48, 0x52, 0x48, 0x41, 0x4b, 0x44, 0x9f, 0x41, 0x4e, 0x54, 0x4d, 0x43, 0x40, 0x51, 0x58, 0x1b, 0x1f, 0xd, 0x44, 0x56, 0x1d, 0x9f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0x40, 0x4b, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, ], [0x4c, 0x4e, 0x51, 0x44, 0x1f, 0x4b, 0x48, 0x41, 0x44, 0x51, 0x40, 0x4b, 0x1f, 0x4b, 0x40, 0x56, 0x52, 0x9f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x41, 0x44, 0x46, 0x48, 0x4d, 0x1f, 0x53, 0x4e, 0x9f, 0x44, 0x52, 0x53, 0x40, 0x41, 0x4b, 0x48, 0x52, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x4c, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x40, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x9f, 0x40, 0x4d, 0x43, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x48, 0x4d, 0x1f, 0x47, 0x48, 0x4c, 0x1d, 0x1f, 0x4e, 0xd1, 0x53, 0x47, 0x44, 0x1f, 0x4e, 0x4b, 0x43, 0x1f, 0x4b, 0x40, 0x56, 0x52, 0x1f, 0x41, 0x44, 0x9f, 0x44, 0x57, 0x4f, 0x40, 0x4d, 0x43, 0x44, 0x43, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x48, 0x4d, 0x53, 0x44, 0x51, 0x4f, 0x51, 0x44, 0x53, 0x44, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x47, 0x48, 0xd2, ], [0x45, 0x40, 0x55, 0x4e, 0x51, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x4c, 0x4e, 0x51, 0x44, 0x9f, 0x4b, 0x48, 0x41, 0x44, 0x51, 0x40, 0x4b, 0x1f, 0x52, 0x44, 0x4d, 0x52, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0xc3, 0x47, 0x44, 0x1f, 0x56, 0x48, 0x4b, 0x4b, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x42, 0x44, 0x4d, 0x52, 0x44, 0x1f, 0x4e, 0x45, 0x1f, 0x40, 0x9f, 0x47, 0x48, 0x46, 0x47, 0x44, 0x51, 0x1f, 0x4e, 0x51, 0x43, 0x44, 0x51, 0x1f, 0x4e, 0x45, 0x9f, 0x41, 0x44, 0x48, 0x4d, 0x46, 0x52, 0x1b, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x8, 0x5d, 0x4c, 0x1f, 0x4d, 0x4e, 0x41, 0x4e, 0x43, 0x58, 0x1a, 0x1f, 0x16, 0x47, 0x4e, 0x9f, 0x40, 0x51, 0x44, 0x1f, 0x58, 0x4e, 0x54, 0x1e, 0x1f, 0x0, 0x51, 0x44, 0x1f, 0x58, 0x4e, 0x54, 0x9f, 0x4d, 0x4e, 0x41, 0x4e, 0x43, 0x58, 0x1d, 0x1f, 0x53, 0x4e, 0x4e, 0x1e, 0x1f, 0x13, 0x47, 0x44, 0x4d, 0x9f, 0x53, 0x47, 0x44, 0x51, 0x44, 0x5d, 0x52, 0x1f, 0x40, 0x1f, 0x4f, 0x40, 0x48, 0x51, 0x1f, 0x4e, 0x45, 0x9f, 0x54, 0x52, 0x1d, 0x1f, 0x43, 0x4e, 0x4d, 0x5d, 0x53, 0x1f, 0x53, 0x44, 0x4b, 0x4b, 0x1a, 0x9f, 0x13, 0x47, 0x44, 0x58, 0x5d, 0x43, 0x1f, 0x41, 0x40, 0x4d, 0x48, 0x52, 0x47, 0x1f, 0x54, 0x52, 0x1d, 0x9f, 0x58, 0x4e, 0x54, 0x1f, 0x4a, 0x4d, 0x4e, 0x56, 0x1b, 0x1f, 0x7, 0x4e, 0x56, 0x9f, 0x43, 0x51, 0x44, 0x40, 0x51, 0x58, 0x1f, 0x53, 0x4e, 0x1f, 0x41, 0x44, 0x9f, ], [0x52, 0x4e, 0x4c, 0x44, 0x41, 0x4e, 0x43, 0x58, 0x1a, 0x1f, 0x7, 0x4e, 0x56, 0x9f, 0x4f, 0x54, 0x41, 0x4b, 0x48, 0x42, 0x1d, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x40, 0x9f, 0x45, 0x51, 0x4e, 0x46, 0x1f, 0x13, 0x4e, 0x1f, 0x53, 0x44, 0x4b, 0x4b, 0x1f, 0x58, 0x4e, 0x54, 0x51, 0x9f, 0x4d, 0x40, 0x4c, 0x44, 0x1f, 0x53, 0x47, 0x44, 0x1f, 0x4b, 0x48, 0x55, 0x44, 0x4b, 0x4e, 0x4d, 0x46, 0x9f, 0x43, 0x40, 0x58, 0x1f, 0x13, 0x4e, 0x1f, 0x40, 0x4d, 0x1f, 0x40, 0x43, 0x4c, 0x48, 0x51, 0x48, 0x4d, 0xc6, 0x41, 0x4e, 0x46, 0x1a, 0x1f, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x48, 0x4d, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, ]], +[[0x7, 0x4e, 0x56, 0x1f, 0x47, 0x40, 0x4f, 0x4f, 0x58, 0x1f, 0x48, 0x52, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x4b, 0x48, 0x53, 0x53, 0x4b, 0x44, 0x1f, 0x52, 0x53, 0x4e, 0x4d, 0x44, 0x1f, 0x13, 0x47, 0x40, 0x53, 0x9f, 0x51, 0x40, 0x4c, 0x41, 0x4b, 0x44, 0x52, 0x1f, 0x48, 0x4d, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x51, 0x4e, 0x40, 0x43, 0x1f, 0x40, 0x4b, 0x4e, 0x4d, 0x44, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x43, 0x4e, 0x44, 0x52, 0x4d, 0x5d, 0x53, 0x1f, 0x42, 0x40, 0x51, 0x44, 0x1f, 0x40, 0x41, 0x4e, 0x54, 0xd3, 0x42, 0x40, 0x51, 0x44, 0x44, 0x51, 0x52, 0x1d, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x44, 0x57, 0x48, 0x46, 0x44, 0x4d, 0x42, 0x48, 0x44, 0x52, 0x1f, 0x4d, 0x44, 0x55, 0x44, 0x51, 0x9f, 0x45, 0x44, 0x40, 0x51, 0x52, 0x1b, 0x1f, 0x16, 0x47, 0x4e, 0x52, 0x44, 0x1f, 0x42, 0x4e, 0x40, 0x53, 0x9f, ], [0x4e, 0x45, 0x1f, 0x44, 0x4b, 0x44, 0x4c, 0x44, 0x4d, 0x53, 0x40, 0x4b, 0x1f, 0x41, 0x51, 0x4e, 0x56, 0xcd, 0x0, 0x1f, 0x4f, 0x40, 0x52, 0x52, 0x48, 0x4d, 0x46, 0x1f, 0x54, 0x4d, 0x48, 0x55, 0x44, 0x51, 0x52, 0xc4, 0x4f, 0x54, 0x53, 0x1f, 0x4e, 0x4d, 0x1b, 0x1f, 0x0, 0x4d, 0x43, 0x9f, 0x48, 0x4d, 0x43, 0x44, 0x4f, 0x44, 0x4d, 0x43, 0x44, 0x4d, 0x53, 0x1f, 0x40, 0x52, 0x1f, 0x53, 0x47, 0xc4, 0x52, 0x54, 0x4d, 0x1d, 0x1f, 0x0, 0x52, 0x52, 0x4e, 0x42, 0x48, 0x40, 0x53, 0x44, 0x52, 0x1f, 0x4e, 0xd1, 0x46, 0x4b, 0x4e, 0x56, 0x52, 0x1f, 0x40, 0x4b, 0x4e, 0x4d, 0x44, 0x1d, 0x9f, 0x5, 0x54, 0x4b, 0x45, 0x48, 0x4b, 0x4b, 0x48, 0x4d, 0x46, 0x9f, 0x40, 0x41, 0x52, 0x4e, 0x4b, 0x54, 0x53, 0x44, 0x1f, 0x43, 0x44, 0x42, 0x51, 0x44, 0x44, 0x1f, 0x8, 0xcd, ], [0x42, 0x40, 0x52, 0x54, 0x40, 0x4b, 0x1f, 0x52, 0x48, 0x4c, 0x4f, 0x4b, 0x48, 0x42, 0x48, 0x53, 0x58, 0x9b, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x48, 0x4d, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, ]], +[[0x1, 0x44, 0x42, 0x40, 0x54, 0x52, 0x44, 0x1f, 0x8, 0x1f, 0x42, 0x4e, 0x54, 0x4b, 0x43, 0x9f, 0x4d, 0x4e, 0x53, 0x1f, 0x52, 0x53, 0x4e, 0x4f, 0x1f, 0x45, 0x4e, 0x51, 0x1f, 0x43, 0x44, 0x40, 0x53, 0xc7, 0x7, 0x44, 0x1f, 0x4a, 0x48, 0x4d, 0x43, 0x4b, 0x58, 0x1f, 0x52, 0x53, 0x4e, 0x4f, 0x4f, 0x44, 0x43, 0x9f, 0x45, 0x4e, 0x51, 0x1f, 0x4c, 0x44, 0x1b, 0x1f, 0x13, 0x47, 0x44, 0x9f, 0x42, 0x40, 0x51, 0x51, 0x48, 0x40, 0x46, 0x44, 0x1f, 0x47, 0x44, 0x4b, 0x43, 0x1f, 0x41, 0x54, 0x53, 0x9f, 0x49, 0x54, 0x52, 0x53, 0x1f, 0x4e, 0x54, 0x51, 0x52, 0x44, 0x4b, 0x55, 0x44, 0x52, 0x1f, 0x0, 0x4d, 0xc3, 0x48, 0x4c, 0x4c, 0x4e, 0x51, 0x53, 0x40, 0x4b, 0x48, 0x53, 0x58, 0x9f, 0x1c, 0x3, 0x48, 0x42, 0x4a, 0x48, 0x4d, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, ]], +[[0x5, 0x4e, 0x51, 0x1d, 0x1f, 0x4b, 0x48, 0x4a, 0x44, 0x1f, 0x40, 0x4b, 0x4c, 0x4e, 0x52, 0x53, 0x9f, 0x44, 0x55, 0x44, 0x51, 0x58, 0x4e, 0x4d, 0x44, 0x1f, 0x44, 0x4b, 0x52, 0x44, 0x1f, 0x48, 0x4d, 0x9f, 0x4e, 0x54, 0x51, 0x1f, 0x42, 0x4e, 0x54, 0x4d, 0x53, 0x51, 0x58, 0x1d, 0x1f, 0x8, 0x9f, 0x52, 0x53, 0x40, 0x51, 0x53, 0x44, 0x43, 0x1f, 0x4e, 0x54, 0x53, 0x1f, 0x56, 0x48, 0x53, 0x47, 0x9f, 0x4c, 0x58, 0x1f, 0x52, 0x47, 0x40, 0x51, 0x44, 0x1f, 0x4e, 0x45, 0x9f, 0x4e, 0x4f, 0x53, 0x48, 0x4c, 0x48, 0x52, 0x4c, 0x1b, 0x1f, 0x8, 0x9f, 0x41, 0x44, 0x4b, 0x48, 0x44, 0x55, 0x44, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x47, 0x40, 0x51, 0x43, 0x9f, 0x56, 0x4e, 0x51, 0x4a, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x4f, 0x51, 0x4e, 0x46, 0x51, 0x44, 0x52, 0x52, 0x9f, ], [0x40, 0x4d, 0x43, 0x1f, 0x40, 0x42, 0x53, 0x48, 0x4e, 0x4d, 0x1d, 0x1f, 0x41, 0x54, 0x53, 0x9f, 0x4d, 0x4e, 0x56, 0x1d, 0x1f, 0x40, 0x45, 0x53, 0x44, 0x51, 0x1f, 0x45, 0x48, 0x51, 0x52, 0x53, 0x9f, 0x41, 0x44, 0x48, 0x4d, 0x46, 0x1f, 0x5d, 0x45, 0x4e, 0x51, 0x5d, 0x9f, 0x52, 0x4e, 0x42, 0x48, 0x44, 0x53, 0x58, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x53, 0x47, 0x44, 0x4d, 0x9f, 0x5d, 0x40, 0x46, 0x40, 0x48, 0x4d, 0x52, 0x53, 0x5d, 0x1f, 0x48, 0x53, 0x1d, 0x1f, 0x8, 0x9f, 0x40, 0x52, 0x52, 0x48, 0x46, 0x4d, 0x1f, 0x4c, 0x58, 0x52, 0x44, 0x4b, 0x45, 0x1f, 0x4d, 0x4e, 0x9f, 0x51, 0x40, 0x4d, 0x4a, 0x1f, 0x4e, 0x51, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4b, 0x48, 0x4c, 0x48, 0x53, 0x9d, 0x40, 0x4d, 0x43, 0x1f, 0x52, 0x54, 0x42, 0x47, 0x1f, 0x40, 0x4d, 0x9f, ], [0x40, 0x53, 0x53, 0x48, 0x53, 0x54, 0x43, 0x44, 0x1f, 0x48, 0x52, 0x1f, 0x55, 0x44, 0x51, 0x58, 0x9f, 0x4c, 0x54, 0x42, 0x47, 0x1f, 0x40, 0x46, 0x40, 0x48, 0x4d, 0x52, 0x53, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x53, 0x51, 0x44, 0x4d, 0x43, 0x1f, 0x4e, 0x45, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x53, 0x48, 0x4c, 0x44, 0x52, 0x1b, 0x1f, 0x1, 0x54, 0x53, 0x1f, 0x4c, 0x58, 0x9f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1f, 0x47, 0x40, 0x52, 0x1f, 0x41, 0x44, 0x42, 0x4e, 0x4c, 0x44, 0x9f, 0x4e, 0x4d, 0x44, 0x1f, 0x4e, 0x45, 0x1f, 0x48, 0x4d, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x44, 0x9f, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x48, 0x4b, 0x48, 0x53, 0x48, 0x44, 0x52, 0x1b, 0x9f, 0x16, 0x47, 0x40, 0x53, 0x1f, 0x40, 0x1f, 0x4f, 0x47, 0x51, 0x40, 0x52, 0x44, 0x1f, 0x1c, 0x9f, ], [0x52, 0x53, 0x48, 0x4b, 0x4b, 0x1f, 0x48, 0x53, 0x5d, 0x52, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x9f, 0x4f, 0x47, 0x51, 0x40, 0x52, 0x44, 0x1f, 0x40, 0x4d, 0x43, 0x1f, 0x40, 0x1f, 0x46, 0x4e, 0x4e, 0x43, 0x9f, 0x55, 0x48, 0x44, 0x56, 0x1f, 0x4e, 0x45, 0x1f, 0x4b, 0x48, 0x45, 0x44, 0x1d, 0x1f, 0x40, 0x4d, 0x43, 0x9f, 0x40, 0x1f, 0x4c, 0x40, 0x4d, 0x1f, 0x52, 0x47, 0x4e, 0x54, 0x4b, 0x43, 0x4d, 0x5d, 0x53, 0x9f, 0x40, 0x42, 0x42, 0x44, 0x4f, 0x53, 0x1f, 0x40, 0x4d, 0x58, 0x1f, 0x4e, 0x53, 0x47, 0x44, 0x51, 0x1b, 0x9f, 0x53, 0x47, 0x40, 0x53, 0x1f, 0x4c, 0x54, 0x42, 0x47, 0x1f, 0x8, 0x5d, 0x55, 0x44, 0x9f, 0x4b, 0x44, 0x40, 0x51, 0x4d, 0x44, 0x43, 0x9f, 0x54, 0x4d, 0x43, 0x44, 0x51, 0x46, 0x51, 0x4e, 0x54, 0x4d, 0x43, 0x1b, 0x1f, 0x14, 0x4d, 0x53, 0x48, 0xcb, ], [0x52, 0x4e, 0x4c, 0x44, 0x1f, 0x46, 0x40, 0x4d, 0x46, 0x1f, 0x52, 0x54, 0x42, 0x42, 0x44, 0x44, 0x43, 0xd2, 0x48, 0x4d, 0x1f, 0x4f, 0x54, 0x53, 0x53, 0x48, 0x4d, 0x46, 0x1f, 0x53, 0x47, 0x44, 0x9f, 0x56, 0x4e, 0x51, 0x4b, 0x43, 0x1f, 0x48, 0x4d, 0x1f, 0x40, 0x1f, 0x52, 0x53, 0x51, 0x40, 0x48, 0x53, 0x9f, 0x49, 0x40, 0x42, 0x4a, 0x44, 0x53, 0x1d, 0x1f, 0x48, 0x53, 0x52, 0x9f, 0x43, 0x44, 0x45, 0x48, 0x4d, 0x48, 0x53, 0x48, 0x4e, 0x4d, 0x1f, 0x48, 0x52, 0x9f, 0x4f, 0x4e, 0x52, 0x52, 0x48, 0x41, 0x48, 0x4b, 0x48, 0x53, 0x58, 0x1b, 0x9f, 0x1c, 0x4, 0x4b, 0x4b, 0x48, 0x52, 0x4e, 0x4d, 0x1f, 0x9f, 0x9f, ]], +] \ No newline at end of file diff --git a/worlds/smw/Names/LocationName.py b/worlds/smw/Names/LocationName.py new file mode 100644 index 0000000000..2b42cd21a4 --- /dev/null +++ b/worlds/smw/Names/LocationName.py @@ -0,0 +1,364 @@ +# Level Definitions +yoshis_house = "Yoshi's House" +yoshis_house_tile = "Yoshi's House - Tile" + +yoshis_island_1_exit_1 = "Yoshi's Island 1 - Normal Exit" +yoshis_island_1_dragon = "Yoshi's Island 1 - Dragon Coins" +yoshis_island_2_exit_1 = "Yoshi's Island 2 - Normal Exit" +yoshis_island_2_dragon = "Yoshi's Island 2 - Dragon Coins" +yoshis_island_3_exit_1 = "Yoshi's Island 3 - Normal Exit" +yoshis_island_3_dragon = "Yoshi's Island 3 - Dragon Coins" +yoshis_island_4_exit_1 = "Yoshi's Island 4 - Normal Exit" +yoshis_island_4_dragon = "Yoshi's Island 4 - Dragon Coins" +yoshis_island_castle = "#1 Iggy's Castle - Normal Exit" +yoshis_island_koopaling = "#1 Iggy's Castle - Boss" + +yellow_switch_palace = "Yellow Switch Palace" + +donut_plains_1_exit_1 = "Donut Plains 1 - Normal Exit" +donut_plains_1_exit_2 = "Donut Plains 1 - Secret Exit" +donut_plains_1_dragon = "Donut Plains 1 - Dragon Coins" +donut_plains_2_exit_1 = "Donut Plains 2 - Normal Exit" +donut_plains_2_exit_2 = "Donut Plains 2 - Secret Exit" +donut_plains_2_dragon = "Donut Plains 2 - Dragon Coins" +donut_plains_3_exit_1 = "Donut Plains 3 - Normal Exit" +donut_plains_3_dragon = "Donut Plains 3 - Dragon Coins" +donut_plains_4_exit_1 = "Donut Plains 4 - Normal Exit" +donut_plains_4_dragon = "Donut Plains 4 - Dragon Coins" +donut_secret_1_exit_1 = "Donut Secret 1 - Normal Exit" +donut_secret_1_exit_2 = "Donut Secret 1 - Secret Exit" +donut_secret_1_dragon = "Donut Secret 1 - Dragon Coins" +donut_secret_2_exit_1 = "Donut Secret 2 - Normal Exit" +donut_secret_2_dragon = "Donut Secret 2 - Dragon Coins" +donut_ghost_house_exit_1 = "Donut Ghost House - Normal Exit" +donut_ghost_house_exit_2 = "Donut Ghost House - Secret Exit" +donut_secret_house_exit_1 = "Donut Secret House - Normal Exit" +donut_secret_house_exit_2 = "Donut Secret House - Secret Exit" +donut_plains_castle = "#2 Morton's Castle - Normal Exit" +donut_plains_koopaling = "#2 Morton's Castle - Boss" + +green_switch_palace = "Green Switch Palace" + +vanilla_dome_1_exit_1 = "Vanilla Dome 1 - Normal Exit" +vanilla_dome_1_exit_2 = "Vanilla Dome 1 - Secret Exit" +vanilla_dome_1_dragon = "Vanilla Dome 1 - Dragon Coins" +vanilla_dome_2_exit_1 = "Vanilla Dome 2 - Normal Exit" +vanilla_dome_2_exit_2 = "Vanilla Dome 2 - Secret Exit" +vanilla_dome_2_dragon = "Vanilla Dome 2 - Dragon Coins" +vanilla_dome_3_exit_1 = "Vanilla Dome 3 - Normal Exit" +vanilla_dome_3_dragon = "Vanilla Dome 3 - Dragon Coins" +vanilla_dome_4_exit_1 = "Vanilla Dome 4 - Normal Exit" +vanilla_dome_4_dragon = "Vanilla Dome 4 - Dragon Coins" +vanilla_secret_1_exit_1 = "Vanilla Secret 1 - Normal Exit" +vanilla_secret_1_exit_2 = "Vanilla Secret 1 - Secret Exit" +vanilla_secret_1_dragon = "Vanilla Secret 1 - Dragon Coins" +vanilla_secret_2_exit_1 = "Vanilla Secret 2 - Normal Exit" +vanilla_secret_2_dragon = "Vanilla Secret 2 - Dragon Coins" +vanilla_secret_3_exit_1 = "Vanilla Secret 3 - Normal Exit" +vanilla_secret_3_dragon = "Vanilla Secret 3 - Dragon Coins" +vanilla_ghost_house_exit_1 = "Vanilla Ghost House - Normal Exit" +vanilla_ghost_house_dragon = "Vanilla Ghost House - Dragon Coins" +vanilla_fortress = "Vanilla Fortress - Normal Exit" +vanilla_reznor = "Vanilla Fortress - Boss" +vanilla_dome_castle = "#3 Lemmy's Castle - Normal Exit" +vanilla_dome_koopaling = "#3 Lemmy's Castle - Boss" + +red_switch_palace = "Red Switch Palace" + +butter_bridge_1_exit_1 = "Butter Bridge 1 - Normal Exit" +butter_bridge_1_dragon = "Butter Bridge 1 - Dragon Coins" +butter_bridge_2_exit_1 = "Butter Bridge 2 - Normal Exit" +butter_bridge_2_dragon = "Butter Bridge 2 - Dragon Coins" +cheese_bridge_exit_1 = "Cheese Bridge - Normal Exit" +cheese_bridge_exit_2 = "Cheese Bridge - Secret Exit" +cheese_bridge_dragon = "Cheese Bridge - Dragon Coins" +cookie_mountain_exit_1 = "Cookie Mountain - Normal Exit" +cookie_mountain_dragon = "Cookie Mountain - Dragon Coins" +soda_lake_exit_1 = "Soda Lake - Normal Exit" +soda_lake_dragon = "Soda Lake - Dragon Coins" +twin_bridges_castle = "#4 Ludwig's Castle - Normal Exit" +twin_bridges_koopaling = "#4 Ludwig's Castle - Boss" + +forest_of_illusion_1_exit_1 = "Forest of Illusion 1 - Normal Exit" +forest_of_illusion_1_exit_2 = "Forest of Illusion 1 - Secret Exit" +forest_of_illusion_2_exit_1 = "Forest of Illusion 2 - Normal Exit" +forest_of_illusion_2_exit_2 = "Forest of Illusion 2 - Secret Exit" +forest_of_illusion_2_dragon = "Forest of Illusion 2 - Dragon Coins" +forest_of_illusion_3_exit_1 = "Forest of Illusion 3 - Normal Exit" +forest_of_illusion_3_exit_2 = "Forest of Illusion 3 - Secret Exit" +forest_of_illusion_3_dragon = "Forest of Illusion 3 - Dragon Coins" +forest_of_illusion_4_exit_1 = "Forest of Illusion 4 - Normal Exit" +forest_of_illusion_4_exit_2 = "Forest of Illusion 4 - Secret Exit" +forest_of_illusion_4_dragon = "Forest of Illusion 4 - Dragon Coins" +forest_ghost_house_exit_1 = "Forest Ghost House - Normal Exit" +forest_ghost_house_exit_2 = "Forest Ghost House - Secret Exit" +forest_ghost_house_dragon = "Forest Ghost House - Dragon Coins" +forest_secret_exit_1 = "Forest Secret - Normal Exit" +forest_secret_dragon = "Forest Secret - Dragon Coins" +forest_fortress = "Forest Fortress - Normal Exit" +forest_reznor = "Forest Fortress - Boss" +forest_castle = "#5 Roy's Castle - Normal Exit" +forest_castle_dragon = "#5 Roy's Castle - Dragon Coins" +forest_koopaling = "#5 Roy's Castle - Boss" + +blue_switch_palace = "Blue Switch Palace" + +chocolate_island_1_exit_1 = "Chocolate Island 1 - Normal Exit" +chocolate_island_1_dragon = "Chocolate Island 1 - Dragon Coins" +chocolate_island_2_exit_1 = "Chocolate Island 2 - Normal Exit" +chocolate_island_2_exit_2 = "Chocolate Island 2 - Secret Exit" +chocolate_island_2_dragon = "Chocolate Island 2 - Dragon Coins" +chocolate_island_3_exit_1 = "Chocolate Island 3 - Normal Exit" +chocolate_island_3_exit_2 = "Chocolate Island 3 - Secret Exit" +chocolate_island_3_dragon = "Chocolate Island 3 - Dragon Coins" +chocolate_island_4_exit_1 = "Chocolate Island 4 - Normal Exit" +chocolate_island_4_dragon = "Chocolate Island 4 - Dragon Coins" +chocolate_island_5_exit_1 = "Chocolate Island 5 - Normal Exit" +chocolate_island_5_dragon = "Chocolate Island 5 - Dragon Coins" +chocolate_ghost_house_exit_1 = "Choco-Ghost House - Normal Exit" +chocolate_secret_exit_1 = "Chocolate Secret - Normal Exit" +chocolate_fortress = "Chocolate Fortress - Normal Exit" +chocolate_reznor = "Chocolate Fortress Defeat" +chocolate_castle = "#6 Wendy's Castle - Normal Exit" +chocolate_koopaling = "#6 Wendy's Castle - Boss" + +sunken_ghost_ship = "Sunken Ghost Ship - Normal Exit" +sunken_ghost_ship_dragon = "Sunken Ghost Ship - Dragon Coins" + +valley_of_bowser_1_exit_1 = "Valley of Bowser 1 - Normal Exit" +valley_of_bowser_1_dragon = "Valley of Bowser 1 - Dragon Coins" +valley_of_bowser_2_exit_1 = "Valley of Bowser 2 - Normal Exit" +valley_of_bowser_2_exit_2 = "Valley of Bowser 2 - Secret Exit" +valley_of_bowser_2_dragon = "Valley of Bowser 2 - Dragon Coins" +valley_of_bowser_3_exit_1 = "Valley of Bowser 3 - Normal Exit" +valley_of_bowser_3_dragon = "Valley of Bowser 3 - Dragon Coins" +valley_of_bowser_4_exit_1 = "Valley of Bowser 4 - Normal Exit" +valley_of_bowser_4_exit_2 = "Valley of Bowser 4 - Secret Exit" +valley_ghost_house_exit_1 = "Valley Ghost House - Normal Exit" +valley_ghost_house_exit_2 = "Valley Ghost House - Secret Exit" +valley_ghost_house_dragon = "Valley Ghost House - Dragon Coins" +valley_fortress = "Valley Fortress - Normal Exit" +valley_reznor = "Valley Fortress - Boss" +valley_castle = "#7 Larry's Castle - Normal Exit" +valley_castle_dragon = "#7 Larry's Castle - Dragon Coins" +valley_koopaling = "#7 Larry's Castle - Boss" + +front_door = "Front Door" +back_door = "Back Door" +bowser = "Bowser" + +star_road_1_exit_1 = "Star Road 1 - Normal Exit" +star_road_1_exit_2 = "Star Road 1 - Secret Exit" +star_road_1_dragon = "Star Road 1 - Dragon Coins" +star_road_2_exit_1 = "Star Road 2 - Normal Exit" +star_road_2_exit_2 = "Star Road 2 - Secret Exit" +star_road_3_exit_1 = "Star Road 3 - Normal Exit" +star_road_3_exit_2 = "Star Road 3 - Secret Exit" +star_road_4_exit_1 = "Star Road 4 - Normal Exit" +star_road_4_exit_2 = "Star Road 4 - Secret Exit" +star_road_5_exit_1 = "Star Road 5 - Normal Exit" +star_road_5_exit_2 = "Star Road 5 - Secret Exit" + +special_zone_1_exit_1 = "Gnarly - Normal Exit" +special_zone_1_dragon = "Gnarly - Dragon Coins" +special_zone_2_exit_1 = "Tubular - Normal Exit" +special_zone_2_dragon = "Tubular - Dragon Coins" +special_zone_3_exit_1 = "Way Cool - Normal Exit" +special_zone_3_dragon = "Way Cool - Dragon Coins" +special_zone_4_exit_1 = "Awesome - Normal Exit" +special_zone_4_dragon = "Awesome - Dragon Coins" +special_zone_5_exit_1 = "Groovy - Normal Exit" +special_zone_5_dragon = "Groovy - Dragon Coins" +special_zone_6_exit_1 = "Mondo - Normal Exit" +special_zone_6_dragon = "Mondo - Dragon Coins" +special_zone_7_exit_1 = "Outrageous - Normal Exit" +special_zone_7_dragon = "Outrageous - Dragon Coins" +special_zone_8_exit_1 = "Funky - Normal Exit" +special_zone_8_dragon = "Funky - Dragon Coins" + + +# Region Definitions +menu_region = "Menu" + +yoshis_island_region = "Yoshi's Island" +donut_plains_region = "Donut Plains" +vanilla_dome_region = "Vanilla Dome" +twin_bridges_region = "Twin Bridges" +forest_of_illusion_region = "Forest of Illusion" +chocolate_island_region = "Chocolate Island" +valley_of_bowser_region = "Valley of Bowser" +star_road_region = "Star Road" +special_zone_region = "Special Zone" + +yoshis_island_1_tile = "Yoshi's Island 1 - Tile" +yoshis_island_1_region = "Yoshi's Island 1" +yoshis_island_2_tile = "Yoshi's Island 2 - Tile" +yoshis_island_2_region = "Yoshi's Island 2" +yoshis_island_3_tile = "Yoshi's Island 3 - Tile" +yoshis_island_3_region = "Yoshi's Island 3" +yoshis_island_4_tile = "Yoshi's Island 4 - Tile" +yoshis_island_4_region = "Yoshi's Island 4" +yoshis_island_castle_tile = "#1 Iggy's Castle - Tile" +yoshis_island_castle_region = "#1 Iggy's Castle" + +yellow_switch_palace_tile = "Yellow Switch Palace - Tile" + +donut_plains_1_tile = "Donut Plains 1 - Tile" +donut_plains_1_region = "Donut Plains 1" +donut_plains_2_tile = "Donut Plains 2 - Tile" +donut_plains_2_region = "Donut Plains 2" +donut_plains_3_tile = "Donut Plains 3 - Tile" +donut_plains_3_region = "Donut Plains 3" +donut_plains_4_tile = "Donut Plains 4 - Tile" +donut_plains_4_region = "Donut Plains 4" +donut_secret_1_tile = "Donut Secret 1 - Tile" +donut_secret_1_region = "Donut Secret 1" +donut_secret_2_tile = "Donut Secret 2 - Tile" +donut_secret_2_region = "Donut Secret 2" +donut_ghost_house_tile = "Donut Ghost House - Tile" +donut_ghost_house_region = "Donut Ghost House" +donut_secret_house_tile = "Donut Secret House - Tile" +donut_secret_house_region = "Donut Secret House" +donut_plains_castle_tile = "#2 Morton's Castle - Tile" +donut_plains_castle_region = "#2 Morton's Castle" +donut_plains_top_secret = "Top Secret Area" +donut_plains_top_secret_tile = "Top Secret Area - Tile" +donut_plains_star_road = "Donut Plains - Star Road" + +green_switch_palace_tile = "Green Switch Palace - Tile" + +vanilla_dome_1_tile = "Vanilla Dome 1 - Tile" +vanilla_dome_1_region = "Vanilla Dome 1" +vanilla_dome_2_tile = "Vanilla Dome 2 - Tile" +vanilla_dome_2_region = "Vanilla Dome 2" +vanilla_dome_3_tile = "Vanilla Dome 3 - Tile" +vanilla_dome_3_region = "Vanilla Dome 3" +vanilla_dome_4_tile = "Vanilla Dome 4 - Tile" +vanilla_dome_4_region = "Vanilla Dome 4" +vanilla_secret_1_tile = "Vanilla Secret 1 - Tile" +vanilla_secret_1_region = "Vanilla Secret 1" +vanilla_secret_2_tile = "Vanilla Secret 2 - Tile" +vanilla_secret_2_region = "Vanilla Secret 2" +vanilla_secret_3_tile = "Vanilla Secret 3 - Tile" +vanilla_secret_3_region = "Vanilla Secret 3" +vanilla_ghost_house_tile = "Vanilla Ghost House - Tile" +vanilla_ghost_house_region = "Vanilla Ghost House" +vanilla_fortress_tile = "Vanilla Fortress - Tile" +vanilla_fortress_region = "Vanilla Fortress" +vanilla_dome_castle_tile = "#3 Lemmy's Castle - Tile" +vanilla_dome_castle_region = "#3 Lemmy's Castle" +vanilla_dome_star_road = "Vanilla Dome - Star Road" + +red_switch_palace_tile = "Red Switch Palace - Tile" + +butter_bridge_1_tile = "Butter Bridge 1 - Tile" +butter_bridge_1_region = "Butter Bridge 1" +butter_bridge_2_tile = "Butter Bridge 2 - Tile" +butter_bridge_2_region = "Butter Bridge 2" +cheese_bridge_tile = "Cheese Bridge - Tile" +cheese_bridge_region = "Cheese Bridge" +cookie_mountain_tile = "Cookie Mountain - Tile" +cookie_mountain_region = "Cookie Mountain" +soda_lake_tile = "Soda Lake - Tile" +soda_lake_region = "Soda Lake" +twin_bridges_castle_tile = "#4 Ludwig's Castle - Tile" +twin_bridges_castle_region = "#4 Ludwig's Castle" +twin_bridges_star_road = "Twin Bridges - Star Road" + +forest_of_illusion_1_tile = "Forest of Illusion 1 - Tile" +forest_of_illusion_1_region = "Forest of Illusion 1" +forest_of_illusion_2_tile = "Forest of Illusion 2 - Tile" +forest_of_illusion_2_region = "Forest of Illusion 2" +forest_of_illusion_3_tile = "Forest of Illusion 3 - Tile" +forest_of_illusion_3_region = "Forest of Illusion 3" +forest_of_illusion_4_tile = "Forest of Illusion 4 - Tile" +forest_of_illusion_4_region = "Forest of Illusion 4" +forest_ghost_house_tile = "Forest Ghost House - Tile" +forest_ghost_house_region = "Forest Ghost House" +forest_secret_tile = "Forest Secret - Tile" +forest_secret_region = "Forest Secret" +forest_fortress_tile = "Forest Fortress - Tile" +forest_fortress_region = "Forest Fortress" +forest_castle_tile = "#5 Roy's Castle - Tile" +forest_castle_region = "#5 Roy's Castle" +forest_star_road = "Forest of Illusion - Star Road" + +blue_switch_palace_tile = "Blue Switch Palace - Tile" + +chocolate_island_1_tile = "Chocolate Island 1 - Tile" +chocolate_island_1_region = "Chocolate Island 1" +chocolate_island_2_tile = "Chocolate Island 2 - Tile" +chocolate_island_2_region = "Chocolate Island 2" +chocolate_island_3_tile = "Chocolate Island 3 - Tile" +chocolate_island_3_region = "Chocolate Island 3" +chocolate_island_4_tile = "Chocolate Island 4 - Tile" +chocolate_island_4_region = "Chocolate Island 4" +chocolate_island_5_tile = "Chocolate Island 5 - Tile" +chocolate_island_5_region = "Chocolate Island 5" +chocolate_ghost_house_tile = "Choco-Ghost House - Tile" +chocolate_ghost_house_region = "Choco-Ghost House" +chocolate_secret_tile = "Chocolate Secret - Tile" +chocolate_secret_region = "Chocolate Secret" +chocolate_fortress_tile = "Chocolate Fortress - Tile" +chocolate_fortress_region = "Chocolate Fortress" +chocolate_castle_tile = "#6 Wendy's Castle - Tile" +chocolate_castle_region = "#6 Wendy's Castle" + +sunken_ghost_ship_tile = "Sunken Ghost Ship - Tile" +sunken_ghost_ship_region = "Sunken Ghost Ship" + +valley_of_bowser_1_tile = "Valley of Bowser 1 - Tile" +valley_of_bowser_1_region = "Valley of Bowser 1" +valley_of_bowser_2_tile = "Valley of Bowser 2 - Tile" +valley_of_bowser_2_region = "Valley of Bowser 2" +valley_of_bowser_3_tile = "Valley of Bowser 3 - Tile" +valley_of_bowser_3_region = "Valley of Bowser 3" +valley_of_bowser_4_tile = "Valley of Bowser 4 - Tile" +valley_of_bowser_4_region = "Valley of Bowser 4" +valley_ghost_house_tile = "Valley Ghost House - Tile" +valley_ghost_house_region = "Valley Ghost House" +valley_fortress_tile = "Valley Fortress - Tile" +valley_fortress_region = "Valley Fortress" +valley_castle_tile = "#7 Larry's Castle - Tile" +valley_castle_region = "#7 Larry's Castle" +valley_star_road = "Valley of Bowser - Star Road" + +front_door_tile = "Front Door - Tile" +back_door_tile = "Back Door - Tile" +bowser_region = "Bowser - Region" + +star_road_donut = "Star Road - Donut Plains" +star_road_1_tile = "Star Road 1 - Tile" +star_road_1_region = "Star Road 1" +star_road_vanilla = "Star Road - Vanilla Dome" +star_road_2_tile = "Star Road 2 - Tile" +star_road_2_region = "Star Road 2" +star_road_twin_bridges = "Star Road - Twin Bridges" +star_road_3_tile = "Star Road 3 - Tile" +star_road_3_region = "Star Road 3" +star_road_forest = "Star Road - Forest of Illusion" +star_road_4_tile = "Star Road 4 - Tile" +star_road_4_region = "Star Road 4" +star_road_valley = "Star Road - Valley of Bowser" +star_road_5_tile = "Star Road 5 - Tile" +star_road_5_region = "Star Road 5" +star_road_special = "Star Road - Special Zone" + +special_star_road = "Special Zone - Star Road" +special_zone_1_tile = "Gnarly - Tile" +special_zone_1_region = "Gnarly" +special_zone_2_tile = "Tubular - Tile" +special_zone_2_region = "Tubular" +special_zone_3_tile = "Way Cool - Tile" +special_zone_3_region = "Way Cool" +special_zone_4_tile = "Awesome - Tile" +special_zone_4_region = "Awesome" +special_zone_5_tile = "Groovy - Tile" +special_zone_5_region = "Groovy" +special_zone_6_tile = "Mondo - Tile" +special_zone_6_region = "Mondo" +special_zone_7_tile = "Outrageous - Tile" +special_zone_7_region = "Outrageous" +special_zone_8_tile = "Funky - Tile" +special_zone_8_region = "Funky" +special_complete = "Special Zone - Star Road - Complete" diff --git a/worlds/smw/Names/TextBox.py b/worlds/smw/Names/TextBox.py new file mode 100644 index 0000000000..cecf088661 --- /dev/null +++ b/worlds/smw/Names/TextBox.py @@ -0,0 +1,140 @@ + +from BaseClasses import MultiWorld + +import math + + +text_mapping = { + "A": 0x00, "B": 0x01, "C": 0x02, "D": 0x03, "E": 0x04, "F": 0x05, "G": 0x06, "H": 0x07, "I": 0x08, "J": 0x09, + "K": 0x0A, "L": 0x0B, "M": 0x0C, "N": 0x0D, "O": 0x0E, "P": 0x0F, "Q": 0x10, "R": 0x11, "S": 0x12, "T": 0x13, + "U": 0x14, "V": 0x15, "W": 0x16, "X": 0x17, "Y": 0x18, "Z": 0x19, + + "!": 0x1A, ".": 0x1B, "-": 0x1C, ",": 0x1D, "?": 0x1E, " ": 0x1F, + + "0": 0x22, "1": 0x23, "2": 0x24, "3": 0x25, "4": 0x26, "5": 0x27, "6": 0x28, "7": 0x29, "8": 0x2A, "9": 0x2B, + + "a": 0x40, "b": 0x41, "c": 0x42, "d": 0x43, "e": 0x44, "f": 0x45, "g": 0x46, "h": 0x47, "i": 0x48, "j": 0x49, + "k": 0x4A, "l": 0x4B, "m": 0x4C, "n": 0x4D, "o": 0x4E, "p": 0x4F, "q": 0x50, "r": 0x51, "s": 0x52, "t": 0x53, + "u": 0x54, "v": 0x55, "w": 0x56, "x": 0x57, "y": 0x58, "z": 0x59, + + "#": 0x5A, "(": 0x5B, ")": 0x5C, "'": 0x5D +} + +title_text_mapping = { + "A": [0x0A, 0x38], "B": [0x0B, 0x38], "C": [0x0C, 0x38], "D": [0x0D, 0x38], "E": [0x0E, 0x38], + "F": [0x0F, 0x38], "G": [0x10, 0x38], "H": [0x11, 0x38], "I": [0x12, 0x38], "J": [0x13, 0x38], + "K": [0x14, 0x38], "L": [0x15, 0x38], "M": [0x16, 0x38], "N": [0x17, 0x38], "O": [0x18, 0x38], + "P": [0x19, 0x38], "Q": [0x1A, 0x38], "R": [0x1B, 0x38], "S": [0x1C, 0x38], "T": [0x1D, 0x38], + "U": [0x1E, 0x38], "V": [0x1F, 0x38], "W": [0x20, 0x38], "X": [0x21, 0x38], "Y": [0x22, 0x38], + "Z": [0x23, 0x38], " ": [0xFC, 0x38], ".": [0x24, 0x38], + "0": [0x00, 0x38], "1": [0x01, 0x38], "2": [0x02, 0x38], "3": [0x03, 0x38], "4": [0x04, 0x38], + "5": [0x05, 0x38], "6": [0x06, 0x38], "7": [0x07, 0x38], "8": [0x08, 0x38], "9": [0x09, 0x38], +} + + +def string_to_bytes(input_string): + out_array = bytearray() + for letter in input_string: + out_array.append(text_mapping[letter] if letter in text_mapping else text_mapping["."]) + + return out_array + + +def generate_text_box(input_string): + out_bytes = bytearray() + box_line_count = 0 + box_line_chr_count = 0 + for word in input_string.split(): + if box_line_chr_count + len(word) > 18: + out_bytes[-1] += 0x80 + box_line_count += 1 + box_line_chr_count = 0 + + out_bytes.extend(string_to_bytes(word)) + box_line_chr_count += len(word) + + if box_line_chr_count < 18: + box_line_chr_count += 1 + out_bytes.append(0x1F) + + for i in range(box_line_count, 8): + out_bytes.append(0x9F) + + return out_bytes + + +def generate_goal_text(world: MultiWorld, player: int): + out_array = bytearray() + if world.goal[player] == "yoshi_egg_hunt": + required_yoshi_eggs = max(math.floor( + world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1) + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" You must acquire") + out_array[-1] += 0x80 + out_array += string_to_bytes(f' {required_yoshi_eggs:02} Yoshi Eggs,') + out_array[-1] += 0x80 + out_array += string_to_bytes("then return here.") + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F, 0x9F]) + else: + bosses_required = world.bosses_required[player].value + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" You must defeat") + out_array[-1] += 0x80 + out_array += string_to_bytes(f' {bosses_required:02} Bosses,') + out_array[-1] += 0x80 + out_array += string_to_bytes("then defeat Bowser") + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F, 0x9F]) + + return out_array + + +def generate_received_text(item_name: str, player_name: str): + out_array = bytearray() + + item_name = item_name[:18] + player_name = player_name[:18] + + item_buffer = max(0, math.floor((18 - len(item_name)) / 2)) + player_buffer = max(0, math.floor((18 - len(player_name)) / 2)) + + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" Received") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * item_buffer) + out_array += string_to_bytes(item_name) + out_array[-1] += 0x80 + out_array += string_to_bytes(" from") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * player_buffer) + out_array += string_to_bytes(player_name) + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F]) + + return out_array + + +def generate_sent_text(item_name: str, player_name: str): + out_array = bytearray() + + item_name = item_name[:18] + player_name = player_name[:18] + + item_buffer = max(0, math.floor((18 - len(item_name)) / 2)) + player_buffer = max(0, math.floor((18 - len(player_name)) / 2)) + + out_array += bytearray([0x9F, 0x9F]) + out_array += string_to_bytes(" Sent") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * item_buffer) + out_array += string_to_bytes(item_name) + out_array[-1] += 0x80 + out_array += string_to_bytes(" to") + out_array[-1] += 0x80 + out_array += bytearray([0x1F] * player_buffer) + out_array += string_to_bytes(player_name) + out_array[-1] += 0x80 + out_array += bytearray([0x9F, 0x9F]) + + return out_array diff --git a/worlds/smw/Options.py b/worlds/smw/Options.py new file mode 100644 index 0000000000..80af63f5a4 --- /dev/null +++ b/worlds/smw/Options.py @@ -0,0 +1,236 @@ +import typing + +from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList + + +class Goal(Choice): + """ + Determines the goal of the seed + Bowser: Defeat Koopalings, reach Bowser's Castle and defeat Bowser + Yoshi Egg Hunt: Find a certain number of Yoshi Eggs + """ + display_name = "Goal" + option_bowser = 0 + option_yoshi_egg_hunt = 1 + default = 0 + + +class BossesRequired(Range): + """ + How many Bosses (Koopalings or Reznor) must be defeated in order to defeat Bowser + """ + display_name = "Bosses Required" + range_start = 0 + range_end = 11 + default = 7 + + +class NumberOfYoshiEggs(Range): + """ + How many Yoshi Eggs are in the pool for Yoshi Egg Hunt + """ + display_name = "Total Number of Yoshi Eggs" + range_start = 1 + range_end = 80 + default = 50 + + +class PercentageOfYoshiEggs(Range): + """ + What Percentage of Yoshi Eggs are required to finish Yoshi Egg Hunt + """ + display_name = "Required Percentage of Yoshi Eggs" + range_start = 1 + range_end = 100 + default = 100 + + +class DragonCoinChecks(Toggle): + """ + Whether collecting 5 Dragon Coins in each level will grant a check + """ + display_name = "Dragon Coin Checks" + + +class BowserCastleDoors(Choice): + """ + How the doors of Bowser's Castle behave + Vanilla: Front and Back Doors behave as vanilla + Fast: Both doors behave as the Back Door + Slow: Both doors behave as the Front Door + "Front Door" requires beating all 8 Rooms + "Back Door" only requires going through the dark hallway to Bowser + """ + display_name = "Bowser Castle Doors" + option_vanilla = 0 + option_fast = 1 + option_slow = 2 + default = 0 + + +class LevelShuffle(Toggle): + """ + Whether levels are shuffled + """ + display_name = "Level Shuffle" + + +class SwapDonutGhostHouseExits(Toggle): + """ + If enabled, this option will swap which overworld direction the two exits of the level at the Donut Ghost House overworld tile go: + False: Normal Exit goes up, Secret Exit goes right. + True: Normal Exit goes right, Secret Exit goes up. + """ + display_name = "Swap Donut GH Exits" + + +class DisplaySentItemPopups(Choice): + """ + What messages to display in-game for items sent + """ + display_name = "Display Sent Item Popups" + option_none = 0 + option_all = 1 + default = 1 + + +class DisplayReceivedItemPopups(Choice): + """ + What messages to display in-game for items received + """ + display_name = "Display Received Item Popups" + option_none = 0 + option_all = 1 + option_progression = 2 + default = 2 + + +class TrapFillPercentage(Range): + """ + Replace a percentage of junk items in the item pool with random traps + """ + display_name = "Trap Fill Percentage" + range_start = 0 + range_end = 100 + default = 0 + + +class BaseTrapWeight(Choice): + """ + Base Class for Trap Weights + """ + option_none = 0 + option_low = 1 + option_medium = 2 + option_high = 4 + default = 2 + + +class IceTrapWeight(BaseTrapWeight): + """ + Likelihood of a receiving a trap which causes the level to become slippery + """ + display_name = "Ice Trap Weight" + + +class StunTrapWeight(BaseTrapWeight): + """ + Likelihood of a receiving a trap which briefly stuns Mario + """ + display_name = "Stun Trap Weight" + + +class LiteratureTrapWeight(BaseTrapWeight): + """ + Likelihood of a receiving a trap which causes the player to read literature + """ + display_name = "Literature Trap Weight" + + +class Autosave(DefaultOnToggle): + """ + Whether a save prompt will appear after every level + """ + display_name = "Autosave" + + +class MusicShuffle(Choice): + """ + Music shuffle type + None: No Music is shuffled + Consistent: Each music track is consistently shuffled throughout the game + Full: Each individual level has a random music track + Singularity: The entire game uses one song for overworld and one song for levels + """ + display_name = "Music Shuffle" + option_none = 0 + option_consistent = 1 + option_full = 2 + option_singularity = 3 + default = 0 + + +class MarioPalette(Choice): + """ + Mario palette color + """ + display_name = "Mario Palette" + option_mario = 0 + option_luigi = 1 + option_wario = 2 + option_waluigi = 3 + option_geno = 4 + option_princess = 5 + option_dark = 6 + option_sponge = 7 + default = 0 + + +class ForegroundPaletteShuffle(Toggle): + """ + Whether to shuffle level foreground palettes + """ + display_name = "Foreground Palette Shuffle" + + +class BackgroundPaletteShuffle(Toggle): + """ + Whether to shuffle level background palettes + """ + display_name = "Background Palette Shuffle" + + +class StartingLifeCount(Range): + """ + How many extra lives to start the game with + """ + display_name = "Starting Life Count" + range_start = 1 + range_end = 99 + default = 5 + + + +smw_options: typing.Dict[str, type(Option)] = { + "death_link": DeathLink, + "goal": Goal, + "bosses_required": BossesRequired, + "number_of_yoshi_eggs": NumberOfYoshiEggs, + "percentage_of_yoshi_eggs": PercentageOfYoshiEggs, + "dragon_coin_checks": DragonCoinChecks, + "bowser_castle_doors": BowserCastleDoors, + "level_shuffle": LevelShuffle, + "swap_donut_gh_exits": SwapDonutGhostHouseExits, + #"display_sent_item_popups": DisplaySentItemPopups, + "display_received_item_popups": DisplayReceivedItemPopups, + "trap_fill_percentage": TrapFillPercentage, + "ice_trap_weight": IceTrapWeight, + "stun_trap_weight": StunTrapWeight, + "literature_trap_weight": LiteratureTrapWeight, + "autosave": Autosave, + "music_shuffle": MusicShuffle, + "mario_palette": MarioPalette, + "foreground_palette_shuffle": ForegroundPaletteShuffle, + "background_palette_shuffle": BackgroundPaletteShuffle, + "starting_life_count": StartingLifeCount, +} diff --git a/worlds/smw/Regions.py b/worlds/smw/Regions.py new file mode 100644 index 0000000000..7af7b888c9 --- /dev/null +++ b/worlds/smw/Regions.py @@ -0,0 +1,1187 @@ +import typing + +from BaseClasses import MultiWorld, Region, RegionType, Entrance +from .Locations import SMWLocation +from .Levels import level_info_dict +from .Names import LocationName, ItemName +from ..generic.Rules import add_rule, set_rule + + +def create_regions(world, player: int, active_locations): + menu_region = create_region(world, player, active_locations, 'Menu', None) + + yoshis_island_region = create_region(world, player, active_locations, LocationName.yoshis_island_region, None) + donut_plains_region = create_region(world, player, active_locations, LocationName.donut_plains_region, None) + vanilla_dome_region = create_region(world, player, active_locations, LocationName.vanilla_dome_region, None) + twin_bridges_region = create_region(world, player, active_locations, LocationName.twin_bridges_region, None) + forest_of_illusion_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_region, None) + chocolate_island_region = create_region(world, player, active_locations, LocationName.chocolate_island_region, None) + valley_of_bowser_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_region, None) + star_road_region = create_region(world, player, active_locations, LocationName.star_road_region, None) + special_zone_region = create_region(world, player, active_locations, LocationName.special_zone_region, None) + + + yoshis_house_tile = create_region(world, player, active_locations, LocationName.yoshis_house_tile, None) + + yoshis_house_region_locations = [] + if world.goal[player] == "yoshi_egg_hunt": + yoshis_house_region_locations.append(LocationName.yoshis_house) + yoshis_house_region = create_region(world, player, active_locations, LocationName.yoshis_house, + yoshis_house_region_locations) + + yoshis_island_1_tile = create_region(world, player, active_locations, LocationName.yoshis_island_1_tile, None) + yoshis_island_1_region = create_region(world, player, active_locations, LocationName.yoshis_island_1_region, None) + yoshis_island_1_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_1_exit_1, + [LocationName.yoshis_island_1_exit_1]) + + yoshis_island_2_tile = create_region(world, player, active_locations, LocationName.yoshis_island_2_tile, None) + yoshis_island_2_region = create_region(world, player, active_locations, LocationName.yoshis_island_2_region, None) + yoshis_island_2_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_2_exit_1, + [LocationName.yoshis_island_2_exit_1]) + + yoshis_island_3_tile = create_region(world, player, active_locations, LocationName.yoshis_island_3_tile, None) + yoshis_island_3_region = create_region(world, player, active_locations, LocationName.yoshis_island_3_region, None) + yoshis_island_3_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_3_exit_1, + [LocationName.yoshis_island_3_exit_1]) + + yoshis_island_4_tile = create_region(world, player, active_locations, LocationName.yoshis_island_4_tile, None) + yoshis_island_4_region = create_region(world, player, active_locations, LocationName.yoshis_island_4_region, None) + yoshis_island_4_exit_1 = create_region(world, player, active_locations, LocationName.yoshis_island_4_exit_1, + [LocationName.yoshis_island_4_exit_1]) + + yoshis_island_castle_tile = create_region(world, player, active_locations, LocationName.yoshis_island_castle_tile, None) + yoshis_island_castle_region = create_region(world, player, active_locations, LocationName.yoshis_island_castle_region, None) + yoshis_island_castle = create_region(world, player, active_locations, LocationName.yoshis_island_castle, + [LocationName.yoshis_island_castle, LocationName.yoshis_island_koopaling]) + + yellow_switch_palace_tile = create_region(world, player, active_locations, LocationName.yellow_switch_palace_tile, None) + yellow_switch_palace = create_region(world, player, active_locations, LocationName.yellow_switch_palace, + [LocationName.yellow_switch_palace]) + + + donut_plains_1_tile = create_region(world, player, active_locations, LocationName.donut_plains_1_tile, None) + donut_plains_1_region = create_region(world, player, active_locations, LocationName.donut_plains_1_region, None) + donut_plains_1_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_1_exit_1, + [LocationName.donut_plains_1_exit_1]) + donut_plains_1_exit_2 = create_region(world, player, active_locations, LocationName.donut_plains_1_exit_2, + [LocationName.donut_plains_1_exit_2]) + + donut_plains_2_tile = create_region(world, player, active_locations, LocationName.donut_plains_2_tile, None) + donut_plains_2_region = create_region(world, player, active_locations, LocationName.donut_plains_2_region, None) + donut_plains_2_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_2_exit_1, + [LocationName.donut_plains_2_exit_1]) + donut_plains_2_exit_2 = create_region(world, player, active_locations, LocationName.donut_plains_2_exit_2, + [LocationName.donut_plains_2_exit_2]) + + donut_plains_3_tile = create_region(world, player, active_locations, LocationName.donut_plains_3_tile, None) + donut_plains_3_region = create_region(world, player, active_locations, LocationName.donut_plains_3_region, None) + donut_plains_3_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_3_exit_1, + [LocationName.donut_plains_3_exit_1]) + + donut_plains_4_tile = create_region(world, player, active_locations, LocationName.donut_plains_4_tile, None) + donut_plains_4_region = create_region(world, player, active_locations, LocationName.donut_plains_4_region, None) + donut_plains_4_exit_1 = create_region(world, player, active_locations, LocationName.donut_plains_4_exit_1, + [LocationName.donut_plains_4_exit_1]) + + donut_secret_1_tile = create_region(world, player, active_locations, LocationName.donut_secret_1_tile, None) + donut_secret_1_region = create_region(world, player, active_locations, LocationName.donut_secret_1_region, None) + donut_secret_1_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_1_exit_1, + [LocationName.donut_secret_1_exit_1]) + donut_secret_1_exit_2 = create_region(world, player, active_locations, LocationName.donut_secret_1_exit_2, + [LocationName.donut_secret_1_exit_2]) + + donut_secret_2_tile = create_region(world, player, active_locations, LocationName.donut_secret_2_tile, None) + donut_secret_2_region = create_region(world, player, active_locations, LocationName.donut_secret_2_region, None) + donut_secret_2_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_2_exit_1, + [LocationName.donut_secret_2_exit_1]) + + donut_ghost_house_tile = create_region(world, player, active_locations, LocationName.donut_ghost_house_tile, None) + donut_ghost_house_region = create_region(world, player, active_locations, LocationName.donut_ghost_house_region, None) + donut_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.donut_ghost_house_exit_1, + [LocationName.donut_ghost_house_exit_1]) + donut_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.donut_ghost_house_exit_2, + [LocationName.donut_ghost_house_exit_2]) + + donut_secret_house_tile = create_region(world, player, active_locations, LocationName.donut_secret_house_tile, None) + donut_secret_house_region = create_region(world, player, active_locations, LocationName.donut_secret_house_region, None) + donut_secret_house_exit_1 = create_region(world, player, active_locations, LocationName.donut_secret_house_exit_1, + [LocationName.donut_secret_house_exit_1]) + donut_secret_house_exit_2 = create_region(world, player, active_locations, LocationName.donut_secret_house_exit_2, + [LocationName.donut_secret_house_exit_2]) + + donut_plains_castle_tile = create_region(world, player, active_locations, LocationName.donut_plains_castle_tile, None) + donut_plains_castle_region = create_region(world, player, active_locations, LocationName.donut_plains_castle_region, None) + donut_plains_castle = create_region(world, player, active_locations, LocationName.donut_plains_castle, + [LocationName.donut_plains_castle, LocationName.donut_plains_koopaling]) + + green_switch_palace_tile = create_region(world, player, active_locations, LocationName.green_switch_palace_tile, None) + green_switch_palace = create_region(world, player, active_locations, LocationName.green_switch_palace, + [LocationName.green_switch_palace]) + + donut_plains_top_secret_tile = create_region(world, player, active_locations, LocationName.donut_plains_top_secret_tile, None) + donut_plains_top_secret = create_region(world, player, active_locations, LocationName.donut_plains_top_secret, None) + + + vanilla_dome_1_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_1_tile, None) + vanilla_dome_1_region = create_region(world, player, active_locations, LocationName.vanilla_dome_1_region, None) + vanilla_dome_1_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_1_exit_1, + [LocationName.vanilla_dome_1_exit_1]) + vanilla_dome_1_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_dome_1_exit_2, + [LocationName.vanilla_dome_1_exit_2]) + + vanilla_dome_2_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_2_tile, None) + vanilla_dome_2_region = create_region(world, player, active_locations, LocationName.vanilla_dome_2_region, None) + vanilla_dome_2_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_2_exit_1, + [LocationName.vanilla_dome_2_exit_1]) + vanilla_dome_2_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_dome_2_exit_2, + [LocationName.vanilla_dome_2_exit_2]) + + vanilla_dome_3_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_3_tile, None) + vanilla_dome_3_region = create_region(world, player, active_locations, LocationName.vanilla_dome_3_region, None) + vanilla_dome_3_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_3_exit_1, + [LocationName.vanilla_dome_3_exit_1]) + + vanilla_dome_4_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_4_tile, None) + vanilla_dome_4_region = create_region(world, player, active_locations, LocationName.vanilla_dome_4_region, None) + vanilla_dome_4_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_dome_4_exit_1, + [LocationName.vanilla_dome_4_exit_1]) + + vanilla_secret_1_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_1_tile, None) + vanilla_secret_1_region = create_region(world, player, active_locations, LocationName.vanilla_secret_1_region, None) + vanilla_secret_1_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_1_exit_1, + [LocationName.vanilla_secret_1_exit_1]) + vanilla_secret_1_exit_2 = create_region(world, player, active_locations, LocationName.vanilla_secret_1_exit_2, + [LocationName.vanilla_secret_1_exit_2]) + + vanilla_secret_2_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_2_tile, None) + vanilla_secret_2_region = create_region(world, player, active_locations, LocationName.vanilla_secret_2_region, None) + vanilla_secret_2_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_2_exit_1, + [LocationName.vanilla_secret_2_exit_1]) + + vanilla_secret_3_tile = create_region(world, player, active_locations, LocationName.vanilla_secret_3_tile, None) + vanilla_secret_3_region = create_region(world, player, active_locations, LocationName.vanilla_secret_3_region, None) + vanilla_secret_3_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_secret_3_exit_1, + [LocationName.vanilla_secret_3_exit_1]) + + vanilla_ghost_house_tile = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_tile, None) + vanilla_ghost_house_region = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_region, None) + vanilla_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.vanilla_ghost_house_exit_1, + [LocationName.vanilla_ghost_house_exit_1]) + + vanilla_fortress_tile = create_region(world, player, active_locations, LocationName.vanilla_fortress_tile, None) + vanilla_fortress_region = create_region(world, player, active_locations, LocationName.vanilla_fortress_region, None) + vanilla_fortress = create_region(world, player, active_locations, LocationName.vanilla_fortress, + [LocationName.vanilla_fortress, LocationName.vanilla_reznor]) + + vanilla_dome_castle_tile = create_region(world, player, active_locations, LocationName.vanilla_dome_castle_tile, None) + vanilla_dome_castle_region = create_region(world, player, active_locations, LocationName.vanilla_dome_castle_region, None) + vanilla_dome_castle = create_region(world, player, active_locations, LocationName.vanilla_dome_castle, + [LocationName.vanilla_dome_castle, LocationName.vanilla_dome_koopaling]) + + red_switch_palace_tile = create_region(world, player, active_locations, LocationName.red_switch_palace_tile, None) + red_switch_palace = create_region(world, player, active_locations, LocationName.red_switch_palace, + [LocationName.red_switch_palace]) + + + butter_bridge_1_tile = create_region(world, player, active_locations, LocationName.butter_bridge_1_tile, None) + butter_bridge_1_region = create_region(world, player, active_locations, LocationName.butter_bridge_1_region, None) + butter_bridge_1_exit_1 = create_region(world, player, active_locations, LocationName.butter_bridge_1_exit_1, + [LocationName.butter_bridge_1_exit_1]) + + butter_bridge_2_tile = create_region(world, player, active_locations, LocationName.butter_bridge_2_tile, None) + butter_bridge_2_region = create_region(world, player, active_locations, LocationName.butter_bridge_2_region, None) + butter_bridge_2_exit_1 = create_region(world, player, active_locations, LocationName.butter_bridge_2_exit_1, + [LocationName.butter_bridge_2_exit_1]) + + cheese_bridge_tile = create_region(world, player, active_locations, LocationName.cheese_bridge_tile, None) + cheese_bridge_region = create_region(world, player, active_locations, LocationName.cheese_bridge_region, None) + cheese_bridge_exit_1 = create_region(world, player, active_locations, LocationName.cheese_bridge_exit_1, + [LocationName.cheese_bridge_exit_1]) + cheese_bridge_exit_2 = create_region(world, player, active_locations, LocationName.cheese_bridge_exit_2, + [LocationName.cheese_bridge_exit_2]) + + cookie_mountain_tile = create_region(world, player, active_locations, LocationName.cookie_mountain_tile, None) + cookie_mountain_region = create_region(world, player, active_locations, LocationName.cookie_mountain_region, None) + cookie_mountain_exit_1 = create_region(world, player, active_locations, LocationName.cookie_mountain_exit_1, + [LocationName.cookie_mountain_exit_1]) + + soda_lake_tile = create_region(world, player, active_locations, LocationName.soda_lake_tile, None) + soda_lake_region = create_region(world, player, active_locations, LocationName.soda_lake_region, None) + soda_lake_exit_1 = create_region(world, player, active_locations, LocationName.soda_lake_exit_1, + [LocationName.soda_lake_exit_1]) + + twin_bridges_castle_tile = create_region(world, player, active_locations, LocationName.twin_bridges_castle_tile, None) + twin_bridges_castle_region = create_region(world, player, active_locations, LocationName.twin_bridges_castle_region, None) + twin_bridges_castle = create_region(world, player, active_locations, LocationName.twin_bridges_castle, + [LocationName.twin_bridges_castle, LocationName.twin_bridges_koopaling]) + + + forest_of_illusion_1_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_tile, None) + forest_of_illusion_1_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_region, None) + forest_of_illusion_1_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_exit_1, + [LocationName.forest_of_illusion_1_exit_1]) + forest_of_illusion_1_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_1_exit_2, + [LocationName.forest_of_illusion_1_exit_2]) + + forest_of_illusion_2_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_tile, None) + forest_of_illusion_2_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_region, None) + forest_of_illusion_2_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_exit_1, + [LocationName.forest_of_illusion_2_exit_1]) + forest_of_illusion_2_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_2_exit_2, + [LocationName.forest_of_illusion_2_exit_2]) + + forest_of_illusion_3_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_tile, None) + forest_of_illusion_3_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_region, None) + forest_of_illusion_3_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_exit_1, + [LocationName.forest_of_illusion_3_exit_1]) + forest_of_illusion_3_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_3_exit_2, + [LocationName.forest_of_illusion_3_exit_2]) + + forest_of_illusion_4_tile = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_tile, None) + forest_of_illusion_4_region = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_region, None) + forest_of_illusion_4_exit_1 = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_exit_1, + [LocationName.forest_of_illusion_4_exit_1]) + forest_of_illusion_4_exit_2 = create_region(world, player, active_locations, LocationName.forest_of_illusion_4_exit_2, + [LocationName.forest_of_illusion_4_exit_2]) + + forest_ghost_house_tile = create_region(world, player, active_locations, LocationName.forest_ghost_house_tile, None) + forest_ghost_house_region = create_region(world, player, active_locations, LocationName.forest_ghost_house_region, None) + forest_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.forest_ghost_house_exit_1, + [LocationName.forest_ghost_house_exit_1]) + forest_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.forest_ghost_house_exit_2, + [LocationName.forest_ghost_house_exit_2]) + + forest_secret_tile = create_region(world, player, active_locations, LocationName.forest_secret_tile, None) + forest_secret_region = create_region(world, player, active_locations, LocationName.forest_secret_region, None) + forest_secret_exit_1 = create_region(world, player, active_locations, LocationName.forest_secret_exit_1, + [LocationName.forest_secret_exit_1]) + + forest_fortress_tile = create_region(world, player, active_locations, LocationName.forest_fortress_tile, None) + forest_fortress_region = create_region(world, player, active_locations, LocationName.forest_fortress_region, None) + forest_fortress = create_region(world, player, active_locations, LocationName.forest_fortress, + [LocationName.forest_fortress, LocationName.forest_reznor]) + + forest_castle_tile = create_region(world, player, active_locations, LocationName.forest_castle_tile, None) + forest_castle_region = create_region(world, player, active_locations, LocationName.forest_castle_region, None) + forest_castle = create_region(world, player, active_locations, LocationName.forest_castle, + [LocationName.forest_castle, LocationName.forest_koopaling]) + + blue_switch_palace_tile = create_region(world, player, active_locations, LocationName.blue_switch_palace_tile, None) + blue_switch_palace = create_region(world, player, active_locations, LocationName.blue_switch_palace, + [LocationName.blue_switch_palace]) + + + chocolate_island_1_tile = create_region(world, player, active_locations, LocationName.chocolate_island_1_tile, None) + chocolate_island_1_region = create_region(world, player, active_locations, LocationName.chocolate_island_1_region, None) + chocolate_island_1_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_1_exit_1, + [LocationName.chocolate_island_1_exit_1]) + + chocolate_island_2_tile = create_region(world, player, active_locations, LocationName.chocolate_island_2_tile, None) + chocolate_island_2_region = create_region(world, player, active_locations, LocationName.chocolate_island_2_region, None) + chocolate_island_2_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_2_exit_1, + [LocationName.chocolate_island_2_exit_1]) + chocolate_island_2_exit_2 = create_region(world, player, active_locations, LocationName.chocolate_island_2_exit_2, + [LocationName.chocolate_island_2_exit_2]) + + chocolate_island_3_tile = create_region(world, player, active_locations, LocationName.chocolate_island_3_tile, None) + chocolate_island_3_region = create_region(world, player, active_locations, LocationName.chocolate_island_3_region, None) + chocolate_island_3_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_3_exit_1, + [LocationName.chocolate_island_3_exit_1]) + chocolate_island_3_exit_2 = create_region(world, player, active_locations, LocationName.chocolate_island_3_exit_2, + [LocationName.chocolate_island_3_exit_2]) + + chocolate_island_4_tile = create_region(world, player, active_locations, LocationName.chocolate_island_4_tile, None) + chocolate_island_4_region = create_region(world, player, active_locations, LocationName.chocolate_island_4_region, None) + chocolate_island_4_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_4_exit_1, + [LocationName.chocolate_island_4_exit_1]) + + chocolate_island_5_tile = create_region(world, player, active_locations, LocationName.chocolate_island_5_tile, None) + chocolate_island_5_region = create_region(world, player, active_locations, LocationName.chocolate_island_5_region, None) + chocolate_island_5_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_island_5_exit_1, + [LocationName.chocolate_island_5_exit_1]) + + chocolate_ghost_house_tile = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_tile, None) + chocolate_ghost_house_region = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_region, None) + chocolate_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_ghost_house_exit_1, + [LocationName.chocolate_ghost_house_exit_1]) + + chocolate_secret_tile = create_region(world, player, active_locations, LocationName.chocolate_secret_tile, None) + chocolate_secret_region = create_region(world, player, active_locations, LocationName.chocolate_secret_region, None) + chocolate_secret_exit_1 = create_region(world, player, active_locations, LocationName.chocolate_secret_exit_1, + [LocationName.chocolate_secret_exit_1]) + + chocolate_fortress_tile = create_region(world, player, active_locations, LocationName.chocolate_fortress_tile, None) + chocolate_fortress_region = create_region(world, player, active_locations, LocationName.chocolate_fortress_region, None) + chocolate_fortress = create_region(world, player, active_locations, LocationName.chocolate_fortress, + [LocationName.chocolate_fortress, LocationName.chocolate_reznor]) + + chocolate_castle_tile = create_region(world, player, active_locations, LocationName.chocolate_castle_tile, None) + chocolate_castle_region = create_region(world, player, active_locations, LocationName.chocolate_castle_region, None) + chocolate_castle = create_region(world, player, active_locations, LocationName.chocolate_castle, + [LocationName.chocolate_castle, LocationName.chocolate_koopaling]) + + sunken_ghost_ship_tile = create_region(world, player, active_locations, LocationName.sunken_ghost_ship_tile, None) + sunken_ghost_ship_region = create_region(world, player, active_locations, LocationName.sunken_ghost_ship_region, None) + sunken_ghost_ship = create_region(world, player, active_locations, LocationName.sunken_ghost_ship, + [LocationName.sunken_ghost_ship]) + + + valley_of_bowser_1_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_tile, None) + valley_of_bowser_1_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_region, None) + valley_of_bowser_1_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_1_exit_1, + [LocationName.valley_of_bowser_1_exit_1]) + + valley_of_bowser_2_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_tile, None) + valley_of_bowser_2_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_region, None) + valley_of_bowser_2_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_exit_1, + [LocationName.valley_of_bowser_2_exit_1]) + valley_of_bowser_2_exit_2 = create_region(world, player, active_locations, LocationName.valley_of_bowser_2_exit_2, + [LocationName.valley_of_bowser_2_exit_2]) + + valley_of_bowser_3_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_tile, None) + valley_of_bowser_3_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_region, None) + valley_of_bowser_3_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_3_exit_1, + [LocationName.valley_of_bowser_3_exit_1]) + + valley_of_bowser_4_tile = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_tile, None) + valley_of_bowser_4_region = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_region, None) + valley_of_bowser_4_exit_1 = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_exit_1, + [LocationName.valley_of_bowser_4_exit_1]) + valley_of_bowser_4_exit_2 = create_region(world, player, active_locations, LocationName.valley_of_bowser_4_exit_2, + [LocationName.valley_of_bowser_4_exit_2]) + + valley_ghost_house_tile = create_region(world, player, active_locations, LocationName.valley_ghost_house_tile, None) + valley_ghost_house_region = create_region(world, player, active_locations, LocationName.valley_ghost_house_region, None) + valley_ghost_house_exit_1 = create_region(world, player, active_locations, LocationName.valley_ghost_house_exit_1, + [LocationName.valley_ghost_house_exit_1]) + valley_ghost_house_exit_2 = create_region(world, player, active_locations, LocationName.valley_ghost_house_exit_2, + [LocationName.valley_ghost_house_exit_2]) + + valley_fortress_tile = create_region(world, player, active_locations, LocationName.valley_fortress_tile, None) + valley_fortress_region = create_region(world, player, active_locations, LocationName.valley_fortress_region, None) + valley_fortress = create_region(world, player, active_locations, LocationName.valley_fortress, + [LocationName.valley_fortress, LocationName.valley_reznor]) + + valley_castle_tile = create_region(world, player, active_locations, LocationName.valley_castle_tile, None) + valley_castle_region = create_region(world, player, active_locations, LocationName.valley_castle_region, None) + valley_castle = create_region(world, player, active_locations, LocationName.valley_castle, + [LocationName.valley_castle, LocationName.valley_koopaling]) + + front_door_tile = create_region(world, player, active_locations, LocationName.front_door_tile, None) + front_door_region = create_region(world, player, active_locations, LocationName.front_door, None) + back_door_tile = create_region(world, player, active_locations, LocationName.back_door_tile, None) + back_door_region = create_region(world, player, active_locations, LocationName.back_door, None) + bowser_region_locations = [] + if world.goal[player] == "bowser": + bowser_region_locations += [LocationName.bowser] + bowser_region = create_region(world, player, active_locations, LocationName.bowser_region, bowser_region_locations) + + + donut_plains_star_road = create_region(world, player, active_locations, LocationName.donut_plains_star_road, None) + vanilla_dome_star_road = create_region(world, player, active_locations, LocationName.vanilla_dome_star_road, None) + twin_bridges_star_road = create_region(world, player, active_locations, LocationName.twin_bridges_star_road, None) + forest_star_road = create_region(world, player, active_locations, LocationName.forest_star_road, None) + valley_star_road = create_region(world, player, active_locations, LocationName.valley_star_road, None) + star_road_donut = create_region(world, player, active_locations, LocationName.star_road_donut, None) + star_road_vanilla = create_region(world, player, active_locations, LocationName.star_road_vanilla, None) + star_road_twin_bridges = create_region(world, player, active_locations, LocationName.star_road_twin_bridges, None) + star_road_forest = create_region(world, player, active_locations, LocationName.star_road_forest, None) + star_road_valley = create_region(world, player, active_locations, LocationName.star_road_valley, None) + star_road_special = create_region(world, player, active_locations, LocationName.star_road_special, None) + special_star_road = create_region(world, player, active_locations, LocationName.special_star_road, None) + + star_road_1_tile = create_region(world, player, active_locations, LocationName.star_road_1_tile, None) + star_road_1_region = create_region(world, player, active_locations, LocationName.star_road_1_region, None) + star_road_1_exit_1 = create_region(world, player, active_locations, LocationName.star_road_1_exit_1, + [LocationName.star_road_1_exit_1]) + star_road_1_exit_2 = create_region(world, player, active_locations, LocationName.star_road_1_exit_2, + [LocationName.star_road_1_exit_2]) + + star_road_2_tile = create_region(world, player, active_locations, LocationName.star_road_2_tile, None) + star_road_2_region = create_region(world, player, active_locations, LocationName.star_road_2_region, None) + star_road_2_exit_1 = create_region(world, player, active_locations, LocationName.star_road_2_exit_1, + [LocationName.star_road_2_exit_1]) + star_road_2_exit_2 = create_region(world, player, active_locations, LocationName.star_road_2_exit_2, + [LocationName.star_road_2_exit_2]) + + star_road_3_tile = create_region(world, player, active_locations, LocationName.star_road_3_tile, None) + star_road_3_region = create_region(world, player, active_locations, LocationName.star_road_3_region, None) + star_road_3_exit_1 = create_region(world, player, active_locations, LocationName.star_road_3_exit_1, + [LocationName.star_road_3_exit_1]) + star_road_3_exit_2 = create_region(world, player, active_locations, LocationName.star_road_3_exit_2, + [LocationName.star_road_3_exit_2]) + + star_road_4_tile = create_region(world, player, active_locations, LocationName.star_road_4_tile, None) + star_road_4_region = create_region(world, player, active_locations, LocationName.star_road_4_region, None) + star_road_4_exit_1 = create_region(world, player, active_locations, LocationName.star_road_4_exit_1, + [LocationName.star_road_4_exit_1]) + star_road_4_exit_2 = create_region(world, player, active_locations, LocationName.star_road_4_exit_2, + [LocationName.star_road_4_exit_2]) + + star_road_5_tile = create_region(world, player, active_locations, LocationName.star_road_5_tile, None) + star_road_5_region = create_region(world, player, active_locations, LocationName.star_road_5_region, None) + star_road_5_exit_1 = create_region(world, player, active_locations, LocationName.star_road_5_exit_1, + [LocationName.star_road_5_exit_1]) + star_road_5_exit_2 = create_region(world, player, active_locations, LocationName.star_road_5_exit_2, + [LocationName.star_road_5_exit_2]) + + + special_zone_1_tile = create_region(world, player, active_locations, LocationName.special_zone_1_tile, None) + special_zone_1_region = create_region(world, player, active_locations, LocationName.special_zone_1_region, None) + special_zone_1_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_1_exit_1, + [LocationName.special_zone_1_exit_1]) + + special_zone_2_tile = create_region(world, player, active_locations, LocationName.special_zone_2_tile, None) + special_zone_2_region = create_region(world, player, active_locations, LocationName.special_zone_2_region, None) + special_zone_2_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_2_exit_1, + [LocationName.special_zone_2_exit_1]) + + special_zone_3_tile = create_region(world, player, active_locations, LocationName.special_zone_3_tile, None) + special_zone_3_region = create_region(world, player, active_locations, LocationName.special_zone_3_region, None) + special_zone_3_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_3_exit_1, + [LocationName.special_zone_3_exit_1]) + + special_zone_4_tile = create_region(world, player, active_locations, LocationName.special_zone_4_tile, None) + special_zone_4_region = create_region(world, player, active_locations, LocationName.special_zone_4_region, None) + special_zone_4_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_4_exit_1, + [LocationName.special_zone_4_exit_1]) + + special_zone_5_tile = create_region(world, player, active_locations, LocationName.special_zone_5_tile, None) + special_zone_5_region = create_region(world, player, active_locations, LocationName.special_zone_5_region, None) + special_zone_5_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_5_exit_1, + [LocationName.special_zone_5_exit_1]) + + special_zone_6_tile = create_region(world, player, active_locations, LocationName.special_zone_6_tile, None) + special_zone_6_region = create_region(world, player, active_locations, LocationName.special_zone_6_region, None) + special_zone_6_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_6_exit_1, + [LocationName.special_zone_6_exit_1]) + + special_zone_7_tile = create_region(world, player, active_locations, LocationName.special_zone_7_tile, None) + special_zone_7_region = create_region(world, player, active_locations, LocationName.special_zone_7_region, None) + special_zone_7_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_7_exit_1, + [LocationName.special_zone_7_exit_1]) + + special_zone_8_tile = create_region(world, player, active_locations, LocationName.special_zone_8_tile, None) + special_zone_8_region = create_region(world, player, active_locations, LocationName.special_zone_8_region, None) + special_zone_8_exit_1 = create_region(world, player, active_locations, LocationName.special_zone_8_exit_1, + [LocationName.special_zone_8_exit_1]) + special_complete = create_region(world, player, active_locations, LocationName.special_complete, None) + + + # Set up the regions correctly. + world.regions += [ + menu_region, + yoshis_island_region, + donut_plains_region, + vanilla_dome_region, + twin_bridges_region, + forest_of_illusion_region, + chocolate_island_region, + valley_of_bowser_region, + star_road_region, + special_zone_region, + yoshis_house_tile, + yoshis_house_region, + yoshis_island_1_tile, + yoshis_island_1_region, + yoshis_island_1_exit_1, + yoshis_island_2_tile, + yoshis_island_2_region, + yoshis_island_2_exit_1, + yoshis_island_3_tile, + yoshis_island_3_region, + yoshis_island_3_exit_1, + yoshis_island_4_tile, + yoshis_island_4_region, + yoshis_island_4_exit_1, + yoshis_island_castle_tile, + yoshis_island_castle_region, + yoshis_island_castle, + yellow_switch_palace_tile, + yellow_switch_palace, + donut_plains_1_tile, + donut_plains_1_region, + donut_plains_1_exit_1, + donut_plains_1_exit_2, + donut_plains_2_tile, + donut_plains_2_region, + donut_plains_2_exit_1, + donut_plains_2_exit_2, + donut_plains_3_tile, + donut_plains_3_region, + donut_plains_3_exit_1, + donut_plains_4_tile, + donut_plains_4_region, + donut_plains_4_exit_1, + donut_secret_1_tile, + donut_secret_1_region, + donut_secret_1_exit_1, + donut_secret_1_exit_2, + donut_secret_2_tile, + donut_secret_2_region, + donut_secret_2_exit_1, + donut_ghost_house_tile, + donut_ghost_house_region, + donut_ghost_house_exit_1, + donut_ghost_house_exit_2, + donut_secret_house_tile, + donut_secret_house_region, + donut_secret_house_exit_1, + donut_secret_house_exit_2, + donut_plains_castle_tile, + donut_plains_castle_region, + donut_plains_castle, + green_switch_palace_tile, + green_switch_palace, + donut_plains_top_secret_tile, + donut_plains_top_secret, + vanilla_dome_1_tile, + vanilla_dome_1_region, + vanilla_dome_1_exit_1, + vanilla_dome_1_exit_2, + vanilla_dome_2_tile, + vanilla_dome_2_region, + vanilla_dome_2_exit_1, + vanilla_dome_2_exit_2, + vanilla_dome_3_tile, + vanilla_dome_3_region, + vanilla_dome_3_exit_1, + vanilla_dome_4_tile, + vanilla_dome_4_region, + vanilla_dome_4_exit_1, + vanilla_secret_1_tile, + vanilla_secret_1_region, + vanilla_secret_1_exit_1, + vanilla_secret_1_exit_2, + vanilla_secret_2_tile, + vanilla_secret_2_region, + vanilla_secret_2_exit_1, + vanilla_secret_3_tile, + vanilla_secret_3_region, + vanilla_secret_3_exit_1, + vanilla_ghost_house_tile, + vanilla_ghost_house_region, + vanilla_ghost_house_exit_1, + vanilla_fortress_tile, + vanilla_fortress_region, + vanilla_fortress, + vanilla_dome_castle_tile, + vanilla_dome_castle_region, + vanilla_dome_castle, + red_switch_palace_tile, + red_switch_palace, + butter_bridge_1_tile, + butter_bridge_1_region, + butter_bridge_1_exit_1, + butter_bridge_2_tile, + butter_bridge_2_region, + butter_bridge_2_exit_1, + cheese_bridge_tile, + cheese_bridge_region, + cheese_bridge_exit_1, + cheese_bridge_exit_2, + cookie_mountain_tile, + cookie_mountain_region, + cookie_mountain_exit_1, + soda_lake_tile, + soda_lake_region, + soda_lake_exit_1, + twin_bridges_castle_tile, + twin_bridges_castle_region, + twin_bridges_castle, + forest_of_illusion_1_tile, + forest_of_illusion_1_region, + forest_of_illusion_1_exit_1, + forest_of_illusion_1_exit_2, + forest_of_illusion_2_tile, + forest_of_illusion_2_region, + forest_of_illusion_2_exit_1, + forest_of_illusion_2_exit_2, + forest_of_illusion_3_tile, + forest_of_illusion_3_region, + forest_of_illusion_3_exit_1, + forest_of_illusion_3_exit_2, + forest_of_illusion_4_tile, + forest_of_illusion_4_region, + forest_of_illusion_4_exit_1, + forest_of_illusion_4_exit_2, + forest_ghost_house_tile, + forest_ghost_house_region, + forest_ghost_house_exit_1, + forest_ghost_house_exit_2, + forest_secret_tile, + forest_secret_region, + forest_secret_exit_1, + forest_fortress_tile, + forest_fortress_region, + forest_fortress, + forest_castle_tile, + forest_castle_region, + forest_castle, + blue_switch_palace_tile, + blue_switch_palace, + chocolate_island_1_tile, + chocolate_island_1_region, + chocolate_island_1_exit_1, + chocolate_island_2_tile, + chocolate_island_2_region, + chocolate_island_2_exit_1, + chocolate_island_2_exit_2, + chocolate_island_3_tile, + chocolate_island_3_region, + chocolate_island_3_exit_1, + chocolate_island_3_exit_2, + chocolate_island_4_tile, + chocolate_island_4_region, + chocolate_island_4_exit_1, + chocolate_island_5_tile, + chocolate_island_5_region, + chocolate_island_5_exit_1, + chocolate_ghost_house_tile, + chocolate_ghost_house_region, + chocolate_ghost_house_exit_1, + chocolate_secret_tile, + chocolate_secret_region, + chocolate_secret_exit_1, + chocolate_fortress_tile, + chocolate_fortress_region, + chocolate_fortress, + chocolate_castle_tile, + chocolate_castle_region, + chocolate_castle, + sunken_ghost_ship_tile, + sunken_ghost_ship_region, + sunken_ghost_ship, + valley_of_bowser_1_tile, + valley_of_bowser_1_region, + valley_of_bowser_1_exit_1, + valley_of_bowser_2_tile, + valley_of_bowser_2_region, + valley_of_bowser_2_exit_1, + valley_of_bowser_2_exit_2, + valley_of_bowser_3_tile, + valley_of_bowser_3_region, + valley_of_bowser_3_exit_1, + valley_of_bowser_4_tile, + valley_of_bowser_4_region, + valley_of_bowser_4_exit_1, + valley_of_bowser_4_exit_2, + valley_ghost_house_tile, + valley_ghost_house_region, + valley_ghost_house_exit_1, + valley_ghost_house_exit_2, + valley_fortress_tile, + valley_fortress_region, + valley_fortress, + valley_castle_tile, + valley_castle_region, + valley_castle, + front_door_tile, + front_door_region, + back_door_tile, + back_door_region, + bowser_region, + donut_plains_star_road, + vanilla_dome_star_road, + twin_bridges_star_road, + forest_star_road, + valley_star_road, + star_road_donut, + star_road_vanilla, + star_road_twin_bridges, + star_road_forest, + star_road_valley, + star_road_special, + special_star_road, + star_road_1_tile, + star_road_1_region, + star_road_1_exit_1, + star_road_1_exit_2, + star_road_2_tile, + star_road_2_region, + star_road_2_exit_1, + star_road_2_exit_2, + star_road_3_tile, + star_road_3_region, + star_road_3_exit_1, + star_road_3_exit_2, + star_road_4_tile, + star_road_4_region, + star_road_4_exit_1, + star_road_4_exit_2, + star_road_5_tile, + star_road_5_region, + star_road_5_exit_1, + star_road_5_exit_2, + special_zone_1_tile, + special_zone_1_region, + special_zone_1_exit_1, + special_zone_2_tile, + special_zone_2_region, + special_zone_2_exit_1, + special_zone_3_tile, + special_zone_3_region, + special_zone_3_exit_1, + special_zone_4_tile, + special_zone_4_region, + special_zone_4_exit_1, + special_zone_5_tile, + special_zone_5_region, + special_zone_5_exit_1, + special_zone_6_tile, + special_zone_6_region, + special_zone_6_exit_1, + special_zone_7_tile, + special_zone_7_region, + special_zone_7_exit_1, + special_zone_8_tile, + special_zone_8_region, + special_zone_8_exit_1, + special_complete, + ] + + + if world.dragon_coin_checks[player]: + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_dragon, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.progressive_powerup, player, 1))) + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_climb, player))) + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_dragon, + lambda state: state.has(ItemName.p_switch, player)) + add_location_to_region(world, player, active_locations, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_swim, player) or + (state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player)))) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_1_region, LocationName.donut_plains_1_dragon, + lambda state: (state.has(ItemName.mario_climb, player) or + state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.progressive_powerup, player, 3) and state.has(ItemName.mario_run, player)))) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_dragon) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_3_region, LocationName.donut_plains_3_dragon, + lambda state: ((state.has(ItemName.mario_spin_jump, player) and state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_climb, player) or + state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + add_location_to_region(world, player, active_locations, LocationName.donut_plains_4_region, LocationName.donut_plains_4_dragon) + add_location_to_region(world, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.donut_secret_2_region, LocationName.donut_secret_2_dragon, + lambda state: (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_dragon, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_run, player) and + (state.has(ItemName.super_star_active, player) or + state.has(ItemName.progressive_powerup, player, 1)))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_dragon, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.p_switch, player) and + (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player)))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_dragon) + add_location_to_region(world, player, active_locations, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_dragon) + add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_dragon, + lambda state: (state.has(ItemName.mario_climb, player) and + state.has(ItemName.mario_carry, player))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_dragon, + lambda state: (state.has(ItemName.mario_run, player) and + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_dragon, + lambda state: state.has(ItemName.mario_climb, player)) + add_location_to_region(world, player, active_locations, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_dragon) + add_location_to_region(world, player, active_locations, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.cheese_bridge_region, LocationName.cheese_bridge_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_climb, player))) + add_location_to_region(world, player, active_locations, LocationName.cookie_mountain_region, LocationName.cookie_mountain_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_climb, player))) + add_location_to_region(world, player, active_locations, LocationName.soda_lake_region, LocationName.soda_lake_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_carry, player))) + add_location_to_region(world, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_dragon, + lambda state: (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.mario_carry, player) or + state.has(ItemName.p_switch, player) or + state.has(ItemName.progressive_powerup, player, 2))) + add_location_to_region(world, player, active_locations, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_dragon, + lambda state: state.has(ItemName.p_switch, player)) + add_location_to_region(world, player, active_locations, LocationName.forest_secret_region, LocationName.forest_secret_dragon) + add_location_to_region(world, player, active_locations, LocationName.forest_castle_region, LocationName.forest_castle_dragon) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_dragon, + lambda state: (state.has(ItemName.blue_switch_palace, player) and + (state.has(ItemName.p_switch, player) or + state.has(ItemName.green_switch_palace, player) or + (state.has(ItemName.yellow_switch_palace, player) or state.has(ItemName.red_switch_palace, player))))) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_dragon) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_dragon, + lambda state: (state.has(ItemName.mario_run, player) and + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_dragon, + lambda state: (state.has(ItemName.mario_swim, player) or + (state.has(ItemName.mario_carry, player) and state.has(ItemName.p_switch, player)))) + add_location_to_region(world, player, active_locations, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship_dragon, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.super_star_active, player) and + state.has(ItemName.progressive_powerup, player, 3))) + add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_dragon) + add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_dragon) + add_location_to_region(world, player, active_locations, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_dragon) + add_location_to_region(world, player, active_locations, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_dragon, + lambda state: state.has(ItemName.p_switch, player)) + add_location_to_region(world, player, active_locations, LocationName.valley_castle_region, LocationName.valley_castle_dragon) + add_location_to_region(world, player, active_locations, LocationName.star_road_1_region, LocationName.star_road_1_dragon, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.progressive_powerup, player, 1))) + add_location_to_region(world, player, active_locations, LocationName.special_zone_1_region, LocationName.special_zone_1_dragon, + lambda state: state.has(ItemName.mario_climb, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_2_region, LocationName.special_zone_2_dragon, + lambda state: state.has(ItemName.p_balloon, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_3_region, LocationName.special_zone_3_dragon, + lambda state: state.has(ItemName.yoshi_activate, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_4_region, LocationName.special_zone_4_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_5_region, LocationName.special_zone_5_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_6_region, LocationName.special_zone_6_dragon, + lambda state: state.has(ItemName.mario_swim, player)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_7_region, LocationName.special_zone_7_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + add_location_to_region(world, player, active_locations, LocationName.special_zone_8_region, LocationName.special_zone_8_dragon, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + + + +def connect_regions(world, player, level_to_tile_dict): + names: typing.Dict[str, int] = {} + + connect(world, player, names, "Menu", LocationName.yoshis_island_region) + connect(world, player, names, LocationName.yoshis_island_region, LocationName.yoshis_island_1_tile) + connect(world, player, names, LocationName.yoshis_island_region, LocationName.yoshis_island_2_tile) + + # Connect regions within levels using rules + connect(world, player, names, LocationName.yoshis_island_1_region, LocationName.yoshis_island_1_exit_1) + connect(world, player, names, LocationName.yoshis_island_2_region, LocationName.yoshis_island_2_exit_1) + connect(world, player, names, LocationName.yoshis_island_3_region, LocationName.yoshis_island_3_exit_1) + connect(world, player, names, LocationName.yoshis_island_4_region, LocationName.yoshis_island_4_exit_1) + connect(world, player, names, LocationName.yoshis_island_castle_region, LocationName.yoshis_island_castle, + lambda state: (state.has(ItemName.mario_climb, player))) + + connect(world, player, names, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_1) + connect(world, player, names, LocationName.donut_plains_1_region, LocationName.donut_plains_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + (state.has(ItemName.yoshi_activate, player) or + state.has(ItemName.green_switch_palace, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + connect(world, player, names, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_1) + connect(world, player, names, LocationName.donut_plains_2_region, LocationName.donut_plains_2_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + (state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.mario_spin_jump, player) and state.has(ItemName.mario_climb, player) and state.has(ItemName.progressive_powerup, player, 1))))) + connect(world, player, names, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.donut_secret_1_region, LocationName.donut_secret_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_swim, player) and + state.has(ItemName.p_switch, player))) + connect(world, player, names, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_1, + lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))) + connect(world, player, names, LocationName.donut_ghost_house_region, LocationName.donut_ghost_house_exit_2, + lambda state: (state.has(ItemName.mario_climb, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))) + connect(world, player, names, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.donut_secret_house_region, LocationName.donut_secret_house_exit_2, + lambda state: (state.has(ItemName.p_switch, player) and state.has(ItemName.mario_carry, player) and + (state.has(ItemName.mario_climb, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + connect(world, player, names, LocationName.donut_plains_3_region, LocationName.donut_plains_3_exit_1) + connect(world, player, names, LocationName.donut_plains_4_region, LocationName.donut_plains_4_exit_1) + connect(world, player, names, LocationName.donut_secret_2_region, LocationName.donut_secret_2_exit_1) + connect(world, player, names, LocationName.donut_plains_castle_region, LocationName.donut_plains_castle) + + connect(world, player, names, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_1, + lambda state: (state.has(ItemName.mario_run, player) and + (state.has(ItemName.super_star_active, player) or + state.has(ItemName.progressive_powerup, player, 1)))) + connect(world, player, names, LocationName.vanilla_dome_1_region, LocationName.vanilla_dome_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + ((state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_climb, player)) or + (state.has(ItemName.yoshi_activate, player) and state.has(ItemName.red_switch_palace, player)) or + (state.has(ItemName.red_switch_palace, player) and state.has(ItemName.mario_climb, player))))) + connect(world, player, names, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_1, + lambda state: (state.has(ItemName.mario_swim, player) and + (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player)))) + connect(world, player, names, LocationName.vanilla_dome_2_region, LocationName.vanilla_dome_2_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.p_switch, player) and + state.has(ItemName.mario_carry, player) and + (state.has(ItemName.mario_climb, player) or state.has(ItemName.yoshi_activate, player)))) + connect(world, player, names, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_1, + lambda state: state.has(ItemName.mario_climb, player)) + connect(world, player, names, LocationName.vanilla_secret_1_region, LocationName.vanilla_secret_1_exit_2, + lambda state: (state.has(ItemName.mario_climb, player) and + (state.has(ItemName.mario_carry, player) and state.has(ItemName.blue_switch_palace, player)))) + connect(world, player, names, LocationName.vanilla_ghost_house_region, LocationName.vanilla_ghost_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.vanilla_dome_3_region, LocationName.vanilla_dome_3_exit_1) + connect(world, player, names, LocationName.vanilla_dome_4_region, LocationName.vanilla_dome_4_exit_1) + connect(world, player, names, LocationName.vanilla_secret_2_region, LocationName.vanilla_secret_2_exit_1) + connect(world, player, names, LocationName.vanilla_secret_3_region, LocationName.vanilla_secret_3_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.vanilla_fortress_region, LocationName.vanilla_fortress, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.vanilla_dome_castle_region, LocationName.vanilla_dome_castle) + + connect(world, player, names, LocationName.butter_bridge_1_region, LocationName.butter_bridge_1_exit_1) + connect(world, player, names, LocationName.butter_bridge_2_region, LocationName.butter_bridge_2_exit_1) + connect(world, player, names, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_1, + lambda state: state.has(ItemName.mario_climb, player)) + connect(world, player, names, LocationName.cheese_bridge_region, LocationName.cheese_bridge_exit_2, + lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))) + connect(world, player, names, LocationName.soda_lake_region, LocationName.soda_lake_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.cookie_mountain_region, LocationName.cookie_mountain_exit_1) + connect(world, player, names, LocationName.twin_bridges_castle_region, LocationName.twin_bridges_castle, + lambda state: (state.has(ItemName.mario_run, player) and + state.has(ItemName.mario_climb, player))) + + connect(world, player, names, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_1) + connect(world, player, names, LocationName.forest_of_illusion_1_region, LocationName.forest_of_illusion_1_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.p_balloon, player))) + connect(world, player, names, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.mario_carry, player))) + connect(world, player, names, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_1, + lambda state: (state.has(ItemName.mario_carry, player) or + state.has(ItemName.yoshi_activate, player))) + connect(world, player, names, LocationName.forest_of_illusion_3_region, LocationName.forest_of_illusion_3_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.progressive_powerup, player, 1))) + connect(world, player, names, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_1) + connect(world, player, names, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.forest_ghost_house_region, LocationName.forest_ghost_house_exit_2, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.forest_secret_region, LocationName.forest_secret_exit_1) + connect(world, player, names, LocationName.forest_fortress_region, LocationName.forest_fortress) + connect(world, player, names, LocationName.forest_castle_region, LocationName.forest_castle) + + connect(world, player, names, LocationName.chocolate_island_1_region, LocationName.chocolate_island_1_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_1) + connect(world, player, names, LocationName.chocolate_island_2_region, LocationName.chocolate_island_2_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_1, + lambda state: (state.has(ItemName.mario_climb, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))) + connect(world, player, names, LocationName.chocolate_island_3_region, LocationName.chocolate_island_3_exit_2, + lambda state: (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))) + connect(world, player, names, LocationName.chocolate_island_4_region, LocationName.chocolate_island_4_exit_1) + connect(world, player, names, LocationName.chocolate_island_5_region, LocationName.chocolate_island_5_exit_1) + connect(world, player, names, LocationName.chocolate_ghost_house_region, LocationName.chocolate_ghost_house_exit_1) + connect(world, player, names, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress) + connect(world, player, names, LocationName.chocolate_secret_region, LocationName.chocolate_secret_exit_1, + lambda state: state.has(ItemName.mario_run, player)) + connect(world, player, names, LocationName.chocolate_castle_region, LocationName.chocolate_castle, + lambda state: (state.has(ItemName.progressive_powerup, player, 1))) + + connect(world, player, names, LocationName.sunken_ghost_ship_region, LocationName.sunken_ghost_ship, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.valley_of_bowser_1_region, LocationName.valley_of_bowser_1_exit_1) + connect(world, player, names, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_1) + connect(world, player, names, LocationName.valley_of_bowser_2_region, LocationName.valley_of_bowser_2_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.valley_of_bowser_3_region, LocationName.valley_of_bowser_3_exit_1) + connect(world, player, names, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_1, + lambda state: state.has(ItemName.mario_climb, player)) + connect(world, player, names, LocationName.valley_of_bowser_4_region, LocationName.valley_of_bowser_4_exit_2, + lambda state: (state.has(ItemName.mario_climb, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.yoshi_activate, player))) + connect(world, player, names, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.valley_ghost_house_region, LocationName.valley_ghost_house_exit_2, + lambda state: (state.has(ItemName.p_switch, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_run, player))) + connect(world, player, names, LocationName.valley_fortress_region, LocationName.valley_fortress, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.valley_castle_region, LocationName.valley_castle) + connect(world, player, names, LocationName.front_door, LocationName.bowser_region, + lambda state: (state.has(ItemName.mario_climb, player) and + state.has(ItemName.mario_run, player) and + state.has(ItemName.mario_swim, player) and + state.has(ItemName.progressive_powerup, player, 1) and + state.has(ItemName.koopaling, player, world.bosses_required[player].value))) + connect(world, player, names, LocationName.back_door, LocationName.bowser_region, + lambda state: state.has(ItemName.koopaling, player, world.bosses_required[player].value)) + + connect(world, player, names, LocationName.star_road_1_region, LocationName.star_road_1_exit_1, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.progressive_powerup, player, 1))) + connect(world, player, names, LocationName.star_road_1_region, LocationName.star_road_1_exit_2, + lambda state: (state.has(ItemName.mario_spin_jump, player) and + state.has(ItemName.mario_carry, player) and + state.has(ItemName.progressive_powerup, player, 1))) + connect(world, player, names, LocationName.star_road_2_region, LocationName.star_road_2_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.star_road_2_region, LocationName.star_road_2_exit_2, + lambda state: (state.has(ItemName.mario_swim, player) and + state.has(ItemName.mario_carry, player))) + connect(world, player, names, LocationName.star_road_3_region, LocationName.star_road_3_exit_1) + connect(world, player, names, LocationName.star_road_3_region, LocationName.star_road_3_exit_2, + lambda state: state.has(ItemName.mario_carry, player)) + connect(world, player, names, LocationName.star_road_4_region, LocationName.star_road_4_exit_1) + connect(world, player, names, LocationName.star_road_4_region, LocationName.star_road_4_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + (state.has(ItemName.yoshi_activate, player) or + (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.red_switch_palace, player))))) + connect(world, player, names, LocationName.star_road_5_region, LocationName.star_road_5_exit_1, + lambda state: state.has(ItemName.p_switch, player)) + connect(world, player, names, LocationName.star_road_5_region, LocationName.star_road_5_exit_2, + lambda state: (state.has(ItemName.mario_carry, player) and + state.has(ItemName.mario_climb, player) and + state.has(ItemName.p_switch, player) and + state.has(ItemName.yellow_switch_palace, player) and + state.has(ItemName.green_switch_palace, player) and + state.has(ItemName.red_switch_palace, player) and + state.has(ItemName.blue_switch_palace, player))) + + connect(world, player, names, LocationName.special_zone_1_region, LocationName.special_zone_1_exit_1, + lambda state: (state.has(ItemName.mario_climb, player) and + (state.has(ItemName.p_switch, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3))))) + connect(world, player, names, LocationName.special_zone_2_region, LocationName.special_zone_2_exit_1, + lambda state: state.has(ItemName.p_balloon, player)) + connect(world, player, names, LocationName.special_zone_3_region, LocationName.special_zone_3_exit_1, + lambda state: (state.has(ItemName.mario_climb, player) or + state.has(ItemName.p_switch, player) or + (state.has(ItemName.mario_run, player) and state.has(ItemName.progressive_powerup, player, 3)))) + connect(world, player, names, LocationName.special_zone_4_region, LocationName.special_zone_4_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.special_zone_5_region, LocationName.special_zone_5_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.special_zone_6_region, LocationName.special_zone_6_exit_1, + lambda state: state.has(ItemName.mario_swim, player)) + connect(world, player, names, LocationName.special_zone_7_region, LocationName.special_zone_7_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + connect(world, player, names, LocationName.special_zone_8_region, LocationName.special_zone_8_exit_1, + lambda state: state.has(ItemName.progressive_powerup, player, 1)) + + + + # Connect levels to each other + for current_level_id, current_level_data in level_info_dict.items(): + # Connect tile regions to correct level regions + + if current_level_id not in level_to_tile_dict.keys(): + continue + + current_tile_id = level_to_tile_dict[current_level_id] + current_tile_data = level_info_dict[current_tile_id] + current_tile_name = current_tile_data.levelName + if ("Star Road - " not in current_tile_name) and (" - Star Road" not in current_tile_name): + current_tile_name += " - Tile" + connect(world, player, names, current_tile_name, current_level_data.levelName) + # Connect Exit regions to next tile regions + if current_tile_data.exit1Path: + next_tile_id = current_tile_data.exit1Path.otherLevelID + if world.swap_donut_gh_exits[player] and current_tile_id == 0x04: + next_tile_id = current_tile_data.exit2Path.otherLevelID + next_tile_name = level_info_dict[next_tile_id].levelName + if ("Star Road - " not in next_tile_name) and (" - Star Road" not in next_tile_name): + next_tile_name += " - Tile" + current_exit_name = (current_level_data.levelName + " - Normal Exit") + connect(world, player, names, current_exit_name, next_tile_name) + if current_tile_data.exit2Path: + next_tile_id = current_tile_data.exit2Path.otherLevelID + if world.swap_donut_gh_exits[player] and current_tile_id == 0x04: + next_tile_id = current_tile_data.exit1Path.otherLevelID + next_tile_name = level_info_dict[next_tile_id].levelName + if ("Star Road - " not in next_tile_name) and (" - Star Road" not in next_tile_name): + next_tile_name += " - Tile" + current_exit_name = (current_level_data.levelName + " - Secret Exit") + connect(world, player, names, current_exit_name, next_tile_name) + + connect(world, player, names, LocationName.donut_plains_star_road, LocationName.star_road_donut) + connect(world, player, names, LocationName.star_road_donut, LocationName.donut_plains_star_road) + connect(world, player, names, LocationName.star_road_donut, LocationName.star_road_1_tile) + connect(world, player, names, LocationName.vanilla_dome_star_road, LocationName.star_road_vanilla) + connect(world, player, names, LocationName.star_road_vanilla, LocationName.vanilla_dome_star_road) + connect(world, player, names, LocationName.star_road_vanilla, LocationName.star_road_2_tile) + connect(world, player, names, LocationName.twin_bridges_star_road, LocationName.star_road_twin_bridges) + connect(world, player, names, LocationName.star_road_twin_bridges, LocationName.twin_bridges_star_road) + connect(world, player, names, LocationName.star_road_twin_bridges, LocationName.star_road_3_tile) + connect(world, player, names, LocationName.forest_star_road, LocationName.star_road_forest) + connect(world, player, names, LocationName.star_road_forest, LocationName.forest_star_road) + connect(world, player, names, LocationName.star_road_forest, LocationName.star_road_4_tile) + connect(world, player, names, LocationName.valley_star_road, LocationName.star_road_valley) + connect(world, player, names, LocationName.star_road_valley, LocationName.valley_star_road) + connect(world, player, names, LocationName.star_road_valley, LocationName.star_road_5_tile) + connect(world, player, names, LocationName.star_road_special, LocationName.special_star_road) + connect(world, player, names, LocationName.special_star_road, LocationName.star_road_special) + connect(world, player, names, LocationName.special_star_road, LocationName.special_zone_1_tile) + + connect(world, player, names, LocationName.star_road_valley, LocationName.front_door_tile) + + + +def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): + ret = Region(name, RegionType.Generic, name, player) + ret.world = world + if locations: + for locationName in locations: + loc_id = active_locations.get(locationName, 0) + if loc_id: + location = SMWLocation(player, locationName, loc_id, ret) + ret.locations.append(location) + + return ret + +def add_location_to_region(world: MultiWorld, player: int, active_locations, region_name: str, location_name: str, + rule: typing.Optional[typing.Callable] = None): + region = world.get_region(region_name, player) + loc_id = active_locations.get(location_name, 0) + if loc_id: + location = SMWLocation(player, location_name, loc_id, region) + region.locations.append(location) + if rule: + add_rule(location, rule) + + + +def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str, + rule: typing.Optional[typing.Callable] = None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if target not in used_names: + used_names[target] = 1 + name = target + else: + used_names[target] += 1 + name = target + (' ' * used_names[target]) + + connection = Entrance(player, name, source_region) + + if rule: + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) diff --git a/worlds/smw/Rom.py b/worlds/smw/Rom.py new file mode 100644 index 0000000000..6c750d74ff --- /dev/null +++ b/worlds/smw/Rom.py @@ -0,0 +1,846 @@ +import Utils +from Patch import read_rom, APDeltaPatch +from .Aesthetics import generate_shuffled_header_data +from .Locations import lookup_id_to_name, all_locations +from .Levels import level_info_dict, full_level_list, submap_level_list, location_id_to_level_id +from .Names.TextBox import generate_goal_text, title_text_mapping, generate_text_box + +USHASH = 'cdd3c8c37322978ca8669b34bc89c804' +ROM_PLAYER_LIMIT = 65535 + +import hashlib +import os +import math + + +ability_rom_data = { + 0xBC0003: [[0x1F2C, 0x7]], # Run 0x80 + 0xBC0004: [[0x1F2C, 0x6]], # Carry 0x40 + 0xBC0005: [[0x1F2C, 0x2]], # Swim 0x04 + 0xBC0006: [[0x1F2C, 0x3]], # Spin Jump 0x08 + 0xBC0007: [[0x1F2C, 0x5]], # Climb 0x20 + 0xBC0008: [[0x1F2C, 0x1]], # Yoshi 0x02 + 0xBC0009: [[0x1F2C, 0x4]], # P-Switch 0x10 + #0xBC000A: [[]] + 0xBC000B: [[0x1F2D, 0x3]], # P-Balloon 0x08 + 0xBC000D: [[0x1F2D, 0x4]], # Super Star 0x10 +} + + +item_rom_data = { + 0xBC0001: [0x18E4, 0x1], # 1-Up Mushroom + + 0xBC0002: [0x1F24, 0x1, 0x1F], # Yoshi Egg + 0xBC0012: [0x1F26, 0x1, 0x09], # Boss Token + + 0xBC000E: [0x1F28, 0x1, 0x1C], # Yellow Switch Palace + 0xBC000F: [0x1F27, 0x1, 0x1C], # Green Switch Palace + 0xBC0010: [0x1F2A, 0x1, 0x1C], # Red Switch Palace + 0xBC0011: [0x1F29, 0x1, 0x1C], # Blue Switch Palace + + 0xBC0013: [0x0086, 0x1, 0x0E], # Ice Trap + 0xBC0014: [0x18BD, 0x7F, 0x18], # Stun Trap +} + +music_rom_data = [ + +] + +level_music_ids = [ + +] + + +class SMWDeltaPatch(APDeltaPatch): + hash = USHASH + game = "Super Mario World" + patch_file_ending = ".apsmw" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +class LocalRom: + + def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None): + self.name = name + self.hash = hash + self.orig_buffer = None + + with open(file, 'rb') as stream: + self.buffer = read_rom(stream) + + def read_bit(self, address: int, bit_number: int) -> bool: + bitflag = (1 << bit_number) + return ((self.buffer[address] & bitflag) != 0) + + def read_byte(self, address: int) -> int: + return self.buffer[address] + + def read_bytes(self, startaddress: int, length: int) -> bytes: + return self.buffer[startaddress:startaddress + length] + + def write_byte(self, address: int, value: int): + self.buffer[address] = value + + def write_bytes(self, startaddress: int, values): + self.buffer[startaddress:startaddress + len(values)] = values + + def write_to_file(self, file): + with open(file, 'wb') as outfile: + outfile.write(self.buffer) + + def read_from_file(self, file): + with open(file, 'rb') as stream: + self.buffer = bytearray(stream.read()) + + +def handle_ability_code(rom): + # Lock Abilities + + #rom.write_byte(0xC581, 0x01) # No Stars + #rom.write_byte(0x62E6, 0x01) # No Star Music + #rom.write_byte(0xC300, 0x01) # No P-Balloons + #rom.write_byte(0xC305, 0x01) # No P-Balloons + + # Run + rom.write_bytes(0x5977, bytearray([0x22, 0x10, 0xBA, 0x03])) # JSL $03BA10 + rom.write_bytes(0x597B, bytearray([0xEA] * 0x04)) + + RUN_SUB_ADDR = 0x01BA10 + rom.write_bytes(RUN_SUB_ADDR + 0x00, bytearray([0xDA])) # PHX + rom.write_bytes(RUN_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(RUN_SUB_ADDR + 0x02, bytearray([0x90, 0x03])) # BCC +0x03 + rom.write_bytes(RUN_SUB_ADDR + 0x04, bytearray([0xC8])) # INY + rom.write_bytes(RUN_SUB_ADDR + 0x05, bytearray([0xA9, 0x70])) # LDA #70 + rom.write_bytes(RUN_SUB_ADDR + 0x07, bytearray([0xAA])) # TAX + rom.write_bytes(RUN_SUB_ADDR + 0x08, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(RUN_SUB_ADDR + 0x0B, bytearray([0x89, 0x80])) # BIT #80 + rom.write_bytes(RUN_SUB_ADDR + 0x0D, bytearray([0xF0, 0x04])) # BEQ +0x04 + rom.write_bytes(RUN_SUB_ADDR + 0x0F, bytearray([0x8A])) # TXA + rom.write_bytes(RUN_SUB_ADDR + 0x10, bytearray([0x8D, 0xE4, 0x13])) # STA $13E4 + rom.write_bytes(RUN_SUB_ADDR + 0x13, bytearray([0x8A])) # TXA + rom.write_bytes(RUN_SUB_ADDR + 0x14, bytearray([0x28])) # PLP + rom.write_bytes(RUN_SUB_ADDR + 0x15, bytearray([0xFA])) # PLX + rom.write_bytes(RUN_SUB_ADDR + 0x16, bytearray([0x6B])) # RTL + # End Run + + # Purple Block Carry + rom.write_bytes(0x726F, bytearray([0x22, 0x28, 0xBA, 0x03])) # JSL $03BA28 + rom.write_bytes(0x7273, bytearray([0xEA] * 0x02)) + + PURPLE_BLOCK_CARRY_SUB_ADDR = 0x01BA28 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x04, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x09, bytearray([0xAD, 0x8F, 0x14])) # LDA $148F + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x0C, bytearray([0x0D, 0x7A, 0x18])) # ORA $187A + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x0F, bytearray([0x80, 0x03])) # BRA +0x03 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x12, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(PURPLE_BLOCK_CARRY_SUB_ADDR + 0x14, bytearray([0x6B])) # RTL + # End Purple Block Carry + + # Springboard Carry + rom.write_bytes(0xE6DA, bytearray([0x22, 0x40, 0xBA, 0x03])) # JSL $03BA40 + rom.write_bytes(0xE6DE, bytearray([0xEA] * 0x04)) + + SPRINGBOARD_CARRY_SUB_ADDR = 0x01BA40 + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x00, bytearray([0x48])) # PHA + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x05, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x07, bytearray([0xF0, 0x08])) # BEQ +0x08 + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x09, bytearray([0xA9, 0x0B])) # LDA #0B + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x0B, bytearray([0x9D, 0xC8, 0x14])) # STA $14C8, X + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x0E, bytearray([0x9E, 0x02, 0x16])) # STZ $1602, X + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x12, bytearray([0x68])) # PLA + rom.write_bytes(SPRINGBOARD_CARRY_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + # End Springboard Carry + + # Shell Carry + rom.write_bytes(0xAA66, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(0xAA69, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(0xAA6B, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(0xAA6D, bytearray([0x22, 0x60, 0xBA, 0x03])) # JSL $03BA60 + rom.write_bytes(0xAA71, bytearray([0xEA] * 0x02)) + + SHELL_CARRY_SUB_ADDR = 0x01BA60 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x01, bytearray([0xA9, 0x0B])) # LDA #0B + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x03, bytearray([0x9D, 0xC8, 0x14])) # STA $14C8, X + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x06, bytearray([0xEE, 0x70, 0x14])) # INC $1470 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x09, bytearray([0xA9, 0x0B])) # LDA #08 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x0B, bytearray([0x8D, 0x98, 0x14])) # STA $1498 + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x0E, bytearray([0x28])) # PLP + rom.write_bytes(SHELL_CARRY_SUB_ADDR + 0x0F, bytearray([0x6B])) # RTL + # End Shell Carry + + # Yoshi Carry + rom.write_bytes(0xF309, bytearray([0x22, 0x70, 0xBA, 0x03])) # JSL $03BA70 + rom.write_bytes(0xF30D, bytearray([0xEA] * 0x06)) + + YOSHI_CARRY_SUB_ADDR = 0x01BA70 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x04, bytearray([0x89, 0x40])) # BIT #40 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x06, bytearray([0xF0, 0x0A])) # BEQ +0x0A + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x08, bytearray([0xA9, 0x12])) # LDA #12 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x0A, bytearray([0x8D, 0xA3, 0x14])) # STA $14A3 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x0D, bytearray([0xA9, 0x21])) # LDA #21 + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x0F, bytearray([0x8D, 0xFC, 0x1D])) # STA $1DFC + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x12, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_CARRY_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + # End Yoshi Carry + + # Climb + rom.write_bytes(0x4D72, bytearray([0x5C, 0x88, 0xBA, 0x03])) # JML $03BA88 + rom.write_bytes(0x4D76, bytearray([0xEA] * 0x03)) + + CLIMB_SUB_ADDR = 0x01BA88 + rom.write_bytes(CLIMB_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(CLIMB_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(CLIMB_SUB_ADDR + 0x04, bytearray([0x89, 0x20])) # BIT #20 + rom.write_bytes(CLIMB_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09 + rom.write_bytes(CLIMB_SUB_ADDR + 0x08, bytearray([0xA5, 0x8B])) # LDA $8B + rom.write_bytes(CLIMB_SUB_ADDR + 0x0A, bytearray([0x85, 0x74])) # STA $74 + rom.write_bytes(CLIMB_SUB_ADDR + 0x0C, bytearray([0x28])) # PLP + rom.write_bytes(CLIMB_SUB_ADDR + 0x0D, bytearray([0x5C, 0x17, 0xDB, 0x00])) # JML $00DB17 + rom.write_bytes(CLIMB_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(CLIMB_SUB_ADDR + 0x12, bytearray([0x5C, 0x76, 0xCD, 0x00])) # JML $00CD76 + # End Climb + + # P-Switch + rom.write_bytes(0xAB1A, bytearray([0x22, 0xA0, 0xBA, 0x03])) # JSL $03BAA0 + rom.write_bytes(0xAB1E, bytearray([0xEA] * 0x01)) + + P_SWITCH_SUB_ADDR = 0x01BAA0 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x04, bytearray([0x89, 0x10])) # BIT #10 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x06, bytearray([0xF0, 0x04])) # BEQ +0x04 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x08, bytearray([0xA9, 0xB0])) # LDA #B0 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x0A, bytearray([0x80, 0x02])) # BRA +0x02 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x0C, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x0E, bytearray([0x99, 0xAD, 0x14])) # STA $14AD + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(P_SWITCH_SUB_ADDR + 0x12, bytearray([0x6B])) # RTL + # End P-Switch + + # Spin Jump + rom.write_bytes(0x5645, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(0x5648, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(0x564A, bytearray([0xF0, 0x12])) # BEQ +0x12 + rom.write_bytes(0x564C, bytearray([0x22, 0xB8, 0xBA, 0x03])) # JSL $03BAB8 + + SPIN_JUMP_SUB_ADDR = 0x01BAB8 + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x01, bytearray([0x1A])) # INC + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x02, bytearray([0x8D, 0x0D, 0x14])) # STA $140D + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x05, bytearray([0xA9, 0x04])) # LDA #04 + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x07, bytearray([0x8D, 0xFC, 0x1D])) # STA $1DFC + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x0A, bytearray([0xA4, 0x76])) # LDY #76 + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x0C, bytearray([0x28])) # PLP + rom.write_bytes(SPIN_JUMP_SUB_ADDR + 0x0D, bytearray([0x6B])) # RTL + # End Spin Jump + + # Spin Jump from Water + rom.write_bytes(0x6A89, bytearray([0x22, 0xF8, 0xBB, 0x03])) # JSL $03BBF8 + rom.write_bytes(0x6A8D, bytearray([0xEA] * 0x05)) + + SPIN_JUMP_WATER_SUB_ADDR = 0x01BBF8 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x06, bytearray([0xF0, 0x09])) # BEQ +0x09 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x08, bytearray([0x1A])) # INC + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x09, bytearray([0x8D, 0x0D, 0x14])) # STA $140D + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x0C, bytearray([0xA9, 0x04])) # LDA #04 + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x0E, bytearray([0x8D, 0xFC, 0x1D])) # STA $1DFC + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x11, bytearray([0x28])) # PLP + rom.write_bytes(SPIN_JUMP_WATER_SUB_ADDR + 0x12, bytearray([0x6B])) # RTL + # End Spin Jump from Water + + # Spin Jump from Springboard + rom.write_bytes(0xE693, bytearray([0x22, 0x0C, 0xBC, 0x03])) # JSL $03BC0C + rom.write_bytes(0xE697, bytearray([0xEA] * 0x01)) + + SPIN_JUMP_SPRING_SUB_ADDR = 0x01BC0C + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x06, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x08, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x0A, bytearray([0x8D, 0x0D, 0x14])) # STA $140D + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x0D, bytearray([0x28])) # PLP + rom.write_bytes(SPIN_JUMP_SPRING_SUB_ADDR + 0x0E, bytearray([0x6B])) # RTL + # End Spin Jump from Springboard + + # Swim + rom.write_bytes(0x5A25, bytearray([0x22, 0xC8, 0xBA, 0x03])) # JSL $03BAC8 + rom.write_bytes(0x5A29, bytearray([0xEA] * 0x04)) + + SWIM_SUB_ADDR = 0x01BAC8 + rom.write_bytes(SWIM_SUB_ADDR + 0x00, bytearray([0x48])) # PHA + rom.write_bytes(SWIM_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SWIM_SUB_ADDR + 0x05, bytearray([0x89, 0x04])) # BIT #04 + rom.write_bytes(SWIM_SUB_ADDR + 0x07, bytearray([0xF0, 0x0C])) # BEQ +0x0C + rom.write_bytes(SWIM_SUB_ADDR + 0x09, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x0A, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x0B, bytearray([0xDD, 0x84, 0xD9])) # CMP $D489, X + rom.write_bytes(SWIM_SUB_ADDR + 0x0E, bytearray([0xB0, 0x03])) # BCS +0x03 + rom.write_bytes(SWIM_SUB_ADDR + 0x10, bytearray([0xBD, 0x84, 0xD9])) # LDA $D489, X + rom.write_bytes(SWIM_SUB_ADDR + 0x13, bytearray([0x80, 0x0A])) # BRA +0x0A + rom.write_bytes(SWIM_SUB_ADDR + 0x15, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x16, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x17, bytearray([0xDD, 0xBE, 0xDE])) # CMP $DEBE, X + rom.write_bytes(SWIM_SUB_ADDR + 0x1A, bytearray([0xB0, 0x03])) # BCS +0x03 + rom.write_bytes(SWIM_SUB_ADDR + 0x1C, bytearray([0xBD, 0xBE, 0xDE])) # LDA $DEBE, X + rom.write_bytes(SWIM_SUB_ADDR + 0x1F, bytearray([0x6B])) # RTL + # End Swim + + # Item Swim + rom.write_bytes(0x59D7, bytearray([0x22, 0xE8, 0xBA, 0x03])) # JSL $03BAE8 + rom.write_bytes(0x59DB, bytearray([0xEA] * 0x02)) + + SWIM_SUB_ADDR = 0x01BAE8 + rom.write_bytes(SWIM_SUB_ADDR + 0x00, bytearray([0x48])) # PHA + rom.write_bytes(SWIM_SUB_ADDR + 0x01, bytearray([0x08])) # PHP + rom.write_bytes(SWIM_SUB_ADDR + 0x02, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(SWIM_SUB_ADDR + 0x05, bytearray([0x89, 0x04])) # BIT #04 + rom.write_bytes(SWIM_SUB_ADDR + 0x07, bytearray([0xF0, 0x0A])) # BEQ +0x0A + rom.write_bytes(SWIM_SUB_ADDR + 0x09, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x0A, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x0B, bytearray([0xC9, 0xF0])) # CMP #F0 + rom.write_bytes(SWIM_SUB_ADDR + 0x0D, bytearray([0xB0, 0x02])) # BCS +0x02 + rom.write_bytes(SWIM_SUB_ADDR + 0x0F, bytearray([0xA9, 0xF0])) # LDA #F0 + rom.write_bytes(SWIM_SUB_ADDR + 0x11, bytearray([0x80, 0x08])) # BRA +0x08 + rom.write_bytes(SWIM_SUB_ADDR + 0x13, bytearray([0x28])) # PLP + rom.write_bytes(SWIM_SUB_ADDR + 0x14, bytearray([0x68])) # PLA + rom.write_bytes(SWIM_SUB_ADDR + 0x15, bytearray([0xC9, 0xFF])) # CMP #FF + rom.write_bytes(SWIM_SUB_ADDR + 0x17, bytearray([0xB0, 0x02])) # BCS +0x02 + rom.write_bytes(SWIM_SUB_ADDR + 0x19, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(SWIM_SUB_ADDR + 0x1B, bytearray([0x6B])) # RTL + # End Item Swim + + # Yoshi + rom.write_bytes(0x109FB, bytearray([0x22, 0x08, 0xBB, 0x03])) # JSL $03BB08 + rom.write_bytes(0x109FF, bytearray([0xEA] * 0x02)) + + YOSHI_SUB_ADDR = 0x01BB08 + rom.write_bytes(YOSHI_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_SUB_ADDR + 0x01, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(YOSHI_SUB_ADDR + 0x04, bytearray([0x89, 0x02])) # BIT #02 + rom.write_bytes(YOSHI_SUB_ADDR + 0x06, bytearray([0xF0, 0x06])) # BEQ +0x06 + rom.write_bytes(YOSHI_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x09, bytearray([0xB9, 0xA1, 0x88])) # LDA $88A1, Y + rom.write_bytes(YOSHI_SUB_ADDR + 0x0C, bytearray([0x80, 0x04])) # BRA +0x04 + rom.write_bytes(YOSHI_SUB_ADDR + 0x0E, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x0F, bytearray([0xB9, 0xA2, 0x88])) # LDA $88A2, Y + rom.write_bytes(YOSHI_SUB_ADDR + 0x12, bytearray([0x9D, 0x1C, 0x15])) # STA $151C, X + rom.write_bytes(YOSHI_SUB_ADDR + 0x15, bytearray([0x6B])) # RTL + # End Yoshi + + # Baby Yoshi + rom.write_bytes(0xA2B8, bytearray([0x22, 0x20, 0xBB, 0x03])) # JSL $03BB20 + rom.write_bytes(0xA2BC, bytearray([0xEA] * 0x01)) + + YOSHI_SUB_ADDR = 0x01BB20 + rom.write_bytes(YOSHI_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_SUB_ADDR + 0x01, bytearray([0x9C, 0x1E, 0x14])) # STZ $141E + rom.write_bytes(YOSHI_SUB_ADDR + 0x04, bytearray([0xAD, 0x2C, 0x1F])) # LDA $1F2C + rom.write_bytes(YOSHI_SUB_ADDR + 0x07, bytearray([0x89, 0x02])) # BIT #02 + rom.write_bytes(YOSHI_SUB_ADDR + 0x09, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(YOSHI_SUB_ADDR + 0x0B, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x0C, bytearray([0xA9, 0x35])) # LDA #35 + rom.write_bytes(YOSHI_SUB_ADDR + 0x0E, bytearray([0x80, 0x03])) # BRA +0x03 + rom.write_bytes(YOSHI_SUB_ADDR + 0x10, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_SUB_ADDR + 0x11, bytearray([0xA9, 0x70])) # LDA #70 + rom.write_bytes(YOSHI_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + # End Baby Yoshi + + # Midway Gate + rom.write_bytes(0x72E4, bytearray([0x22, 0x38, 0xBB, 0x03])) # JSL $03BB38 + + MIDWAY_GATE_SUB_ADDR = 0x01BB38 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x04, bytearray([0x89, 0x01])) # BIT #01 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x09, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x0B, bytearray([0x85, 0x19])) # STA $19 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x0D, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(MIDWAY_GATE_SUB_ADDR + 0x10, bytearray([0x6B])) # RTL + # End Midway Gate + + # Mushroom + rom.write_bytes(0x5156, bytearray([0x22, 0x50, 0xBB, 0x03])) # JSL $03BB50 + rom.write_bytes(0x515A, bytearray([0xEA] * 0x04)) + + MUSHROOM_SUB_ADDR = 0x01BB50 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x04, bytearray([0x89, 0x01])) # BIT #01 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x06, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x09, bytearray([0xE6, 0x19])) # INC $19 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x0B, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x0D, bytearray([0x28])) # PLP + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x0E, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x10, bytearray([0x85, 0x71])) # STA $72 + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x12, bytearray([0x64, 0x9D])) # STZ $9D + rom.write_bytes(MUSHROOM_SUB_ADDR + 0x14, bytearray([0x6B])) # RTL + # End Mushroom + + # Take Damage + rom.write_bytes(0x5142, bytearray([0x22, 0x65, 0xBB, 0x03])) # JSL $03BB65 + rom.write_bytes(0x5146, bytearray([0x60] * 0x01)) # RTS + + DAMAGE_SUB_ADDR = 0x01BB65 + rom.write_bytes(DAMAGE_SUB_ADDR + 0x00, bytearray([0x8D, 0x97, 0x14])) # STA $1497 + rom.write_bytes(DAMAGE_SUB_ADDR + 0x03, bytearray([0x80, 0xF4])) # BRA -0x0C + # End Take Damage + + # Fire Flower Cycle + rom.write_bytes(0x5187, bytearray([0x22, 0x6A, 0xBB, 0x03])) # JSL $03BB6A + rom.write_bytes(0x518B, bytearray([0x60] * 0x01)) # RTS + + PALETTE_CYCLE_SUB_ADDR = 0x01BB6A + rom.write_bytes(PALETTE_CYCLE_SUB_ADDR + 0x00, bytearray([0xCE, 0x9B, 0x14])) # DEC $149B + rom.write_bytes(PALETTE_CYCLE_SUB_ADDR + 0x03, bytearray([0xF0, 0xEF])) # BEQ -0x11 + rom.write_bytes(PALETTE_CYCLE_SUB_ADDR + 0x05, bytearray([0x6B])) # RTL + # End Fire Flower Cycle + + # Pipe Exit + rom.write_bytes(0x526D, bytearray([0x22, 0x70, 0xBB, 0x03])) # JSL $03BB70 + rom.write_bytes(0x5271, bytearray([0x60, 0xEA] * 0x01)) # RTS, NOP + + PIPE_EXIT_SUB_ADDR = 0x01BB70 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x00, bytearray([0x9C, 0x19, 0x14])) # STZ $1419 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x03, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x05, bytearray([0x85, 0x71])) # STA $72 + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x07, bytearray([0x64, 0x9D])) # STZ $9D + rom.write_bytes(PIPE_EXIT_SUB_ADDR + 0x09, bytearray([0x6B])) # RTL + # End Pipe Exit + + # Cape Transform + rom.write_bytes(0x5168, bytearray([0x22, 0x7A, 0xBB, 0x03])) # JSL $03BB7A + rom.write_bytes(0x516C, bytearray([0xEA] * 0x01)) # RTS, NOP + rom.write_bytes(0x516D, bytearray([0xF0, 0xD1])) # BEQ -0x2F + + CAPE_TRANSFORM_SUB_ADDR = 0x01BB7A + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x00, bytearray([0xA5, 0x19])) # LDA $19 + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x02, bytearray([0x4A])) # LSR + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x03, bytearray([0xD0, 0xDF])) # BNE -0x21 + rom.write_bytes(CAPE_TRANSFORM_SUB_ADDR + 0x05, bytearray([0x6B])) # RTL + # End Cape Transform + + # Fire Flower + rom.write_bytes(0xC5F7, bytearray([0x22, 0x80, 0xBB, 0x03])) # JSL $03BB80 + + FIRE_FLOWER_SUB_ADDR = 0x01BB80 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x04, bytearray([0x89, 0x02])) # BIT #02 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x09, bytearray([0xA9, 0x03])) # LDA #03 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x0B, bytearray([0x85, 0x19])) # STA $19 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x0D, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(FIRE_FLOWER_SUB_ADDR + 0x10, bytearray([0x6B])) # RTL + # End Fire Flower + + # Cape + rom.write_bytes(0xC598, bytearray([0x22, 0x91, 0xBB, 0x03])) # JSL $03BB91 + + CAPE_SUB_ADDR = 0x01BB91 + rom.write_bytes(CAPE_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(CAPE_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(CAPE_SUB_ADDR + 0x04, bytearray([0x89, 0x04])) # BIT #04 + rom.write_bytes(CAPE_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(CAPE_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(CAPE_SUB_ADDR + 0x09, bytearray([0xA9, 0x02])) # LDA #02 + rom.write_bytes(CAPE_SUB_ADDR + 0x0B, bytearray([0x85, 0x19])) # STA $19 + rom.write_bytes(CAPE_SUB_ADDR + 0x0D, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(CAPE_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(CAPE_SUB_ADDR + 0x10, bytearray([0x6B])) # RTL + # End Cape + + # P-Balloon + rom.write_bytes(0xC2FF, bytearray([0x22, 0xA2, 0xBB, 0x03])) # JSL $03BBA2 + rom.write_bytes(0xC303, bytearray([0xEA] * 0x06)) + + P_BALLOON_SUB_ADDR = 0x01BBA2 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x04, bytearray([0x89, 0x08])) # BIT #08 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x06, bytearray([0xF0, 0x0D])) # BEQ +0x0D + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x09, bytearray([0xA9, 0x09])) # LDA #09 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x0B, bytearray([0x8D, 0xF3, 0x13])) # STA $13F3 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x0E, bytearray([0xA9, 0xFF])) # LDA #FF + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x10, bytearray([0x8D, 0x91, 0x18])) # STA $1891 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x13, bytearray([0x80, 0x0B])) # BRA +0x0B + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x15, bytearray([0x28])) # PLP + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x16, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x18, bytearray([0x8D, 0xF3, 0x13])) # STA $13F3 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x1B, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x1D, bytearray([0x8D, 0x91, 0x18])) # STA $1891 + rom.write_bytes(P_BALLOON_SUB_ADDR + 0x20, bytearray([0x6B])) # RTL + # End P-Balloon + + # Star + rom.write_bytes(0xC580, bytearray([0x22, 0xC8, 0xBB, 0x03])) # JSL $03BBC8 + rom.write_bytes(0xC584, bytearray([0xEA] * 0x01)) + + STAR_SUB_ADDR = 0x01BBC8 + rom.write_bytes(STAR_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(STAR_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(STAR_SUB_ADDR + 0x04, bytearray([0x89, 0x10])) # BIT #10 + rom.write_bytes(STAR_SUB_ADDR + 0x06, bytearray([0xF0, 0x08])) # BEQ +0x08 + rom.write_bytes(STAR_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(STAR_SUB_ADDR + 0x09, bytearray([0xA9, 0xFF])) # LDA #FF + rom.write_bytes(STAR_SUB_ADDR + 0x0B, bytearray([0x8D, 0x90, 0x14])) # STA $1490 + rom.write_bytes(STAR_SUB_ADDR + 0x0E, bytearray([0x80, 0x06])) # BRA +0x06 + rom.write_bytes(STAR_SUB_ADDR + 0x10, bytearray([0x28])) # PLP + rom.write_bytes(STAR_SUB_ADDR + 0x11, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(STAR_SUB_ADDR + 0x13, bytearray([0x8D, 0x90, 0x14])) # STA $1490 + rom.write_bytes(STAR_SUB_ADDR + 0x16, bytearray([0x6B])) # RTL + # End Star + + # Star Timer + rom.write_bytes(0x62E3, bytearray([0x22, 0xE0, 0xBB, 0x03])) # JSL $03BBE0 + + STAR_TIMER_SUB_ADDR = 0x01BBE0 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x01, bytearray([0xAD, 0x2D, 0x1F])) # LDA $1F2D + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x04, bytearray([0x89, 0x10])) # BIT #10 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x06, bytearray([0xF0, 0x07])) # BEQ +0x07 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x09, bytearray([0xA5, 0x13])) # LDA $13 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x0B, bytearray([0xC0, 0x1E])) # CPY #1E + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x0D, bytearray([0x80, 0x05])) # BRA +0x05 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x0F, bytearray([0x28])) # PLP + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x10, bytearray([0xA5, 0x13])) # LDA $13 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x12, bytearray([0xC0, 0x01])) # CPY #01 + rom.write_bytes(STAR_TIMER_SUB_ADDR + 0x14, bytearray([0x6B])) # RTL + # End Star Timer + + return + + +def handle_yoshi_box(rom): + + rom.write_bytes(0xEC3D, bytearray([0xEA] * 0x03)) # NOP Lines that cause Yoshi Rescue Box normally + + rom.write_bytes(0x2B20F, bytearray([0x20, 0x60, 0xDC])) # JSR $05DC60 + + YOSHI_BOX_SUB_ADDR = 0x02DC60 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x01, bytearray([0xAD, 0x26, 0x14])) # LDA $1426 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x04, bytearray([0xC9, 0x03])) # CMP #03 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x06, bytearray([0xF0, 0x06])) # BEQ +0x06 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x09, bytearray([0xB9, 0xD9, 0xA5])) # LDA $A5B9, Y + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x0C, bytearray([0x80, 0x08])) # BRA +0x08 + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x0E, bytearray([0x28])) # PLP + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x0F, bytearray([0xDA])) # PHX + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x10, bytearray([0xBB])) # TYX + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x11, bytearray([0xBF, 0x00, 0xC2, 0x7E])) # LDA $7EC200, X + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x15, bytearray([0xFA])) # PLX + rom.write_bytes(YOSHI_BOX_SUB_ADDR + 0x16, bytearray([0x60])) # RTS + + return + + +def handle_bowser_damage(rom): + + rom.write_bytes(0x1A509, bytearray([0x20, 0x50, 0xBC])) # JSR $03BC50 + + BOWSER_BALLS_SUB_ADDR = 0x01BC50 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x01, bytearray([0xAD, 0x48, 0x0F])) # LDA $F48 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x04, bytearray([0xCF, 0xA1, 0xBF, 0x03])) # CMP $03BFA1 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x08, bytearray([0x90, 0x06])) # BCC +0x06 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0A, bytearray([0x28])) # PLP + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0B, bytearray([0xEE, 0xB8, 0x14])) # INC $14B8 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0E, bytearray([0x80, 0x01])) # BRA +0x01 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x10, bytearray([0x28])) # PLP + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x11, bytearray([0x60])) # RTS + + return + + +def handle_level_shuffle(rom, active_level_dict): + rom.write_bytes(0x37600, bytearray([0x00] * 0x800)) # Duplicate Level Table + + rom.write_bytes(0x2D89C, bytearray([0x00, 0xF6, 0x06])) # Level Load Pointer + rom.write_bytes(0x20F46, bytearray([0x00, 0xF6, 0x06])) # Mid Gate Pointer + rom.write_bytes(0x20E7B, bytearray([0x00, 0xF6, 0x06])) # Level Name Pointer + rom.write_bytes(0x21543, bytearray([0x00, 0xF6, 0x06])) # Also Level Name Pointer? + rom.write_bytes(0x20F64, bytearray([0x00, 0xF6, 0x06])) # Level Beaten Pointer + + ### Fix Translevel Check + rom.write_bytes(0x2D8AE, bytearray([0x20, 0x00, 0xDD])) # JSR $DD00 + rom.write_bytes(0x2D8B1, bytearray([0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA])) # NOP NOP NOP NOP NOP + + rom.write_bytes(0x2D7CB, bytearray([0x20, 0x00, 0xDD])) # JSR $DD00 + rom.write_bytes(0x2D7CE, bytearray([0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA])) # NOP NOP NOP NOP NOP + + rom.write_bytes(0x2DD00, bytearray([0xDA])) # PHX + rom.write_bytes(0x2DD01, bytearray([0x08])) # PHP + rom.write_bytes(0x2DD02, bytearray([0xE2, 0x30])) # SEP #30 + rom.write_bytes(0x2DD04, bytearray([0xAE, 0xBF, 0x13])) # LDX $13BF + rom.write_bytes(0x2DD07, bytearray([0xE0, 0x25])) # CPX #25 + rom.write_bytes(0x2DD09, bytearray([0x90, 0x04])) # BCC $DD0F + rom.write_bytes(0x2DD0B, bytearray([0xA2, 0x01])) # LDX #01 + rom.write_bytes(0x2DD0D, bytearray([0x80, 0x02])) # BRA $DD11 + rom.write_bytes(0x2DD0F, bytearray([0xA2, 0x00])) # LDX #00 + rom.write_bytes(0x2DD11, bytearray([0x86, 0x0F])) # STX $0F + rom.write_bytes(0x2DD13, bytearray([0x28])) # PLP + rom.write_bytes(0x2DD14, bytearray([0xFA])) # PLX + rom.write_bytes(0x2DD15, bytearray([0x60])) # RTS + ### End Fix Translevel Check + + ### Fix Snake Blocks + rom.write_bytes(0x192FB, bytearray([0x20, 0x1D, 0xBC])) # JSR $03BC1D + + SNAKE_BLOCKS_SUB_ADDR = 0x01BC1D + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x01, bytearray([0xAD, 0xBF, 0x13])) # LDA $13BF + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x04, bytearray([0xC9, 0x20])) # CMP #20 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x06, bytearray([0xF0, 0x05])) # BEQ +0x05 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x08, bytearray([0x28])) # PLP + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x09, bytearray([0xA9, 0x01])) # LDA #01 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x0B, bytearray([0x80, 0x03])) # BRA +0x03 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x0D, bytearray([0x28])) # PLP + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x0E, bytearray([0xA9, 0x00])) # LDA #00 + rom.write_bytes(SNAKE_BLOCKS_SUB_ADDR + 0x10, bytearray([0x60])) # RTS + ### End Fix Snake Blocks + + for level_id, level_data in level_info_dict.items(): + if level_id not in active_level_dict.keys(): + continue + + tile_id = active_level_dict[level_id] + tile_data = level_info_dict[tile_id] + + if level_id > 0x80: + level_id = level_id - 0x50 + + rom.write_byte(tile_data.levelIDAddress, level_id) + rom.write_byte(0x2D608 + level_id, tile_data.eventIDValue) + + for level_id, tile_id in active_level_dict.items(): + rom.write_byte(0x37F70 + level_id, tile_id) + + +def handle_collected_paths(rom): + rom.write_bytes(0x1F5B, bytearray([0x22, 0x30, 0xBC, 0x03])) # JSL $03BC30 + rom.write_bytes(0x1F5F, bytearray([0xEA] * 0x02)) + + COLLECTED_PATHS_SUB_ADDR = 0x01BC30 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x01, bytearray([0xAD, 0x00, 0x01])) # LDA $0100 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x04, bytearray([0xC9, 0x0B])) # CMP #0B + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x06, bytearray([0xD0, 0x04])) # BNE +0x04 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x08, bytearray([0x22, 0xAD, 0xDA, 0x04])) # JSL $04DAAD + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x0C, bytearray([0x28])) # PLP + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x0D, bytearray([0xEE, 0x00, 0x01])) # INC $0100 + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x10, bytearray([0xAD, 0xAF, 0x0D])) # LDA $0DAF + rom.write_bytes(COLLECTED_PATHS_SUB_ADDR + 0x13, bytearray([0x6B])) # RTL + + +def handle_music_shuffle(rom, world, player): + from .Aesthetics import generate_shuffled_level_music, generate_shuffled_ow_music, level_music_address_data, ow_music_address_data + + shuffled_level_music = generate_shuffled_level_music(world, player) + for i in range(len(shuffled_level_music)): + rom.write_byte(level_music_address_data[i], shuffled_level_music[i]) + + shuffled_ow_music = generate_shuffled_ow_music(world, player) + for i in range(len(shuffled_ow_music)): + for addr in ow_music_address_data[i]: + rom.write_byte(addr, shuffled_ow_music[i]) + + +def handle_mario_palette(rom, world, player): + from .Aesthetics import mario_palettes, fire_mario_palettes, ow_mario_palettes + + chosen_palette = world.mario_palette[player].value + + rom.write_bytes(0x32C8, bytes(mario_palettes[chosen_palette])) + rom.write_bytes(0x32F0, bytes(fire_mario_palettes[chosen_palette])) + rom.write_bytes(0x359C, bytes(ow_mario_palettes[chosen_palette])) + + +def handle_swap_donut_gh_exits(rom): + rom.write_bytes(0x2567C, bytes([0xC0])) + rom.write_bytes(0x25873, bytes([0xA9])) + rom.write_bytes(0x25875, bytes([0x85])) + rom.write_bytes(0x25954, bytes([0x92])) + rom.write_bytes(0x25956, bytes([0x0A])) + rom.write_bytes(0x25E31, bytes([0x00, 0x00, 0xD8, 0x04, 0x24, 0x00, 0x98, 0x04, 0x48, 0x00, 0xD8, 0x03, 0x6C, 0x00, 0x56, 0x03, + 0x90, 0x00, 0x56, 0x03, 0xB4, 0x00, 0x56, 0x03, 0x10, 0x05, 0x18, 0x05, 0x28, 0x09, 0x24, 0x05, + 0x38, 0x0B, 0x14, 0x07, 0xEC, 0x09, 0x12, 0x05, 0xF0, 0x09, 0xD2, 0x04, 0xF4, 0x09, 0x92, 0x04])) + rom.write_bytes(0x26371, bytes([0x32])) + + +def patch_rom(world, rom, player, active_level_dict): + local_random = world.slot_seeds[player] + + goal_text = generate_goal_text(world, player) + + rom.write_bytes(0x2A6E2, goal_text) + rom.write_byte(0x2B1D8, 0x80) + + intro_text = generate_text_box("Bowser has stolen all of Mario's abilities. Can you help Mario travel across Dinosaur land to get them back and save the Princess from him?") + rom.write_bytes(0x2A5D9, intro_text) + + # Force all 8 Bowser's Castle Rooms + rom.write_byte(0x3A680, 0xD4) + rom.write_byte(0x3A684, 0xD4) + rom.write_byte(0x3A688, 0xD4) + rom.write_byte(0x3A68C, 0xD4) + rom.write_byte(0x3A705, 0xD3) + rom.write_byte(0x3A763, 0xD2) + rom.write_byte(0x3A800, 0xD1) + rom.write_byte(0x3A83D, 0xCF) + rom.write_byte(0x3A932, 0xCE) + rom.write_byte(0x3A9E1, 0xCD) + rom.write_byte(0x3AA75, 0xCC) + + # Prevent Title Screen Deaths + rom.write_byte(0x1C6A, 0x80) + + # Title Screen Text + player_name_bytes = bytearray() + player_name = world.get_player_name(player) + for i in range(16): + char = " " + if i < len(player_name): + char = world.get_player_name(player)[i] + upper_char = char.upper() + if upper_char not in title_text_mapping: + for byte in title_text_mapping["."]: + player_name_bytes.append(byte) + else: + for byte in title_text_mapping[upper_char]: + player_name_bytes.append(byte) + + rom.write_bytes(0x2B7F1, player_name_bytes) # MARIO A + rom.write_bytes(0x2B726, player_name_bytes) # MARIO A + + rom.write_bytes(0x2B815, bytearray([0xFC, 0x38] * 0x10)) # MARIO B + rom.write_bytes(0x2B74A, bytearray([0xFC, 0x38] * 0x10)) # MARIO B + rom.write_bytes(0x2B839, bytearray([0x71, 0x31, 0x74, 0x31, 0x2D, 0x31, 0x84, 0x30, + 0x82, 0x30, 0x6F, 0x31, 0x73, 0x31, 0x70, 0x31, + 0x71, 0x31, 0x75, 0x31, 0x83, 0x30, 0xFC, 0x38, + 0xFC, 0x38, 0xFC, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # MARIO C + rom.write_bytes(0x2B76E, bytearray([0xFC, 0x38] * 0x10)) # MARIO C + rom.write_bytes(0x2B79E, bytearray([0xFC, 0x38] * 0x05)) # EMPTY + rom.write_bytes(0x2B7AE, bytearray([0xFC, 0x38] * 0x05)) # EMPTY + rom.write_bytes(0x2B8A8, bytearray([0xFC, 0x38] * 0x0D)) # 2 PLAYER GAME + + rom.write_bytes(0x2B85D, bytearray([0xFC, 0x38] * 0x0A)) # ERASE + + rom.write_bytes(0x2B88E, bytearray([0x2C, 0x31, 0x73, 0x31, 0x75, 0x31, 0x82, 0x30, 0x30, 0x31, 0xFC, 0x38, 0x31, 0x31, 0x73, 0x31, + 0x73, 0x31, 0x7C, 0x30, 0xFC, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # 1 Player Game + + rom.write_bytes(0x2B6D7, bytearray([0xFC, 0x38, 0xFC, 0x38, 0x16, 0x38, 0x18, 0x38, 0x0D, 0x38, 0xFC, 0x38, 0x0B, 0x38, 0x22, 0x38, + 0xFC, 0x38, 0x19, 0x38, 0x18, 0x38, 0x1B, 0x38, 0x22, 0x38, 0x10, 0x38, 0x18, 0x38, 0x17, 0x38, + 0x0E, 0x38, 0xFC, 0x38, 0xFC, 0x38])) # Mod by PoryGone + + # Title Options + rom.write_bytes(0x1E6A, bytearray([0x01])) + rom.write_bytes(0x1E6C, bytearray([0x01])) + rom.write_bytes(0x1E6E, bytearray([0x01])) + + # Always allow Start+Select + rom.write_bytes(0x2267, bytearray([0xEA, 0xEA])) + + # Always bring up save prompt on beating a level + if world.autosave[player]: + rom.write_bytes(0x20F93, bytearray([0x00])) + + # Starting Life Count + rom.write_bytes(0x1E25, bytearray([world.starting_life_count[player].value - 1])) + + # Repurpose Bonus Stars counter for Boss Token or Yoshi Eggs + rom.write_bytes(0x3F1AA, bytearray([0x00] * 0x20)) + rom.write_bytes(0x20F9F, bytearray([0xEA] * 0x3B)) + + # Prevent Switch Palaces setting the Switch Palace flags + rom.write_bytes(0x6EC9A, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x6EB1, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x6EB4, bytearray([0xEA, 0xEA, 0xEA])) + + handle_ability_code(rom) + + handle_yoshi_box(rom) + handle_bowser_damage(rom) + + handle_collected_paths(rom) + + # Handle Level Shuffle + handle_level_shuffle(rom, active_level_dict) + + # Handle Music Shuffle + if world.music_shuffle[player] != "none": + handle_music_shuffle(rom, world, player) + + generate_shuffled_header_data(rom, world, player) + + if world.swap_donut_gh_exits[player]: + handle_swap_donut_gh_exits(rom) + + handle_mario_palette(rom, world, player) + + # Store all relevant option results in ROM + rom.write_byte(0x01BFA0, world.goal[player].value) + rom.write_byte(0x01BFA1, world.bosses_required[player].value) + required_yoshi_eggs = max(math.floor( + world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1) + rom.write_byte(0x01BFA2, required_yoshi_eggs) + #rom.write_byte(0x01BFA3, world.display_sent_item_popups[player].value) + rom.write_byte(0x01BFA4, world.display_received_item_popups[player].value) + rom.write_byte(0x01BFA5, world.death_link[player].value) + rom.write_byte(0x01BFA6, world.dragon_coin_checks[player].value) + rom.write_byte(0x01BFA7, world.swap_donut_gh_exits[player].value) + + + from Main import __version__ + rom.name = bytearray(f'SMW{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21] + rom.name.extend([0] * (21 - len(rom.name))) + rom.write_bytes(0x7FC0, rom.name) + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if USHASH != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["smw_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name diff --git a/worlds/smw/Rules.py b/worlds/smw/Rules.py new file mode 100644 index 0000000000..bf9fedb805 --- /dev/null +++ b/worlds/smw/Rules.py @@ -0,0 +1,20 @@ +import math + +from BaseClasses import MultiWorld +from .Names import LocationName, ItemName +from ..AutoWorld import LogicMixin +from ..generic.Rules import add_rule, set_rule + + +def set_rules(world: MultiWorld, player: int): + + if world.goal[player] == "yoshi_egg_hunt": + required_yoshi_eggs = max(math.floor( + world.number_of_yoshi_eggs[player].value * (world.percentage_of_yoshi_eggs[player].value / 100.0)), 1) + + add_rule(world.get_location(LocationName.yoshis_house, player), + lambda state: state.has(ItemName.yoshi_egg, player, required_yoshi_eggs)) + else: + add_rule(world.get_location(LocationName.bowser, player), lambda state: state.has(ItemName.mario_carry, player)) + + world.completion_condition[player] = lambda state: state.has(ItemName.victory, player) diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py new file mode 100644 index 0000000000..46ce4de7ae --- /dev/null +++ b/worlds/smw/__init__.py @@ -0,0 +1,253 @@ +import os +import typing +import math +import threading + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from .Items import SMWItem, ItemData, item_table +from .Locations import SMWLocation, all_locations, setup_locations +from .Options import smw_options +from .Regions import create_regions, connect_regions +from .Levels import full_level_list, generate_level_list, location_id_to_level_id +from .Rules import set_rules +from ..generic.Rules import add_rule +from .Names import ItemName, LocationName +from ..AutoWorld import WebWorld, World +from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch +import Patch + + +class SMWWeb(WebWorld): + theme = "grass" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Super Mario World randomizer connected to an Archipelago Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["PoryGone"] + ) + + tutorials = [setup_en] + + +class SMWWorld(World): + """ + Super Mario World is an action platforming game. + The Princess has been kidnapped by Bowser again, but Mario has somehow + lost all of his abilities. Can he get them back in time to save the Princess? + """ + game: str = "Super Mario World" + option_definitions = smw_options + topology_present = False + data_version = 1 + required_client_version = (0, 3, 5) + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = all_locations + + active_level_dict: typing.Dict[int,int] + web = SMWWeb() + + def __init__(self, world: MultiWorld, player: int): + self.rom_name_available_event = threading.Event() + super().__init__(world, player) + + @classmethod + def stage_assert_generate(cls, world): + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + + def _get_slot_data(self): + return { + #"death_link": self.world.death_link[self.player].value, + "active_levels": self.active_level_dict, + } + + def _create_items(self, name: str): + data = item_table[name] + return [self.create_item(name)] * data.quantity + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in smw_options: + option = getattr(self.world, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def generate_basic(self): + itempool: typing.List[SMWItem] = [] + + self.active_level_dict = dict(zip(generate_level_list(self.world, self.player), full_level_list)) + self.topology_present = self.world.level_shuffle[self.player] + + connect_regions(self.world, self.player, self.active_level_dict) + + # Add Boss Token amount requirements for Worlds + add_rule(self.world.get_region(LocationName.donut_plains_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 1)) + add_rule(self.world.get_region(LocationName.vanilla_dome_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 2)) + add_rule(self.world.get_region(LocationName.forest_of_illusion_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 4)) + add_rule(self.world.get_region(LocationName.chocolate_island_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 5)) + add_rule(self.world.get_region(LocationName.valley_of_bowser_1_tile, self.player).entrances[0], lambda state: state.has(ItemName.koopaling, self.player, 6)) + + total_required_locations = 96 + if self.world.dragon_coin_checks[self.player]: + total_required_locations += 49 + + itempool += [self.create_item(ItemName.mario_run)] + itempool += [self.create_item(ItemName.mario_carry)] + itempool += [self.create_item(ItemName.mario_swim)] + itempool += [self.create_item(ItemName.mario_spin_jump)] + itempool += [self.create_item(ItemName.mario_climb)] + itempool += [self.create_item(ItemName.yoshi_activate)] + itempool += [self.create_item(ItemName.p_switch)] + itempool += [self.create_item(ItemName.p_balloon)] + itempool += [self.create_item(ItemName.super_star_active)] + itempool += [self.create_item(ItemName.progressive_powerup)] * 3 + itempool += [self.create_item(ItemName.yellow_switch_palace)] + itempool += [self.create_item(ItemName.green_switch_palace)] + itempool += [self.create_item(ItemName.red_switch_palace)] + itempool += [self.create_item(ItemName.blue_switch_palace)] + + if self.world.goal[self.player] == "yoshi_egg_hunt": + itempool += [self.create_item(ItemName.yoshi_egg)] * self.world.number_of_yoshi_eggs[self.player] + self.world.get_location(LocationName.yoshis_house, self.player).place_locked_item(self.create_item(ItemName.victory)) + else: + self.world.get_location(LocationName.bowser, self.player).place_locked_item(self.create_item(ItemName.victory)) + + junk_count = total_required_locations - len(itempool) + trap_weights = [] + trap_weights += ([ItemName.ice_trap] * self.world.ice_trap_weight[self.player].value) + trap_weights += ([ItemName.stun_trap] * self.world.stun_trap_weight[self.player].value) + trap_weights += ([ItemName.literature_trap] * self.world.literature_trap_weight[self.player].value) + trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.world.trap_fill_percentage[self.player].value / 100.0)) + junk_count -= trap_count + + trap_pool = [] + for i in range(trap_count): + trap_item = self.world.random.choice(trap_weights) + trap_pool += [self.create_item(trap_item)] + + itempool += trap_pool + + itempool += [self.create_item(ItemName.one_up_mushroom)] * junk_count + + boss_location_names = [LocationName.yoshis_island_koopaling, LocationName.donut_plains_koopaling, LocationName.vanilla_dome_koopaling, + LocationName.twin_bridges_koopaling, LocationName.forest_koopaling, LocationName.chocolate_koopaling, + LocationName.valley_koopaling, LocationName.vanilla_reznor, LocationName.forest_reznor, LocationName.chocolate_reznor, LocationName.valley_reznor] + + for location_name in boss_location_names: + self.world.get_location(location_name, self.player).place_locked_item(self.create_item(ItemName.koopaling)) + + self.world.itempool += itempool + + + def generate_output(self, output_directory: str): + try: + world = self.world + player = self.player + + rom = LocalRom(get_base_rom_path()) + patch_rom(self.world, rom, self.player, self.active_level_dict) + + outfilepname = f'_P{player}' + outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ + if world.player_name[player] != 'Player%d' % player else '' + + rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') + rom.write_to_file(rompath) + self.rom_name = rom.name + + patch = SMWDeltaPatch(os.path.splitext(rompath)[0]+SMWDeltaPatch.patch_file_ending, player=player, + player_name=world.player_name[player], patched_path=rompath) + patch.write() + except: + raise + finally: + if os.path.exists(rompath): + os.unlink(rompath) + self.rom_name_available_event.set() # make sure threading continues and errors are collected + + def modify_multidata(self, multidata: dict): + import base64 + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]] + + def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + if self.topology_present: + world_names = [ + LocationName.yoshis_island_region, + LocationName.donut_plains_region, + LocationName.vanilla_dome_region, + LocationName.twin_bridges_region, + LocationName.forest_of_illusion_region, + LocationName.chocolate_island_region, + LocationName.valley_of_bowser_region, + LocationName.star_road_region, + LocationName.special_zone_region, + ] + world_cutoffs = [ + 0x07, + 0x13, + 0x1F, + 0x26, + 0x30, + 0x39, + 0x44, + 0x4F, + 0x59 + ] + er_hint_data = {} + for loc_name, level_data in location_id_to_level_id.items(): + level_id = level_data[0] + + if level_id not in self.active_level_dict: + continue + + keys_list = list(self.active_level_dict.keys()) + level_index = keys_list.index(level_id) + for i in range(len(world_cutoffs)): + if level_index >= world_cutoffs[i]: + continue + + if self.world.dragon_coin_checks[self.player].value == 0 and "Dragon Coins" in loc_name: + continue + + location = self.world.get_location(loc_name, self.player) + er_hint_data[location.address] = world_names[i] + break + + hint_data[self.player] = er_hint_data + + def create_regions(self): + location_table = setup_locations(self.world, self.player) + create_regions(self.world, self.player, location_table) + + def create_item(self, name: str, force_non_progression=False) -> Item: + data = item_table[name] + + if force_non_progression: + classification = ItemClassification.filler + elif name == ItemName.yoshi_egg: + classification = ItemClassification.progression_skip_balancing + elif data.progression: + classification = ItemClassification.progression + elif data.trap: + classification = ItemClassification.trap + else: + classification = ItemClassification.filler + + created_item = SMWItem(name, classification, data.code, self.player) + + return created_item + + def set_rules(self): + set_rules(self.world, self.player) diff --git a/worlds/smw/docs/en_Super Mario World.md b/worlds/smw/docs/en_Super Mario World.md new file mode 100644 index 0000000000..87a96e558b --- /dev/null +++ b/worlds/smw/docs/en_Super Mario World.md @@ -0,0 +1,43 @@ +# Super Mario World + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +Mario's basic abilities are removed, and placed into the item pool as items that any player can find. This includes: +- Carry +- Climb +- Run +- P-Switch +- Swim +- Spin Jump +- Yoshi + +Additionally, the ability to use powerups (Mushrooms, Fire Flowers, Capes, Stars, and P-Balloons) are shuffled into the item pool, as are the Four Switch Palaces. + +## What is the goal of Super Mario World when randomized? + +There are two goals which can be chosen: +- `Bowser`: Reach Bowser's Castle and defeat Bowser, after defeating a certain number of bosses. +- `Yoshi Egg Hunt`: Collect a certain number of Yoshi Eggs, then return to Yoshi's House + +## What items and locations get shuffled? + +Each unique level exit awards a location check. Optionally, collecting five Dragon Coins in each level can also award a location check. +Mario's various abilities and powerups as described above are placed into the item pool. +If the player is playing Yoshi Egg Hunt, a certain number of Yoshi Eggs will be placed into the item pool. +Any additional items that are needed to fill out the item pool with be 1-Up Mushrooms. + +## Which items can be in another player's world? + +Any shuffled item can be in other players' worlds. + +## What does another world's item look like in Super Mario World + +Items do not have an appearance in Super Mario World. + +## When the player receives an item, what happens? + +The player can choose to receive a text box in-game when they receive an item. Regardless of that choice, items will be queued, and granted when the player next enters a level. diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md new file mode 100644 index 0000000000..178b7392b7 --- /dev/null +++ b/worlds/smw/docs/setup_en.md @@ -0,0 +1,149 @@ +# Super Mario World Randomizer Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Super Mario World Patch Setup` + + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) + - RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, + - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other + compatible hardware +- Your legally obtained Super Mario World ROM file, probably named `Super Mario World (USA).sfc` + +## Installation Procedures + +### Windows Setup + +1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this, + or you are on an older version, you may run the installer again to install the SNI Client. +2. During setup, you will be asked to locate your base ROM file. This is your Super Mario World ROM file. +3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Super Mario World Player Settings Page](/games/Super%20Mario%20World/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whomever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apsmw` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x-rr + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these + menu options: + `Config --> Cores --> SNES --> BSNES` + Once you have changed the loaded core, you must restart BizHawk. +2. Load your ROM file if it hasn't already been loaded. +3. Click on the Tools menu and click on **Lua Console** +4. Click the button to open a new Lua script. +5. Select the `Connector.lua` file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. + +##### RetroArch 1.10.3 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to +read ROM data. + +#### With hardware + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) + +1. Close your emulator, which may have auto-launched. +2. Power on your device and load the ROM. + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. From dd7d3a02a4cf6af1bb5a7dffb71feeb32d960366 Mon Sep 17 00:00:00 2001 From: Jarno Date: Thu, 29 Sep 2022 20:18:21 +0200 Subject: [PATCH 024/105] WebHost: Fixed Oculus Ring from showing up on tracker (#1065) --- WebHostLib/templates/timespinnerTracker.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/timespinnerTracker.html index bd589e1068..82565316ab 100644 --- a/WebHostLib/templates/timespinnerTracker.html +++ b/WebHostLib/templates/timespinnerTracker.html @@ -41,7 +41,7 @@ {% endif %} - {% if 'FacebookMode' in options %} + {% if 'EyeSpy' in options %} {% else %} From bee1fd9b5acb4f94fd9f8a0742b59b808ea26727 Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:04:04 +0200 Subject: [PATCH 025/105] Subnautica: Updated Setup Guide (#1062) - Added sections for console commands and known issues. - Updated "Resuming" section to reflect current functionality. - Removed implication that one might have to create the QMods folder. (If it's missing, then you've already messed up step 1.) - Renamed "Connect Menu" to "connect form" to be less confusing. Generally fixed word capitalization to conform to standard English. Minor wording changes. --- worlds/subnautica/docs/setup_en.md | 43 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md index 665cb8b336..bd4a92cc7c 100644 --- a/worlds/subnautica/docs/setup_en.md +++ b/worlds/subnautica/docs/setup_en.md @@ -7,37 +7,50 @@ - Archipelago Mod for Subnautica from: [Subnautica Archipelago Mod Releases Page](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc/releases) -## Installation Procedures +## Installation Procedure 1. Install QModManager4 as per its instructions. -2. The folder you installed QModManager4 into will now have a /QMods directory. It might appear after a start of - Subnautica. You can also create this folder yourself. +2. The Subnautica game directory should now contain a `QMods` folder. Unpack the Archipelago Mod into this folder, so that `Subnautica/QMods/Archipelago/` is a valid path. -3. Unpack the Archipelago Mod into this folder, so that Subnautica/QMods/Archipelago/ is a valid path. - -4. Start Subnautica. You should see a Connect Menu in the topleft of your main Menu. +3. Start Subnautica. You should see a connect form with three text boxes in the top left of your main menu. ## Connecting -Using the Connect Menu in Subnautica's Main Menu you enter your connection info to connect to an Archipelago Multiworld. -Menu points: +Use the connect form in Subnautica's main menu to enter your connection information to connect to an Archipelago multiworld. +Connection information consists of: - Host: the full url that you're trying to connect to, such as `archipelago.gg:38281`. - - PlayerName: your name in the multiworld. Can also be called Slot Name and is the name you entered when creating your settings. + - PlayerName: your name in the multiworld. Can also be called "slot name" and is the name you entered when creating your settings. - Password: optional password, leave blank if no password was set. After the connection is made, start a new game. You should start to see Archipelago chat messages to appear, such as a message announcing that you joined the multiworld. ## Resuming -When loading a savegame it will automatically attempt to resume the connection that was active when the savegame was made. -If that connection information is no longer valid, such as if the server's IP and/or port has changed, the Connect Menu will reappear after loading. Use the Connect Menu before or after loading the savegame to connect to the new instance. +Savegames store their connection information and automatically attempt to reestablish the connection upon loading. +If the connection information is no longer valid, such as if the server's IP and/or port have changed, +you need to use the connect form on the main menu beforehand. -Warning: Currently it is not checked if this is the correct multiworld belonging to that savegame, please ensure that yourself beforehand. +Warning: Currently it is not checked whether a loaded savegame belongs to the multiworld you are connecting to. Please ensure that yourself beforehand. + +## Console Commands + +The mod adds the following console commands: + - `silent` toggles Archipelago chat messages appearing. + - `deathlink` toggles death link. + +To enable the console in Subnautica, press `F3` and `F8`, then uncheck "Disable Console" in the top left. Press `F3` and `F8` again to close the menus. +To enter a console command, press `Enter`. + +## Known Issues + +- Do not attempt playing vanilla saves while the mod is installed, as the mod will override the scan information of the savegame. +- When exiting to the main menu the mod's state is not properly reset. Loading a savegame from here will break various things. + If you want to reload a save it is recommended you restart the game entirely. +- Attempting to load a savegame containing no longer valid connection information without entering valid information on the main menu will hang on the loading screen. ## Troubleshooting -If you don't see the Connect Menu within the Main Menu, check that you see a file named `qmodmanager_log-Subnautica.txt` in the Subnautica game directory. If not, -QModManager4 is not correctly installed, otherwise open it and look -for `[Info : BepInEx] Loading [Archipelago`. If it doesn't show this, then +If you don't see the connect form on the main menu screen, check whether you see a file named `qmodmanager_log-Subnautica.txt` in the Subnautica game directory. If not, +QModManager4 is not correctly installed, otherwise open it and look for `Loading [Archipelago`. If the file doesn't contain this text, then QModManager4 didn't find the Archipelago mod, so check your paths. From 0191df88d78830dd02ed2e37e3927c248995841a Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 25 Sep 2022 15:21:39 +0200 Subject: [PATCH 026/105] Doc: network protocol: clarify want_reply --- docs/network protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index d5c56a62b4..0e7a53f3cf 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -371,7 +371,7 @@ Used to write data to the server's data storage, that data can then be shared ac | ------ | ----- | ------ | | key | str | The key to manipulate. | | default | any | The default value to use in case the key has no value on the server. | -| want_reply | bool | If set, the server will send a [SetReply](#SetReply) response back to the client. | +| want_reply | bool | If true, the server will send a [SetReply](#SetReply) response back to the client. | | operations | list\[[DataStorageOperation](#DataStorageOperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. | Additional arguments sent in this package will also be added to the [SetReply](#SetReply) package it triggers. From d897aaade2a42205dcbb7f7cfd69c430d03ad733 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 29 Sep 2022 23:15:12 +0200 Subject: [PATCH 027/105] Docs: Ensure Discord links are permanent. (#1064) --- WebHostLib/misc.py | 2 +- WebHostLib/static/assets/faq/faq_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 03cd03b624..6978e27c28 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -151,7 +151,7 @@ def favicon(): @app.route('/discord') def discord(): - return redirect("https://discord.gg/archipelago") + return redirect("https://discord.gg/8Z65BR2") @app.route('/datapackage') diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/faq_en.md index cd144d7eff..6ad50a50f6 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/faq_en.md @@ -46,7 +46,7 @@ the website is not required to generate them. ## How do I get started? If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join -our discord server at the [Archipelago Discord](https://discord.gg/archipelago). There are always people ready to answer +our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer any questions you might have. ## What are some common terms I should know? From 8ab0b410c33c108f4e02d1c2ec903830f5e2236b Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 29 Sep 2022 23:53:22 +0200 Subject: [PATCH 028/105] sm64ex: Document new connection status notifications --- worlds/sm64ex/docs/setup_en.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 1b4ae6dfb4..d77e091359 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -52,7 +52,8 @@ Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requi The Name in this case is the one specified in your generated .yaml file. In case you are using the Archipelago Website, the IP should be `archipelago.gg`. -If everything worked out, you will see a textbox informing you the connection has been established after the story intro. +Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. +Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. # Playing offline From 61e39f355d2da15d58dc10557c56f56384e5527c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 30 Sep 2022 00:36:30 +0200 Subject: [PATCH 029/105] Core remove legacy patch (#1047) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- LttPAdjuster.py | 6 +- OoTClient.py | 3 +- Patch.py | 418 ++----------------------------------- SNIClient.py | 20 +- Utils.py | 10 + WebHostLib/__init__.py | 7 +- WebHostLib/api/generate.py | 3 +- WebHostLib/downloads.py | 12 +- WebHostLib/tracker.py | 3 +- WebHostLib/upload.py | 19 +- data/basepatch.apbp | Bin 117900 -> 0 bytes data/basepatch.bsdiff4 | Bin 0 -> 114127 bytes docs/adding games.md | 4 +- worlds/Files.py | 156 ++++++++++++++ worlds/alttp/Rom.py | 48 ++--- worlds/dkc3/Rom.py | 9 +- worlds/factorio/Mod.py | 4 +- worlds/sm/Rom.py | 5 +- worlds/smw/Rom.py | 10 +- worlds/smw/__init__.py | 4 +- worlds/smz3/Rom.py | 7 +- worlds/soe/Patch.py | 2 +- 22 files changed, 259 insertions(+), 491 deletions(-) delete mode 100644 data/basepatch.apbp create mode 100644 data/basepatch.bsdiff4 create mode 100644 worlds/Files.py diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 469e8920b3..9fab226c67 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -139,7 +139,7 @@ def adjust(args): vanillaRom = args.baserom if not os.path.exists(vanillaRom) and not os.path.isabs(vanillaRom): vanillaRom = local_path(vanillaRom) - if os.path.splitext(args.rom)[-1].lower() in {'.apbp', '.aplttp'}: + if os.path.splitext(args.rom)[-1].lower() == '.aplttp': import Patch meta, args.rom = Patch.create_rom_file(args.rom) @@ -195,7 +195,7 @@ def adjustGUI(): romEntry2 = Entry(romDialogFrame, textvariable=romVar2) def RomSelect2(): - rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")]) + rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".aplttp")), ("All Files", "*")]) romVar2.set(rom) romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2) @@ -725,7 +725,7 @@ def get_rom_options_frame(parent=None): vars.auto_apply = StringVar(value=adjuster_settings.auto_apply) autoApplyFrame = Frame(romOptionsFrame) autoApplyFrame.grid(row=9, column=0, columnspan=2, sticky=W) - filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .apbp files") + filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .aplttp files") filler.pack(side=TOP, expand=True, fill=X) askRadio = Radiobutton(autoApplyFrame, text='Ask', variable=vars.auto_apply, value='ask') askRadio.pack(side=LEFT, padx=5, pady=5) diff --git a/OoTClient.py b/OoTClient.py index fbe2b35d1a..b3c58612f3 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -5,7 +5,8 @@ import multiprocessing import subprocess from asyncio import StreamReader, StreamWriter -from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, \ +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, gui_enabled, \ ClientCommandProcessor, logger, get_base_parser import Utils from worlds import network_data_package diff --git a/Patch.py b/Patch.py index 6ac75dc9dd..4ff0e9602a 100644 --- a/Patch.py +++ b/Patch.py @@ -1,274 +1,33 @@ from __future__ import annotations -import shutil -import json -import bsdiff4 # type: ignore -import yaml import os -import lzma -import threading -import concurrent.futures -import zipfile import sys -from typing import ClassVar, List, Tuple, Optional, Dict, Any, Union, BinaryIO +from typing import Tuple, Optional, TypedDict -import ModuleUpdate -ModuleUpdate.update() +if __name__ == "__main__": + import ModuleUpdate + ModuleUpdate.update() -import Utils - -current_patch_version = 5 +from worlds.Files import AutoPatchRegister, APDeltaPatch -class AutoPatchRegister(type): - patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {} - file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {} - - def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchRegister: - # construct class - new_class = super().__new__(cls, name, bases, dct) - if "game" in dct: - AutoPatchRegister.patch_types[dct["game"]] = new_class - if not dct["patch_file_ending"]: - raise Exception(f"Need an expected file ending for {name}") - AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class - return new_class - - @staticmethod - def get_handler(file: str) -> Optional[AutoPatchRegister]: - for file_ending, handler in AutoPatchRegister.file_endings.items(): - if file.endswith(file_ending): - return handler - return None - - -class APContainer: - """A zipfile containing at least archipelago.json""" - version: int = current_patch_version - compression_level: int = 9 - compression_method: int = zipfile.ZIP_DEFLATED - game: Optional[str] = None - - # instance attributes: - path: Optional[str] - player: Optional[int] - player_name: str - server: str - - def __init__(self, path: Optional[str] = None, player: Optional[int] = None, - player_name: str = "", server: str = ""): - self.path = path - self.player = player - self.player_name = player_name - self.server = server - - def write(self, file: Optional[Union[str, BinaryIO]] = None) -> None: - zip_file = file if file else self.path - if not zip_file: - raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.") - with zipfile.ZipFile(zip_file, "w", self.compression_method, True, self.compression_level) \ - as zf: - if file: - self.path = zf.filename - self.write_contents(zf) - - def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: - manifest = self.get_manifest() - try: - manifest_str = json.dumps(manifest) - except Exception as e: - raise Exception(f"Manifest {manifest} did not convert to json.") from e - else: - opened_zipfile.writestr("archipelago.json", manifest_str) - - def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None: - """Read data into patch object. file can be file-like, such as an outer zip file's stream.""" - zip_file = file if file else self.path - if not zip_file: - raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.") - with zipfile.ZipFile(zip_file, "r") as zf: - if file: - self.path = zf.filename - self.read_contents(zf) - - def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None: - with opened_zipfile.open("archipelago.json", "r") as f: - manifest = json.load(f) - if manifest["compatible_version"] > self.version: - raise Exception(f"File (version: {manifest['compatible_version']}) too new " - f"for this handler (version: {self.version})") - self.player = manifest["player"] - self.server = manifest["server"] - self.player_name = manifest["player_name"] - - def get_manifest(self) -> Dict[str, Any]: - return { - "server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise - "player": self.player, - "player_name": self.player_name, - "game": self.game, - # minimum version of patch system expected for patching to be successful - "compatible_version": 4, - "version": current_patch_version, - } - - -class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): - """An APContainer that additionally has delta.bsdiff4 - containing a delta patch to get the desired file, often a rom.""" - - hash: Optional[str] # base checksum of source file - patch_file_ending: str = "" - delta: Optional[bytes] = None - result_file_ending: str = ".sfc" - source_data: bytes - - def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: - self.patched_path = patched_path - super(APDeltaPatch, self).__init__(*args, **kwargs) - - def get_manifest(self) -> Dict[str, Any]: - 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 - def get_source_data(cls) -> bytes: - """Get Base data""" - raise NotImplementedError() - - @classmethod - def get_source_data_with_cache(cls) -> bytes: - if not hasattr(cls, "source_data"): - cls.source_data = cls.get_source_data() - return cls.source_data - - def write_contents(self, opened_zipfile: zipfile.ZipFile): - super(APDeltaPatch, self).write_contents(opened_zipfile) - # write Delta - opened_zipfile.writestr("delta.bsdiff4", - bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()), - compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression - - def read_contents(self, opened_zipfile: zipfile.ZipFile): - super(APDeltaPatch, self).read_contents(opened_zipfile) - self.delta = opened_zipfile.read("delta.bsdiff4") - - def patch(self, target: str): - """Base + Delta -> Patched""" - if not self.delta: - self.read() - result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta) - with open(target, "wb") as f: - f.write(result) - - -# legacy patch handling follows: GAME_ALTTP = "A Link to the Past" GAME_SM = "Super Metroid" GAME_SOE = "Secret of Evermore" GAME_SMZ3 = "SMZ3" GAME_DKC3 = "Donkey Kong Country 3" + GAME_SMW = "Super Mario World" -supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"} - -preferred_endings = { - GAME_ALTTP: "apbp", - GAME_SM: "apm3", - GAME_SOE: "apsoe", - GAME_SMZ3: "apsmz", - GAME_DKC3: "apdkc3" -} -def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: - if game == GAME_ALTTP: - from worlds.alttp.Rom import LTTPJPN10HASH as HASH - elif game == GAME_SM: - from worlds.sm.Rom import SMJUHASH as HASH - elif game == GAME_SOE: - from worlds.soe.Patch import USHASH as HASH - elif game == GAME_SMZ3: - from worlds.alttp.Rom import LTTPJPN10HASH as ALTTPHASH - from worlds.sm.Rom import SMJUHASH as SMHASH - HASH = ALTTPHASH + SMHASH - elif game == GAME_DKC3: - from worlds.dkc3.Rom import USHASH as HASH - else: - raise RuntimeError(f"Selected game {game} for base rom not found.") - patch = yaml.dump({"meta": metadata, - "patch": patch, - "game": game, - # minimum version of patch system expected for patching to be successful - "compatible_version": 3, - "version": current_patch_version, - "base_checksum": HASH}) - return patch.encode(encoding="utf-8-sig") +class RomMeta(TypedDict): + server: str + player: Optional[int] + player_name: str -def generate_patch(rom: bytes, metadata: Optional[Dict[str, Any]] = None, game: str = GAME_ALTTP) -> bytes: - if metadata is None: - metadata = {} - patch = bsdiff4.diff(get_base_rom_data(game), rom) - return generate_yaml(patch, metadata, game) - - -def create_patch_file(rom_file_to_patch: str, - server: str = "", - destination: Optional[str] = None, - player: int = 0, - player_name: str = "", - game: str = GAME_ALTTP) -> str: - meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise - "player_id": player, - "player_name": player_name} - bytes = generate_patch(load_bytes(rom_file_to_patch), - meta, - game) - target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ( - ".apbp" if game == GAME_ALTTP - else ".apsmz" if game == GAME_SMZ3 - else ".apdkc3" if game == GAME_DKC3 - else ".apm3") - write_lzma(bytes, target) - return target - - -def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[Dict[str, Any], str, bytearray]: - data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) - game_name = data["game"] - if not ignore_version and data["compatible_version"] > current_patch_version: - raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") - patched_data: bytearray = bsdiff4.patch(get_base_rom_data(game_name), data["patch"]) - rom_hash = patched_data[int(0x7FC0):int(0x7FD5)] - data["meta"]["hash"] = "".join(chr(x) for x in rom_hash) - target = os.path.splitext(patch_file)[0] + ".sfc" - return data["meta"], target, patched_data - - -def get_base_rom_data(game: str) -> bytes: - if game == GAME_ALTTP: - from worlds.alttp.Rom import get_base_rom_bytes - elif game == "alttp": # old version for A Link to the Past - from worlds.alttp.Rom import get_base_rom_bytes - elif game == GAME_SM: - from worlds.sm.Rom import get_base_rom_bytes - elif game == GAME_SOE: - from worlds.soe.Patch import get_base_rom_path - get_base_rom_bytes = lambda: bytes(read_rom(open(get_base_rom_path(), "rb"))) - elif game == GAME_SMZ3: - from worlds.smz3.Rom import get_base_rom_bytes - elif game == GAME_DKC3: - from worlds.dkc3.Rom import get_base_rom_bytes - else: - raise RuntimeError("Selected game for base rom not found.") - return get_base_rom_bytes() - - -def create_rom_file(patch_file: str) -> Tuple[Dict[str, Any], str]: +def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]: auto_handler = AutoPatchRegister.get_handler(patch_file) if auto_handler: handler: APDeltaPatch = auto_handler(patch_file) @@ -277,157 +36,10 @@ def create_rom_file(patch_file: str) -> Tuple[Dict[str, Any], str]: return {"server": handler.server, "player": handler.player, "player_name": handler.player_name}, target - else: - data, target, patched_data = create_rom_bytes(patch_file) - with open(target, "wb") as f: - f.write(patched_data) - return data, target - - -def update_patch_data(patch_data: bytes, server: str = "") -> bytes: - data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig")) - data["meta"]["server"] = server - bytes = generate_yaml(data["patch"], data["meta"], data["game"]) - return lzma.compress(bytes) - - -def load_bytes(path: str) -> bytes: - with open(path, "rb") as f: - return f.read() - - -def write_lzma(data: bytes, path: str): - with lzma.LZMAFile(path, 'wb') as f: - f.write(data) - - -def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray: - """Reads rom into bytearray and optionally strips off any smc header""" - buffer = bytearray(stream.read()) - if strip_header and len(buffer) % 0x400 == 0x200: - return buffer[0x200:] - return buffer + raise NotImplementedError(f"No Handler for {patch_file} found.") if __name__ == "__main__": - host = Utils.get_public_ipv4() - options = Utils.get_options()['server_options'] - if options['host']: - host = options['host'] - - address = f"{host}:{options['port']}" - ziplock = threading.Lock() - print(f"Host for patches to be created is {address}") - with concurrent.futures.ThreadPoolExecutor() as pool: - for rom in sys.argv: - try: - if rom.endswith(".sfc"): - print(f"Creating patch for {rom}") - result = pool.submit(create_patch_file, rom, address) - result.add_done_callback(lambda task: print(f"Created patch {task.result()}")) - - elif rom.endswith(".apbp"): - print(f"Applying patch {rom}") - data, target = create_rom_file(rom) - # romfile, adjusted = Utils.get_adjuster_settings(target) - adjuster_settings = Utils.get_adjuster_settings(GAME_ALTTP) - adjusted = False - if adjuster_settings: - import pprint - from worlds.alttp.Rom import get_base_rom_path - adjuster_settings.rom = target - adjuster_settings.baserom = get_base_rom_path() - adjuster_settings.world = None - whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", - "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", - "reduceflashing", "deathlink"} - printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist} - if hasattr(adjuster_settings, "sprite_pool"): - sprite_pool = {} - for sprite in getattr(adjuster_settings, "sprite_pool"): - if sprite in sprite_pool: - sprite_pool[sprite] += 1 - else: - sprite_pool[sprite] = 1 - if sprite_pool: - printed_options["sprite_pool"] = sprite_pool - - adjust_wanted = str('no') - if not hasattr(adjuster_settings, 'auto_apply') or 'ask' in adjuster_settings.auto_apply: - adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n" - f"{pprint.pformat(printed_options)}\n" - f"Enter yes, no, always or never: ") - if adjuster_settings.auto_apply == 'never': # never adjust, per user request - adjust_wanted = 'no' - elif adjuster_settings.auto_apply == 'always': - adjust_wanted = 'yes' - - if adjust_wanted and "never" in adjust_wanted: - adjuster_settings.auto_apply = 'never' - Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings) - - elif adjust_wanted and "always" in adjust_wanted: - adjuster_settings.auto_apply = 'always' - Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings) - - if adjust_wanted and adjust_wanted.startswith("y"): - if hasattr(adjuster_settings, "sprite_pool"): - from LttPAdjuster import AdjusterWorld - adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool")) - - adjusted = True - import LttPAdjuster - _, romfile = LttPAdjuster.adjust(adjuster_settings) - - if hasattr(adjuster_settings, "world"): - delattr(adjuster_settings, "world") - else: - adjusted = False - if adjusted: - try: - shutil.move(romfile, target) - romfile = target - except Exception as e: - print(e) - print(f"Created rom {romfile if adjusted else target}.") - if 'server' in data: - Utils.persistent_store("servers", data['hash'], data['server']) - print(f"Host is {data['server']}") - elif rom.endswith(".apm3") \ - or rom.endswith(".apsmz") \ - or rom.endswith(".apdkc3"): - print(f"Applying patch {rom}") - data, target = create_rom_file(rom) - print(f"Created rom {target}.") - if 'server' in data: - Utils.persistent_store("servers", data['hash'], data['server']) - print(f"Host is {data['server']}") - - elif rom.endswith(".zip"): - print(f"Updating host in patch files contained in {rom}") - - def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str) -> str: - data = zfr.read(zfinfo) - if zfinfo.filename.endswith(".apbp") or \ - zfinfo.filename.endswith(".apm3") or \ - zfinfo.filename.endswith(".apdkc3"): - data = update_patch_data(data, server) - with ziplock: - zfw.writestr(zfinfo, data) - return zfinfo.filename - - futures: List[concurrent.futures.Future[str]] = [] - with zipfile.ZipFile(rom, "r") as zfr: - updated_zip = os.path.splitext(rom)[0] + "_updated.zip" - with zipfile.ZipFile(updated_zip, "w", compression=zipfile.ZIP_DEFLATED, - compresslevel=9) as zfw: - for zfname in zfr.namelist(): - futures.append(pool.submit(_handle_zip_file_entry, zfr.getinfo(zfname), address)) - for future in futures: - print(f"File {future.result()} added to {os.path.split(updated_zip)[1]}") - - except: - import traceback - - traceback.print_exc() - input("Press enter to close.") + for file in sys.argv[1:]: + meta_data, result_file = create_rom_file(file) + print(f"Patch with meta-data {meta_data} was written to {result_file}") diff --git a/SNIClient.py b/SNIClient.py index 9170c845e3..188822bce7 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -15,10 +15,13 @@ import typing from json import loads, dumps -from Utils import init_logging, messagebox +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser + +import Utils if __name__ == "__main__": - init_logging("SNIClient", exception_logger="Client") + Utils.init_logging("SNIClient", exception_logger="Client") import colorama import websockets @@ -28,10 +31,9 @@ from worlds.alttp import Regions, Shops from worlds.alttp.Rom import ROM_PLAYER_LIMIT from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT from worlds.smz3.Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT -import Utils -from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3, GAME_SMW + snes_logger = logging.getLogger("SNES") from MultiServer import mark_raw @@ -1336,20 +1338,18 @@ async def main(): try: meta, romfile = Patch.create_rom_file(args.diff_file) except Exception as e: - messagebox('Error', str(e), True) + Utils.messagebox('Error', str(e), True) raise - if "server" in meta: - args.connect = meta["server"] + args.connect = meta["server"] logging.info(f"Wrote rom file to {romfile}") if args.diff_file.endswith(".apsoe"): import webbrowser - webbrowser.open("http://www.evermizer.com/apclient/" + - (f"#server={meta['server']}" if "server" in meta else "")) + webbrowser.open(f"http://www.evermizer.com/apclient/#server={meta['server']}") logging.info("Starting Evermizer Client in your Browser...") import time time.sleep(3) sys.exit() - elif args.diff_file.endswith((".apbp", ".apz3", ".aplttp")): + elif args.diff_file.endswith(".aplttp"): adjustedromfile, adjusted = get_alttp_settings(romfile) asyncio.create_task(run_game(adjustedromfile if adjusted else romfile)) else: diff --git a/Utils.py b/Utils.py index c5fc00035a..707415453a 100644 --- a/Utils.py +++ b/Utils.py @@ -11,6 +11,8 @@ import io import collections import importlib import logging +from typing import BinaryIO + from yaml import load, load_all, dump, SafeLoader try: @@ -632,3 +634,11 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset else: return element.lower() return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) + + +def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray: + """Reads rom into bytearray and optionally strips off any smc header""" + buffer = bytearray(stream.read()) + if strip_header and len(buffer) % 0x400 == 0x200: + return buffer[0x200:] + return buffer diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index b7bf4e38d1..f9c49c5a20 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -10,7 +10,6 @@ from flask_compress import Compress from werkzeug.routing import BaseConverter from Utils import title_sorted -from .models import * UPLOAD_FOLDER = os.path.relpath('uploads') LOGS_FOLDER = os.path.relpath('logs') @@ -73,8 +72,10 @@ def register(): """Import submodules, triggering their registering on flask routing. Note: initializes worlds subsystem.""" # has automatic patch integration - import Patch - app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types + import worlds.AutoWorld + import worlds.Files + app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: \ + game_name in worlds.Files.AutoPatchRegister.patch_types from WebHostLib.customserver import run_server_process # to trigger app routing picking up on it diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index faad50e1c6..45cca66ef7 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -7,7 +7,8 @@ from . import api_endpoints from flask import request, session, url_for from pony.orm import commit -from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR +from WebHostLib import app +from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR from WebHostLib.check import get_yaml_data, roll_options from WebHostLib.generate import get_meta diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index c3a373c2e9..0386d1b0ae 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -5,8 +5,9 @@ from io import BytesIO from flask import send_file, Response, render_template from pony.orm import select -from Patch import update_patch_data, preferred_endings, AutoPatchRegister -from WebHostLib import app, Slot, Room, Seed, cache +from worlds.Files import AutoPatchRegister +from . import app, cache +from .models import Slot, Room, Seed @app.route("/dl_patch//") @@ -41,12 +42,7 @@ def download_patch(room_id, patch_id): new_file.seek(0) return send_file(new_file, as_attachment=True, download_name=fname) else: - patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}") - patch_data = BytesIO(patch_data) - - fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \ - f"{preferred_endings[patch.game]}" - return send_file(patch_data, as_attachment=True, download_name=fname) + return "Old Patch file, no longer compatible." @app.route("/dl_spoiler/") diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index fb5df81c9a..8bbf7465d3 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -8,7 +8,8 @@ import datetime from uuid import UUID from worlds.alttp import Items -from WebHostLib import app, cache, Room +from . import app, cache +from .models import Room from Utils import restricted_loads from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name from MultiServer import Context diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 6907bb2acd..173411bb64 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -1,6 +1,5 @@ import typing import zipfile -import lzma import json import base64 import MultiServer @@ -10,9 +9,10 @@ from io import BytesIO from flask import request, flash, redirect, url_for, session, render_template from pony.orm import flush, select -from WebHostLib import app, Seed, Room, Slot -from Utils import parse_yaml, VersionException, __version__ -from Patch import preferred_endings, AutoPatchRegister +from . import app +from .models import Seed, Room, Slot +from Utils import VersionException, __version__ +from worlds.Files import AutoPatchRegister from NetUtils import NetworkSlot, SlotType banned_zip_contents = (".sfc",) @@ -38,17 +38,6 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s player_name=patch.player_name, player_id=patch.player, game=patch.game)) - elif file.filename.endswith(tuple(preferred_endings.values())): - data = zfile.open(file, "r").read() - yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig")) - if yaml_data["version"] < 2: - return "Old format cannot be uploaded (outdated .apbp)" - metadata = yaml_data["meta"] - - slots.add(Slot(data=data, - player_name=metadata["player_name"], - player_id=metadata["player_id"], - game=yaml_data["game"])) elif file.filename.endswith(".apmc"): data = zfile.open(file, "r").read() diff --git a/data/basepatch.apbp b/data/basepatch.apbp deleted file mode 100644 index 2a30d9f8c2801a726f1f35c5c7f1e32882bede05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 117900 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

oNAV_tAgsG^l^QUIFy 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%0Dpfj4Y*Qma32f6B5 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 J00004Sz3sRll}kz diff --git a/data/basepatch.bsdiff4 b/data/basepatch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..a578b248f57721cfc7e8c31da233bf732ed27db8 GIT binary patch literal 114127 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 wa4TS7uXZs4i)~=>>)= AutoPatchRegister: + # construct class + new_class = super().__new__(mcs, name, bases, dct) + if "game" in dct: + AutoPatchRegister.patch_types[dct["game"]] = new_class + if not dct["patch_file_ending"]: + raise Exception(f"Need an expected file ending for {name}") + AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class + return new_class + + @staticmethod + def get_handler(file: str) -> Optional[AutoPatchRegister]: + for file_ending, handler in AutoPatchRegister.file_endings.items(): + if file.endswith(file_ending): + return handler + return None + + +current_patch_version: int = 5 + + +class APContainer: + """A zipfile containing at least archipelago.json""" + version: int = current_patch_version + compression_level: int = 9 + compression_method: int = zipfile.ZIP_DEFLATED + game: Optional[str] = None + + # instance attributes: + path: Optional[str] + player: Optional[int] + player_name: str + server: str + + def __init__(self, path: Optional[str] = None, player: Optional[int] = None, + player_name: str = "", server: str = ""): + self.path = path + self.player = player + self.player_name = player_name + self.server = server + + def write(self, file: Optional[Union[str, BinaryIO]] = None) -> None: + zip_file = file if file else self.path + if not zip_file: + raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.") + with zipfile.ZipFile(zip_file, "w", self.compression_method, True, self.compression_level) \ + as zf: + if file: + self.path = zf.filename + self.write_contents(zf) + + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + manifest = self.get_manifest() + try: + manifest_str = json.dumps(manifest) + except Exception as e: + raise Exception(f"Manifest {manifest} did not convert to json.") from e + else: + opened_zipfile.writestr("archipelago.json", manifest_str) + + def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None: + """Read data into patch object. file can be file-like, such as an outer zip file's stream.""" + zip_file = file if file else self.path + if not zip_file: + raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.") + with zipfile.ZipFile(zip_file, "r") as zf: + if file: + self.path = zf.filename + self.read_contents(zf) + + def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + with opened_zipfile.open("archipelago.json", "r") as f: + manifest = json.load(f) + if manifest["compatible_version"] > self.version: + raise Exception(f"File (version: {manifest['compatible_version']}) too new " + f"for this handler (version: {self.version})") + self.player = manifest["player"] + self.server = manifest["server"] + self.player_name = manifest["player_name"] + + def get_manifest(self) -> Dict[str, Any]: + return { + "server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise + "player": self.player, + "player_name": self.player_name, + "game": self.game, + # minimum version of patch system expected for patching to be successful + "compatible_version": 4, + "version": current_patch_version, + } + + +class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): + """An APContainer that additionally has delta.bsdiff4 + containing a delta patch to get the desired file, often a rom.""" + + hash: Optional[str] # base checksum of source file + patch_file_ending: str = "" + delta: Optional[bytes] = None + result_file_ending: str = ".sfc" + source_data: bytes + + def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: + self.patched_path = patched_path + super(APDeltaPatch, self).__init__(*args, **kwargs) + + def get_manifest(self) -> Dict[str, Any]: + 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 + def get_source_data(cls) -> bytes: + """Get Base data""" + raise NotImplementedError() + + @classmethod + def get_source_data_with_cache(cls) -> bytes: + if not hasattr(cls, "source_data"): + cls.source_data = cls.get_source_data() + return cls.source_data + + def write_contents(self, opened_zipfile: zipfile.ZipFile): + super(APDeltaPatch, self).write_contents(opened_zipfile) + # write Delta + opened_zipfile.writestr("delta.bsdiff4", + bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()), + compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression + + def read_contents(self, opened_zipfile: zipfile.ZipFile): + super(APDeltaPatch, self).read_contents(opened_zipfile) + self.delta = opened_zipfile.read("delta.bsdiff4") + + def patch(self, target: str): + """Base + Delta -> Patched""" + if not self.delta: + self.read() + result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta) + with open(target, "wb") as f: + f.write(result) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 9ca3a355e1..24a1588c52 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1,11 +1,12 @@ from __future__ import annotations import Utils -from Patch import read_rom +import worlds.AutoWorld +import worlds.Files -LTTPJPN10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9952c2a3ec1b421e408df0d20c8f0c7f' -ROM_PLAYER_LIMIT = 255 +LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173" +RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f" +ROM_PLAYER_LIMIT: int = 255 import io import json @@ -34,7 +35,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, parse_yaml +from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items from worlds.alttp.EntranceShuffle import door_addresses from worlds.alttp.Options import smallkey_shuffle @@ -57,13 +58,13 @@ class LocalRom(object): self.orig_buffer = None with open(file, 'rb') as stream: - self.buffer = read_rom(stream) + self.buffer = read_snes_rom(stream) if patch: self.patch_base_rom() self.orig_buffer = self.buffer.copy() if vanillaRom: with open(vanillaRom, 'rb') as vanillaStream: - self.orig_buffer = read_rom(vanillaStream) + self.orig_buffer = read_snes_rom(vanillaStream) def read_byte(self, address: int) -> int: return self.buffer[address] @@ -123,29 +124,24 @@ class LocalRom(object): return expected == buffermd5.hexdigest() def patch_base_rom(self): - if os.path.isfile(local_path('basepatch.sfc')): - with open(local_path('basepatch.sfc'), 'rb') as stream: + if os.path.isfile(user_path('basepatch.sfc')): + with open(user_path('basepatch.sfc'), 'rb') as stream: buffer = bytearray(stream.read()) if self.verify(buffer): self.buffer = buffer - if not os.path.exists(local_path('data', 'basepatch.apbp')): - Patch.create_patch_file(local_path('basepatch.sfc')) return - if not os.path.isfile(local_path('data', 'basepatch.apbp')): - raise RuntimeError('Base patch unverified. Unable to continue.') + with open(local_path("data", "basepatch.bsdiff4"), "rb") as f: + delta = f.read() - if os.path.isfile(local_path('data', 'basepatch.apbp')): - _, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.apbp'), ignore_version=True) - if self.verify(buffer): - self.buffer = bytearray(buffer) - with open(user_path('basepatch.sfc'), 'wb') as stream: - stream.write(buffer) - return - raise RuntimeError('Base patch unverified. Unable to continue.') - - raise RuntimeError('Could not find Base Patch. Unable to continue.') + buffer = bsdiff4.patch(get_base_rom_bytes(), delta) + if self.verify(buffer): + self.buffer = bytearray(buffer) + with open(user_path('basepatch.sfc'), 'wb') as stream: + stream.write(buffer) + return + raise RuntimeError('Base patch unverified. Unable to continue.') def write_crc(self): crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF @@ -544,7 +540,7 @@ class Sprite(): def get_vanilla_sprite_data(self): file_name = get_base_rom_path() - base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) Sprite.sprite = base_rom_bytes[0x80000:0x87000] Sprite.palette = base_rom_bytes[0xDD308:0xDD380] Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9] @@ -2906,7 +2902,7 @@ hash_alphabet = [ ] -class LttPDeltaPatch(Patch.APDeltaPatch): +class LttPDeltaPatch(worlds.Files.APDeltaPatch): hash = LTTPJPN10HASH game = "A Link to the Past" patch_file_ending = ".aplttp" @@ -2920,7 +2916,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: file_name = get_base_rom_path(file_name) - base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 2bb5221a60..6308e95860 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -1,5 +1,6 @@ import Utils -from Patch import read_rom, APDeltaPatch +from Utils import read_snes_rom +from worlds.Files import APDeltaPatch from .Locations import lookup_id_to_name, all_locations from .Levels import level_list, level_dict @@ -440,13 +441,13 @@ class LocalRom(object): self.orig_buffer = None with open(file, 'rb') as stream: - self.buffer = read_rom(stream) + self.buffer = read_snes_rom(stream) #if patch: # self.patch_rom() # self.orig_buffer = self.buffer.copy() #if vanillaRom: # with open(vanillaRom, 'rb') as vanillaStream: - # self.orig_buffer = read_rom(vanillaStream) + # self.orig_buffer = read_snes_rom(vanillaStream) def read_bit(self, address: int, bit_number: int) -> bool: bitflag = (1 << bit_number) @@ -724,7 +725,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: file_name = get_base_rom_path(file_name) - base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 89666ffbdd..018816d90a 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -11,6 +11,8 @@ import shutil import Utils import Patch +import worlds.AutoWorld +import worlds.Files from . import Options from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \ @@ -57,7 +59,7 @@ recipe_time_ranges = { } -class FactorioModFile(Patch.APContainer): +class FactorioModFile(worlds.Files.APContainer): game = "Factorio" compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index e2957fe00f..e5f5bc7a37 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -3,7 +3,8 @@ import os import json import Utils -from Patch import read_rom, APDeltaPatch +from Utils import read_snes_rom +from worlds.Files import APDeltaPatch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' ROM_PLAYER_LIMIT = 65535 # max archipelago player ID. note, SM ROM itself will only store 201 names+ids max @@ -22,7 +23,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: file_name = get_base_rom_path(file_name) - base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) diff --git a/worlds/smw/Rom.py b/worlds/smw/Rom.py index 6c750d74ff..34bdd2f0ea 100644 --- a/worlds/smw/Rom.py +++ b/worlds/smw/Rom.py @@ -1,8 +1,7 @@ import Utils -from Patch import read_rom, APDeltaPatch +from worlds.Files import APDeltaPatch from .Aesthetics import generate_shuffled_header_data -from .Locations import lookup_id_to_name, all_locations -from .Levels import level_info_dict, full_level_list, submap_level_list, location_id_to_level_id +from .Levels import level_info_dict from .Names.TextBox import generate_goal_text, title_text_mapping, generate_text_box USHASH = 'cdd3c8c37322978ca8669b34bc89c804' @@ -69,7 +68,7 @@ class LocalRom: self.orig_buffer = None with open(file, 'rb') as stream: - self.buffer = read_rom(stream) + self.buffer = Utils.read_snes_rom(stream) def read_bit(self, address: int, bit_number: int) -> bool: bitflag = (1 << bit_number) @@ -827,7 +826,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: file_name = get_base_rom_path(file_name) - base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) @@ -837,6 +836,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: get_base_rom_bytes.base_rom_bytes = base_rom_bytes return base_rom_bytes + def get_base_rom_path(file_name: str = "") -> str: options = Utils.get_options() if not file_name: diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py index 46ce4de7ae..c31d4af7f6 100644 --- a/worlds/smw/__init__.py +++ b/worlds/smw/__init__.py @@ -14,7 +14,6 @@ from ..generic.Rules import add_rule from .Names import ItemName, LocationName from ..AutoWorld import WebWorld, World from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch -import Patch class SMWWeb(WebWorld): @@ -146,6 +145,7 @@ class SMWWorld(World): def generate_output(self, output_directory: str): + rompath = "" # if variable is not declared finally clause may fail try: world = self.world player = self.player @@ -167,9 +167,9 @@ class SMWWorld(World): except: raise finally: + self.rom_name_available_event.set() # make sure threading continues and errors are collected if os.path.exists(rompath): os.unlink(rompath) - self.rom_name_available_event.set() # make sure threading continues and errors are collected def modify_multidata(self, multidata: dict): import base64 diff --git a/worlds/smz3/Rom.py b/worlds/smz3/Rom.py index a355636fed..3fec151dc6 100644 --- a/worlds/smz3/Rom.py +++ b/worlds/smz3/Rom.py @@ -2,7 +2,8 @@ import hashlib import os import Utils -from Patch import read_rom, APDeltaPatch +from Utils import read_snes_rom +from worlds.Files import APDeltaPatch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' LTTPJPN10HASH = '03a63945398191337e896e5771f77173' @@ -23,7 +24,7 @@ def get_base_rom_bytes() -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) if not base_rom_bytes: sm_file_name = get_sm_base_rom_path() - sm_base_rom_bytes = bytes(read_rom(open(sm_file_name, "rb"))) + sm_base_rom_bytes = bytes(read_snes_rom(open(sm_file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(sm_base_rom_bytes) @@ -31,7 +32,7 @@ def get_base_rom_bytes() -> bytes: raise Exception('Supplied Base Rom does not match known MD5 for SM Japan+US release. ' 'Get the correct game and version, then dump it') lttp_file_name = get_lttp_base_rom_path() - lttp_base_rom_bytes = bytes(read_rom(open(lttp_file_name, "rb"))) + lttp_base_rom_bytes = bytes(read_snes_rom(open(lttp_file_name, "rb"))) basemd5 = hashlib.md5() basemd5.update(lttp_base_rom_bytes) diff --git a/worlds/soe/Patch.py b/worlds/soe/Patch.py index 21bdd94220..f6a0a69f55 100644 --- a/worlds/soe/Patch.py +++ b/worlds/soe/Patch.py @@ -2,7 +2,7 @@ import bsdiff4 import yaml from typing import Optional import Utils -from Patch import APDeltaPatch +from worlds.Files import APDeltaPatch import os From 060a04700d8a1edf974a7da084b11eb0152e5814 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 30 Sep 2022 04:58:19 +0200 Subject: [PATCH 030/105] Core: allow generic access to indirect_connections (#1056) --- BaseClasses.py | 14 ++++++++++---- test/minor_glitches/TestMinor.py | 6 ++---- test/owg/TestVanillaOWG.py | 6 ++---- test/vanilla/TestVanilla.py | 6 ++---- worlds/alttp/__init__.py | 8 ++++++-- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 634c2a833f..c5e7640b17 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -51,8 +51,10 @@ class MultiWorld(): non_local_items: Dict[int, Options.NonLocalItems] progression_balancing: Dict[int, Options.ProgressionBalancing] completion_condition: Dict[int, Callable[[CollectionState], bool]] + indirect_connections: Dict[Region, Set[Entrance]] exclude_locations: Dict[int, Options.ExcludeLocations] + class AttributeProxy(): def __init__(self, rule): self.rule = rule @@ -89,6 +91,7 @@ class MultiWorld(): self.customitemarray = [] self.shuffle_ganon = True self.spoiler = Spoiler(self) + self.indirect_connections = {} self.fix_trock_doors = self.AttributeProxy( lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') self.fix_skullwoods_exit = self.AttributeProxy( @@ -406,6 +409,11 @@ class MultiWorld(): def clear_entrance_cache(self): self._cached_entrances = None + def register_indirect_condition(self, region: Region, entrance: Entrance): + """Report that access to this Region can result in unlocking this Entrance, + state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic.""" + self.indirect_connections.setdefault(region, set()).add(entrance) + def get_locations(self) -> List[Location]: if self._cached_locations is None: self._cached_locations = [location for region in self.regions for location in region.locations] @@ -610,7 +618,6 @@ class CollectionState(): self.collect(item, True) def update_reachable_regions(self, player: int): - from worlds.alttp.EntranceShuffle import indirect_connections self.stale[player] = False rrp = self.reachable_regions[player] bc = self.blocked_connections[player] @@ -618,7 +625,7 @@ class CollectionState(): start = self.world.get_region('Menu', player) # init on first call - this can't be done on construction since the regions don't exist yet - if not start in rrp: + if start not in rrp: rrp.add(start) bc.update(start.exits) queue.extend(start.exits) @@ -638,8 +645,7 @@ class CollectionState(): self.path[new_region] = (new_region.name, self.path.get(connection, None)) # Retry connections if the new region can unblock them - if new_region.name in indirect_connections: - new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) + for new_entrance in self.world.indirect_connections.get(new_region, set()): if new_entrance in bc and new_entrance not in queue: queue.append(new_entrance) diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index 81c09cfb27..41cb13161a 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -23,10 +23,8 @@ class TestMinor(TestBase): self.world.set_default_common_options() self.world.logic[1] = "minorglitches" self.world.difficulty_requirements[1] = difficulties['normal'] - create_regions(self.world, 1) - create_dungeons(self.world, 1) - create_shops(self.world, 1) - link_entrances(self.world, 1) + self.world.worlds[1].er_seed = 0 + self.world.worlds[1].create_regions() self.world.worlds[1].create_items() self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.itempool.extend(get_dungeon_item_pool(self.world)) diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index e5489117a7..a4367fb55e 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -24,10 +24,8 @@ class TestVanillaOWG(TestBase): self.world.set_default_common_options() self.world.difficulty_requirements[1] = difficulties['normal'] self.world.logic[1] = "owglitches" - create_regions(self.world, 1) - create_dungeons(self.world, 1) - create_shops(self.world, 1) - link_entrances(self.world, 1) + self.world.worlds[1].er_seed = 0 + self.world.worlds[1].create_regions() self.world.worlds[1].create_items() self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.itempool.extend(get_dungeon_item_pool(self.world)) diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index e5ee73406a..c9fa3f763b 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -22,10 +22,8 @@ class TestVanilla(TestBase): self.world.set_default_common_options() self.world.logic[1] = "noglitches" self.world.difficulty_requirements[1] = difficulties['normal'] - create_regions(self.world, 1) - create_dungeons(self.world, 1) - create_shops(self.world, 1) - link_entrances(self.world, 1) + self.world.worlds[1].er_seed = 0 + self.world.worlds[1].create_regions() self.world.worlds[1].create_items() self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.itempool.extend(get_dungeon_item_pool(self.world)) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index bbdd941127..88d2c2f29f 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -7,7 +7,7 @@ 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 +from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, indirect_connections 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 @@ -19,7 +19,7 @@ from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enem from .Rules import set_rules from .Shops import create_shops, ShopSlotFill from .SubClasses import ALttPItem -from ..AutoWorld import World, WebWorld, LogicMixin +from worlds.AutoWorld import World, WebWorld, LogicMixin lttp_logger = logging.getLogger("A Link to the Past") @@ -223,6 +223,10 @@ class ALTTPWorld(World): world.random = old_random plando_connect(world, player) + for region_name, entrance_name in indirect_connections.items(): + world.register_indirect_condition(self.world.get_region(region_name, player), + self.world.get_entrance(entrance_name, player)) + def collect_item(self, state: CollectionState, item: Item, remove=False): item_name = item.name if item_name.startswith('Progressive '): From 4943d26160a04d26b66472e9626d146eb809db84 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 1 Oct 2022 02:47:31 +0200 Subject: [PATCH 031/105] APWorld: make it behave more like a regular world - set sys.modules so it can be imported with worlds.* - overwrite __package__ so it can reference ..generic - fix deprecation warning --- worlds/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worlds/__init__.py b/worlds/__init__.py index e36eb275a3..039f12eb93 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -1,7 +1,9 @@ import importlib -import zipimport import os +import sys import typing +import warnings +import zipimport folder = os.path.dirname(__file__) @@ -39,7 +41,14 @@ world_sources.sort() for world_source in world_sources: if world_source.is_zip: importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) - importer.load_module(world_source.path.split(".", 1)[0]) + spec = importer.find_spec(world_source.path.split(".", 1)[0]) + mod = importlib.util.module_from_spec(spec) + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" + sys.modules[mod.__name__] = mod + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") + importer.exec_module(mod) else: importlib.import_module(f".{world_source.path}", "worlds") From e9e15e854df37146920f9da697c91cd378f83f3b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Oct 2022 15:24:05 +0200 Subject: [PATCH 032/105] SC2: make apworld compatible (#1024) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Starcraft2Client.py | 42 ++++++++++++++++++------------------- worlds/sc2wol/LogicMixin.py | 2 +- worlds/sc2wol/Regions.py | 4 ++-- worlds/sc2wol/__init__.py | 5 ++--- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index d91adffb08..c1eed74b4c 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -12,21 +12,9 @@ import typing import queue from pathlib import Path -import nest_asyncio -import sc2 -from sc2.bot_ai import BotAI -from sc2.data import Race -from sc2.main import run_game -from sc2.player import Bot - -import NetUtils -from MultiServer import mark_raw +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser from Utils import init_logging, is_windows -from worlds.sc2wol import SC2WoLWorld -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 if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -34,10 +22,21 @@ if __name__ == "__main__": logger = logging.getLogger("Client") sc2_logger = logging.getLogger("Starcraft2") -import colorama +import nest_asyncio +import sc2 +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 import SC2WoLWorld +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 -from NetUtils import ClientStatus, RawJSONtoTextParser -from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser +import colorama +from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser +from MultiServer import mark_raw nest_asyncio.apply() max_bonus: int = 8 @@ -357,8 +356,9 @@ class SC2Context(CommonContext): self.ui = SC2Manager(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - - Builder.load_file(Utils.local_path(os.path.dirname(SC2WoLWorld.__file__), "Starcraft2.kv")) + import pkgutil + data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode() + Builder.load_string(data) async def shutdown(self): await super(SC2Context, self).shutdown() @@ -440,8 +440,8 @@ wol_default_categories = [ ] -def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]: - network_item: NetUtils.NetworkItem +def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]: + network_item: NetworkItem accumulators: typing.List[int] = [0 for _ in type_flaggroups] for network_item in items: diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 7a08142672..52bb6b09a8 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -1,5 +1,5 @@ from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin +from worlds.AutoWorld import LogicMixin class SC2WoLLogic(LogicMixin): diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 4e20752982..8219a982c9 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -1,8 +1,8 @@ -from typing import List, Set, Dict, Tuple, Optional, Callable, NamedTuple +from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from .Locations import LocationData from .Options import get_option_value -from worlds.sc2wol.MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \ +from .MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \ no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list import random diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 4f9b33609f..6d056df808 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -1,15 +1,14 @@ import typing -from typing import List, Set, Tuple, NamedTuple +from typing import List, Set, Tuple from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification -from ..AutoWorld import WebWorld +from worlds.AutoWorld import WebWorld, World from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \ basic_unit from .Locations import get_locations from .Regions import create_regions from .Options import sc2wol_options, get_option_value from .LogicMixin import SC2WoLLogic -from ..AutoWorld import World class Starcraft2WoLWebWorld(WebWorld): From 411cd51a9296b5f705a445403d2cce5f05a3a725 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Oct 2022 16:22:24 +0200 Subject: [PATCH 033/105] SC2: dynamically create Beat Events, preventing copy-paste errors (#1023) --- worlds/sc2wol/Locations.py | 76 +++++++------------------------------- 1 file changed, 13 insertions(+), 63 deletions(-) diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index f69abd48e3..14dd25fd52 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -27,13 +27,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104), LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105), LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106), - LocationData("Liberation Day", "Beat Liberation Day", None), LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, lambda state: state._sc2wol_has_common_unit(world, player)), - LocationData("The Outlaws", "Beat The Outlaws", None, - lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301), @@ -41,26 +38,20 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, lambda state: state._sc2wol_has_common_unit(world, player)), - 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_anti_air(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_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)), LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403, 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_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, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), - LocationData("Outbreak", "Beat Outbreak", None, - 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_competent_anti_air(world, player)), @@ -73,9 +64,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_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_competent_anti_air(world, player)), @@ -88,9 +76,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_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_competent_anti_air(world, player)), @@ -101,9 +86,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, lambda state: state._sc2wol_has_common_unit(world, player) and state._sc2wol_has_anti_air(world, player)), - LocationData("Smash and Grab", "Beat Smash and Grab", None, - lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_anti_air(world, player)), LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, lambda state: state._sc2wol_has_common_unit(world, player) and state._sc2wol_has_anti_air(world, player) and @@ -114,10 +96,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, lambda state: state._sc2wol_has_common_unit(world, player)), - LocationData("The Dig", "Beat The Dig", None, - lambda state: state._sc2wol_has_common_unit(world, player) and - 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) and state._sc2wol_has_anti_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, @@ -132,8 +110,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, lambda state: state._sc2wol_has_air(world, player)), - 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_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), @@ -142,8 +118,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, lambda state: state._sc2wol_beats_protoss_deathball(world, player)), - LocationData("Supernova", "Beat Supernova", None, - 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._sc2wol_has_air(world, player) and @@ -170,19 +144,12 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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._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_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)), - LocationData("Devil's Playground", "Beat Devil's Playground", None, - 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_competent_anti_air(world, player)), @@ -193,28 +160,21 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_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_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), - LocationData("Breakout", "Beat Breakout", None), LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600), LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601), LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602), LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603), LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604), 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) 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), - LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None, - lambda state: state._sc2wol_has_train_killers(world, player)), LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, @@ -224,10 +184,9 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803), LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804, lambda state: state._sc2wol_has_common_unit(world, player)), - 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_competent_anti_air(world, player)), + 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: Odin", SC2WOL_LOC_ID_OFFSET + 1901), LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, lambda state: state._sc2wol_has_competent_anti_air(world, player) and @@ -239,9 +198,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, 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_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)), LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001, @@ -251,8 +207,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003, lambda state: state._sc2wol_has_competent_comp(world, player)), 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, 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), @@ -264,44 +218,34 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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), LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), - LocationData("Whispers of Doom", "Beat Whispers of Doom", None), LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301), LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302), LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, lambda state: state._sc2wol_has_protoss_common_units(world, player)), - LocationData("A Sinister Turn", "Beat A Sinister Turn", None, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401), LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, 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), 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, 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, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("Gates of Hell", "Beat Gates of Hell", None), LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700), LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701), LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702), LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703), - LocationData("Belly of the Beast", "Beat Belly of the Beast", None), LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800, lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801, @@ -314,9 +258,15 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("Shatter the Sky", "Beat Shatter the Sky", None, - lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("All-In", "All-In: Victory", None) ] - return tuple(location_table) + beat_events = [] + + for location_data in location_table: + if location_data.name.endswith((": Victory", ": Defeat")): + beat_events.append( + location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None) + ) + + return tuple(location_table + beat_events) From b8e467fbb8125b6382b1d8482d2c202d9c613009 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 1 Oct 2022 17:38:39 +0200 Subject: [PATCH 034/105] ModuleUpdate: skip disabled/hidden folders (#1070) * ModuleUpdate: skip non-worlds * ModuleUpdate: don't skip _* folders - _* folders may be used for libraries - this means to properly disable a world, it has to be renamed with a preceding `.` --- ModuleUpdate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 17eb0906b1..1fe7030e4e 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -13,10 +13,12 @@ update_ran = getattr(sys, "frozen", False) # don't run update if environment is if not update_ran: for entry in os.scandir(os.path.join(local_dir, "worlds")): - if entry.is_dir(): - req_file = os.path.join(entry.path, "requirements.txt") - if os.path.exists(req_file): - requirements_files.add(req_file) + # skip .* (hidden / disabled) folders + if not entry.name.startswith("."): + if entry.is_dir(): + req_file = os.path.join(entry.path, "requirements.txt") + if os.path.exists(req_file): + requirements_files.add(req_file) def update_command(): From fdd7ffb0895a36689d3cd5fa092668a1915ad360 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 2 Oct 2022 07:53:18 -0700 Subject: [PATCH 035/105] Core: move output file name logic into core (#1066) * move output file name logic into core I see the same logic with small variations in each different world implementation. It seems to me, it would be better in the core to keep it consistent. * missed a few * remove review comment * + smw * double quote strings Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * revert change to DS3 output file name Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- BaseClasses.py | 7 +++++++ worlds/alttp/__init__.py | 6 +----- worlds/dkc3/__init__.py | 6 +----- worlds/minecraft/__init__.py | 2 +- worlds/oot/__init__.py | 2 +- worlds/sm/__init__.py | 6 ++---- worlds/sm64ex/__init__.py | 2 +- worlds/smw/__init__.py | 6 +----- worlds/smz3/__init__.py | 6 ++---- worlds/soe/__init__.py | 3 +-- worlds/v6/__init__.py | 2 +- 11 files changed, 19 insertions(+), 29 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c5e7640b17..d32749f5f1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -300,6 +300,13 @@ class MultiWorld(): def get_file_safe_player_name(self, player: int) -> str: return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*') + def get_out_file_name_base(self, player: int) -> str: + """ the base name (without file extension) for each player's output file for a seed """ + return f"AP_{self.seed_name}_P{player}" \ + + (f"_{self.get_file_safe_player_name(player).replace(' ', '_')}" + if (self.player_name[player] != f"Player{player}") + else '') + def initialize_regions(self, regions=None): for region in regions if regions else self.regions: region.world = self diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 88d2c2f29f..169d21ab8c 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -400,11 +400,7 @@ class ALTTPWorld(World): deathlink=world.death_link[player], allowcollect=world.allow_collect[player]) - outfilepname = f'_P{player}' - outfilepname += f"_{world.get_file_safe_player_name(player).replace(' ', '_')}" \ - if world.player_name[player] != 'Player%d' % player else '' - - rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') + rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc") rom.write_to_file(rompath) patch = LttPDeltaPatch(os.path.splitext(rompath)[0]+LttPDeltaPatch.patch_file_ending, player=player, player_name=world.player_name[player], patched_path=rompath) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index 5c575b85b5..1389f83efd 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -146,11 +146,7 @@ class DKC3World(World): self.active_level_list.append(LocationName.rocket_rush_region) - outfilepname = f'_P{player}' - outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ - if world.player_name[player] != 'Player%d' % player else '' - - rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') + rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc") rom.write_to_file(rompath) self.rom_name = rom.name diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 6e7addb2d0..fd5752bd40 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -150,7 +150,7 @@ class MinecraftWorld(World): def generate_output(self, output_directory: str): data = self._get_mc_data() - filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apmc" + filename = f"{self.world.get_out_file_name_base(self.player)}.apmc" with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b65882c874..2536c3d4c9 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -819,7 +819,7 @@ class OOTWorld(World): # Seed hint RNG, used for ganon text lines also self.hint_rng = self.world.slot_seeds[self.player] - outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}" + outfile_name = self.world.get_out_file_name_base(self.player) rom = Rom(file=get_options()['oot_options']['rom_file']) if self.hints != 'none': buildWorldGossipHints(self) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 0bf12ca7eb..d901303215 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -505,10 +505,8 @@ class SMWorld(World): romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs) def generate_output(self, output_directory: str): - outfilebase = 'AP_' + self.world.seed_name - outfilepname = f'_P{self.player}' - outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" - outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc') + outfilebase = self.world.get_out_file_name_base(self.player) + outputFilename = os.path.join(output_directory, f"{outfilebase}.sfc") try: self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 88b2261a23..8cf2f74350 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -173,7 +173,7 @@ class SM64World(World): } } } - filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apsm64ex" + filename = f"{self.world.get_out_file_name_base(self.player)}.apsm64ex" with open(os.path.join(output_directory, filename), 'w') as f: json.dump(data, f) diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py index c31d4af7f6..77931b7c72 100644 --- a/worlds/smw/__init__.py +++ b/worlds/smw/__init__.py @@ -153,11 +153,7 @@ class SMWWorld(World): rom = LocalRom(get_base_rom_path()) patch_rom(self.world, rom, self.player, self.active_level_dict) - outfilepname = f'_P{player}' - outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ - if world.player_name[player] != 'Player%d' % player else '' - - rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') + rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc") rom.write_to_file(rompath) self.rom_name = rom.name diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b796c2a43c..753fb556ae 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -426,11 +426,9 @@ class SMZ3World(World): base_combined_rom[addr + offset] = byte offset += 1 - outfilebase = 'AP_' + self.world.seed_name - outfilepname = f'_P{self.player}' - outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \ + outfilebase = self.world.get_out_file_name_base(self.player) - filename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc') + filename = os.path.join(output_directory, f"{outfilebase}.sfc") with open(filename, "wb") as binary_file: binary_file.write(base_combined_rom) patch = SMZ3DeltaPatch(os.path.splitext(filename)[0]+SMZ3DeltaPatch.patch_file_ending, player=self.player, diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index a0dc41c3ce..4885fd3179 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -333,8 +333,7 @@ class SoEWorld(World): switches.extend(('--available-fragments', str(self.available_fragments), '--required-fragments', str(self.required_fragments))) rom_file = get_base_rom_path() - out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_' - f'{self.world.get_file_safe_player_name(self.player)}') + out_base = output_path(output_directory, self.world.get_out_file_name_base(self.player)) out_file = out_base + '.sfc' placement_file = out_base + '.txt' patch_file = out_base + '.apsoe' diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 38690e5a00..9c5fdbc8a1 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -91,6 +91,6 @@ class V6World(World): } } } - filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apv6" + filename = f"{self.world.get_out_file_name_base(self.player)}.apv6" with open(os.path.join(output_directory, filename), 'w') as f: json.dump(data, f) From 8a6c9ff4b83201057f1fdc05efdb8216afb12bdb Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 2 Oct 2022 20:51:14 +0200 Subject: [PATCH 036/105] WebHost: clear yaml template folder before populating it --- WebHostLib/options.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 6807d54689..2d908ca962 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -15,7 +15,13 @@ handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hin def create(): target_folder = local_path("WebHostLib", "static", "generated") - os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True) + yaml_folder = os.path.join(target_folder, "configs") + os.makedirs(yaml_folder, exist_ok=True) + + for file in os.listdir(yaml_folder): + full_path: str = os.path.join(yaml_folder, file) + if os.path.isfile(full_path): + os.unlink(full_path) def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]): data = {} From 4c266e6efff69219528d419ffa644a06ca4c9e8b Mon Sep 17 00:00:00 2001 From: Gertimoshka <77289248+Gertimoshka@users.noreply.github.com> Date: Fri, 7 Oct 2022 00:53:20 +0300 Subject: [PATCH 037/105] hostRoom.css Changes (#957) * hostRoom.css Changes Makes the console be a scrollable object, for easier use with commands * Update hostRoom.css * Requested Change Requested Change --- WebHostLib/static/styles/hostRoom.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index 94ec24bfa6..827f74c04d 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -55,4 +55,6 @@ border: 1px solid #2a6c2f; border-radius: 6px; color: #000000; + overflow-y: auto; + max-height: 400px; } From 38b7bdfe60d8deda6677ceb718f309121c6a2b1a Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Fri, 7 Oct 2022 00:01:07 +0200 Subject: [PATCH 038/105] WebHost: Fixed some document titles (#1063) --- WebHostLib/static/assets/tutorial.js | 5 +++++ WebHostLib/templates/check.html | 1 - WebHostLib/templates/generate.html | 1 - WebHostLib/templates/hostGame.html | 1 - WebHostLib/templates/startPlaying.html | 1 - 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/WebHostLib/static/assets/tutorial.js b/WebHostLib/static/assets/tutorial.js index 23d2f076fc..39fc356913 100644 --- a/WebHostLib/static/assets/tutorial.js +++ b/WebHostLib/static/assets/tutorial.js @@ -27,6 +27,11 @@ window.addEventListener('load', () => { tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); adjustHeaderWidth(); + const title = document.querySelector('h1') + if (title) { + document.title = title.textContent; + } + // Reset the id of all header divs to something nicer const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); diff --git a/WebHostLib/templates/check.html b/WebHostLib/templates/check.html index 64f19b0f9c..04b51340b5 100644 --- a/WebHostLib/templates/check.html +++ b/WebHostLib/templates/check.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Mystery Check Result diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index aa16a47d35..eff42700a7 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Generate Game diff --git a/WebHostLib/templates/hostGame.html b/WebHostLib/templates/hostGame.html index 55d155c74a..2bcb993af5 100644 --- a/WebHostLib/templates/hostGame.html +++ b/WebHostLib/templates/hostGame.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Upload Multidata diff --git a/WebHostLib/templates/startPlaying.html b/WebHostLib/templates/startPlaying.html index 157d6de243..436af3df07 100644 --- a/WebHostLib/templates/startPlaying.html +++ b/WebHostLib/templates/startPlaying.html @@ -1,7 +1,6 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ super() }} Start Playing {% endblock %} From af6a72c3c3633cc7ad92b7381a7daa8af416162b Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 4 Oct 2022 23:50:02 +0200 Subject: [PATCH 039/105] AppImage: provide LD_LIBRARY_PATH this fixes libssl1.1 not being found --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 11c993774c..19d042189b 100644 --- a/setup.py +++ b/setup.py @@ -289,6 +289,7 @@ tmp="${{exe#*/}}" if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then exe="{default_exe.parent}/$exe" fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib" $APPDIR/$exe "$@" """) launcher_filename.chmod(0o755) From 7b3ef012b9b1eef893a251b8dd8eaa08aafc7db9 Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Sun, 9 Oct 2022 04:10:22 +0200 Subject: [PATCH 040/105] Factorio: Prevent pipes from breaking on invalid UTF-8 in client (#1078) --- FactorioClient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FactorioClient.py b/FactorioClient.py index 6797578a3a..1efca05d3c 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -211,6 +211,8 @@ async def game_watcher(ctx: FactorioContext): def stream_factorio_output(pipe, queue, process): + pipe.reconfigure(errors="replace") + def queuer(): while process.poll() is None: text = pipe.readline().strip() From 3297be7902eb907b2bcf45602c2d7d66b2d115f6 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 9 Oct 2022 04:13:52 +0200 Subject: [PATCH 041/105] The Witness: Expert & Hints (#1072) --- worlds/witness/Options.py | 26 +- worlds/witness/WitnessItems.txt | 2 + worlds/witness/WitnessLogic.txt | 42 +- worlds/witness/WitnessLogicExpert.txt | 932 ++++++++++++++++++ worlds/witness/__init__.py | 59 +- worlds/witness/hints.py | 281 ++++++ worlds/witness/items.py | 24 +- worlds/witness/locations.py | 9 +- worlds/witness/player_logic.py | 71 +- worlds/witness/regions.py | 26 +- worlds/witness/rules.py | 55 +- worlds/witness/settings/Audio_Logs.txt | 49 + .../witness/settings/Disable_Unrandomized.txt | 8 + worlds/witness/settings/Symbol_Shuffle.txt | 6 +- worlds/witness/static_logic.py | 163 +-- worlds/witness/utils.py | 21 + 16 files changed, 1638 insertions(+), 136 deletions(-) create mode 100644 worlds/witness/WitnessLogicExpert.txt create mode 100644 worlds/witness/hints.py create mode 100644 worlds/witness/settings/Audio_Logs.txt diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 2cb9ade007..b858eb1954 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -7,7 +7,7 @@ from Options import Toggle, DefaultOnToggle, Option, Range, Choice # "Play the randomizer in hardmode" # display_name = "Hard Mode" -class DisableNonRandomizedPuzzles(DefaultOnToggle): +class DisableNonRandomizedPuzzles(Toggle): """Disables puzzles that cannot be randomized. This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard. The lasers for those areas will be activated as you solve optional puzzles throughout the island.""" @@ -59,8 +59,9 @@ class ShuffleVaultBoxes(Toggle): class ShufflePostgame(Toggle): - """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.""" + """Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal. + Use this if you don't play with forfeit on victory. IMPORTANT NOTE: The possibility of your second + "Progressive Dots" showing up in the Caves is ignored, they will still be considered "postgame" in base settings.""" display_name = "Shuffle Postgame" @@ -75,6 +76,13 @@ class VictoryCondition(Choice): option_mountain_box_long = 3 +class PuzzleRandomization(Choice): + """Puzzles in this randomizer are randomly generated. This setting changes the difficulty/types of puzzles.""" + display_name = "Puzzle Randomization" + option_sigma_normal = 0 + option_sigma_expert = 1 + + class MountainLasers(Range): """Sets the amount of beams required to enter the final area.""" display_name = "Required Lasers for Mountain Entry" @@ -108,8 +116,17 @@ class PuzzleSkipAmount(Range): default = 5 +class HintAmount(Range): + """Adds hints to Audio Logs. Hints will have the same number of duplicates, as many as will fit. Remaining Audio + Logs will have junk hints.""" + display_name = "Hints on Audio Logs" + range_start = 0 + range_end = 49 + default = 10 + + the_witness_options: Dict[str, type] = { - # "hard_mode": HardMode, + "puzzle_randomization": PuzzleRandomization, "shuffle_symbols": ShuffleSymbols, "shuffle_doors": ShuffleDoors, "shuffle_lasers": ShuffleLasers, @@ -123,6 +140,7 @@ the_witness_options: Dict[str, type] = { "early_secret_area": EarlySecretArea, "trap_percentage": TrapPercentage, "puzzle_skip_amount": PuzzleSkipAmount, + "hint_amount": HintAmount, } diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index fd9b10f97a..d58edab0cb 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -15,6 +15,8 @@ Progression: 71 - Black/White Squares 72 - Colored Squares 80 - Arrows +200 - Progressive Dots - Dots,Full Dots +260 - Progressive Stars - Stars,Stars + Same Colored Symbol Usefuls: 101 - Functioning Brain - False diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 650cf14c52..236d220701 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -120,7 +120,7 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers +158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots 158653 - 0x0339E (Vault Box) - 0x0CC7B - True 158602 - 0x17CE7 (Discard) - True - Triangles 158076 - 0x00698 (Surface 1) - True - True @@ -338,7 +338,7 @@ Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5: 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: +Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40: 158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True 158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Shapers & Dots & Symmetry Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F @@ -358,7 +358,7 @@ Door - 0x04F8F (Tower Shortcut) - 0x0361B 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: +Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: 158207 - 0x03713 (Shortcut Panel) - True - True Door - 0x0364E (Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True @@ -390,11 +390,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 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 +158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots & Full Dots +158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Shapers & Dots & Full Dots +158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Rotated Shapers & Dots & Full Dots +158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots +158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers Door - 0x28A61 (Tinted Glass Door) - 0x28998 @@ -421,7 +421,7 @@ Town Red Rooftop (Town): 158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True Town Wooden Rooftop (Town): -158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser +158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots Town Church (Town): 158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True @@ -819,14 +819,14 @@ Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True -Caves (Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challenge - 0x019A5: +Caves (Caves) - Main Island - 0x2D73F | 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 +158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Triangles & Full Dots +158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Triangles & Full Dots +158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Triangles & Full Dots +158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Triangles & Full Dots 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 @@ -849,12 +849,12 @@ Caves (Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challeng 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 +158482 - 0x17FB9 (Left Upstairs Single) - True - Shapers & Dots & Negative Shapers & Full Dots +158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots & Full Dots +158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Stars & Dots & Full Dots +158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars & Full Dots +158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Shapers & Dots & Full Dots +158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Rotated Shapers & Dots & Full 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 @@ -913,7 +913,7 @@ Door - 0x09E87 (Town Shortcut) - 0x09E85 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 +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full 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 diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt new file mode 100644 index 0000000000..3c44006054 --- /dev/null +++ b/worlds/witness/WitnessLogicExpert.txt @@ -0,0 +1,932 @@ +First Hallway (First Hallway) - Entry - True - Tutorial - 0x00182: +158000 - 0x00064 (Straight) - True - True +158001 - 0x00182 (Bend) - 0x00064 - True + +Tutorial (Tutorial) - Outside Tutorial - True: +158002 - 0x00293 (Front Center) - True - Dots +158003 - 0x00295 (Center Left) - 0x00293 - Dots +158004 - 0x002C2 (Front Left) - 0x00295 - Dots +158005 - 0x0A3B5 (Back Left) - True - Full Dots +158006 - 0x0A3B2 (Back Right) - True - Full Dots +158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 - Symmetry & Dots +158008 - 0x03505 (Gate Close) - 0x2FAF6 - False +158009 - 0x0C335 (Pillar) - True - Triangles - True +158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots + +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: +158650 - 0x033D4 (Vault) - True - Full Dots & Squares & Black/White Squares +158651 - 0x03481 (Vault Box) - 0x033D4 - True +158013 - 0x0005D (Shed Row 1) - True - Full Dots +158014 - 0x0005E (Shed Row 2) - 0x0005D - Full Dots +158015 - 0x0005F (Shed Row 3) - 0x0005E - Full Dots +158016 - 0x00060 (Shed Row 4) - 0x0005F - Full Dots +158017 - 0x00061 (Shed Row 5) - 0x00060 - Full Dots +158018 - 0x018AF (Tree Row 1) - True - Squares & Black/White Squares +158019 - 0x0001B (Tree Row 2) - 0x018AF - Squares & Black/White Squares +158020 - 0x012C9 (Tree Row 3) - 0x0001B - Squares & Black/White Squares +158021 - 0x0001C (Tree Row 4) - 0x012C9 - Squares & Black/White Squares & Dots +158022 - 0x0001D (Tree Row 5) - 0x0001C - Squares & Black/White Squares & Dots +158023 - 0x0001E (Tree Row 6) - 0x0001D - Squares & Black/White Squares & Dots +158024 - 0x0001F (Tree Row 7) - 0x0001E - Squares & Black/White Squares & Full Dots +158025 - 0x00020 (Tree Row 8) - 0x0001F - Squares & Black/White Squares & Full Dots +158026 - 0x00021 (Tree Row 9) - 0x00020 - Squares & Black/White Squares & Full Dots +Door - 0x03BA2 (Outpost Path) - 0x0A3B5 + +Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: +158011 - 0x0A171 (Outpost Entry Panel) - True - Full Dots & Triangles +Door - 0x0A170 (Outpost Entry) - 0x0A171 + +Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3: +158012 - 0x04CA4 (Outpost Exit Panel) - True - Full Dots & Shapers & Rotated Shapers +Door - 0x04CA3 (Outpost Exit) - 0x04CA4 +158600 - 0x17CFB (Discard) - True - Arrows + +Main Island () - Outside Tutorial - True: + +Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: +158027 - 0x01A54 (Entry Panel) - True - Symmetry +Door - 0x01A29 (Entry) - 0x01A54 +158601 - 0x3C12B (Discard) - True - Arrows + +Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED: +158028 - 0x00086 (Back Wall 1) - True - Symmetry & Dots +158029 - 0x00087 (Back Wall 2) - 0x00086 - Symmetry & Dots +158030 - 0x00059 (Back Wall 3) - 0x00087 - Symmetry & Dots +158031 - 0x00062 (Back Wall 4) - 0x00059 - Symmetry & Dots +158032 - 0x0005C (Back Wall 5) - 0x00062 - Symmetry & Dots +158033 - 0x0008D (Front 1) - 0x0005C - Symmetry +158034 - 0x00081 (Front 2) - 0x0008D - Symmetry +158035 - 0x00083 (Front 3) - 0x00081 - Symmetry +158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry & Dots +158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry & Dots +158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots +Door - 0x0D7ED (Back Wall) - 0x0005C + +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 (Lower Panel) - 0x0343A - Triangles +Door - 0x17F3E (Lower) - 0x000B0 + +Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269: +158041 - 0x00022 (Right 1) - True - Symmetry & Triangles +158042 - 0x00023 (Right 2) - 0x00022 - Symmetry & Triangles +158043 - 0x00024 (Right 3) - 0x00023 - Symmetry & Triangles +158044 - 0x00025 (Right 4) - 0x00024 - Symmetry & Triangles +158045 - 0x00026 (Right 5) - 0x00025 - Symmetry & Triangles +158046 - 0x0007C (Back 1) - 0x00026 - Symmetry & Colored Dots & Dots +158047 - 0x0007E (Back 2) - 0x0007C - Symmetry & Colored Squares +158048 - 0x00075 (Back 3) - 0x0007E - Symmetry & Stars +158049 - 0x00073 (Back 4) - 0x00075 - Symmetry & Shapers +158050 - 0x00077 (Back 5) - 0x00073 - Symmetry & Triangles +158051 - 0x00079 (Back 6) - 0x00077 - Symmetry & Dots & Colored Dots & Eraser +158052 - 0x00065 (Left 1) - 0x00079 - Symmetry & Colored Dots & Triangles +158053 - 0x0006D (Left 2) - 0x00065 - Symmetry & Colored Dots & Triangles +158054 - 0x00072 (Left 3) - 0x0006D - Symmetry & Colored Dots & Triangles +158055 - 0x0006F (Left 4) - 0x00072 - Symmetry & Colored Dots & Triangles +158056 - 0x00070 (Left 5) - 0x0006F - Symmetry & Colored Dots & Triangles +158057 - 0x00071 (Left 6) - 0x00070 - Symmetry & Triangles +158058 - 0x00076 (Left 7) - 0x00071 - Symmetry & Triangles +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 (Upper Panel) - 0x00076 - Symmetry & Triangles +Door - 0x18269 (Upper) - 0x1C349 + +Symmetry Island Upper (Symmetry Island): +158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots +158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots +158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots +158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots +158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots +158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +158700 - 0x0360D (Laser Panel) - 0x00A68 - True +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 (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 (Second Gate) - 0x032FF + +Orchard End (Orchard): + +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: +158652 - 0x0CC7B (Vault) - True - Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares +158653 - 0x0339E (Vault Box) - 0x0CC7B - True +158602 - 0x17CE7 (Discard) - True - Arrows +158076 - 0x00698 (Surface 1) - True - Reflection +158077 - 0x0048F (Surface 2) - 0x00698 - Reflection +158078 - 0x09F92 (Surface 3) - 0x0048F & 0x09FA0 - Reflection +158079 - 0x09FA0 (Surface 3 Control) - 0x0048F - True +158080 - 0x0A036 (Surface 4) - 0x09F92 - Reflection +158081 - 0x09DA6 (Surface 5) - 0x09F92 - Reflection +158082 - 0x0A049 (Surface 6) - 0x09F92 - Reflection +158083 - 0x0A053 (Surface 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection +158084 - 0x09F94 (Surface 8) - 0x0A053 & 0x09F86 - Reflection +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 (Light Room 1) - 0x09FAA - Reflection +158089 - 0x006E3 (Light Room 2) - 0x09FAA - Reflection +158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection +Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D + +Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B: +158091 - 0x00C72 (Pond Room 1) - True - Reflection +158092 - 0x0129D (Pond Room 2) - 0x00C72 - Reflection +158093 - 0x008BB (Pond Room 3) - 0x0129D - Reflection +158094 - 0x0078D (Pond Room 4) - 0x008BB - Reflection +158095 - 0x18313 (Pond Room 5) - 0x0078D - Reflection +158096 - 0x0A249 (Flood Room Entry Panel) - 0x18313 - Reflection +Door - 0x0A24B (Flood Room Entry) - 0x0A249 + +Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: +158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True +158098 - 0x1831E (Reduce Water Level Far Right) - True - True +158099 - 0x1C260 (Reduce Water Level Near Left) - True - True +158100 - 0x1831C (Reduce Water Level Near Right) - True - True +158101 - 0x1C2F3 (Raise Water Level Far Left) - True - True +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 Room 1) - 0x1C260 & 0x1831C - Reflection +158106 - 0x01205 (Flood Room 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection +158107 - 0x181AB (Flood Room 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection +158108 - 0x0117A (Flood Room 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection +158109 - 0x17ECA (Flood Room 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection +158110 - 0x18076 (Flood Room 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection +Door - 0x0C316 (Elevator Room Entry) - 0x18076 + +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +158111 - 0x17C31 (Final Transparent) - True - Reflection +158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - Reflection +158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True +158115 - 0x0A15C (Final Bent 1) - True - Reflection +158116 - 0x09FFF (Final Bent 2) - 0x0A15C - Reflection +158117 - 0x0A15F (Final Bent 3) - 0x09FFF - Reflection + +Desert Lowest Level Inbetween Shortcuts (Desert): + +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F: +158118 - 0x09E57 (Entry 1 Panel) - True - Squares & Black/White Squares & Triangles +158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser +158603 - 0x17CF0 (Discard) - True - Arrows +158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Triangles & Stars & Stars + Same Colored Symbol +Laser - 0x01539 (Laser) - 0x03612 +Door - 0x09D6F (Entry 1) - 0x09E57 + +Quarry Between Entrys (Quarry) - Quarry - 0x17C07: +158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Triangles +Door - 0x17C07 (Entry 2) - 0x17C09 + +Quarry (Quarry) - Quarry Mill Ground Floor - 0x02010: +158121 - 0x01E5A (Mill Entry Left Panel) - True - Squares & Black/White Squares & Stars & Stars + Same Colored Symbol +158122 - 0x01E59 (Mill Entry Right Panel) - True - Triangles +Door - 0x02010 (Mill Entry) - 0x01E59 & 0x01E5A + +Quarry Mill Ground Floor (Quarry Mill) - Quarry - 0x275FF - Quarry Mill Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +158123 - 0x275ED (Side Exit Panel) - True - True +Door - 0x275FF (Side Exit) - 0x275ED +158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser +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 (Lower Row 1) - True - Dots & Eraser +158126 - 0x01489 (Lower Row 2) - 0x00E0C - Triangles & Eraser +158127 - 0x0148A (Lower Row 3) - 0x01489 - Triangles & Eraser +158128 - 0x014D9 (Lower Row 4) - 0x0148A - Triangles & Eraser +158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Triangles & Eraser +158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Triangles & 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 (Upper Row 1) - True - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158135 - 0x005F1 (Upper Row 2) - 0x00557 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158136 - 0x00620 (Upper Row 3) - 0x005F1 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158137 - 0x009F5 (Upper Row 4) - 0x00620 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158138 - 0x0146C (Upper Row 5) - 0x009F5 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158139 - 0x3C12D (Upper Row 6) - 0x0146C - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158140 - 0x03686 (Upper Row 7) - 0x3C12D - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158141 - 0x014E9 (Upper Row 8) - 0x03686 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol +158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser +Door - 0x0368A (Stairs) - 0x03677 +158143 - 0x3C125 (Control Room Left) - 0x0367C - Squares & Black/White Squares & Full Dots & Eraser +158144 - 0x0367C (Control Room Right) - 0x014E9 - Squares & Colored Squares & Triangles & Eraser & Stars & Stars + Same Colored Symbol + +Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: +158146 - 0x034D4 (Intro Left) - True - Stars & Eraser +158147 - 0x021D5 (Intro Right) - True - Shapers & Eraser +158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers +158166 - 0x17CA6 (Boat Spawn) - True - Boat +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 (Front Row 1) - True - Shapers & Eraser & Negative Shapers +158150 - 0x021B4 (Front Row 2) - 0x021B3 - Shapers & Eraser & Negative Shapers +158151 - 0x021B0 (Front Row 3) - 0x021B4 - Shapers & Eraser & Negative Shapers +158152 - 0x021AF (Front Row 4) - 0x021B0 - Shapers & Eraser & Negative Shapers +158153 - 0x021AE (Front Row 5) - 0x021AF - Shapers & Eraser & Negative Shapers +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 (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 - Eraser & Shapers +158162 - 0x3C124 (Back First Row 7) - 0x09DB1 - Eraser & Shapers +158163 - 0x09DB3 (Back First Row 8) - 0x3C124 - Eraser & Shapers & Stars & Stars + Same Colored Symbol +158164 - 0x09DB4 (Back First Row 9) - 0x09DB3 - Eraser & Shapers & Stars & Stars + Same Colored Symbol +158165 - 0x275FA (Hook Control) - True - Shapers & Eraser +158167 - 0x0A3CB (Back Second Row 1) - 0x09DB4 - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol +158168 - 0x0A3CC (Back Second Row 2) - 0x0A3CB - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol +158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol + +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 (Intro 6) - 0x0A8DC - Shadows Avoid +158172 - 0x0AC7A (Intro 7) - 0x0AC74 - Shadows Avoid +158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - Shadows Avoid +158174 - 0x386FA (Far 1) - 0x0A8E0 - Shadows Avoid & Environment +158175 - 0x1C33F (Far 2) - 0x386FA - Shadows Avoid & Environment +158176 - 0x196E2 (Far 3) - 0x1C33F - Shadows Avoid & Environment +158177 - 0x1972A (Far 4) - 0x196E2 - Shadows Avoid & Environment +158178 - 0x19809 (Far 5) - 0x1972A - Shadows Avoid & Environment +158179 - 0x19806 (Far 6) - 0x19809 - Shadows Avoid & Environment +158180 - 0x196F8 (Far 7) - 0x19806 - Shadows Avoid & Environment +158181 - 0x1972F (Far 8) - 0x196F8 - Shadows Avoid & Environment +Door - 0x194B2 (Laser Entry Right) - 0x1972F +158182 - 0x19797 (Near 1) - 0x0A8E0 - Shadows Follow +158183 - 0x1979A (Near 2) - 0x19797 - Shadows Follow +158184 - 0x197E0 (Near 3) - 0x1979A - Shadows Follow +158185 - 0x197E8 (Near 4) - 0x197E0 - Shadows Follow +158186 - 0x197E5 (Near 5) - 0x197E8 - Shadows Follow +Door - 0x19665 (Laser Entry Left) - 0x197E5 + +Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: +158187 - 0x334DC (Door Timer Inside) - True - True +158188 - 0x198B5 (Intro 1) - True - Shadows Avoid +158189 - 0x198BD (Intro 2) - 0x198B5 - Shadows Avoid +158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - Shadows Avoid +Door - 0x19865 (Quarry Barrier) - 0x198BF +Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF +158191 - 0x19771 (Intro 4) - 0x198BF - Shadows Avoid +158192 - 0x0A8DC (Intro 5) - 0x19771 - Shadows Avoid +Door - 0x1855B (Ledge Barrier) - 0x0A8DC +Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC + +Shadows Laser Room (Shadows): +158703 - 0x19650 (Laser Panel) - True - Shadows Avoid & Shadows Follow +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 +158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True +158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Pressure Plates & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol +Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139 +Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA + +Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - True: +Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139 +158194 - 0x019DC (Hedge Maze 2) - True - Environment +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) - 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) - 0x01A0F + +Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - True: +158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True +158200 - 0x01BE9 (Pressure Plates 2) - PP2 Weirdness - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares & Shapers & Rotated Shapers +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 & Black/White Squares & Triangles & Shapers & Rotated Shapers +Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3 + +Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40: +158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True +158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Pressure Plates & Shapers & Triangles & Stars & Stars + Same Colored Symbol +Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F +158604 - 0x17D27 (Discard) - True - Arrows +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 +158655 - 0x03535 (Vault Box) - 0x00AFB - True +158605 - 0x17D28 (Discard) - True - Arrows + +Keep Tower (Keep) - Keep - 0x04F8F: +158206 - 0x0361B (Tower Shortcut Panel) - True - True +Door - 0x04F8F (Tower Shortcut) - 0x0361B +158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - Environment & Sound +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01BE9 - Shapers & Rotated Shapers & Triangles & Stars & Stars + Same Colored Symbol & Colored Squares & Black/White Squares +Laser - 0x014BB (Laser) - 0x0360E | 0x03317 + +Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: +158207 - 0x03713 (Shortcut Panel) - True - True +Door - 0x0364E (Shortcut) - 0x03713 +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 - Environment +158211 - 0x00038 (Outside 2) - 0x09D9B & 0x00290 - Environment +158212 - 0x00037 (Outside 3) - 0x09D9B & 0x00038 - Environment +Door - 0x03750 (Garden Entry) - 0x00037 +158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True +Laser - 0x17C65 (Laser) - 0x17CA4 + +Inside Monastery (Monastery): +158213 - 0x09D9B (Shutters Control) - True - Dots +158214 - 0x193A7 (Inside 1) - 0x00037 - Environment +158215 - 0x193AA (Inside 2) - 0x193A7 - Environment +158216 - 0x193AB (Inside 3) - 0x193AA - Environment +158217 - 0x193A6 (Inside 4) - 0x193AB - Environment + +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 Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles +Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 +158707 - 0x09F98 (Desert Laser Redirect) - True - True +158220 - 0x18590 (Transparent) - True - Symmetry & Environment +158221 - 0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment +158222 - 0x28938 (Apple Tree) - 0x28AE3 - Environment +158223 - 0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection +158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Triangles & Full Dots +158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Triangles & Full Dots +158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Triangles & Full Dots +158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Triangles & Full Dots +158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Triangles & Full Dots +Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 +158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol +Door - 0x28A61 (Tinted Glass Door) - 0x28A0D +158226 - 0x28A0D (Church Entry Panel) - 0x28998 - Stars & RGB & Environment +Door - 0x03BB0 (Church Entry) - 0x03C08 +158228 - 0x28A79 (Maze Stair Control) - True - Environment +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 - Arrows + +Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A: +158229 - 0x2896A (Maze Rooftop Bridge Control) - True - Shapers + +Town Red Rooftop (Town): +158607 - 0x17C71 (Rooftop Discard) - True - Arrows +158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Shapers +158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Shapers +158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Shapers +158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Shapers +158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Shapers +158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - Reflection + +Town Wooden Rooftop (Town): +158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Triangles & Full Dots & Eraser + +Town Church (Town): +158227 - 0x28A69 (Church Lattice) - 0x03BB0 - Environment + +RGB House (Town) - RGB Room - 0x2897B: +158242 - 0x034E4 (Sound Room Left) - True - Sound +158243 - 0x034E3 (Sound Room Right) - True - Sound & Sound Dots +Door - 0x2897B (RGB House Stairs) - 0x034E4 & 0x034E3 + +RGB Room (Town): +158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & RGB & Squares & Colored Squares & Triangles +158245 - 0x03C0C (RGB Room Left) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares & Eraser +158246 - 0x03C08 (RGB Room Right) - 0x334D8 & 0x03C0C - RGB & Symmetry & Dots & Colored Dots & Triangles + +Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C: +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 +Laser - 0x032F9 (Laser) - 0x032F5 + +Windmill Interior (Windmill) - Theater - 0x17F88: +158247 - 0x17D02 (Turn Control) - True - Dots +158248 - 0x17F89 (Theater Entry Panel) - True - Squares & Black/White Squares & Eraser & Triangles +Door - 0x17F88 (Theater Entry) - 0x17F89 + +Theater (Theater) - Town - 0x0A16D | 0x3CCDF: +158656 - 0x00815 (Video Input) - True - True +158657 - 0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True +158658 - 0x03552 (Desert Video) - 0x00815 & 0x0339E - True +158659 - 0x0354E (Jungle Video) - 0x00815 & 0x03702 - True +158660 - 0x03549 (Challenge Video) - 0x00815 & 0x0356B - True +158661 - 0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True +158662 - 0x03545 (Mountain Video) - 0x00815 & 0x03542 - True +158249 - 0x0A168 (Exit Left Panel) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Eraser +158250 - 0x33AB2 (Exit Right Panel) - True - Eraser & Triangles & Shapers +Door - 0x0A16D (Exit Left) - 0x0A168 +Door - 0x3CCDF (Exit Right) - 0x33AB2 +158608 - 0x17CF7 (Discard) - True - Arrows + +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 (First Row 1) - True - Sound +158253 - 0x00767 (First Row 2) - 0x002C4 - Sound +158254 - 0x002C6 (First Row 3) - 0x00767 - Sound +158255 - 0x0070E (Second Row 1) - 0x002C6 - Sound +158256 - 0x0070F (Second Row 2) - 0x0070E - Sound +158257 - 0x0087D (Second Row 3) - 0x0070F - Sound +158258 - 0x002C7 (Second Row 4) - 0x0087D - Sound +158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True +Door - 0x1475B (Popup Wall) - 0x17CAB +158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound & Sound Dots & Symmetry +158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots & Symmetry +158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots & Symmetry +158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots & Symmetry +158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots & Symmetry +158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots & Symmetry +158709 - 0x03616 (Laser Panel) - 0x014B2 - True +Laser - 0x00274 (Laser) - 0x03616 +158266 - 0x337FA (Laser Shortcut Panel) - True - True +Door - 0x3873B (Laser Shortcut) - 0x337FA + +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: +158267 - 0x17CAA (Monastery Shortcut Panel) - True - Environment +Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA +158663 - 0x15ADD (Vault) - True - Environment & Black/White Squares & Dots +158664 - 0x03702 (Vault Box) - 0x15ADD - True + +Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: +158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares & Colored Squares +Door - 0x0C2A4 (Entry) - 0x17C2E + +Bunker (Bunker) - Bunker Glass Room - 0x17C79: +158269 - 0x09F7D (Intro Left 1) - True - Squares & Colored Squares +158270 - 0x09FDC (Intro Left 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares +158271 - 0x09FF7 (Intro Left 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares +158272 - 0x09F82 (Intro Left 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares +158273 - 0x09FF8 (Intro Left 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares +158274 - 0x09D9F (Intro Back 1) - 0x09FF8 - Squares & Colored Squares & Black/White Squares +158275 - 0x09DA1 (Intro Back 2) - 0x09D9F - Squares & Colored Squares +158276 - 0x09DA2 (Intro Back 3) - 0x09DA1 - Squares & Colored Squares +158277 - 0x09DAF (Intro Back 4) - 0x09DA2 - Squares & Colored Squares +158278 - 0x0A099 (Tinted Glass Door Panel) - 0x09DAF - True +Door - 0x17C79 (Tinted Glass Door) - 0x0A099 + +Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: +158279 - 0x0A010 (Glass Room 1) - True - Squares & Colored Squares & RGB & Environment +158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment +158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment +Door - 0x0C2A3 (UV Room Entry) - 0x0A01F + +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 (UV Room 1) - 0x34BC5 - Squares & Colored Squares & RGB & Environment +158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB +Door - 0x0A08D (Elevator Room Entry) - 0x17E67 + +Bunker Elevator Section (Bunker) - Bunker Laser Platform - 0x0A079: +158286 - 0x0A079 (Elevator Control) - True - Squares & Colored Squares & Black/White Squares & RGB + +Bunker Laser Platform (Bunker): +158710 - 0x09DE0 (Laser Panel) - True - True +Laser - 0x0C2B2 (Laser) - 0x09DE0 + +Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True: +158287 - 0x0056E (Entry Panel) - True - Rotated Shapers & Black/White Squares & Triangles +Door - 0x00C1C (Entry) - 0x0056E + +Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: +158288 - 0x00469 (Intro Front 1) - True - Black/White Squares & Shapers +158289 - 0x00472 (Intro Front 2) - 0x00469 - Black/White Squares & Shapers & Rotated Shapers +158290 - 0x00262 (Intro Front 3) - 0x00472 - Black/White Squares & Rotated Shapers +158291 - 0x00474 (Intro Front 4) - 0x00262 - Black/White Squares & Shapers & Rotated Shapers +158292 - 0x00553 (Intro Front 5) - 0x00474 - Black/White Squares & Shapers & Rotated Shapers +158293 - 0x0056F (Intro Front 6) - 0x00553 - Black/White Squares & Shapers & Rotated Shapers +158294 - 0x00390 (Intro Back 1) - 0x0056F - Shapers & Triangles +158295 - 0x010CA (Intro Back 2) - 0x00390 - Shapers & Rotated Shapers & Triangles +158296 - 0x00983 (Intro Back 3) - 0x010CA - Rotated Shapers & Triangles +158297 - 0x00984 (Intro Back 4) - 0x00983 - Shapers & Rotated Shapers & Triangles +158298 - 0x00986 (Intro Back 5) - 0x00984 - Shapers & Rotated Shapers & Triangles +158299 - 0x00985 (Intro Back 6) - 0x00986 - Rotated Shapers & Triangles & Black/White Squares +158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers & Rotated Shapers & Triangles & Black/White Squares +158301 - 0x181A9 (Intro Back 8) - 0x00987 - Rotated Shapers & Triangles & Black/White Squares + +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +158302 - 0x00609 (Sliding Bridge) - True - Shapers & Black/White Squares + +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 - Rotated Shapers +158314 - 0x0097F (Platform Row 2) - 0x00982 - Rotated Shapers +158315 - 0x0098F (Platform Row 3) - 0x0097F - Rotated Shapers +158316 - 0x00990 (Platform Row 4) - 0x0098F - Rotated Shapers +Door - 0x184B7 (Between Bridges First Door) - 0x00990 +158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Rotated Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Rotated Shapers +Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E +Door - 0x04B7F (Cyan Water Pump) - 0x00006 + +Swamp Cyan Underwater (Swamp): +158307 - 0x00002 (Cyan Underwater 1) - True - Shapers & Negative Shapers & Black/White Squares +158308 - 0x00004 (Cyan Underwater 2) - 0x00002 - Shapers & Negative Shapers & Triangles +158309 - 0x00005 (Cyan Underwater 3) - 0x00004 - Shapers & Negative Shapers & Triangles & Black/White Squares +158310 - 0x013E6 (Cyan Underwater 4) - 0x00005 - Shapers & Negative Shapers & Triangles & Black/White Squares +158311 - 0x00596 (Cyan Underwater 5) - 0x013E6 - Shapers & Negative Shapers & Triangles & Black/White Squares +158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers + +Swamp Between Bridges Near (Swamp) - Swamp Between Bridges Far - 0x18507: +158303 - 0x00999 (Between Bridges Near Row 1) - 0x00990 - Rotated Shapers +158304 - 0x0099D (Between Bridges Near Row 2) - 0x00999 - Rotated Shapers +158305 - 0x009A0 (Between Bridges Near Row 3) - 0x0099D - Rotated Shapers +158306 - 0x009A1 (Between Bridges Near Row 4) - 0x009A0 - Rotated Shapers +Door - 0x18507 (Between Bridges Second Door) - 0x009A1 + +Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay: +158319 - 0x00007 (Between Bridges Far Row 1) - 0x009A1 - Rotated Shapers & Full Dots +158320 - 0x00008 (Between Bridges Far Row 2) - 0x00007 - Rotated Shapers & Full Dots +158321 - 0x00009 (Between Bridges Far Row 3) - 0x00008 - Rotated Shapers & Shapers & Full Dots +158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers & Shapers & Full Dots +Door - 0x183F2 (Red Water Pump) - 0x00596 + +Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1: +158323 - 0x00001 (Red Underwater 1) - True - Shapers & Negative Shapers & Full Dots +158324 - 0x014D2 (Red Underwater 2) - True - Shapers & Negative Shapers & Full Dots +158325 - 0x014D4 (Red Underwater 3) - True - Shapers & Negative Shapers & Full Dots +158326 - 0x014D1 (Red Underwater 4) - True - Shapers & Negative Shapers & Full Dots +Door - 0x305D5 (Red Underwater Exit) - 0x014D1 + +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 & Stars & Colored Squares & Triangles & Stars + Same Colored Symbol + +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: +158328 - 0x09DB8 (Boat Spawn) - True - Boat +158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Full Dots +158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Full Dots +158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Shapers & Full Dots +158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Shapers & Full Dots +158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers +Door - 0x18482 (Blue Water Pump) - 0x00E3A + +Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A + +Swamp Purple Underwater (Swamp): +158333 - 0x009A6 (Purple Underwater) - True - Shapers & Triangles & Black/White Squares & Rotated Shapers + +Swamp Blue Underwater (Swamp): +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 + +Swamp Laser Area (Swamp) - Outside Swamp - 0x2D880: +158711 - 0x03615 (Laser Panel) - True - True +Laser - 0x00BF6 (Laser) - 0x03615 +158341 - 0x17C05 (Laser Shortcut Left Panel) - True - Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol +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 +158344 - 0x0288C (First Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles +Door - 0x0C309 (First Door) - 0x0288C + +Treehouse Between Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310: +158345 - 0x02886 (Second Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles +Door - 0x0C310 (Second Door) - 0x02886 + +Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4: +158346 - 0x17D72 (Yellow Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles +158347 - 0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars & Stars + Same Colored Symbol & Triangles +158348 - 0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars & Stars + Same Colored Symbol & Triangles +158349 - 0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars & Stars + Same Colored Symbol & Triangles +158350 - 0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars & Stars + Same Colored Symbol & Triangles +158351 - 0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars & Stars + Same Colored Symbol & Triangles +158352 - 0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars & Stars + Same Colored Symbol & Triangles +158353 - 0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars & Stars + Same Colored Symbol & Triangles +158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars & Stars + Same Colored Symbol & Triangles + +Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: +158355 - 0x0A182 (Third Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles & Colored Squares +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 + +Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C: +158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Full Dots +158358 - 0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Full Dots +158359 - 0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Full Dots +158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Full Dots +158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Full Dots + +Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: +158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles +158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars & Stars + Same Colored Symbol & Triangles +158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars & Stars + Same Colored Symbol & Triangles +158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Triangles +158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars & Stars + Same Colored Symbol & Triangles +158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars & Stars + Same Colored Symbol & Triangles +158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars & Stars + Same Colored Symbol & Triangles +158398 - 0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars & Stars + Same Colored Symbol & Triangles +158399 - 0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars & Stars + Same Colored Symbol & Triangles +158400 - 0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Triangles +158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars & Stars + Same Colored Symbol & Triangles +158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Triangles + +Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: +158404 - 0x037FF (Bridge Control) - True - Stars +Door - 0x0C32D (Drawbridge) - 0x037FF + +Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: +158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol +158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol +158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Black/White Squares & Triangles & Stars + Same Colored Symbol +158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Black/White Squares & Colored Squares & Triangles & Stars + Same Colored Symbol +158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol +158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol +158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Colored Squares & Triangles & Stars + Same Colored Symbol + +Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDE - Treehouse Laser Room Back Platform - 0x17DDB: +158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Black/White Squares +158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC & 0x03613 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Triangles +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 & Negative Shapers & Stars + Same Colored Symbol +158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Triangles +158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Triangles +158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Arrows + +Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC + +Treehouse Laser Room Back Platform (Treehouse): +158611 - 0x17FA0 (Laser Discard) - True - Arrows + +Treehouse Laser Room (Treehouse): +158712 - 0x03613 (Laser Panel) - True - True +158403 - 0x17CBC (Laser House Door Timer Inside) - True - True +Laser - 0x028A4 (Laser) - 0x03613 + +Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +158612 - 0x17C42 (Discard) - True - Arrows +158665 - 0x002A6 (Vault) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol +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 & Black/White Squares & Stars + Same Colored Symbol & Triangles +158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True + +Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: +158408 - 0x09E39 (Light Bridge Controller) - True - Eraser & Triangles + +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 & Triangles +158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Triangles & Shapers +158412 - 0x09E69 (Right Row 4) - 0x09E72 - Stars & Black/White Squares & Stars + Same Colored Symbol & Rotated Shapers +158413 - 0x09E7B (Right Row 5) - 0x09E69 - Stars & Black/White Squares & Stars + Same Colored Symbol & Eraser & Dots & Triangles & Shapers +158414 - 0x09E73 (Left Row 1) - True - Black/White Squares & Triangles +158415 - 0x09E75 (Left Row 2) - 0x09E73 - Black/White Squares & Shapers & Rotated Shapers +158416 - 0x09E78 (Left Row 3) - 0x09E75 - Stars & Triangles & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158417 - 0x09E79 (Left Row 4) - 0x09E78 - Stars & Colored Squares & Stars + Same Colored Symbol & Triangles +158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158419 - 0x09E6F (Left Row 6) - 0x09E6C - Symmetry & Stars & Colored Squares & Black/White Squares & Stars + Same Colored Symbol & Symmetry +158420 - 0x09E6B (Left Row 7) - 0x09E6F - Symmetry & Full Dots & Triangles +158421 - 0x33AF5 (Back Row 1) - True - Symmetry & Black/White Squares & Triangles +158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Symmetry & Stars & Triangles & Stars + Same Colored Symbol +158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Stars & Shapers & Stars + Same Colored Symbol +158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles +Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B + +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 & Triangles & Stars + Same Colored Symbol +158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars +158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser +Door - 0x09FFB (Staircase Near) - 0x09FD8 + +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: + +Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: +Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 + +Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): +158431 - 0x09E86 (Light Bridge Controller Near) - True - Shapers & Dots + +Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07: +158432 - 0x09FCC (Far Row 1) - True - Triangles +158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares & Stars & Stars + Same Colored Symbol +158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars & Triangles & Stars + Same Colored Symbol +158435 - 0x09FD0 (Far Row 4) - 0x09FCF - Rotated Shapers & Negative Shapers +158436 - 0x09FD1 (Far Row 5) - 0x09FD0 - Dots +158437 - 0x09FD2 (Far Row 6) - 0x09FD1 - Rotated Shapers +Door - 0x09E07 (Staircase Far) - 0x09FD2 + +Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2): +158438 - 0x09ED8 (Light Bridge Controller Far) - True - Shapers & Dots + +Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay: +158613 - 0x17F93 (Elevator Discard) - True - Arrows + +Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: +158439 - 0x09EEB (Elevator Control Panel) - True - Dots + +Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Negative Shapers +158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser +158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser +158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser +158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +Door - 0x09F89 (Exit) - 0x09FDA + +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141: +158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows +158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars +158446 - 0x01987 (Final Room Entry Right) - True - Squares & Colored Squares & Dots +Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 + +Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: +Door - 0x17F33 (Rock Open) - True + +Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +158447 - 0x00FF8 (Caves Entry Panel) - True - Arrows & Black/White Squares +Door - 0x2D77D (Caves Entry) - 0x00FF8 +158448 - 0x334E1 (Rock Control) - True - True + +Caves (Caves) - Main Island - 0x2D73F | 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 (Blue Tunnel Right First 1) - True - Arrows +158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Arrows +158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Arrows +158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Arrows +158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Arrows & Symmetry +158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Arrows & Triangles +158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Arrows & Triangles +158461 - 0x0097B (Blue Tunnel Left Second 3) - 0x00973 - Arrows & Triangles +158462 - 0x0097D (Blue Tunnel Left Second 4) - 0x0097B - Arrows & Triangles +158463 - 0x0097E (Blue Tunnel Left Second 5) - 0x0097D - Arrows & Triangles +158464 - 0x00994 (Blue Tunnel Right Second 1) - True - Arrows & Shapers & Rotated Shapers +158465 - 0x334D5 (Blue Tunnel Right Second 2) - 0x00994 - Arrows & Rotated Shapers +158466 - 0x00995 (Blue Tunnel Right Second 3) - 0x334D5 - Arrows & Shapers & Rotated Shapers +158467 - 0x00996 (Blue Tunnel Right Second 4) - 0x00995 - Arrows & Shapers & Rotated Shapers +158468 - 0x00998 (Blue Tunnel Right Second 5) - 0x00996 - Arrows & Shapers +158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Arrows & Stars +158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Arrows & Symmetry +158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Arrows & Shapers & Negative Shapers +158472 - 0x32962 (First Floor Left) - True - Full Dots & Rotated Shapers +158473 - 0x32966 (First Floor Grounded) - True - Stars & Triangles & Rotated Shapers & Black/White Squares & Stars + Same Colored Symbol +158474 - 0x01A31 (First Floor Middle) - True - Stars +158475 - 0x00B71 (First Floor Right) - True - Full Dots & Eraser & Stars & Stars + Same Colored Symbol & Colored Squares & Shapers & Negative Shapers +158478 - 0x288EA (First Wooden Beam) - True - Stars +158479 - 0x288FC (Second Wooden Beam) - True - Shapers & Eraser +158480 - 0x289E7 (Third Wooden Beam) - True - Eraser & Triangles +158481 - 0x288AA (Fourth Wooden Beam) - True - Full Dots & Negative Shapers & Shapers +158482 - 0x17FB9 (Left Upstairs Single) - True - Full Dots & Arrows & Black/White Squares +158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Full Dots & Arrows +158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Full Dots & Arrows +158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Full Dots & Arrows +158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Full Dots & Arrows +158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Full Dots & Arrows +158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots & Black/White Squares & Colored Squares +158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol & Triangles +158491 - 0x0008C (Right Upstairs Left Row 4) - 0x0008B - Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers +158492 - 0x0008A (Right Upstairs Left Row 5) - 0x0008C - Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Eraser & Triangles +158493 - 0x00089 (Right Upstairs Left Row 6) - 0x0008A - Shapers & Negative Shapers & Dots +158494 - 0x0006A (Right Upstairs Left Row 7) - 0x00089 - Stars & Dots +158495 - 0x0006C (Right Upstairs Left Row 8) - 0x0006A - Dots & Stars & Stars + Same Colored Symbol & Eraser +158496 - 0x00027 (Right Upstairs Right Row 1) - True - Colored Squares & Black/White Squares & Eraser +158497 - 0x00028 (Right Upstairs Right Row 2) - 0x00027 - Shapers & Symmetry +158498 - 0x00029 (Right Upstairs Right Row 3) - 0x00028 - Symmetry & Triangles & Eraser +158476 - 0x09DD5 (Lone Pillar) - True - Arrows +Door - 0x019A5 (Pillar Door) - 0x09DD5 +158449 - 0x021D7 (Mountain Shortcut Panel) - True - Stars & Stars + Same Colored Symbol & Triangles & Eraser +Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7 +158450 - 0x17CF2 (Swamp Shortcut Panel) - True - Arrows +Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2 + +Path to Challenge (Caves) - Challenge - 0x0A19A: +158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Arrows & Stars + Same Colored Symbol +Door - 0x0A19A (Challenge Entry) - 0x0A16E + +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 +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 +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 +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +158518 - 0x039B4 (Tunnels Entry Panel) - True - Arrows +Door - 0x0348A (Tunnels Entry) - 0x039B4 + +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 (Theater Shortcut) - 0x27732 +158520 - 0x2773D (Desert Shortcut Panel) - True - True +Door - 0x27263 (Desert Shortcut) - 0x2773D +158521 - 0x09E85 (Town Shortcut Panel) - True - Arrows +Door - 0x09E87 (Town Shortcut) - 0x09E85 + +Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961: +158522 - 0x0383A (Right Pillar 1) - True - Stars & Eraser & Triangles & Stars + Same Colored Symbol +158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Full Dots & Triangles +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Shapers & Stars & Negative Shapers & Stars + Same Colored Symbol +158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Eraser & Symmetry & Stars & Stars + Same Colored Symbols & Negative Shapers & Shapers +158526 - 0x0383D (Left Pillar 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol +158527 - 0x0383F (Left Pillar 2) - 0x0383D - Triangles +158528 - 0x03859 (Left Pillar 3) - 0x0383F - Symmetry & Shapers & Black/White Squares +158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles + +Elevator (Mountain Final Room): +158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158531 - 0x3D9A7 (Elevator Door Close Right) - True - 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 + +Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 0f8f0d75c0..7f5b9d2bfb 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -1,18 +1,20 @@ """ Archipelago init file for The Witness """ - import typing from BaseClasses import Region, RegionType, Location, MultiWorld, Item, Entrance, Tutorial, ItemClassification +from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ + get_priority_hint_items, make_hints, generate_joke_hints from ..AutoWorld import World, WebWorld -from .player_logic import StaticWitnessLogic, WitnessPlayerLogic +from .player_logic import WitnessPlayerLogic +from .static_logic import StaticWitnessLogic from .locations import WitnessPlayerLocations, StaticWitnessLocations from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems 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 .utils import best_junk_to_add_based_on_weights, get_audio_logs from logging import warning @@ -36,7 +38,7 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 7 + data_version = 8 static_logic = StaticWitnessLogic() static_locat = StaticWitnessLocations() @@ -59,6 +61,8 @@ class WitnessWorld(World): 'door_hexes': self.items.DOORS, 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME, 'disabled_panels': self.player_logic.COMPLETELY_DISABLED_CHECKS, + 'log_ids_to_hints': self.log_ids_to_hints, + 'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE } def generate_early(self): @@ -77,6 +81,8 @@ class WitnessWorld(World): self.items = WitnessPlayerItems(self.locat, self.world, self.player, self.player_logic) self.regio = WitnessRegions(self.locat) + self.log_ids_to_hints = dict() + self.junk_items_created = {key: 0 for key in self.items.JUNK_WEIGHTS.keys()} def generate_basic(self): @@ -84,17 +90,18 @@ class WitnessWorld(World): pool = [] items_by_name = dict() for item in self.items.ITEM_TABLE: - witness_item = self.create_item(item) - if item in self.items.PROGRESSION_TABLE: - pool.append(witness_item) - items_by_name[item] = witness_item + for i in range(0, self.items.PROG_ITEM_AMOUNTS[item]): + if item in self.items.PROGRESSION_TABLE: + witness_item = self.create_item(item) + pool.append(witness_item) + items_by_name[item] = witness_item less_junk = 0 # Put good item on first check if symbol shuffle is on symbols = is_option_enabled(self.world, self.player, "shuffle_symbols") - if symbols: + if symbols and get_option_value(self.world, self.player, "puzzle_randomization") != 1: random_good_item = self.world.random.choice(self.items.GOOD_ITEMS) first_check = self.world.get_location( @@ -138,9 +145,39 @@ class WitnessWorld(World): set_rules(self.world, self.player, self.player_logic, self.locat) def fill_slot_data(self) -> dict: - slot_data = self._get_slot_data() + hint_amount = get_option_value(self.world, self.player, "hint_amount") - slot_data["hard_mode"] = False + credits_hint = ("This Randomizer", "is brought to you by", "NewSoupVi, Jarno, jbzdarkid, sigma144", -1) + + audio_logs = get_audio_logs().copy() + + if hint_amount != 0: + generated_hints = make_hints(self.world, self.player, hint_amount) + + self.world.random.shuffle(audio_logs) + + duplicates = len(audio_logs) // hint_amount + + for _ in range(0, hint_amount): + hint = generated_hints.pop() + + for _ in range(0, duplicates): + audio_log = audio_logs.pop() + self.log_ids_to_hints[int(audio_log, 16)] = hint + + if audio_logs: + audio_log = audio_logs.pop() + self.log_ids_to_hints[int(audio_log, 16)] = credits_hint + + joke_hints = generate_joke_hints(self.world, len(audio_logs)) + + while audio_logs: + audio_log = audio_logs.pop() + self.log_ids_to_hints[int(audio_log, 16)] = joke_hints.pop() + + # generate hints done + + slot_data = self._get_slot_data() for option_name in the_witness_options: slot_data[option_name] = get_option_value( diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py new file mode 100644 index 0000000000..3ee010ea2e --- /dev/null +++ b/worlds/witness/hints.py @@ -0,0 +1,281 @@ +from BaseClasses import MultiWorld +from .Options import is_option_enabled, get_option_value + +joke_hints = [ + ("Quaternions", "break", "my brain"), + ("Eclipse", "has nothing", "but you should do it anyway"), + ("", "Beep", ""), + ("Putting in custom subtitles", "shouldn't have been", "as hard as it was..."), + ("BK mode", "is right", "around the corner"), + ("", "You can do it!", ""), + ("", "I believe in you!", ""), + ("The person playing", "is", "cute <3"), + ("dash dot, dash dash dash", "dash, dot dot dot dot, dot dot", "dash dot, dash dash dot"), + ("When you think about it,", "there are actually a lot of", "bubbles in a stream"), + ("Never gonna give you up", "Never gonna let you down", "Never gonna run around and desert you"), + ("Thanks to", "the Archipelago developers", "for making this possible."), + ("Have you tried ChecksFinder?", "If you like puzzles,", "you might enjoy it!"), + ("Have you tried Dark Souls III?", "A tough game like this", "feels better when friends are helping you!"), + ("Have you tried Donkey Kong Country 3?", "A legendary game", "from a golden age of platformers!"), + ("Have you tried Factorio?", "Alone in an unknown world.", "Sound familiar?"), + ("Have you tried Final Fantasy?", "Experience a classic game", "improved to fit modern standards!"), + ("Have you tried Hollow Knight?", "Another independent hit", "revolutionising a genre!"), + ("Have you tried A Link to the Past?", "The Archipelago game", "that started it all!"), + ("Have you tried Meritous?", "You should know that obscure games", "are often groundbreaking!"), + ("Have you tried Ocarine of Time?", "One of the biggest randomizers,", "big inspiration for this one's features!"), + ("Have you tried Raft?", "Haven't you always wanted to explore", "the ocean surrounding this island?"), + ("Have you tried Risk of Rain 2?", "I haven't either.", "But I hear it's incredible!"), + ("Have you tried Rogue Legacy?", "After solving so many puzzles", "it's the perfect way to rest your brain."), + ("Have you tried Secret of Evermore?", "I haven't either", "But I hear it's great!"), + ("Have you tried Slay the Spire?", "Experience the thrill of combat", "without needing fast fingers!"), + ("Have you tried SMZ3?", "Why play one incredible game", "when you can play 2 at once?"), + ("Have you tried Starcraft 2?", "Use strategy and management", "to crush your enemies!"), + ("Have you tried Super Mario 64?", "3-dimensional games like this", "owe everything to that game."), + ("Have you tried Super Metroid?", "A classic game", "that started a whole genre."), + ("Have you tried Timespinner?", "Everyone who plays it", "ends up loving it!"), + ("Have you tried VVVVVV?", "Experience the essence of gaming", "distilled into its purest form!"), + ("Have you tried The Witness?", "Oh. I guess you already have.", " Thanks for playing!"), + ("One day I was fascinated", "by the subject of", "generation of waves by wind"), + ("I don't like sandwiches", "Why would you think I like sandwiches?", "Have you ever seen me with a sandwich?"), + ("Where are you right now?", "I'm at soup!", "What do you mean you're at soup?"), + ("Remember to ask", "in the Archipelago Discord", "what the Functioning Brain does."), + ("", "Don't use your puzzle skips", "you might need them later"), + ("", "For an extra challenge", "Try playing blindfolded"), + ("Go to the top of the mountain", "and see if you can see", "your house"), + ("Yellow = Red + Green", "Cyan = Green + Blue", "Magenta = Red + Blue"), + ("", "Maybe that panel really is unsolvable", ""), + ("", "Did you make sure it was plugged in?", ""), + ("", "Do not look into laser with remaining eye", ""), + ("", "Try pressing Space to jump", ""), + ("The Witness is a Doom clone.", "Just replace the demons", "with puzzles"), + ("", "Test Hint please ignore", ""), + ("Shapers can never be placed", "outside the panel boundaries", "even if subtracted."), + ("", "The Keep laser panels use", "the same trick on both sides!"), + ("Can't get past a door? Try going around.", "Can't go around? Try building a", "nether portal."), + ("", "We've been trying to reach you", "about your car's extended warranty"), + ("I hate this game. I hate this game.", "I hate this game.", "-chess player Bobby Fischer"), + ("Dear Mario,", "Please come to the castle.", "I've baked a cake for you!"), + ("Have you tried waking up?", "", "Yeah, me neither."), + ("Why do they call it The Witness,", "when wit game the player view", "play of with the game."), + ("", "THE WIND FISH IN NAME ONLY", "FOR IT IS NEITHER"), + ("Like this game? Try The Wit.nes,", "Understand, INSIGHT, Taiji", "What the Witness?, and Tametsi."), + ("", "In a race", "It's survival of the Witnesst"), + ("", "This hint has been removed", "We apologize for your inconvenience."), + ("", "O-----------", ""), + ("Circle is draw", "Square is separate", "Line is win"), + ("Circle is draw", "Star is pair", "Line is win"), + ("Circle is draw", "Circle is copy", "Line is win"), + ("Circle is draw", "Dot is eat", "Line is win"), + ("Circle is start", "Walk is draw", "Line is win"), + ("Circle is start", "Line is win", "Witness is you"), + ("Can't find any items?", "Consider a relaxing boat trip", "around the island"), + ("", "Don't forget to like, comment, and subscribe", ""), + ("Ah crap, gimme a second.", "[papers rustling]", "Sorry, nothing."), + ("", "Trying to get a hint?", "Too bad."), + ("", "Here's a hint:", "Get good at the game."), + ("", "I'm still not entirely sure", "what we're witnessing here."), + ("Have you found a red page yet?", "No?", "Then have you found a blue page?"), + ( + "And here we see the Witness player,", + "seeking answers where there are none-", + "Did someone turn on the loudspeaker?" + ), + ( + "Hints suggested by:", + "IHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi,", + "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch." + ), +] + + +def get_always_hint_items(world: MultiWorld, player: int): + priority = [ + "Boat", + "Mountain Bottom Floor Final Room Entry (Door)", + "Caves Mountain Shortcut (Door)", + "Caves Swamp Shortcut (Door)", + "Caves Exits to Main Island", + ] + + difficulty = get_option_value(world, player, "puzzle_randomization") + discards = is_option_enabled(world, player, "shuffle_discards") + + if discards: + if difficulty == 1: + priority.append("Arrows") + else: + priority.append("Triangles") + + return priority + + +def get_always_hint_locations(world: MultiWorld, player: int): + return { + "Swamp Purple Underwater", + "Shipwreck Vault Box", + "Challenge Vault Box", + "Mountain Bottom Floor Discard", + } + + +def get_priority_hint_items(world: MultiWorld, player: int): + priority = { + "Negative Shapers", + "Sound Dots", + "Colored Dots", + "Stars + Same Colored Symbol", + "Swamp Entry (Panel)", + "Swamp Laser Shortcut (Door)", + } + + if is_option_enabled(world, player, "shuffle_lasers"): + lasers = { + "Symmetry Laser", + "Desert Laser", + "Town Laser", + "Keep Laser", + "Swamp Laser", + "Treehouse Laser", + "Monastery Laser", + "Jungle Laser", + "Quarry Laser", + "Bunker Laser", + "Shadows Laser", + } + + if get_option_value(world, player, "doors") >= 2: + priority.add("Desert Laser") + lasers.remove("Desert Laser") + priority.update(world.random.sample(lasers, 2)) + + else: + priority.update(world.random.sample(lasers, 3)) + + return priority + + +def get_priority_hint_locations(world: MultiWorld, player: int): + return { + "Town RGB Room Left", + "Town RGB Room Right", + "Treehouse Green Bridge 7", + "Treehouse Green Bridge Discard", + "Shipwreck Discard", + "Desert Vault Box", + "Mountainside Vault Box", + "Mountainside Discard", + } + + +def make_hint_from_item(world: MultiWorld, player: int, item: str): + location_obj = world.find_item(item, player).item.location + location_name = location_obj.name + if location_obj.player != player: + location_name += " (" + world.get_player_name(location_obj.player) + ")" + + return location_name, item, location_obj.address if(location_obj.player == player) else -1 + + +def make_hint_from_location(world: MultiWorld, player: int, location: str): + location_obj = world.get_location(location, player) + item_obj = world.get_location(location, player).item + item_name = item_obj.name + if item_obj.player != player: + item_name += " (" + world.get_player_name(item_obj.player) + ")" + + return location, item_name, location_obj.address if(location_obj.player == player) else -1 + + +def make_hints(world: MultiWorld, player: int, hint_amount: int): + hints = list() + + prog_items_in_this_world = { + item.name for item in world.get_items() + if item.player == player and item.code and item.advancement + } + loc_in_this_world = { + location.name for location in world.get_locations() + if location.player == player and not location.event + } + + always_locations = [ + location for location in get_always_hint_locations(world, player) + if location in loc_in_this_world + ] + always_items = [ + item for item in get_always_hint_items(world, player) + if item in prog_items_in_this_world + ] + priority_locations = [ + location for location in get_priority_hint_locations(world, player) + if location in loc_in_this_world + ] + priority_items = [ + item for item in get_priority_hint_items(world, player) + if item in prog_items_in_this_world + ] + + always_hint_pairs = dict() + + for item in always_items: + hint_pair = make_hint_from_item(world, player, item) + always_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) + + for location in always_locations: + hint_pair = make_hint_from_location(world, player, location) + always_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) + + priority_hint_pairs = dict() + + for item in priority_items: + hint_pair = make_hint_from_item(world, player, item) + priority_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) + + for location in priority_locations: + hint_pair = make_hint_from_location(world, player, location) + priority_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) + + for loc, item in always_hint_pairs.items(): + if item[1]: + hints.append((item[0], "can be found at", loc, item[2])) + else: + hints.append((loc, "contains", item[0], item[2])) + + next_random_hint_is_item = world.random.randint(0, 2) + + prog_items_in_this_world = sorted(list(prog_items_in_this_world)) + locations_in_this_world = sorted(list(loc_in_this_world)) + + world.random.shuffle(prog_items_in_this_world) + world.random.shuffle(locations_in_this_world) + + while len(hints) < hint_amount: + if priority_hint_pairs: + loc = world.random.choice(list(priority_hint_pairs.keys())) + item = priority_hint_pairs[loc] + del priority_hint_pairs[loc] + + if item[1]: + hints.append((item[0], "can be found at", loc, item[2])) + else: + hints.append((loc, "contains", item[0], item[2])) + continue + + if next_random_hint_is_item: + if not prog_items_in_this_world: + next_random_hint_is_item = not next_random_hint_is_item + continue + + hint = make_hint_from_item(world, player, prog_items_in_this_world.pop()) + hints.append((hint[1], "can be found at", hint[0], hint[2])) + else: + hint = make_hint_from_location(world, player, locations_in_this_world.pop()) + hints.append((hint[0], "contains", hint[1], hint[2])) + + next_random_hint_is_item = not next_random_hint_is_item + + return hints + + +def generate_joke_hints(world: MultiWorld, amount: int): + return [(x, y, z, -1) for (x, y, z) in world.random.sample(joke_hints, amount)] diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 9ffd5a1173..cbb1554096 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -2,6 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy +from collections import defaultdict from typing import Dict, NamedTuple, Optional, Set from BaseClasses import Item, MultiWorld @@ -96,6 +97,10 @@ class WitnessPlayerItems: Class that defines Items for a single world """ + @staticmethod + def code(item_name: str): + return StaticWitnessItems.ALL_ITEM_TABLE[item_name].code + def __init__(self, locat: WitnessPlayerLocations, world: MultiWorld, player: int, player_logic: WitnessPlayerLogic): """Adds event items after logic changes due to options""" self.EVENT_ITEM_TABLE = dict() @@ -105,6 +110,8 @@ class WitnessPlayerItems: self.ITEM_ID_TO_DOOR_HEX = dict() self.DOORS = set() + self.PROG_ITEM_AMOUNTS = defaultdict(lambda: 1) + self.SYMBOLS_NOT_IN_THE_GAME = set() self.EXTRA_AMOUNTS = { @@ -118,8 +125,17 @@ class WitnessPlayerItems: if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS: self.SYMBOLS_NOT_IN_THE_GAME.add(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code) else: + if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS: + self.PROG_ITEM_AMOUNTS[item[0]] = len(player_logic.MULTI_LISTS[item[0]]) + self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]] + self.MULTI_LISTS_BY_CODE = dict() + + for item in self.PROG_ITEM_AMOUNTS: + multi_list = player_logic.MULTI_LISTS[item] + self.MULTI_LISTS_BY_CODE[self.code(item)] = [self.code(single_item) for single_item in multi_list] + for entity_hex, items in player_logic.DOOR_ITEMS_BY_ID.items(): entity_hex_int = int(entity_hex, 16) @@ -138,11 +154,11 @@ class WitnessPlayerItems: if doors and symbols: self.GOOD_ITEMS = [ - "Dots", "Black/White Squares", "Symmetry" + "Progressive Dots", "Black/White Squares", "Symmetry" ] elif symbols: self.GOOD_ITEMS = [ - "Dots", "Black/White Squares", "Stars", + "Progressive Dots", "Black/White Squares", "Progressive Stars", "Shapers", "Symmetry" ] @@ -151,6 +167,10 @@ class WitnessPlayerItems: if not is_option_enabled(world, player, "disable_non_randomized_puzzles"): self.GOOD_ITEMS.append("Colored Squares") + self.GOOD_ITEMS = [ + StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS + ] + for event_location in locat.EVENT_LOCATION_TABLE: location = player_logic.EVENT_ITEM_PAIRS[event_location] self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index ea0728b16c..99bd3ece83 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -3,7 +3,8 @@ Defines constants for different types of locations in the game """ from .Options import is_option_enabled, get_option_value -from .player_logic import StaticWitnessLogic, WitnessPlayerLogic +from .player_logic import WitnessPlayerLogic +from .static_logic import StaticWitnessLogic class StaticWitnessLocations: @@ -52,8 +53,6 @@ class StaticWitnessLocations: "Desert Light Room 3", "Desert Pond Room 5", "Desert Flood Room 6", - "Desert Final Bent 3", - "Desert Final Hexagonal", "Desert Laser Panel", "Quarry Mill Lower Row 6", @@ -247,6 +246,10 @@ class WitnessPlayerLocations: StaticWitnessLocations.GENERAL_LOCATIONS ) + if get_option_value(world, player, "puzzle_randomization") == 1: + self.CHECK_LOCATIONS.remove("Keep Pressure Plates 4") + self.CHECK_LOCATIONS.add("Keep Pressure Plates 2") + 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") diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 4840ea0a5d..a58ad8ef7d 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -39,7 +39,7 @@ class WitnessPlayerLogic: if panel_hex in self.COMPLETELY_DISABLED_CHECKS: return frozenset() - check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] + check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex] these_items = frozenset({frozenset()}) @@ -47,17 +47,21 @@ class WitnessPlayerLogic: these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] these_items = frozenset({ - subset.intersection(self.PROG_ITEMS_ACTUALLY_IN_THE_GAME) + subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI) for subset in these_items }) + for subset in these_items: + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) + if panel_hex in self.DOOR_ITEMS_BY_ID: door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]}) all_options = set() - for items_option in these_items: - for dependentItem in door_items: + for dependentItem in door_items: + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem) + for items_option in these_items: all_options.add(items_option.union(dependentItem)) if panel_hex != "0x28A0D": @@ -76,11 +80,11 @@ class WitnessPlayerLogic: dependent_items_for_option = frozenset({frozenset()}) for option_panel in option: - dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel) + dep_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX.get(option_panel) if option_panel in self.COMPLETELY_DISABLED_CHECKS: new_items = frozenset() - elif option_panel in {"7 Lasers", "11 Lasers"}: + elif option_panel in {"7 Lasers", "11 Lasers", "PP2 Weirdness"}: 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 @@ -113,20 +117,26 @@ class WitnessPlayerLogic: """Makes a single logic adjustment based on additional logic file""" if adj_type == "Items": - if line not in StaticWitnessItems.ALL_ITEM_TABLE: - raise RuntimeError("Item \"" + line + "\" does not exit.") + line_split = line.split(" - ") + item = line_split[0] - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(line) + if item not in StaticWitnessItems.ALL_ITEM_TABLE: + raise RuntimeError("Item \"" + item + "\" does not exit.") - if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: - panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] + self.THEORETICAL_ITEMS.add(item) + self.THEORETICAL_ITEMS_NO_MULTI.update(StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(item, [item])) + + if item in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: + panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[item][2] for panel_hex in panel_hexes: - self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(line) + self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(item) return if adj_type == "Remove Items": - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.discard(line) + self.THEORETICAL_ITEMS.discard(line) + for i in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(line, [line]): + self.THEORETICAL_ITEMS_NO_MULTI.discard(i) if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] @@ -265,11 +275,22 @@ class WitnessPlayerLogic: self.REQUIREMENTS_BY_HEX[check_hex] = indep_requirement + for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: + if item not in self.THEORETICAL_ITEMS: + corresponding_multi = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item] + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(corresponding_multi) + multi_list = StaticWitnessLogic.PROGRESSIVE_TO_ITEMS[StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]] + multi_list = [item for item in multi_list if item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI] + self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1 + self.MULTI_LISTS[corresponding_multi] = multi_list + else: + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) + def make_event_item_pair(self, panel): """ Makes a pair of an event panel and its event item """ - name = StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" + name = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["checkName"] + " Solved" pair = (name, self.EVENT_ITEM_NAMES[panel]) return pair @@ -287,7 +308,7 @@ class WitnessPlayerLogic: if panel == "TrueOneWay": continue - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: + if self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: self.EVENT_PANELS_FROM_REGIONS.add(panel) self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) @@ -306,12 +327,24 @@ class WitnessPlayerLogic: self.EVENT_PANELS_FROM_PANELS = set() self.EVENT_PANELS_FROM_REGIONS = set() + self.THEORETICAL_ITEMS = set() + self.THEORETICAL_ITEMS_NO_MULTI = set() + self.MULTI_AMOUNTS = dict() + self.MULTI_LISTS = dict() + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() self.DOOR_ITEMS_BY_ID = dict() self.STARTING_INVENTORY = set() - self.CONNECTIONS_BY_REGION_NAME = copy.copy(StaticWitnessLogic.STATIC_CONNECTIONS_BY_REGION_NAME) - self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(StaticWitnessLogic.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) + self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization") + + if self.DIFFICULTY == 0: + self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal + elif self.DIFFICULTY == 1: + self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert + + self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME) + self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) self.REQUIREMENTS_BY_HEX = dict() # Determining which panels need to be events is a difficult process. @@ -333,6 +366,7 @@ class WitnessPlayerLogic: "0x019DC": "Keep Hedges 2 Knowledge", "0x019E7": "Keep Hedges 3 Knowledge", "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", + "0x01BE9": "Keep Laser Panel (Pressure Plates) Activates", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Mill Requirement Met", "0x009A1": "Swamp Between Bridges Far 1 Activates", @@ -374,6 +408,9 @@ class WitnessPlayerLogic: "0x0356B": "Challenge Video Pattern Knowledge", "0x0A15F": "Desert Laser Panel Shutters Open (1)", "0x012D7": "Desert Laser Panel Shutters Open (2)", + "0x03613": "Treehouse Orange Bridge 13 Turns On", + "0x17DEC": "Treehouse Laser House Access Requirement", + "0x03C08": "Town Church Entry Opens", } self.ALWAYS_EVENT_NAMES_BY_HEX = { diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 143f3e77e5..e17acf7343 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -4,7 +4,8 @@ and connects them with the proper requirements """ from BaseClasses import MultiWorld, Entrance -from . import StaticWitnessLogic +from .static_logic import StaticWitnessLogic +from .Options import get_option_value from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic @@ -39,7 +40,7 @@ class WitnessRegions: connection = Entrance( player, - source + " to " + target + " via " + str(panel_hex_to_solve_set), + source + " to " + target, source_region ) @@ -58,16 +59,23 @@ class WitnessRegions: create_region(world, player, 'Menu', self.locat, None, ["The Splashscreen?"]), ] + difficulty = get_option_value(world, player, "puzzle_randomization") + + if difficulty == 1: + reference_logic = StaticWitnessLogic.sigma_expert + else: + reference_logic = StaticWitnessLogic.sigma_normal + all_locations = set() - for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items(): + for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): locations_for_this_region = [ - StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"] - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE + reference_logic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"] + if reference_logic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE ] locations_for_this_region += [ - StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" for panel in region["panels"] - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" in self.locat.EVENT_LOCATION_TABLE + reference_logic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" for panel in region["panels"] + if reference_logic.CHECKS_BY_HEX[panel]["checkName"] + " Solved" in self.locat.EVENT_LOCATION_TABLE ] all_locations = all_locations | set(locations_for_this_region) @@ -76,7 +84,7 @@ class WitnessRegions: create_region(world, player, region_name, self.locat, locations_for_this_region) ] - for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items(): + for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: if connection[0] == "Entry": continue @@ -87,7 +95,7 @@ class WitnessRegions: for subset in connection[1]: if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}): - if all({StaticWitnessLogic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): + if all({reference_logic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): self.connect(world, player, connection[0], region_name, player_logic, frozenset({subset})) self.connect(world, player, region_name, connection[0], player_logic, connection[1]) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 2b9888b361..02efc5ef4c 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -91,13 +91,64 @@ class WitnessLogic(LogicMixin): if not self._witness_has_lasers(world, player, get_option_value(world, player, "challenge_lasers")): valid_option = False break + elif item == "PP2 Weirdness": + hedge_2_access = ( + self.can_reach("Keep 2nd Maze to Keep", "Entrance", player) + or self.can_reach("Keep to Keep 2nd Maze", "Entrance", player) + ) + + hedge_3_access = ( + self.can_reach("Keep 3rd Maze to Keep", "Entrance", player) + or self.can_reach("Keep 2nd Maze to Keep 3rd Maze", "Entrance", player) + and hedge_2_access + ) + + hedge_4_access = ( + self.can_reach("Keep 4th Maze to Keep", "Entrance", player) + or self.can_reach("Keep 3rd Maze to Keep 4th Maze", "Entrance", player) + and hedge_3_access + ) + + hedge_access = ( + self.can_reach("Keep 4th Maze to Keep Tower", "Entrance", player) + and self.can_reach("Keep", "Region", player) + and hedge_4_access + ) + + backwards_to_fourth = ( + self.can_reach("Keep", "Region", player) + and self.can_reach("Keep 4th Pressure Plate to Keep Tower", "Entrance", player) + and ( + self.can_reach("Keep Tower to Keep", "Entrance", player) + or hedge_access + ) + ) + + backwards_access = ( + self.can_reach("Keep 3rd Pressure Plate to Keep 4th Pressure Plate", "Entrance", player) + and backwards_to_fourth + + or self.can_reach("Main Island", "Region", player) + and self.can_reach("Keep 4th Pressure Plate to Shadows", "Entrance", player) + ) + + front_access = ( + self.can_reach("Keep to Keep 2nd Pressure Plate", 'Entrance', player) + and self.can_reach("Keep", "Region", player) + ) + + if not (front_access and backwards_access): + valid_option = False + break elif item in player_logic.EVENT_PANELS: if not self._witness_can_solve_panel(item, world, player, player_logic, locat): valid_option = False break elif not self.has(item, player): - valid_option = False - break + prog_dict = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE + if not (item in prog_dict and self.has(prog_dict[item], player, player_logic.MULTI_AMOUNTS[item])): + valid_option = False + break if valid_option: return True diff --git a/worlds/witness/settings/Audio_Logs.txt b/worlds/witness/settings/Audio_Logs.txt new file mode 100644 index 0000000000..27ce126778 --- /dev/null +++ b/worlds/witness/settings/Audio_Logs.txt @@ -0,0 +1,49 @@ +0x3C0F7 +0x3C0FD +0x32A00 +0x3C0FE +0x3C100 +0x3C0F4 +0x3C102 +0x3C10D +0x3C10E +0x3C10B +0x0074F +0x012C7 +0x329FF +0x3C106 +0x33AFF +0x011F9 +0x00763 +0x32A08 +0x3C101 +0x3C0FF +0x3C103 +0x00A0F +0x339A9 +0x015C0 +0x33B36 +0x3C10C +0x32A0E +0x329FE +0x32A07 +0x00761 +0x3C109 +0x33B37 +0x3C107 +0x3C0F3 +0x015B7 +0x3C10A +0x32A0A +0x015C1 +0x3C12A +0x3C104 +0x3C105 +0x339A8 +0x0050A +0x338BD +0x3C135 +0x338C9 +0x338D7 +0x338C1 +0x338CA \ No newline at end of file diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index 4f26e3136a..43c69409fc 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -52,6 +52,10 @@ Disabled Locations: 0x019E7 (Keep Hedge Maze 3) 0x01A0F (Keep Hedge Maze 4) 0x0360E (Laser Hedges) +0x03307 (First Gate) +0x03313 (Second Gate) +0x0C128 (Entry Inner) +0x0C153 (Entry Outer) 0x00B10 (Monastery Entry Left) 0x00C92 (Monastery Entry Right) 0x00290 (Monastery Outside 1) @@ -83,6 +87,10 @@ Disabled Locations: 0x15ADD (River Outside Vault) 0x03702 (River Vault Box) 0x17CAA (Monastery Shortcut Panel) +0x0C2A4 (Bunker Entry) +0x17C79 (Tinted Glass Door) +0x0C2A3 (UV Room Entry) +0x0A08D (Elevator Room Entry) 0x17C2E (Door to Bunker) 0x09F7D (Bunker Intro Left 1) 0x09FDC (Bunker Intro Left 2) diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/settings/Symbol_Shuffle.txt index d03391f5c5..3d0342f5e2 100644 --- a/worlds/witness/settings/Symbol_Shuffle.txt +++ b/worlds/witness/settings/Symbol_Shuffle.txt @@ -1,5 +1,6 @@ Items: -Dots +Arrows +Progressive Dots Colored Dots Sound Dots Symmetry @@ -8,7 +9,6 @@ Eraser Shapers Rotated Shapers Negative Shapers -Stars -Stars + Same Colored Symbol +Progressive Stars Black/White Squares Colored Squares \ No newline at end of file diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 646957c462..18a0e19cff 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -1,73 +1,15 @@ import os -from .utils import define_new_region, parse_lambda +from .utils import define_new_region, parse_lambda, lazy -class StaticWitnessLogic: - ALL_SYMBOL_ITEMS = set() - ALL_DOOR_ITEMS = set() - ALL_DOOR_ITEMS_AS_DICT = dict() - ALL_USEFULS = set() - ALL_TRAPS = set() - ALL_BOOSTS = set() - CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict() - - EVENT_PANELS_FROM_REGIONS = set() - - # All regions with a list of panels in them and the connections to other regions, before logic adjustments - ALL_REGIONS_BY_NAME = dict() - STATIC_CONNECTIONS_BY_REGION_NAME = dict() - - CHECKS_BY_HEX = dict() - CHECKS_BY_NAME = dict() - STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() - - def parse_items(self): - """ - Parses currently defined items from WitnessItems.txt - """ - - path = os.path.join(os.path.dirname(__file__), "WitnessItems.txt") - with open(path, "r", encoding="utf-8") as file: - current_set = self.ALL_SYMBOL_ITEMS - - for line in file.readlines(): - line = line.strip() - - if line == "Progression:": - current_set = self.ALL_SYMBOL_ITEMS - continue - if line == "Boosts:": - current_set = self.ALL_BOOSTS - continue - if line == "Traps:": - current_set = self.ALL_TRAPS - continue - if line == "Usefuls:": - current_set = self.ALL_USEFULS - continue - if line == "Doors:": - current_set = self.ALL_DOOR_ITEMS - continue - if line == "": - continue - - line_split = line.split(" - ") - - if current_set is self.ALL_USEFULS: - current_set.add((line_split[1], int(line_split[0]), line_split[2] == "True")) - elif current_set is self.ALL_DOOR_ITEMS: - new_door = (line_split[1], int(line_split[0]), frozenset(line_split[2].split(","))) - current_set.add(new_door) - self.ALL_DOOR_ITEMS_AS_DICT[line_split[1]] = new_door - else: - current_set.add((line_split[1], int(line_split[0]))) - - def read_logic_file(self): +class StaticWitnessLogicObj: + def read_logic_file(self, file_path="WitnessLogic.txt"): """ Reads the logic file and does the initial population of data structures """ - path = os.path.join(os.path.dirname(__file__), "WitnessLogic.txt") + path = os.path.join(os.path.dirname(__file__), file_path) + with open(path, "r", encoding="utf-8") as file: current_region = dict() @@ -157,6 +99,99 @@ class StaticWitnessLogic: current_region["panels"].add(check_hex) + def __init__(self, file_path="WitnessLogic.txt"): + # All regions with a list of panels in them and the connections to other regions, before logic adjustments + self.ALL_REGIONS_BY_NAME = dict() + self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() + + self.CHECKS_BY_HEX = dict() + self.CHECKS_BY_NAME = dict() + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() + + self.read_logic_file(file_path) + + +class StaticWitnessLogic: + ALL_SYMBOL_ITEMS = set() + ITEMS_TO_PROGRESSIVE = dict() + PROGRESSIVE_TO_ITEMS = dict() + ALL_DOOR_ITEMS = set() + ALL_DOOR_ITEMS_AS_DICT = dict() + ALL_USEFULS = set() + ALL_TRAPS = set() + ALL_BOOSTS = set() + CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict() + + ALL_REGIONS_BY_NAME = dict() + STATIC_CONNECTIONS_BY_REGION_NAME = dict() + + CHECKS_BY_HEX = dict() + CHECKS_BY_NAME = dict() + STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() + + def parse_items(self): + """ + Parses currently defined items from WitnessItems.txt + """ + + path = os.path.join(os.path.dirname(__file__), "WitnessItems.txt") + with open(path, "r", encoding="utf-8") as file: + current_set = self.ALL_SYMBOL_ITEMS + + for line in file.readlines(): + line = line.strip() + + if line == "Progression:": + current_set = self.ALL_SYMBOL_ITEMS + continue + if line == "Boosts:": + current_set = self.ALL_BOOSTS + continue + if line == "Traps:": + current_set = self.ALL_TRAPS + continue + if line == "Usefuls:": + current_set = self.ALL_USEFULS + continue + if line == "Doors:": + current_set = self.ALL_DOOR_ITEMS + continue + if line == "": + continue + + line_split = line.split(" - ") + + if current_set is self.ALL_USEFULS: + current_set.add((line_split[1], int(line_split[0]), line_split[2] == "True")) + elif current_set is self.ALL_DOOR_ITEMS: + new_door = (line_split[1], int(line_split[0]), frozenset(line_split[2].split(","))) + current_set.add(new_door) + self.ALL_DOOR_ITEMS_AS_DICT[line_split[1]] = new_door + else: + if len(line_split) > 2: + progressive_items = line_split[2].split(",") + for i, value in enumerate(progressive_items): + self.ITEMS_TO_PROGRESSIVE[value] = line_split[1] + self.PROGRESSIVE_TO_ITEMS[line_split[1]] = progressive_items + current_set.add((line_split[1], int(line_split[0]))) + continue + current_set.add((line_split[1], int(line_split[0]))) + + @lazy + def sigma_expert(self) -> StaticWitnessLogicObj: + return StaticWitnessLogicObj("WitnessLogicExpert.txt") + + @lazy + def sigma_normal(self) -> StaticWitnessLogicObj: + return StaticWitnessLogicObj("WitnessLogic.txt") + def __init__(self): self.parse_items() - self.read_logic_file() + + self.ALL_REGIONS_BY_NAME.update(self.sigma_normal.ALL_REGIONS_BY_NAME) + self.STATIC_CONNECTIONS_BY_REGION_NAME.update(self.sigma_normal.STATIC_CONNECTIONS_BY_REGION_NAME) + + self.CHECKS_BY_HEX.update(self.sigma_normal.CHECKS_BY_HEX) + self.CHECKS_BY_NAME.update(self.sigma_normal.CHECKS_BY_NAME) + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX.update(self.sigma_normal.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) + diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index 809b2b1c3d..a69dabbb7c 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -89,6 +89,22 @@ def parse_lambda(lambda_string): return lambda_set +class lazy(object): + def __init__(self, func, name=None): + self.func = func + self.name = name if name is not None else func.__name__ + self.__doc__ = func.__doc__ + + def __get__(self, instance, class_): + if instance is None: + res = self.func(class_) + setattr(class_, self.name, res) + return res + res = self.func(instance) + setattr(instance, self.name, res) + return res + + def get_adjustment_file(adjustment_file): path = os.path.join(os.path.dirname(__file__), adjustment_file) @@ -134,3 +150,8 @@ def get_doors_max_list(): @cache_argsless def get_laser_shuffle(): return get_adjustment_file("settings/Laser_Shuffle.txt") + + +@cache_argsless +def get_audio_logs(): + return get_adjustment_file("settings/Audio_Logs.txt") From 414ebf2640f9323f128e6bec319a56062ca5818b Mon Sep 17 00:00:00 2001 From: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Date: Sat, 8 Oct 2022 22:19:17 -0400 Subject: [PATCH 042/105] SC2: Add an automated installation process for the maps and mod within SC2Client. (#928) --- Starcraft2Client.py | 158 ++++++++++++++++-- .../docs/en_Starcraft 2 Wings of Liberty.md | 2 +- worlds/sc2wol/docs/setup_en.md | 20 +-- 3 files changed, 149 insertions(+), 31 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index c1eed74b4c..de0a90411e 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -114,12 +114,40 @@ class StarcraftClientProcessor(ClientCommandProcessor): """Manually set the SC2 install directory (if the automatic detection fails).""" if path: os.environ["SC2PATH"] = path - check_mod_install() + is_mod_installed_correctly() return True else: sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") return False + def _cmd_download_data(self, force: bool = False) -> bool: + """Download the most recent release of the necessary files for playing SC2 with + Archipelago. force should be True or False. force=True will overwrite your files.""" + if "SC2PATH" not in os.environ: + check_game_install_path() + + if os.path.exists(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt"): + with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "r") as f: + current_ver = f.read() + else: + current_ver = None + + tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData', current_version=current_ver, force_download=force) + + if tempzip != '': + try: + import zipfile + zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"]) + sc2_logger.info(f"Download complete. Version {version} installed.") + with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f: + f.write(version) + finally: + os.remove(tempzip) + else: + sc2_logger.warning("Download aborted/failed. Read the log for more information.") + return False + return True + class SC2Context(CommonContext): command_processor = StarcraftClientProcessor @@ -158,10 +186,13 @@ class SC2Context(CommonContext): 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() + # Looks for the required maps and mods for SC2. Runs check_game_install_path. + is_mod_installed_correctly() + if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"): + with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f: + current_ver = f.read() + if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver): + sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.") def on_print_json(self, args: dict): # goes to this world @@ -820,18 +851,53 @@ def check_game_install_path() -> bool: return False -def check_mod_install() -> bool: - # Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path. - try: - # Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user. - if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))): - sc2_logger.info(f"Archipelago mod found at {modfile}.") - return True - else: - sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.") - except KeyError: - sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.") - return False +def is_mod_installed_correctly() -> bool: + """Searches for all required files.""" + if "SC2PATH" not in os.environ: + check_game_install_path() + + mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign') + modfile = os.environ["SC2PATH"] / Path("Mods/Archipelago.SC2Mod") + wol_required_maps = [ + "ap_thanson01.SC2Map", "ap_thanson02.SC2Map", "ap_thanson03a.SC2Map", "ap_thanson03b.SC2Map", + "ap_thorner01.SC2Map", "ap_thorner02.SC2Map", "ap_thorner03.SC2Map", "ap_thorner04.SC2Map", "ap_thorner05s.SC2Map", + "ap_traynor01.SC2Map", "ap_traynor02.SC2Map", "ap_traynor03.SC2Map", + "ap_ttosh01.SC2Map", "ap_ttosh02.SC2Map", "ap_ttosh03a.SC2Map", "ap_ttosh03b.SC2Map", + "ap_ttychus01.SC2Map", "ap_ttychus02.SC2Map", "ap_ttychus03.SC2Map", "ap_ttychus04.SC2Map", "ap_ttychus05.SC2Map", + "ap_tvalerian01.SC2Map", "ap_tvalerian02a.SC2Map", "ap_tvalerian02b.SC2Map", "ap_tvalerian03.SC2Map", + "ap_tzeratul01.SC2Map", "ap_tzeratul02.SC2Map", "ap_tzeratul03.SC2Map", "ap_tzeratul04.SC2Map" + ] + needs_files = False + + # Check for maps. + missing_maps = [] + for mapfile in wol_required_maps: + if not os.path.isfile(mapdir / mapfile): + missing_maps.append(mapfile) + if len(missing_maps) >= 19: + sc2_logger.warning(f"All map files missing from {mapdir}.") + needs_files = True + elif len(missing_maps) > 0: + for map in missing_maps: + sc2_logger.debug(f"Missing {map} from {mapdir}.") + sc2_logger.warning(f"Missing {len(missing_maps)} map files.") + needs_files = True + else: # Must be no maps missing + sc2_logger.info(f"All maps found in {mapdir}.") + + # Check for mods. + if os.path.isfile(modfile): + sc2_logger.info(f"Archipelago mod found at {modfile}.") + else: + sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.") + needs_files = True + + # Final verdict. + if needs_files: + sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.") + return False + else: + return True class DllDirectory: @@ -870,6 +936,64 @@ class DllDirectory: return False +def download_latest_release_zip(owner: str, repo: str, current_version: str = None, force_download=False) -> (str, str): + """Downloads the latest release of a GitHub repo to the current directory as a .zip file.""" + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_version = r1.json()["tag_name"] + sc2_logger.info(f"Latest version: {latest_version}.") + else: + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.") + sc2_logger.warning(f"text: {r1.text}") + return "", current_version + + if (force_download is False) and (current_version == latest_version): + sc2_logger.info("Latest version already installed.") + return "", current_version + + sc2_logger.info(f"Attempting to download version {latest_version} of {repo}.") + download_url = r1.json()["assets"][0]["browser_download_url"] + + r2 = requests.get(download_url, headers=headers) + if r2.status_code == 200: + with open(f"{repo}.zip", "wb") as fh: + fh.write(r2.content) + sc2_logger.info(f"Successfully downloaded {repo}.zip.") + return f"{repo}.zip", latest_version + else: + sc2_logger.warning(f"Status code: {r2.status_code}") + sc2_logger.warning("Download failed.") + sc2_logger.warning(f"text: {r2.text}") + return "", current_version + + +def is_mod_update_available(owner: str, repo: str, current_version: str) -> bool: + import requests + + headers = {"Accept": 'application/vnd.github.v3+json'} + url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + + r1 = requests.get(url, headers=headers) + if r1.status_code == 200: + latest_version = r1.json()["tag_name"] + if current_version != latest_version: + return True + else: + return False + + else: + sc2_logger.warning(f"Failed to reach GitHub while checking for updates.") + sc2_logger.warning(f"Status code: {r1.status_code}") + sc2_logger.warning(f"text: {r1.text}") + return False + + if __name__ == '__main__': colorama.init() asyncio.run(main()) diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md index 8fa20c86f9..f7c8519a2a 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -15,7 +15,7 @@ missions. When you receive items, they will immediately become available, even d notified via a text box in the top-right corner of the game screen. (The text client for StarCraft 2 also records all items in all worlds.) -Missions are launched only through the text client. The Hyperion is never visited. Aditionally, credits are not used. +Missions are launched only through the text client. The Hyperion is never visited. Additionally, credits are not used. ## What is the goal of this game when randomized? diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md index 1539a21291..267c8430aa 100644 --- a/worlds/sc2wol/docs/setup_en.md +++ b/worlds/sc2wol/docs/setup_en.md @@ -13,9 +13,10 @@ to obtain a config file for StarCraft 2. 1. Install StarCraft 2 and Archipelago using the first two links above. (The StarCraft 2 client for Archipelago is included by default.) -2. Click the third link above and follow the instructions there. -3. Linux users should also follow the instructions found at the bottom of this page - (["Running in Linux"](#running-in-linux)). + - Linux users should also follow the instructions found at the bottom of this page + (["Running in Linux"](#running-in-linux)). +2. Run ArchipelagoStarcraft2Client.exe. +3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above. ## Where do I get a config file (aka "YAML") for this game? @@ -40,16 +41,9 @@ Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en ## The game isn't launching when I try to start a mission. -First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If the below fix doesn't -work for you, and you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) -tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to -your message. - -### Check your installation - -Make sure you've followed the installation instructions completely. Specifically, make sure that you've placed the Maps -and Mods folders directly inside the StarCraft II installation folder. They should be in the same location as the -SC2Data, Support, Support64, and Versions folders. +First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out +the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a +specific description of what's going wrong and attach your log file to your message. ## Running in Linux From 3cbbf905d14ac95b9c1b3f85bc96f7ffc005f2b1 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sat, 8 Oct 2022 19:20:01 -0700 Subject: [PATCH 043/105] Docs: how to run web host and generate template yamls (#1071) --- docs/running from source.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/running from source.md b/docs/running from source.md index 24486146f8..2bda62ec1a 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -19,6 +19,10 @@ After this, you should be able to run the programs. * With yaml(s) in the `Players` folder, `Generate.py` will generate the multiworld archive. * `MultiServer.py`, with the filename of the generated archive as a command line parameter, will host the multiworld locally. * `--log_network` is a command line parameter useful for debugging. + * `WebHost.py` will host the website on your computer. + * You can copy `docs/webhost configuration sample.yaml` to `config.yaml` + to change WebHost options (like the web hosting port number). + * As a side effect, `WebHost.py` creates the template yamls for all the games in `WebHostLib/static/generated`. ## Windows From 4c0c93b083cd0bca4896f24440a753d341f9fe50 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 9 Oct 2022 17:16:59 -0500 Subject: [PATCH 044/105] core: allow string defaults in yaml templates (#1051) * allow string defaults in yaml templates * have default_converter handle strings * handle all default values in the yaml * allow for random range options * yaml dump dicts * strip the whities * rip out the converter * accidentally stripped the dicts * goodbye readability --- WebHostLib/options.py | 12 +++++------- WebHostLib/templates/options.yaml | 18 +++++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 2d908ca962..db1e57fdec 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -31,9 +31,12 @@ def create(): data.update({ option.range_start: 0, option.range_end: 0, - "random": 0, "random-low": 0, "random-high": 0, option.default: 50 }) + for sub_option in {"random", "random-low", "random-high"}: + if sub_option != option.default: + data[sub_option] = 0 + notes = { special: "minimum value without special meaning", option.range_start: "minimum value", @@ -49,11 +52,6 @@ def create(): return data, notes - def default_converter(default_value): - if isinstance(default_value, (set, frozenset)): - 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!" @@ -79,7 +77,7 @@ def create(): 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, + dictify_range=dictify_range, ) del file_data diff --git a/WebHostLib/templates/options.yaml b/WebHostLib/templates/options.yaml index 11009106b8..3c21ecfb1d 100644 --- a/WebHostLib/templates/options.yaml +++ b/WebHostLib/templates/options.yaml @@ -43,14 +43,18 @@ requires: {%- if option.range_start is defined and option.range_start is number %} {{- range_option(option) -}} {%- elif option.options -%} - {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %} + {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %} {{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} - {%- endfor -%} - {% if option.default == "random" %} - random: 50 - {%- endif -%} + {%- endfor -%} + {% if option.name_lookup[option.default] not in option.options %} + {{ option.default }}: 50 + {%- endif -%} + {%- elif option.default is string %} + {{ option.default }}: 50 + {%- elif option.default is iterable and option.default is not mapping %} + {{ option.default | list }} {%- else %} - {{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }} - {%- endif -%} + {{ yaml_dump(option.default) | indent(4, first=false) }} + {%- endif -%} {%- endfor %} {% if not options %}{}{% endif %} From 106d630ad7aecb1fa842d8ad2a14f1627000fe31 Mon Sep 17 00:00:00 2001 From: Joethepic <60947591+Joethepic@users.noreply.github.com> Date: Sun, 9 Oct 2022 19:26:33 -0500 Subject: [PATCH 045/105] HK: changes dreamers to skip prog balance (#1077) --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 1667ab81f7..9ed0c929bb 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -637,7 +637,7 @@ class HKItem(Item): def __init__(self, name, advancement, code, type: str, player: int = None): if name == "Mimic_Grub": classification = ItemClassification.trap - elif type in ("Grub", "DreamWarrior", "Root", "Egg"): + elif type in ("Grub", "DreamWarrior", "Root", "Egg", "Dreamer"): classification = ItemClassification.progression_skip_balancing elif type == "Charm" and name not in progression_charms: classification = ItemClassification.progression_skip_balancing From 099c4fca3c4d6da34bbfee43be4fbce64740c3f4 Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Mon, 10 Oct 2022 09:10:01 +0200 Subject: [PATCH 046/105] Docs: Polished Trigger and Plando guides (#1080) * Docs: Polished Trigger and Plando guides * Docs: Trigger/Plando guide polish PR suggestions by SoldierofOrder Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * Docs: More Trigger/Plando guide polish Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> --- worlds/generic/docs/plando_en.md | 83 ++++++++++++------------- worlds/generic/docs/triggers_en.md | 97 ++++++++++++++---------------- 2 files changed, 88 insertions(+), 92 deletions(-) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index fa3edd1fa1..c9f70fcb90 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -2,23 +2,24 @@ ## What is Plando? -The purposes of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and +The purpose of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and changes it up by allowing you to plan out certain aspects of the game by placing certain items in certain locations, certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`, -and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported -by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary. +and `connection plando`. Every game in Archipelago supports item plando but the other plando options are only supported +by certain games. Currently, only A Link to the Past supports text and boss plando. Support for connection plando may +vary. ### Enabling Plando -On the website plando will already be enabled. If you will be generating the game locally plando features must be +On the website, plando will already be enabled. If you will be generating the game locally, plando features must be enabled (opt-in). -* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text +* To opt-in go to the Archipelago installation (default: `C:\ProgramData\Archipelago`), open `host.yaml` with a text editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such as `plando_options: bosses, items, texts, connections`. -* You can add the necessary plando modules for your settings to the `requires` section of your yaml. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: +* You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: ```yaml requires: @@ -27,45 +28,45 @@ enabled (opt-in). ``` ## Item Plando -Item plando allows a player to place an item in a specific location or specific locations, place multiple items into a +Item plando allows a player to place an item in a specific location or specific locations, or place multiple items into a list of specific locations both in their own game or in another player's game. -* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either item and location, or items - and locations. +* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either `item` and + `location`, or `items` and `locations`. * `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or false and defaults to true if omitted. * `world` is the target world to place the item in. * It gets ignored if only one world is generated. * Can be a number, name, true, false, null, or a list. False is the default. - * If a number is used it targets that slot or player number in the multiworld. - * If a name is used it will target the world with that player name. - * If set to true it will be any player's world besides your own. - * If set to false it will target your own world. - * If set to null it will target a random world in the multiworld. + * If a number is used, it targets that slot or player number in the multiworld. + * If a name is used, it will target the world with that player name. + * If set to true, it will be any player's world besides your own. + * If set to false, it will target your own world. + * If set to null, it will target a random world in the multiworld. * If a list of names is used, it will target the games with the player names specified. - * `force` determines whether the generator will fail if the item can't be placed in the location can be true, false, + * `force` determines whether the generator will fail if the item can't be placed in the location. Can be true, false, or silent. Silent is the default. - * If set to true the item must be placed and the generator will throw an error if it is unable to do so. - * If set to false the generator will log a warning if the placement can't be done but will still generate. - * If set to silent and the placement fails it will be ignored entirely. + * If set to true, the item must be placed and the generator will throw an error if it is unable to do so. + * If set to false, the generator will log a warning if the placement can't be done but will still generate. + * If set to silent and the placement fails, it will be ignored entirely. * `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and if omitted will default to 100. * Single Placement is when you use a plando block to place a single item at a single location. * `item` is the item you would like to place and `location` is the location to place it. * Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted. - * `items` defines the items to use and a number letting you place multiple of it. You can use true instead of a number to have it use however many of that item are in your item pool. + * `items` defines the items to use, each with a number for the amount. Using `true` instead of a number uses however many of that item are in your item pool. * `locations` is a list of possible locations those items can be placed in. * Using the multi placement method, placements are picked randomly. - * Instead of a number, you can use true + * `count` can be used to set the maximum number of items placed from the block. The default is 1 if using `item` and False if using `items` - * If a number is used it will try to place this number of items. - * If set to false it will try to place as many items from the block as it can. - * If `min` and `max` are defined, it will try to place a number of items between these two numbers at random + * If a number is used, it will try to place this number of items. + * If set to false, it will try to place as many items from the block as it can. + * If `min` and `max` are defined, it will try to place a number of items between these two numbers at random. ### Available Items and Locations -A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is caps-sensitive. +A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is case-sensitive. ### Examples @@ -142,43 +143,43 @@ plando_items: min: 1 max: 4 ``` -1. This block has a 50% chance to occur, and if it does will place either the Empire Orb or Radiant Orb on another player's -Starter Chest 1 and removes the chosen item from the item pool. +1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another +player's Starter Chest 1 and removes the chosen item from the item pool. 2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots in their own dungeon major item chests. 3. This block will always trigger and will lock boss relics on the bosses. -4. This block has an 80% chance of occurring and when it does will place all but 1 of the items randomly among the four -locations chosen here. +4. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the +four locations chosen here. 5. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into other players' Master Sword Pedestals or Boss Relic 1 locations. 6. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords -into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy +into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy. ## Boss Plando -As this is currently only supported by A Link to the Past instead of explaining here please refer to the +As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en) ## Text Plando -As this is currently only supported by A Link to the Past instead of explaining here please refer to the +As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en) ## Connections Plando This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with their -connections is different I will only explain the basics here while more specifics for Link to the Past connection plando -can be found in its plando guide. +connections is different, I will only explain the basics here, while more specifics for A Link to the Past connection +plando can be found in its plando guide. -* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options support +* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options supports subweights. * `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100. * Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance shuffle. * `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate. -[Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852) +[A Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852) [Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62) @@ -186,7 +187,7 @@ can be found in its plando guide. ```yaml plando_connections: - # example block 1 - Link to the Past + # example block 1 - A Link to the Past - entrance: Cave Shop (Lake Hylia) exit: Cave 45 direction: entrance @@ -206,9 +207,9 @@ plando_connections: direction: both ``` -1. These connections are decoupled so going into the lake hylia cave shop will take you to the inside of cave 45 and - when you leave the interior you will exit to the cave 45 ledge. Going into the cave 45 entrance will then take you to - the lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take - you to their locations as normal but leaving old man cave will exit at Agahnim's Tower. -2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the +1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and + when you leave the interior, you will exit to the Cave 45 ledge. Going into the Cave 45 entrance will then take you to + the Lake Hylia Cave Shop. Walking into the entrance for the Old Man Cave and Agahnim's Tower entrance will both take + you to their locations as normal, but leaving Old Man Cave will exit at Agahnim's Tower. +2. This will force a Nether fortress and a village to be the Overworld structures for your game. Note that for the Minecraft connection plando to work structure shuffle must be enabled. diff --git a/worlds/generic/docs/triggers_en.md b/worlds/generic/docs/triggers_en.md index 03ce9c65cb..a9ffebb466 100644 --- a/worlds/generic/docs/triggers_en.md +++ b/worlds/generic/docs/triggers_en.md @@ -8,36 +8,31 @@ about 5 minutes to read. Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you -can do with triggers is the custom mercenary mode YAML that was created using entirely triggers and plando. +can do with triggers is the [custom mercenary mode YAML +](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) that was +created using entirely triggers and plando. -Mercenary mode -YAML: [Mercenary Mode YAML on GitHub](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) - -For more information on plando you can reference the general plando guide or the Link to the Past plando guide. - -General plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) - -Link to the Past plando guide: [LttP Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en) +For more information on plando, you can reference the [general plando guide](/tutorial/Archipelago/plando/en) or the +[A Link to the Past plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en). ## Trigger use -Triggers may be defined in either the root or in the relevant game sections. Generally, The best place to do this is the -bottom of the yaml for clear organization. +Triggers may be defined in either the root or in the relevant game sections. Generally, the best place to do this is the +bottom of the YAML for clear organization. -- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`, and - `option_result` from which it will react to and then an `options` section for the definition of what will happen. -- `option_category` is the defining section from which the option is defined in. +Each trigger consists of four parts: +- `option_category` specifies the section which the triggering option is defined in. - Example: `A Link to the Past` - - This is the root category the option is located in. If the option you're triggering off of is in root then you + - This is the category the option is located in. If the option you're triggering off of is in root then you would use `null`, otherwise this is the game for which you want this option trigger to activate. -- `option_name` is the option setting from which the triggered choice is going to react to. +- `option_name` specifies the name of the triggering option. - Example: `shop_item_slots` - - This can be any option from any category defined in the yaml file in either root or a game section. -- `option_result` is the result of this option setting from which you would like to react. + - This can be any option from any category defined in the YAML file in either root or a game section. +- `option_result` specifies the value of the option that activates this trigger. - Example: `15` - Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple - results you would need multiple triggers for this. -- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring + results, you would need multiple triggers for this. +- `options` is where you define what will happen when the trigger activates. This can be something as simple as ensuring another option also gets selected or placing an item in a certain location. It is possible to have multiple things happen in this section. - Example: @@ -47,10 +42,10 @@ bottom of the yaml for clear organization. Rupees (300): 2 ``` -This format must be: +The general format is: ```yaml - root option: + category: option to change: desired result ``` @@ -70,8 +65,8 @@ The above examples all together will end up looking like this: Rupees(300): 2 ``` -For this example if the generator happens to roll 15 shuffled in shop item slots for your game you'll be granted 600 -rupees at the beginning. These can also be used to change other options. +For this example, if the generator happens to roll 15 shuffled in shop item slots for your game, you'll be granted 600 +rupees at the beginning. Triggers can also be used to change other options. For example: @@ -85,9 +80,9 @@ For example: Inverted: true ``` -In this example if your world happens to roll SpecificKeycards then your game will also start in inverted. +In this example, if your world happens to roll SpecificKeycards, then your game will also start in inverted. -It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in +It is also possible to use imaginary values in options to trigger specific settings. You can use these made-up values in either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1 AND setting 2". @@ -97,33 +92,33 @@ For example: triggers: - option_category: Secret of Evermore option_name: doggomizer - option_result: pupdunk - options: - Secret of Evermore: - difficulty: - normal: 50 - pupdunk_hard: 25 - pupdunk_mystery: 25 - exp_modifier: - 150: 50 - 200: 50 - - option_category: Secret of Evermore - option_name: difficulty - option_result: pupdunk_hard - options: - Secret of Evermore: - fix_wings_glitch: false - difficulty: hard - - option_category: Secret of Evermore - option_name: difficulty - option_result: pupdunk_mystery - options: - Secret of Evermore: - fix_wings_glitch: false - difficulty: mystery + option_result: pupdunk + options: + Secret of Evermore: + difficulty: + normal: 50 + pupdunk_hard: 25 + pupdunk_mystery: 25 + exp_modifier: + 150: 50 + 200: 50 + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_hard + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: hard + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_mystery + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: mystery ``` -In this example (thanks to @Black-Sliver) if the `pupdunk` option is rolled then the difficulty values will be rolled +In this example (thanks to @Black-Sliver), if the `pupdunk` option is rolled, then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". \ No newline at end of file From 37a40499fa7aec4bf034579014d7eb48bebab2f0 Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Fri, 7 Oct 2022 14:26:45 +0200 Subject: [PATCH 047/105] Docs/sm64ex: address common questions - Added bolded note that one must use a new file with each new seed - Added troubleshooting section for when one didn't use a new file - Worded compilation step 8 more explicitly - Added link to compilation options list - Added link to TextClient for using commands --- worlds/sm64ex/docs/setup_en.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index d77e091359..acf9432fe5 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -3,8 +3,10 @@ ## Required Software - Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported) -- Either of [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or -- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually. +- Either of + - [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or + - Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually +- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex. You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex. @@ -25,7 +27,9 @@ Then follow the steps below 6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox 7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet 8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly -9. Set Build Options. Recommended: `-jn` where `n` is the Number of CPU Cores, to build faster. +9. Set Build Options and press build. + - Recommended: To build faster, use `-jn` where `n` is the number of CPU cores to use (e.g., `-j4` to use 4 cores). + - Optional: Add options from [this list](https://github.com/sm64pc/sm64ex/wiki/Build-options), separated by spaces (e.g., `-j4 BETTERCAMERA=1`). 10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes After it's done, the Build list should have another entry titled with what you named the folder in step 7. @@ -55,6 +59,9 @@ In case you are using the Archipelago Website, the IP should be `archipelago.gg` Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. +**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient. +Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file. + # Playing offline To play offline, first generate a seed on the game's settings page. @@ -83,6 +90,11 @@ with its name. When using a US Rom, the In-Game messages are missing some letters: `J Q V X Z` and `?`. The Japanese Version should have no problem displaying these. +### Toad does not have an item for me. + +This happens when you load an existing file that had already received an item from that toad. +To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress. + ### What happens if I lose connection? SM64EX tries to reconnect a few times, so be patient. From 9f684b3dc04ee9ec106896938b10c31789266c6b Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Tue, 11 Oct 2022 02:38:00 -0400 Subject: [PATCH 048/105] SMZ3: Shield Upgrade typo fix (#1088) fixed "Shield Uprade" typo See lordlou/alttp_sm_combo_randomizer_rom@3da5313 --- worlds/smz3/data/zsm.ips | Bin 1470833 -> 1470833 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index 2d6027d5e5875bc3fead9306d855e3f0f77ee168..25e543d987b36e60c6b3c34807a168e290bca8c9 100644 GIT binary patch delta 89 zcmezPDDvZ@$c7fi7N!>F7M2#)7Pc1l7LFFqEnJ5VOh0mfOKAF(16+FTOb5Aum>Yt& delta 97 zcmezPDDvZ@$c7fi7N!>F7M2#)7Pc1l7LFFqEnJ5VOh0gdON4!)%>8$Kr8^nf Date: Wed, 12 Oct 2022 13:28:32 -0500 Subject: [PATCH 049/105] core: Generic boss plando handler (#1044) * fix some blunders i made when implementing this * move generic functions to core class * move lttp specific stuff out and split up from_text a bit for more modularity * slightly optimize from_text call order * don't make changes on github apparently. reading hard * Metaclass Magic * do a check against the base class * copy paste strikes again * use option default instead of hardcoded "none". check locations and bosses aren't reused. * throw dupe location error for lttp * generic singularity support with a bool * forgot to enable it for lttp * better error handling * PlandoBosses: fix inheritance of singularity * Tests: PlandoBosses * fix case insensitive tests * Tests: cleanup PlandoBosses tests * f in the chat * oop * split location into a different variable Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * pass the list of options as `option_list` Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Options.py | 119 ++++++++++++++++++++++++++- test/options/TestPlandoBosses.py | 136 +++++++++++++++++++++++++++++++ test/options/__init__.py | 0 worlds/alttp/Bosses.py | 2 +- worlds/alttp/Options.py | 113 ++++--------------------- 5 files changed, 271 insertions(+), 99 deletions(-) create mode 100644 test/options/TestPlandoBosses.py create mode 100644 test/options/__init__.py diff --git a/Options.py b/Options.py index 567ac8dbc6..c243c8feb4 100644 --- a/Options.py +++ b/Options.py @@ -427,7 +427,6 @@ class TextChoice(Choice): 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: @@ -467,6 +466,124 @@ class TextChoice(Choice): raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") +class BossMeta(AssembleOptions): + def __new__(mcs, name, bases, attrs): + if name != "PlandoBosses": + assert "bosses" in attrs, f"Please define valid bosses for {name}" + attrs["bosses"] = frozenset((boss.lower() for boss in attrs["bosses"])) + assert "locations" in attrs, f"Please define valid locations for {name}" + attrs["locations"] = frozenset((location.lower() for location in attrs["locations"])) + cls = super().__new__(mcs, name, bases, attrs) + assert not cls.duplicate_bosses or "singularity" in cls.options, f"Please define option_singularity for {name}" + return cls + + +class PlandoBosses(TextChoice, metaclass=BossMeta): + """Generic boss shuffle option that supports plando. Format expected is + 'location1-boss1;location2-boss2;shuffle_mode'. + If shuffle_mode is not provided in the string, this will be the default shuffle mode. Must override can_place_boss, + which passes a plando boss and location. Check if the placement is valid for your game here.""" + bosses: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + locations: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + + duplicate_bosses: bool = False + + @classmethod + def from_text(cls, text: str): + # set all of our text to lower case for name checking + text = text.lower() + 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) + return cls.get_shuffle_mode(options) + + @classmethod + def get_shuffle_mode(cls, option_list: typing.List[str]): + # find out what mode 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 option_list: + shuffle = random.choice(list(cls.options)) + option_list.remove("random") + options = ";".join(option_list) + f";{shuffle}" + boss_class = cls(options) + else: + for option in option_list: + if option in cls.options: + options = ";".join(option_list) + break + else: + if cls.duplicate_bosses and len(option_list) == 1: + if cls.valid_boss_name(option_list[0]): + # this doesn't exist in this class but it's a forced option for classes where this is called + options = option_list[0] + ";singularity" + else: + options = option_list[0] + f";{cls.name_lookup[cls.default]}" + else: + options = ";".join(option_list) + f";{cls.name_lookup[cls.default]}" + boss_class = cls(options) + return boss_class + + @classmethod + def validate_plando_bosses(cls, options: typing.List[str]) -> None: + used_locations = [] + used_bosses = [] + for option in options: + # check if a shuffle mode was provided in the incorrect location + 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!") + elif "-" in option: + location, boss = option.split("-") + if location in used_locations: + raise ValueError(f"Duplicate Boss Location {location} not allowed.") + if not cls.duplicate_bosses and boss in used_bosses: + raise ValueError(f"Duplicate Boss {boss} not allowed.") + used_locations.append(location) + used_bosses.append(boss) + if not cls.valid_boss_name(boss): + raise ValueError(f"{boss.title()} is not a valid boss name.") + if not cls.valid_location_name(location): + raise ValueError(f"{location.title()} is not a valid boss location name.") + if not cls.can_place_boss(boss, location): + raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.") + else: + if cls.duplicate_bosses: + if not cls.valid_boss_name(option): + raise ValueError(f"{option} is not a valid boss name.") + else: + raise ValueError(f"{option.title()} is not formatted correctly.") + + @classmethod + def can_place_boss(cls, boss: str, location: str) -> bool: + raise NotImplementedError + + @classmethod + def valid_boss_name(cls, value: str) -> bool: + return value 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 Range(NumericOption): range_start = 0 range_end = 1 diff --git a/test/options/TestPlandoBosses.py b/test/options/TestPlandoBosses.py new file mode 100644 index 0000000000..3c218b69aa --- /dev/null +++ b/test/options/TestPlandoBosses.py @@ -0,0 +1,136 @@ +import unittest +import Generate +from Options import PlandoBosses + + +class SingleBosses(PlandoBosses): + bosses = {"B1", "B2"} + locations = {"L1", "L2"} + + option_vanilla = 0 + option_shuffle = 1 + + @staticmethod + def can_place_boss(boss: str, location: str) -> bool: + if boss == "b1" and location == "l1": + return False + return True + + +class MultiBosses(SingleBosses): + bosses = SingleBosses.bosses # explicit copy required + locations = SingleBosses.locations + duplicate_bosses = True + + option_singularity = 2 # required when duplicate_bosses = True + + +class TestPlandoBosses(unittest.TestCase): + def testCI(self): + """Bosses, locations and modes are supposed to be case-insensitive""" + self.assertEqual(MultiBosses.from_any("L1-B2").value, "l1-b2;vanilla") + self.assertEqual(MultiBosses.from_any("ShUfFlE").value, SingleBosses.option_shuffle) + + def testRandom(self): + """Validate random is random""" + import random + random.seed(0) + value1 = MultiBosses.from_any("random") + random.seed(0) + value2 = MultiBosses.from_text("random") + self.assertEqual(value1, value2) + for n in range(0, 100): + if MultiBosses.from_text("random") != value1: + break + else: + raise Exception("random is not random") + + def testShuffleMode(self): + """Test that simple modes (no Plando) work""" + self.assertEqual(MultiBosses.from_any("shuffle"), MultiBosses.option_shuffle) + self.assertNotEqual(MultiBosses.from_any("vanilla"), MultiBosses.option_shuffle) + + def testPlacement(self): + """Test that valid placements work and invalid placements fail""" + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-b1") + MultiBosses.from_any("l1-b2;l2-b1") + + def testMixed(self): + """Test that shuffle is applied for remaining locations""" + self.assertIn("shuffle", MultiBosses.from_any("l1-b2;l2-b1;shuffle").value) + self.assertIn("vanilla", MultiBosses.from_any("l1-b2;l2-b1").value) + + def testUnknown(self): + """Test that unknown values throw exceptions""" + # unknown boss + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-b0") + # unknown location + with self.assertRaises(ValueError): + MultiBosses.from_any("l0-b1") + # swapped boss-location + with self.assertRaises(ValueError): + MultiBosses.from_any("b2-b2") + # boss name in place of mode (no singularity) + with self.assertRaises(ValueError): + SingleBosses.from_any("b1") + with self.assertRaises(ValueError): + SingleBosses.from_any("l2-b2;b1") + # location name in place of mode + with self.assertRaises(ValueError): + MultiBosses.from_any("l1") + with self.assertRaises(ValueError): + MultiBosses.from_any("l2-b2;l1") + # mode name in place of location + with self.assertRaises(ValueError): + MultiBosses.from_any("shuffle-b2;vanilla") + with self.assertRaises(ValueError): + MultiBosses.from_any("shuffle-b2;l2-b2") + # mode name in place of boss + with self.assertRaises(ValueError): + MultiBosses.from_any("l2-shuffle;vanilla") + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-shuffle;l2-b2") + + def testOrder(self): + """Can't use mode in random places""" + with self.assertRaises(ValueError): + MultiBosses.from_any("shuffle;l2-b2") + + def testDuplicateBoss(self): + """Can place the same boss twice""" + MultiBosses.from_any("l1-b2;l2-b2") + with self.assertRaises(ValueError): + SingleBosses.from_any("l1-b2;l2-b2") + + def testDuplicateLocation(self): + """Can't use the same location twice""" + with self.assertRaises(ValueError): + MultiBosses.from_any("l1-b2;l1-b2") + + def testSingularity(self): + """Test automatic singularity mode""" + self.assertIn(";singularity", MultiBosses.from_any("b2").value) + + def testPlandoSettings(self): + """Test that plando settings verification works""" + plandoed_string = "l1-b2;l2-b1" + mixed_string = "l1-b2;shuffle" + regular_string = "shuffle" + plandoed = MultiBosses.from_any(plandoed_string) + mixed = MultiBosses.from_any(mixed_string) + regular = MultiBosses.from_any(regular_string) + + # plando should work with boss plando + plandoed.verify(None, "Player", Generate.PlandoSettings.bosses) + self.assertTrue(plandoed.value.startswith(plandoed_string)) + # plando should fall back to default without boss plando + plandoed.verify(None, "Player", Generate.PlandoSettings.items) + self.assertEqual(plandoed, MultiBosses.option_vanilla) + # mixed should fall back to mode + mixed.verify(None, "Player", Generate.PlandoSettings.items) # should produce a warning and still work + self.assertEqual(mixed, MultiBosses.option_shuffle) + # mode stuff should just work + regular.verify(None, "Player", Generate.PlandoSettings.items) + self.assertEqual(regular, MultiBosses.option_shuffle) diff --git a/test/options/__init__.py b/test/options/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 1c381b9a1c..870b3c7c2f 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -3,7 +3,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict from BaseClasses import Boss from Fill import FillError -from .Options import Bosses +from .Options import LTTPBosses as Bosses def BossFactory(boss: str, player: int) -> Optional[Boss]: diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index b13d99f1e7..de6c479bd3 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,7 +1,7 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice, PlandoBosses class Logic(Choice): @@ -138,7 +138,7 @@ class WorldState(Choice): option_inverted = 2 -class Bosses(TextChoice): +class LTTPBosses(PlandoBosses): """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. @@ -152,7 +152,9 @@ class Bosses(TextChoice): option_chaos = 3 option_singularity = 4 - bosses: set = { + duplicate_bosses = True + + bosses = { "Armos Knights", "Lanmolas", "Moldorm", @@ -165,7 +167,7 @@ class Bosses(TextChoice): "Trinexx", } - locations: set = { + locations = { "Ganons Tower Top", "Tower of Hera", "Skull Woods", @@ -181,99 +183,16 @@ class Bosses(TextChoice): "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}.") + def can_place_boss(cls, boss: str, location: str) -> bool: + from worlds.alttp.Bosses import can_place_boss + level = '' + words = location.split(" ") + if words[-1] in ("top", "middle", "bottom"): + level = words[-1] + location = " ".join(words[:-1]) + location = location.title().replace("Of", "of") + return can_place_boss(boss.title(), location, level) class Enemies(Choice): @@ -497,7 +416,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "hints": Hints, "scams": Scams, "restrict_dungeon_item_on_boss": RestrictBossItem, - "boss_shuffle": Bosses, + "boss_shuffle": LTTPBosses, "pot_shuffle": PotShuffle, "enemy_shuffle": EnemyShuffle, "killable_thieves": KillableThieves, From 0afb7096deecab237b87a6d000aa55ef03ed6891 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 12 Oct 2022 23:46:07 -0400 Subject: [PATCH 050/105] Core: improve sweep_for_events efficiency (#1092) --- BaseClasses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d32749f5f1..d91be28c77 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -689,14 +689,14 @@ class CollectionState(): def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None: if locations is None: locations = self.world.get_filled_locations() - new_locations = True + reachable_events = True # since the loop has a good chance to run more than once, only filter the events once locations = {location for location in locations if location.event and not key_only or getattr(location.item, "locked_dungeon_item", False)} - while new_locations: + while reachable_events: reachable_events = {location for location in locations if location.can_reach(self)} - new_locations = reachable_events - self.events - for event in new_locations: + locations -= reachable_events + for event in reachable_events: self.events.add(event) assert isinstance(event.item, Item), "tried to collect Event with no Item" self.collect(event.item, True, event) From 30a4bcbbbe83a6febe72765814e7d59cbece7f57 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 13 Oct 2022 01:45:52 -0400 Subject: [PATCH 051/105] [Pokemon Red and Blue] Initial implementation (#1016) --- .gitignore | 3 + Launcher.py | 2 + PokemonClient.py | 319 +++ README.md | 1 + Utils.py | 5 + data/lua/PKMN_RB/core.dll | Bin 0 -> 29184 bytes data/lua/PKMN_RB/json.lua | 389 ++++ data/lua/PKMN_RB/pkmn_rb.lua | 238 +++ data/lua/PKMN_RB/socket.lua | 132 ++ host.yaml | 8 + inno_setup.iss | 87 +- worlds/Files.py | 2 +- worlds/pokemon_rb/LICENSE | 21 + worlds/pokemon_rb/__init__.py | 254 +++ worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 0 -> 29178 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 0 -> 29339 bytes .../docs/en_Pokemon Red and Blue.md | 55 + worlds/pokemon_rb/docs/setup_en.md | 84 + worlds/pokemon_rb/items.py | 176 ++ worlds/pokemon_rb/locations.py | 1719 +++++++++++++++++ worlds/pokemon_rb/logic.py | 73 + worlds/pokemon_rb/options.py | 481 +++++ worlds/pokemon_rb/poke_data.py | 1212 ++++++++++++ worlds/pokemon_rb/regions.py | 305 +++ worlds/pokemon_rb/rom.py | 614 ++++++ worlds/pokemon_rb/rom_addresses.py | 588 ++++++ worlds/pokemon_rb/rules.py | 165 ++ worlds/pokemon_rb/text.py | 147 ++ 28 files changed, 7078 insertions(+), 2 deletions(-) create mode 100644 PokemonClient.py create mode 100644 data/lua/PKMN_RB/core.dll create mode 100644 data/lua/PKMN_RB/json.lua create mode 100644 data/lua/PKMN_RB/pkmn_rb.lua create mode 100644 data/lua/PKMN_RB/socket.lua create mode 100644 worlds/pokemon_rb/LICENSE create mode 100644 worlds/pokemon_rb/__init__.py create mode 100644 worlds/pokemon_rb/basepatch_blue.bsdiff4 create mode 100644 worlds/pokemon_rb/basepatch_red.bsdiff4 create mode 100644 worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md create mode 100644 worlds/pokemon_rb/docs/setup_en.md create mode 100644 worlds/pokemon_rb/items.py create mode 100644 worlds/pokemon_rb/locations.py create mode 100644 worlds/pokemon_rb/logic.py create mode 100644 worlds/pokemon_rb/options.py create mode 100644 worlds/pokemon_rb/poke_data.py create mode 100644 worlds/pokemon_rb/regions.py create mode 100644 worlds/pokemon_rb/rom.py create mode 100644 worlds/pokemon_rb/rom_addresses.py create mode 100644 worlds/pokemon_rb/rules.py create mode 100644 worlds/pokemon_rb/text.py diff --git a/.gitignore b/.gitignore index 8a7246210f..925a4bd0c7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ *.z64 *.n64 *.nes +*.gb +*.gbc +*.gba *.wixobj *.lck *.db3 diff --git a/Launcher.py b/Launcher.py index 9f9aaa4fb2..c24e0c819d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -145,6 +145,8 @@ components: Iterable[Component] = ( Component('OoT Adjuster', 'OoTAdjuster'), # FF1 Component('FF1 Client', 'FF1Client'), + # Pokémon + Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), # ChecksFinder Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 diff --git a/PokemonClient.py b/PokemonClient.py new file mode 100644 index 0000000000..2328243de5 --- /dev/null +++ b/PokemonClient.py @@ -0,0 +1,319 @@ +import asyncio +import json +import time +import os +import bsdiff4 +import subprocess +import zipfile +import hashlib +from asyncio import StreamReader, StreamWriter +from typing import List + + +import Utils +from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ + get_base_parser + +from worlds.pokemon_rb.locations import location_data + +location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}} +location_bytes_bits = {} +for location in location_data: + if location.ram_address is not None: + if type(location.ram_address) == list: + location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address + location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit}, + {'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}] + else: + location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address + location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit} + +SYSTEM_MESSAGE_ID = 0 + +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + +DISPLAY_MSGS = True + + +class GBCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_gb(self): + """Check Gameboy Connection State""" + if isinstance(self.ctx, GBContext): + logger.info(f"Gameboy Status: {self.ctx.gb_status}") + + +class GBContext(CommonContext): + command_processor = GBCommandProcessor + game = 'Pokemon Red and Blue' + items_handling = 0b101 + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.gb_streams: (StreamReader, StreamWriter) = None + self.gb_sync_task = None + self.messages = {} + self.locations_array = None + self.gb_status = CONNECTION_INITIAL_STATUS + self.awaiting_rom = False + self.display_msgs = True + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(GBContext, self).server_auth(password_requested) + if not self.auth: + self.awaiting_rom = True + logger.info('Awaiting connection to Bizhawk to get Player information') + return + + await self.send_connect() + + def _set_message(self, msg: str, msg_id: int): + if DISPLAY_MSGS: + self.messages[(time.time(), msg_id)] = msg + + def on_package(self, cmd: str, args: dict): + if cmd == 'Connected': + self.locations_array = None + elif cmd == "RoomInfo": + self.seed_name = args['seed_name'] + elif cmd == 'Print': + 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 run_gui(self): + from kvui import GameManager + + class GBManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Pokémon Client" + + self.ui = GBManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +def get_payload(ctx: GBContext): + current_time = time.time() + return json.dumps( + { + "items": [item.item for item in ctx.items_received], + "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() + if key[0] > current_time - 10} + } + ) + + +async def parse_locations(data: List, ctx: GBContext): + locations = [] + flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20], + "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]} + + # Check for clear problems + if len(flags['Rod']) > 1: + return + if flags["EventFlag"][1] + flags["EventFlag"][8] + flags["EventFlag"][9] + flags["EventFlag"][12] \ + + flags["EventFlag"][61] + flags["EventFlag"][62] + flags["EventFlag"][63] + flags["EventFlag"][64] \ + + flags["EventFlag"][65] + flags["EventFlag"][66] + flags["EventFlag"][67] + flags["EventFlag"][68] \ + + flags["EventFlag"][69] + flags["EventFlag"][70] != 0: + return + + for flag_type, loc_map in location_map.items(): + for flag, loc_id in loc_map.items(): + if flag_type == "list": + if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit'] + and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']): + locations.append(loc_id) + elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']: + locations.append(loc_id) + if flags["EventFlag"][280] & 1 and not ctx.finished_game: + await ctx.send_msgs([ + {"cmd": "StatusUpdate", + "status": 30} + ]) + ctx.finished_game = True + if locations == ctx.locations_array: + return + ctx.locations_array = locations + if locations is not None: + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}]) + + +async def gb_sync_task(ctx: GBContext): + logger.info("Starting GB connector. Use /gb for status information") + while not ctx.exit_event.is_set(): + error_status = None + if ctx.gb_streams: + (reader, writer) = ctx.gb_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + # Data will return a dict with up to two fields: + # 1. A keepalive response of the Players Name (always) + # 2. An array representing the memory values of the locations area (if in game) + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + #print(data_decoded) + + if ctx.seed_name and ctx.seed_name != bytes(data_decoded['seedName']).decode(): + msg = "The server is running a different multiworld than your client is. (invalid seed_name)" + logger.info(msg, extra={'compact_gui': True}) + ctx.gui_error('Error', msg) + error_status = CONNECTION_RESET_STATUS + ctx.seed_name = bytes(data_decoded['seedName']).decode() + if not ctx.auth: + ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) + if ctx.auth == '': + logger.info("Invalid ROM detected. No player name built into the ROM.") + if ctx.awaiting_rom: + await ctx.server_auth(False) + if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \ + and not error_status and ctx.auth: + # Not just a keep alive ping, parse + asyncio.create_task(parse_locations(data_decoded['locations'], ctx)) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.gb_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.gb_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.gb_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.gb_streams = None + if ctx.gb_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to Gameboy") + ctx.gb_status = CONNECTION_CONNECTED_STATUS + else: + ctx.gb_status = f"Was tentatively connected but error occured: {error_status}" + elif error_status: + ctx.gb_status = error_status + logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates") + else: + try: + logger.debug("Attempting to connect to Gameboy") + ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10) + ctx.gb_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.gb_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.gb_status = CONNECTION_REFUSED_STATUS + continue + + +async def run_game(romfile): + auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True) + if auto_start is True: + import webbrowser + webbrowser.open(romfile) + elif os.path.isfile(auto_start): + subprocess.Popen([auto_start, romfile], + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +async def patch_and_run_game(game_version, patch_file, ctx): + base_name = os.path.splitext(patch_file)[0] + comp_path = base_name + '.gb' + with open(Utils.local_path(Utils.get_options()["pokemon_rb_options"][f"{game_version}_rom_file"]), "rb") as stream: + base_rom = bytes(stream.read()) + try: + with open(Utils.local_path('lib', 'worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream: + base_patch = bytes(stream.read()) + except FileNotFoundError: + with open(Utils.local_path('worlds', 'pokemon_rb', f'basepatch_{game_version}.bsdiff4'), 'rb') as stream: + base_patch = bytes(stream.read()) + base_patched_rom_data = bsdiff4.patch(base_rom, base_patch) + basemd5 = hashlib.md5() + basemd5.update(base_patched_rom_data) + + with zipfile.ZipFile(patch_file, 'r') as patch_archive: + with patch_archive.open('delta.bsdiff4', 'r') as stream: + patch = stream.read() + patched_rom_data = bsdiff4.patch(base_patched_rom_data, patch) + + written_hash = patched_rom_data[0xFFCC:0xFFDC] + if written_hash == basemd5.digest(): + with open(comp_path, "wb") as patched_rom_file: + patched_rom_file.write(patched_rom_data) + + asyncio.create_task(run_game(comp_path)) + else: + msg = "Patch supplied was not generated with the same base patch version as this client. Patching failed." + logger.warning(msg) + ctx.gui_error('Error', msg) + + +if __name__ == '__main__': + + Utils.init_logging("PokemonClient") + + options = Utils.get_options() + + async def main(): + parser = get_base_parser() + parser.add_argument('patch_file', default="", type=str, nargs="?", + help='Path to an APRED or APBLUE patch file') + args = parser.parse_args() + + ctx = GBContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync") + + if args.patch_file: + ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower() + if ext == "apred": + logger.info("APRED file supplied, beginning patching process...") + asyncio.create_task(patch_and_run_game("red", args.patch_file, ctx)) + elif ext == "apblue": + logger.info("APBLUE file supplied, beginning patching process...") + asyncio.create_task(patch_and_run_game("blue", args.patch_file, ctx)) + else: + logger.warning(f"Unknown patch file extension {ext}") + + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.gb_sync_task: + await ctx.gb_sync_task + + + import colorama + + colorama.init() + + asyncio.run(main()) + colorama.deinit() diff --git a/README.md b/README.md index a82282037b..9615b0fba4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Currently, the following games are supported: * Donkey Kong Country 3 * Dark Souls 3 * Super Mario World +* Pokémon Red and Blue For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/Utils.py b/Utils.py index 707415453a..7df5ce5978 100644 --- a/Utils.py +++ b/Utils.py @@ -295,6 +295,11 @@ def get_default_options() -> OptionsType: "sni": "SNI", "rom_start": True, }, + "pokemon_rb_options": { + "red_rom_file": "Pokemon Red (UE) [S][!].gb", + "blue_rom_file": "Pokemon Blue (UE) [S][!].gb", + "rom_start": True + } } return options diff --git a/data/lua/PKMN_RB/core.dll b/data/lua/PKMN_RB/core.dll new file mode 100644 index 0000000000000000000000000000000000000000..3e9569571ab0947dcb7bcd789dc9c06c009d072d GIT binary patch literal 29184 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 B{xARl literal 0 HcmV?d00001 diff --git a/data/lua/PKMN_RB/json.lua b/data/lua/PKMN_RB/json.lua new file mode 100644 index 0000000000..a1f6e4ede2 --- /dev/null +++ b/data/lua/PKMN_RB/json.lua @@ -0,0 +1,389 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +function error(err) + print(err) +end + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + print("invalid table: sparse array") + print(n) + print("VAL:") + print(val) + print("STACK:") + print(stack) + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + --local line_count = 1 + --local col_count = 1 + --for i = 1, idx - 1 do + -- col_count = col_count + 1 + -- if str:sub(i, i) == "\n" then + -- line_count = line_count + 1 + -- col_count = 1 + -- end + -- end + -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + return ( parse(str, next_char(str, 1, space_chars, true)) ) +end + + +return json \ No newline at end of file diff --git a/data/lua/PKMN_RB/pkmn_rb.lua b/data/lua/PKMN_RB/pkmn_rb.lua new file mode 100644 index 0000000000..7518a5f12b --- /dev/null +++ b/data/lua/PKMN_RB/pkmn_rb.lua @@ -0,0 +1,238 @@ +local socket = require("socket") +local json = require('json') +local math = require('math') + +local STATE_OK = "Ok" +local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" +local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" +local STATE_UNINITIALIZED = "Uninitialized" + +local APIndex = 0x1A6E +local APItemAddress = 0x00FF +local EventFlagAddress = 0x1735 +local MissableAddress = 0x161A +local HiddenItemsAddress = 0x16DE +local RodAddress = 0x1716 +local InGame = 0x1A71 + +local ItemsReceived = nil +local playerName = nil +local seedName = nil + +local prevstate = "" +local curstate = STATE_UNINITIALIZED +local gbSocket = nil +local frame = 0 + +local u8 = nil +local wU8 = nil +local u16 + +--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded +local function defineMemoryFunctions() + local memDomain = {} + local domains = memory.getmemorydomainlist() + --if domains[1] == "System Bus" then + -- --NesHawk + -- isNesHawk = true + -- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + -- memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end + -- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + --elseif domains[1] == "WRAM" then + -- --QuickNES + -- memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + -- memDomain["saveram"] = function() memory.usememorydomain("WRAM") end + -- memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + --end + memDomain["rom"] = function() memory.usememorydomain("ROM") end + memDomain["wram"] = function() memory.usememorydomain("WRAM") end + return memDomain +end + +local memDomain = defineMemoryFunctions() +u8 = memory.read_u8 +wU8 = memory.write_u8 +u16 = memory.read_u16_le +function uRange(address, bytes) + data = memory.readbyterange(address - 1, bytes + 1) + data[0] = nil + return data +end + + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end + +function processBlock(block) + if block == nil then + return + end + local itemsBlock = block["items"] + memDomain.wram() + if itemsBlock ~= nil then-- and u8(0x116B) ~= 0x00 then + -- print(itemsBlock) + ItemsReceived = itemsBlock + + end +end + +function difference(a, b) + local aa = {} + for k,v in pairs(a) do aa[v]=true end + for k,v in pairs(b) do aa[v]=nil end + local ret = {} + local n = 0 + for k,v in pairs(a) do + if aa[v] then n=n+1 ret[n]=v end + end + return ret +end + +function generateLocationsChecked() + memDomain.wram() + events = uRange(EventFlagAddress, 0x140) + missables = uRange(MissableAddress, 0x20) + hiddenitems = uRange(HiddenItemsAddress, 0x0E) + rod = u8(RodAddress) + + data = {} + + table.foreach(events, function(k, v) table.insert(data, v) end) + table.foreach(missables, function(k, v) table.insert(data, v) end) + table.foreach(hiddenitems, function(k, v) table.insert(data, v) end) + table.insert(data, rod) + + return data +end +function generateSerialData() + memDomain.wram() + status = u8(0x1A73) + if status == 0 then + return nil + end + return uRange(0x1A76, u8(0x1A74)) +end +local function arrayEqual(a1, a2) + if #a1 ~= #a2 then + return false + end + + for i, v in ipairs(a1) do + if v ~= a2[i] then + return false + end + end + + return true +end + +function receive() + l, e = gbSocket:receive() + if e == 'closed' then + if curstate == STATE_OK then + print("Connection closed") + end + curstate = STATE_UNINITIALIZED + return + elseif e == 'timeout' then + --print("timeout") -- this keeps happening for some reason? just hide it + return + elseif e ~= nil then + print(e) + curstate = STATE_UNINITIALIZED + return + end + if l ~= nil then + processBlock(json.decode(l)) + end + -- Determine Message to send back + memDomain.rom() + newPlayerName = uRange(0xFFF0, 0x10) + newSeedName = uRange(0xFFDC, 20) + if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then + print("ROM changed, quitting") + curstate = STATE_UNINITIALIZED + return + end + playerName = newPlayerName + seedName = newSeedName + local retTable = {} + retTable["playerName"] = playerName + retTable["seedName"] = seedName + memDomain.wram() + if u8(InGame) == 0xAC then + retTable["locations"] = generateLocationsChecked() + serialData = generateSerialData() + if serialData ~= nil then + retTable["serial"] = serialData + end + end + msg = json.encode(retTable).."\n" + local ret, error = gbSocket:send(msg) + if ret == nil then + print(error) + elseif curstate == STATE_INITIAL_CONNECTION_MADE then + curstate = STATE_TENTATIVELY_CONNECTED + elseif curstate == STATE_TENTATIVELY_CONNECTED then + print("Connected!") + curstate = STATE_OK + end +end + +function main() + if (is23Or24Or25 or is26To28) == false then + print("Must use a version of bizhawk 2.3.1 or higher") + return + end + server, error = socket.bind('localhost', 17242) + + while true do + if not (curstate == prevstate) then + print("Current state: "..curstate) + prevstate = curstate + end + if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then + if (frame % 60 == 0) then + receive() + if u8(InGame) == 0xAC then + ItemIndex = u16(APIndex) + if ItemsReceived[ItemIndex + 1] ~= nil then + wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000) + end + end + end + elseif (curstate == STATE_UNINITIALIZED) then + if (frame % 60 == 0) then + + print("Waiting for client.") + + emu.frameadvance() + server:settimeout(2) + print("Attempting to connect") + local client, timeout = server:accept() + if timeout == nil then + -- print('Initial Connection Made') + curstate = STATE_INITIAL_CONNECTION_MADE + gbSocket = client + gbSocket:settimeout(0) + end + end + end + emu.frameadvance() + end +end + +main() diff --git a/data/lua/PKMN_RB/socket.lua b/data/lua/PKMN_RB/socket.lua new file mode 100644 index 0000000000..a98e952115 --- /dev/null +++ b/data/lua/PKMN_RB/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/host.yaml b/host.yaml index b114135520..f36f014af4 100644 --- a/host.yaml +++ b/host.yaml @@ -138,6 +138,14 @@ dkc3_options: # True for operating system default program # Alternatively, a path to a program to open the .sfc file with rom_start: true +pokemon_rb_options: + # File names of the Pokemon Red and Blue roms + red_rom_file: "Pokemon Red (UE) [S][!].gb" + blue_rom_file: "Pokemon Blue (UE) [S][!].gb" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program + # Alternatively, a path to a program to open the .gb file with + rom_start: true smw_options: # File name of the SMW US rom rom_file: "Super Mario World (USA).sfc" diff --git a/inno_setup.iss b/inno_setup.iss index 9a2a40444e..e097798ca4 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -59,6 +59,8 @@ Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: ful Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning +Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting +Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting Name: "server"; Description: "Server"; Types: full hosting Name: "client"; Description: "Clients"; Types: full playing Name: "client/sni"; Description: "SNI Client"; Types: full playing @@ -70,6 +72,9 @@ Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing +Name: "client/pkmn"; Description: "Pokemon Client" +Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576 +Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576 Name: "client/cf"; Description: "ChecksFinder"; Types: full playing Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing @@ -84,6 +89,8 @@ Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Countr Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot +Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r +Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp @@ -98,6 +105,7 @@ Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1 +Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2 Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall @@ -111,6 +119,7 @@ Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactor Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1 +Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2 @@ -121,6 +130,7 @@ Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\Archipela Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1 +Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2 @@ -179,6 +189,16 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archip Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot +Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/red +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/red +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/red +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/red + +Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/blue +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/blue +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/blue +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/blue + Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server @@ -234,6 +254,12 @@ var SoERomFilePage: TInputFileWizardPage; var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; +var redrom: string; +var RedROMFilePage: TInputFileWizardPage; + +var bluerom: string; +var BlueROMFilePage: TInputFileWizardPage; + function GetSNESMD5OfFile(const rom: string): string; var data: AnsiString; begin @@ -281,6 +307,21 @@ begin '.sfc'); end; +function AddGBRomPage(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'GB ROM files|*.gb;*.gbc|All files|*.*', + '.gb'); +end; + procedure AddOoTRomPage(); begin ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); @@ -425,6 +466,38 @@ begin Result := ''; end; +function GetRedROMPath(Param: string): string; +begin + if Length(redrom) > 0 then + Result := redrom + else if Assigned(RedRomFilePage) then + begin + R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc') + if R <> 0 then + MsgBox('Pokemon Red ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := RedROMFilePage.Values[0] + end + else + Result := ''; + end; + +function GetBlueROMPath(Param: string): string; +begin + if Length(bluerom) > 0 then + Result := bluerom + else if Assigned(BlueRomFilePage) then + begin + R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b') + if R <> 0 then + MsgBox('Pokemon Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := BlueROMFilePage.Values[0] + end + else + Result := ''; + end; + procedure InitializeWizard(); begin AddOoTRomPage(); @@ -448,6 +521,14 @@ begin soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); + + redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc'); + if Length(redrom) = 0 then + RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb'); + + bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b'); + if Length(redrom) = 0 then + BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb'); end; @@ -466,4 +547,8 @@ begin Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot')); -end; \ No newline at end of file + if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red')); + if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue')); +end; diff --git a/worlds/Files.py b/worlds/Files.py index 6f81a0e2ea..ac1acbf322 100644 --- a/worlds/Files.py +++ b/worlds/Files.py @@ -99,7 +99,7 @@ class APContainer: "player_name": self.player_name, "game": self.game, # minimum version of patch system expected for patching to be successful - "compatible_version": 4, + "compatible_version": 5, "version": current_patch_version, } diff --git a/worlds/pokemon_rb/LICENSE b/worlds/pokemon_rb/LICENSE new file mode 100644 index 0000000000..0dc1b2dcd0 --- /dev/null +++ b/worlds/pokemon_rb/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Alex "Alchav" Avery + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py new file mode 100644 index 0000000000..d4ca79ff9a --- /dev/null +++ b/worlds/pokemon_rb/__init__.py @@ -0,0 +1,254 @@ +from typing import TextIO +import os +import logging + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from Fill import fill_restrictive, FillError, sweep_from_pool +from ..AutoWorld import World, WebWorld +from ..generic.Rules import add_item_rule +from .items import item_table, item_groups +from .locations import location_data, PokemonRBLocation +from .regions import create_regions +from .logic import PokemonLogic +from .options import pokemon_rb_options +from .rom_addresses import rom_addresses +from .text import encode_text +from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\ + process_static_pokemon +from .rules import set_rules + +import worlds.pokemon_rb.poke_data as poke_data + + +class PokemonWebWorld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Pokemon Red and Blue with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["Alchav"] + )] + + +class PokemonRedBlueWorld(World): + """Pokémon Red and Pokémon Blue are the original monster-collecting turn-based RPGs. Explore the Kanto region with + your Pokémon, catch more than 150 unique creatures, earn badges from the region's Gym Leaders, and challenge the + Elite Four to become the champion!""" + # -MuffinJets#4559 + game = "Pokemon Red and Blue" + option_definitions = pokemon_rb_options + remote_items = False + data_version = 1 + topology_present = False + + item_name_to_id = {name: data.id for name, data in item_table.items()} + location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"} + item_name_groups = item_groups + + web = PokemonWebWorld() + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.fly_map = None + self.fly_map_code = None + self.extra_badges = {} + self.type_chart = None + self.local_poke_data = None + self.learnsets = None + self.trainer_name = None + self.rival_name = None + + @classmethod + def stage_assert_generate(cls, world): + versions = set() + for player in world.player_ids: + if world.worlds[player].game == "Pokemon Red and Blue": + versions.add(world.game_version[player].current_key) + for version in versions: + if not os.path.exists(get_base_rom_path(version)): + raise FileNotFoundError(get_base_rom_path(version)) + + def generate_early(self): + def encode_name(name, t): + try: + if len(encode_text(name)) > 7: + raise IndexError(f"{t} name too long for player {self.world.player_name[self.player]}. Must be 7 characters or fewer.") + return encode_text(name, length=8, whitespace="@", safety=True) + except KeyError as e: + raise KeyError(f"Invalid character(s) in {t} name for player {self.world.player_name[self.player]}") from e + self.trainer_name = encode_name(self.world.trainer_name[self.player].value, "Player") + self.rival_name = encode_name(self.world.rival_name[self.player].value, "Rival") + + if self.world.badges_needed_for_hm_moves[self.player].value >= 2: + badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"] + if self.world.badges_needed_for_hm_moves[self.player].value == 3: + badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", + "Soul Badge", "Volcano Badge", "Earth Badge"] + self.world.random.shuffle(badges) + badges_to_add += [badges.pop(), badges.pop()] + hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"] + self.world.random.shuffle(hm_moves) + self.extra_badges = {} + for badge in badges_to_add: + self.extra_badges[hm_moves.pop()] = badge + + process_pokemon_data(self) + + def create_items(self) -> None: + locations = [location for location in location_data if location.type == "Item"] + item_pool = [] + for location in locations: + if "Hidden" in location.name and not self.world.randomize_hidden_items[self.player].value: + continue + if "Rock Tunnel B1F" in location.region and not self.world.extra_key_items[self.player].value: + continue + if location.name == "Celadon City - Mansion Lady" and not self.world.tea[self.player].value: + continue + item = self.create_item(location.original_item) + if location.event: + self.world.get_location(location.name, self.player).place_locked_item(item) + elif ("Badge" not in item.name or self.world.badgesanity[self.player].value) and \ + (item.name != "Oak's Parcel" or self.world.old_man[self.player].value != 1): + item_pool.append(item) + self.world.random.shuffle(item_pool) + + self.world.itempool += item_pool + + + def pre_fill(self): + + process_wild_pokemon(self) + process_static_pokemon(self) + + if self.world.old_man[self.player].value == 1: + item = self.create_item("Oak's Parcel") + locations = [] + for location in self.world.get_locations(): + if location.player == self.player and location.item is None and location.can_reach(self.world.state) \ + and location.item_rule(item): + locations.append(location) + self.world.random.choice(locations).place_locked_item(item) + + + + if not self.world.badgesanity[self.player].value: + self.world.non_local_items[self.player].value -= self.item_name_groups["Badges"] + for i in range(5): + try: + badges = [] + badgelocs = [] + for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", + "Marsh Badge", "Volcano Badge", "Earth Badge"]: + badges.append(self.create_item(badge)) + for loc in ["Pewter Gym - Brock 1", "Cerulean Gym - Misty 1", "Vermilion Gym - Lt. Surge 1", + "Celadon Gym - Erika 1", "Fuchsia Gym - Koga 1", "Saffron Gym - Sabrina 1", + "Cinnabar Gym - Blaine 1", "Viridian Gym - Giovanni 1"]: + badgelocs.append(self.world.get_location(loc, self.player)) + state = self.world.get_all_state(False) + self.world.random.shuffle(badges) + self.world.random.shuffle(badgelocs) + fill_restrictive(self.world, state, badgelocs.copy(), badges, True, True) + except FillError: + for location in badgelocs: + location.item = None + continue + break + else: + raise FillError(f"Failed to place badges for player {self.player}") + + locs = [self.world.get_location("Fossil - Choice A", self.player), + self.world.get_location("Fossil - Choice B", self.player)] + for loc in locs: + add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"] + or i.name == "Master Ball") + + loc = self.world.get_location("Pallet Town - Player's PC", self.player) + if loc.item is None: + locs.append(loc) + + for loc in locs: + unplaced_items = [] + if loc.name in self.world.priority_locations[self.player].value: + add_item_rule(loc, lambda i: i.advancement) + for item in self.world.itempool: + if item.player == self.player and loc.item_rule(item): + self.world.itempool.remove(item) + state = sweep_from_pool(self.world.state, self.world.itempool + unplaced_items) + if state.can_reach(loc, "Location", self.player): + loc.place_locked_item(item) + break + else: + unplaced_items.append(item) + self.world.itempool += unplaced_items + + intervene = False + test_state = self.world.get_all_state(False) + if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player): + intervene = True + elif self.world.accessibility[self.player].current_key != "minimal": + if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player): + intervene = True + if intervene: + # the way this is handled will be improved significantly in the future when I add options to + # let you choose the exact weights for HM compatibility + logging.warning( + f"HM-compatible Pokémon possibly missing, placing Mew on Route 1 for player {self.player}") + loc = self.world.get_location("Route 1 - Wild Pokemon - 1", self.player) + loc.item = self.create_item("Mew") + + def create_regions(self): + if self.world.free_fly_location[self.player].value: + fly_map_code = self.world.random.randint(5, 9) + if fly_map_code == 9: + fly_map_code = 10 + if fly_map_code == 5: + fly_map_code = 4 + else: + fly_map_code = 0 + self.fly_map = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town", + "Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau", + "Saffron City"][fly_map_code] + self.fly_map_code = fly_map_code + create_regions(self.world, self.player) + self.world.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player) + + def set_rules(self): + set_rules(self.world, self.player) + + def create_item(self, name: str) -> Item: + return PokemonRBItem(name, self.player) + + def generate_output(self, output_directory: str): + generate_output(self, output_directory) + + def write_spoiler_header(self, spoiler_handle: TextIO): + if self.world.free_fly_location[self.player].value: + spoiler_handle.write('Fly unlocks: %s\n' % self.fly_map) + if self.extra_badges: + for hm_move, badge in self.extra_badges.items(): + spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n") + + def write_spoiler(self, spoiler_handle): + if self.world.randomize_type_matchup_types[self.player].value or \ + self.world.randomize_type_matchup_type_effectiveness[self.player].value: + spoiler_handle.write(f"\n\nType matchups ({self.world.player_name[self.player]}):\n\n") + for matchup in self.type_chart: + spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n") + + def get_filler_item_name(self) -> str: + return self.world.random.choice([item for item in item_table if item_table[item].classification in + [ItemClassification.filler, ItemClassification.trap]]) + + +class PokemonRBItem(Item): + game = "Pokemon Red and Blue" + type = None + + def __init__(self, name, player: int = None): + item_data = item_table[name] + super(PokemonRBItem, self).__init__( + name, + item_data.classification, + item_data.id, player + ) diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..c688ebede013ea52b66bb5629fc26a5a07b05719 GIT binary patch literal 29178 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

Sc+Mkq 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 n2Qpk6J

:` on the textfield on top and press enter (if the +server uses password, type in the bottom textfield `/connect
: [password]`) + +Now you are ready to start your adventure in Kanto. diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py new file mode 100644 index 0000000000..53722c02a4 --- /dev/null +++ b/worlds/pokemon_rb/items.py @@ -0,0 +1,176 @@ +from BaseClasses import ItemClassification +from .poke_data import pokemon_data + +class ItemData: + def __init__(self, id, classification, groups): + self.groups = groups + self.classification = classification + self.id = None if id is None else id + 172000000 + +item_table = { + "Master Ball": ItemData(1, ItemClassification.useful, ["Consumables", "Poke Balls"]), + "Ultra Ball": ItemData(2, ItemClassification.filler, ["Consumables", "Poke Balls"]), + "Great Ball": ItemData(3, ItemClassification.filler, ["Consumables", "Poke Balls"]), + "Poke Ball": ItemData(4, ItemClassification.filler, ["Consumables", "Poke Balls"]), + "Town Map": ItemData(5, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]), + "Bicycle": ItemData(6, ItemClassification.progression, ["Unique", "Key Items"]), + # "Flippers": ItemData(7, ItemClassification.progression), + #"Safari Ball": ItemData(8, ItemClassification.filler), + #"Pokedex": ItemData(9, ItemClassification.filler), + "Moon Stone": ItemData(10, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Antidote": ItemData(11, ItemClassification.filler, ["Consumables"]), + "Burn Heal": ItemData(12, ItemClassification.filler, ["Consumables"]), + "Ice Heal": ItemData(13, ItemClassification.filler, ["Consumables"]), + "Awakening": ItemData(14, ItemClassification.filler, ["Consumables"]), + "Paralyze Heal": ItemData(15, ItemClassification.filler, ["Consumables"]), + "Full Restore": ItemData(16, ItemClassification.filler, ["Consumables"]), + "Max Potion": ItemData(17, ItemClassification.filler, ["Consumables"]), + "Hyper Potion": ItemData(18, ItemClassification.filler, ["Consumables"]), + "Super Potion": ItemData(19, ItemClassification.filler, ["Consumables"]), + "Potion": ItemData(20, ItemClassification.filler, ["Consumables"]), + "Boulder Badge": ItemData(21, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Cascade Badge": ItemData(22, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Thunder Badge": ItemData(23, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Rainbow Badge": ItemData(24, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Soul Badge": ItemData(25, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Marsh Badge": ItemData(26, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Volcano Badge": ItemData(27, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Earth Badge": ItemData(28, ItemClassification.progression, ["Unique", "Key Items", "Badges"]), + "Escape Rope": ItemData(29, ItemClassification.filler, ["Consumables"]), + "Repel": ItemData(30, ItemClassification.filler, ["Consumables"]), + "Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]), + "Fire Stone": ItemData(32, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Thunder Stone": ItemData(33, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Water Stone": ItemData(34, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Iron": ItemData(37, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Carbos": ItemData(38, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Calcium": ItemData(39, ItemClassification.filler, ["Consumables", "Vitamins"]), + "Rare Candy": ItemData(40, ItemClassification.useful, ["Consumables"]), + "Dome Fossil": ItemData(41, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]), + "Helix Fossil": ItemData(42, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]), + "Secret Key": ItemData(43, ItemClassification.progression, ["Unique", "Key Items"]), + "Bike Voucher": ItemData(45, ItemClassification.progression, ["Unique", "Key Items"]), + "X Accuracy": ItemData(46, ItemClassification.filler, ["Consumables", "Battle Items"]), + "Leaf Stone": ItemData(47, ItemClassification.useful, ["Unique", "Evolution Stones"]), + "Card Key": ItemData(48, ItemClassification.progression, ["Unique", "Key Items"]), + "Nugget": ItemData(49, ItemClassification.filler, []), + #"Laptop": ItemData(50, ItemClassification.useful, ["Unique"]), + "Poke Doll": ItemData(51, ItemClassification.filler, ["Consumables"]), + "Full Heal": ItemData(52, ItemClassification.filler, ["Consumables"]), + "Revive": ItemData(53, ItemClassification.filler, ["Consumables"]), + "Max Revive": ItemData(54, ItemClassification.filler, ["Consumables"]), + "Guard Spec": ItemData(55, ItemClassification.filler, ["Consumables", "Battle Items"]), + "Super Repel": ItemData(56, ItemClassification.filler, ["Consumables"]), + "Max Repel": ItemData(57, ItemClassification.filler, ["Consumables"]), + "Dire Hit": ItemData(58, ItemClassification.filler, ["Consumables", "Battle Items"]), + #"Coin": ItemData(59, ItemClassification.filler), + "Fresh Water": ItemData(60, ItemClassification.filler, ["Consumables"]), + "Soda Pop": ItemData(61, ItemClassification.filler, ["Consumables"]), + "Lemonade": ItemData(62, ItemClassification.filler, ["Consumables"]), + "S.S. Ticket": ItemData(63, ItemClassification.progression, ["Unique", "Key Items"]), + "Gold Teeth": ItemData(64, ItemClassification.progression, ["Unique", "Key Items"]), + "X Attack": ItemData(65, ItemClassification.filler, ["Consumables", "Battle Items"]), + "X Defend": ItemData(66, ItemClassification.filler, ["Consumables", "Battle Items"]), + "X Speed": ItemData(67, ItemClassification.filler, ["Consumables", "Battle Items"]), + "X Special": ItemData(68, ItemClassification.filler, ["Consumables", "Battle Items"]), + "Coin Case": ItemData(69, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]), + "Oak's Parcel": ItemData(70, ItemClassification.progression, ["Unique", "Key Items"]), + "Item Finder": ItemData(71, ItemClassification.progression, ["Unique", "Key Items"]), + "Silph Scope": ItemData(72, ItemClassification.progression, ["Unique", "Key Items"]), + "Poke Flute": ItemData(73, ItemClassification.progression, ["Unique", "Key Items"]), + "Lift Key": ItemData(74, ItemClassification.progression, ["Unique", "Key Items"]), + "Exp. All": ItemData(75, ItemClassification.useful, ["Unique"]), + "Old Rod": ItemData(76, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]), + "Good Rod": ItemData(77, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]), + "Super Rod": ItemData(78, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]), + "PP Up": ItemData(79, ItemClassification.filler, ["Consumables"]), + "Ether": ItemData(80, ItemClassification.filler, ["Consumables"]), + "Max Ether": ItemData(81, ItemClassification.filler, ["Consumables"]), + "Elixir": ItemData(82, ItemClassification.filler, ["Consumables"]), + "Max Elixir": ItemData(83, ItemClassification.filler, ["Consumables"]), + "Tea": ItemData(84, ItemClassification.progression, ["Unique", "Key Items"]), + # "Master Sword": ItemData(85, ItemClassification.progression), + # "Flute": ItemData(86, ItemClassification.progression), + # "Titan's Mitt": ItemData(87, ItemClassification.progression), + # "Lamp": ItemData(88, ItemClassification.progression), + "Plant Key": ItemData(89, ItemClassification.progression, ["Unique", "Key Items"]), + "Mansion Key": ItemData(90, ItemClassification.progression, ["Unique", "Key Items"]), + "Hideout Key": ItemData(91, ItemClassification.progression, ["Unique", "Key Items"]), + "Safari Pass": ItemData(93, ItemClassification.progression, ["Unique", "Key Items"]), + "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs"]), + "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs"]), + "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs"]), + "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs"]), + "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs"]), + "TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]), + "TM02 Razor Wind": ItemData(202, ItemClassification.useful, ["Unique", "TMs"]), + "TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]), + "TM04 Whirlwind": ItemData(204, ItemClassification.filler, ["Unique", "TMs"]), + "TM05 Mega Kick": ItemData(205, ItemClassification.useful, ["Unique", "TMs"]), + "TM06 Toxic": ItemData(206, ItemClassification.useful, ["Unique", "TMs"]), + "TM07 Horn Drill": ItemData(207, ItemClassification.useful, ["Unique", "TMs"]), + "TM08 Body Slam": ItemData(208, ItemClassification.useful, ["Unique", "TMs"]), + "TM09 Take Down": ItemData(209, ItemClassification.useful, ["Unique", "TMs"]), + "TM10 Double Edge": ItemData(210, ItemClassification.useful, ["Unique", "TMs"]), + "TM11 Bubble Beam": ItemData(211, ItemClassification.useful, ["Unique", "TMs"]), + "TM12 Water Gun": ItemData(212, ItemClassification.useful, ["Unique", "TMs"]), + "TM13 Ice Beam": ItemData(213, ItemClassification.useful, ["Unique", "TMs"]), + "TM14 Blizzard": ItemData(214, ItemClassification.useful, ["Unique", "TMs"]), + "TM15 Hyper Beam": ItemData(215, ItemClassification.useful, ["Unique", "TMs"]), + "TM16 Pay Day": ItemData(216, ItemClassification.useful, ["Unique", "TMs"]), + "TM17 Submission": ItemData(217, ItemClassification.useful, ["Unique", "TMs"]), + "TM18 Counter": ItemData(218, ItemClassification.filler, ["Unique", "TMs"]), + "TM19 Seismic Toss": ItemData(219, ItemClassification.useful, ["Unique", "TMs"]), + "TM20 Rage": ItemData(220, ItemClassification.useful, ["Unique", "TMs"]), + "TM21 Mega Drain": ItemData(221, ItemClassification.useful, ["Unique", "TMs"]), + "TM22 Solar Beam": ItemData(222, ItemClassification.useful, ["Unique", "TMs"]), + "TM23 Dragon Rage": ItemData(223, ItemClassification.useful, ["Unique", "TMs"]), + "TM24 Thunderbolt": ItemData(224, ItemClassification.useful, ["Unique", "TMs"]), + "TM25 Thunder": ItemData(225, ItemClassification.useful, ["Unique", "TMs"]), + "TM26 Earthquake": ItemData(226, ItemClassification.useful, ["Unique", "TMs"]), + "TM27 Fissure": ItemData(227, ItemClassification.useful, ["Unique", "TMs"]), + "TM28 Dig": ItemData(228, ItemClassification.useful, ["Unique", "TMs"]), + "TM29 Psychic": ItemData(229, ItemClassification.useful, ["Unique", "TMs"]), + "TM30 Teleport": ItemData(230, ItemClassification.filler, ["Unique", "TMs"]), + "TM31 Mimic": ItemData(231, ItemClassification.useful, ["Unique", "TMs"]), + "TM32 Double Team": ItemData(232, ItemClassification.useful, ["Unique", "TMs"]), + "TM33 Reflect": ItemData(233, ItemClassification.useful, ["Unique", "TMs"]), + "TM34 Bide": ItemData(234, ItemClassification.filler, ["Unique", "TMs"]), + "TM35 Metronome": ItemData(235, ItemClassification.useful, ["Unique", "TMs"]), + "TM36 Self Destruct": ItemData(236, ItemClassification.useful, ["Unique", "TMs"]), + "TM37 Egg Bomb": ItemData(237, ItemClassification.useful, ["Unique", "TMs"]), + "TM38 Fire Blast": ItemData(238, ItemClassification.useful, ["Unique", "TMs"]), + "TM39 Swift": ItemData(239, ItemClassification.useful, ["Unique", "TMs"]), + "TM40 Skull Bash": ItemData(240, ItemClassification.filler, ["Unique", "TMs"]), + "TM41 Soft Boiled": ItemData(241, ItemClassification.useful, ["Unique", "TMs"]), + "TM42 Dream Eater": ItemData(242, ItemClassification.useful, ["Unique", "TMs"]), + "TM43 Sky Attack": ItemData(243, ItemClassification.filler, ["Unique", "TMs"]), + "TM44 Rest": ItemData(244, ItemClassification.useful, ["Unique", "TMs"]), + "TM45 Thunder Wave": ItemData(245, ItemClassification.useful, ["Unique", "TMs"]), + "TM46 Psywave": ItemData(246, ItemClassification.filler, ["Unique", "TMs"]), + "TM47 Explosion": ItemData(247, ItemClassification.useful, ["Unique", "TMs"]), + "TM48 Rock Slide": ItemData(248, ItemClassification.useful, ["Unique", "TMs"]), + "TM49 Tri Attack": ItemData(249, ItemClassification.useful, ["Unique", "TMs"]), + "TM50 Substitute": ItemData(250, ItemClassification.useful, ["Unique", "TMs"]), + + "Fuji Saved": ItemData(None, ItemClassification.progression, []), + "Silph Co Liberated": ItemData(None, ItemClassification.progression, []), + "Become Champion": ItemData(None, ItemClassification.progression, []) +} +item_table.update( + {pokemon: ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()} +) +item_table.update( + {f"Missable {pokemon}": ItemData(None, ItemClassification.useful, []) for pokemon in pokemon_data.keys()} +) +item_table.update( + {f"Static {pokemon}": ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()} +) + + +item_groups = {} +for item, data in item_table.items(): + for group in data.groups: + item_groups[group] = item_groups.get(group, []) + [item] \ No newline at end of file diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py new file mode 100644 index 0000000000..a619336f57 --- /dev/null +++ b/worlds/pokemon_rb/locations.py @@ -0,0 +1,1719 @@ + +from BaseClasses import Location +from .rom_addresses import rom_addresses +loc_id_start = 17200000 + +class LocationData: + def __init__(self, region, name, original_item, rom_address=None, ram_address=None, event=False, type="Item"): + self.region = region + if "Route" in region: + region = " ".join(region.split()[:2]) + self.name = region + " - " + name + self.original_item = original_item + self.rom_address = rom_address + self.ram_address = ram_address + self.event = event + self.type = type + +class EventFlag: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + +class Missable: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + +class Hidden: + def __init__(self, flag): + self.byte = int(flag / 8) + self.bit = flag % 8 + self.flag = flag + + +class Rod: + def __init__(self, flag): + self.byte = 0 + self.bit = flag + self.flag = flag + +# def get_locations(player=None): +location_data = [ + + LocationData("Vermilion City", "Fishing Guru", "Old Rod", rom_addresses["Rod_Vermilion_City_Fishing_Guru"], Rod(3)), + LocationData("Fuchsia City", "Fishing Guru's Brother", "Good Rod", rom_addresses["Rod_Fuchsia_City_Fishing_Brother"], Rod(4)), + LocationData("Route 12 South", "Fishing Guru's Brother", "Super Rod", rom_addresses["Rod_Route12_Fishing_Brother"], Rod(5)), + + LocationData("Pallet Town", "Player's PC", "Potion", rom_addresses['PC_Item'], EventFlag(1),), + LocationData("Celadon City", "Mansion Lady", "Tea", rom_addresses["Event_Mansion_Lady"], EventFlag(2)), + LocationData("Pallet Town", "Rival's Sister", "Town Map", rom_addresses["Event_Rivals_Sister"], EventFlag(24)), + LocationData("Pallet Town", "Oak's Post-Route-22-Rival Gift", "Poke Ball", rom_addresses["Event_Oaks_Gift"], EventFlag(36)), + LocationData("Route 1", "Free Sample Man", "Potion", rom_addresses["Event_Free_Sample"], EventFlag(960)), + LocationData("Viridian City", "Sleepy Guy", "TM42 Dream Eater", rom_addresses["Event_Sleepy_Guy"], + EventFlag(41)), + LocationData("Viridian City", "Pokemart", "Oak's Parcel", rom_addresses["Event_Pokemart_Quest"], + EventFlag(57)), + LocationData("Viridian Gym", "Giovanni 2", "TM27 Fissure", rom_addresses["Event_Viridian_Gym"], EventFlag(80)), + LocationData("Route 2 East", "Oak's Aide", "HM05 Flash", rom_addresses["Event_Route_2_Oaks_Aide"], + EventFlag(984)), + LocationData("Pewter City", "Museum", "Old Amber", rom_addresses["Event_Museum"], EventFlag(105)), + LocationData("Pewter Gym", "Brock 2", "TM34 Bide", rom_addresses["Event_Pewter_Gym"], EventFlag(118)), + LocationData("Cerulean City", "Bicycle Shop", "Bicycle", rom_addresses["Event_Bicycle_Shop"], EventFlag(192)), + LocationData("Cerulean Gym", "Misty 2", "TM11 Bubble Beam", rom_addresses["Event_Cerulean_Gym"], + EventFlag(190)), + LocationData("Route 24", "Nugget Bridge", "Nugget", rom_addresses["Event_Nugget_Bridge"], EventFlag(1344)), + LocationData("Route 25", "Bill", "S.S. Ticket", rom_addresses["Event_Bill"], EventFlag(1372)), + LocationData("Lavender Town", "Mr. Fuji", "Poke Flute", rom_addresses["Event_Fuji"], EventFlag(296)), + LocationData("Route 12 North", "Mourning Girl", "TM39 Swift", rom_addresses["Event_Mourning_Girl"], + EventFlag(1152)), + LocationData("Vermilion City", "Pokemon Fan Club", "Bike Voucher", rom_addresses["Event_Pokemon_Fan_Club"], + EventFlag(337)), + LocationData("Vermilion Gym", "Lt. Surge 2", "TM24 Thunderbolt", rom_addresses["Event_Vermillion_Gym"], + EventFlag(358)), + LocationData("S.S. Anne 2F", "Captain", "HM01 Cut", rom_addresses["Event_SS_Anne_Captain"], EventFlag(1504)), + LocationData("Route 11 East", "Oak's Aide", "Item Finder", rom_addresses["Event_Rt11_Oaks_Aide"], + EventFlag(1151)), + LocationData("Celadon City", "Stranded Man", "TM41 Soft Boiled", rom_addresses["Event_Stranded_Man"], + EventFlag(384)), + LocationData("Celadon City", "Thirsty Girl Gets Water", "TM13 Ice Beam", + rom_addresses["Event_Thirsty_Girl_Water"], EventFlag(396)), + LocationData("Celadon City", "Thirsty Girl Gets Soda Pop", "TM48 Rock Slide", + rom_addresses["Event_Thirsty_Girl_Soda"], EventFlag(397)), + LocationData("Celadon City", "Thirsty Girl Gets Lemonade", "TM49 Tri Attack", + rom_addresses["Event_Thirsty_Girl_Lemonade"], EventFlag(398)), + LocationData("Celadon City", "Counter Man", "TM18 Counter", rom_addresses["Event_Counter"], EventFlag(399)), + LocationData("Celadon City", "Gambling Addict", "Coin Case", rom_addresses["Event_Gambling_Addict"], + EventFlag(480)), + LocationData("Celadon Gym", "Erika 2", "TM21 Mega Drain", rom_addresses["Event_Celadon_Gym"], EventFlag(424)), + LocationData("Silph Co 11F", "Silph Co President", "Master Ball", rom_addresses["Event_Silph_Co_President"], + EventFlag(1933)), + LocationData("Silph Co 2F", "Woman", "TM36 Self Destruct", rom_addresses["Event_Scared_Woman"], + EventFlag(1791)), + LocationData("Route 16 North", "House Woman", "HM02 Fly", rom_addresses["Event_Rt16_House_Woman"], EventFlag(1230)), + LocationData("Route 15", "Oak's Aide", "Exp. All", rom_addresses["Event_Rt_15_Oaks_Aide"], EventFlag(1200)), + LocationData("Fuchsia City", "Safari Zone Warden", "HM04 Strength", rom_addresses["Event_Warden"], EventFlag(568)), + LocationData("Fuchsia Gym", "Koga 2", "TM06 Toxic", rom_addresses["Event_Fuschia_Gym"], EventFlag(600)), + LocationData("Safari Zone West", "Secret House", "HM03 Surf", rom_addresses["Event_Safari_Zone_Secret_House"], EventFlag(2176)), + LocationData("Cinnabar Island", "Lab Scientist", "TM35 Metronome", rom_addresses["Event_Lab_Scientist"], EventFlag(727)), + LocationData("Cinnabar Gym", "Blaine 2", "TM38 Fire Blast", rom_addresses["Event_Cinnabar_Gym"], + EventFlag(664)), + LocationData("Copycat's House", "Copycat", "TM31 Mimic", rom_addresses["Event_Copycat"], EventFlag(832)), + LocationData("Saffron City", "Mr. Psychic", "TM29 Psychic", rom_addresses["Event_Mr_Psychic"], EventFlag(944)), + LocationData("Saffron Gym", "Sabrina 2", "TM46 Psywave", rom_addresses["Event_Saffron_Gym"], EventFlag(864)), + LocationData("Fossil", "Choice A", "Dome Fossil", + [rom_addresses["Event_Dome_Fossil"], rom_addresses["Event_Dome_Fossil_B"], + rom_addresses["Dome_Fossil_Text"]], EventFlag(0x57E)), + LocationData("Fossil", "Choice B", "Helix Fossil", + [rom_addresses["Event_Helix_Fossil"], rom_addresses["Event_Helix_Fossil_B"], + rom_addresses["Helix_Fossil_Text"]], EventFlag(0x57F)), + + LocationData("Cerulean City", "Rocket Thief", "TM28 Dig", rom_addresses["Event_Rocket_Thief"], + Missable(6)), + LocationData("Route 2 East", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"], + Missable(25)), + LocationData("Route 2 East", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)), + LocationData("Route 4", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)), + LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)), + LocationData("Route 12 North", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)), + LocationData("Route 12 South", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)), + LocationData("Route 15", "Item", "TM20 Rage", rom_addresses["Missable_Route_15_Item"], Missable(32)), + LocationData("Route 24", "Item", "TM45 Thunder Wave", rom_addresses["Missable_Route_24_Item"], Missable(37)), + LocationData("Route 25", "Item", "TM19 Seismic Toss", rom_addresses["Missable_Route_25_Item"], Missable(38)), + LocationData("Viridian Gym", "Item", "Revive", rom_addresses["Missable_Viridian_Gym_Item"], Missable(51)), + LocationData("Cerulean Cave 1F", "Southwest Item", "Full Restore", rom_addresses["Missable_Cerulean_Cave_1F_Item_1"], + Missable(53)), + LocationData("Cerulean Cave 1F", "Northeast Item", "Max Elixir", rom_addresses["Missable_Cerulean_Cave_1F_Item_2"], + Missable(54)), + LocationData("Cerulean Cave 1F", "Northwest Item", "Nugget", rom_addresses["Missable_Cerulean_Cave_1F_Item_3"], + Missable(55)), + LocationData("Pokemon Tower 3F", "North Item", "Escape Rope", rom_addresses["Missable_Pokemon_Tower_3F_Item"], + Missable(57)), + LocationData("Pokemon Tower 4F", "East Item", "Elixir", rom_addresses["Missable_Pokemon_Tower_4F_Item_1"], + Missable(58)), + LocationData("Pokemon Tower 4F", "West Item", "Awakening", rom_addresses["Missable_Pokemon_Tower_4F_Item_2"], + Missable(59)), + LocationData("Pokemon Tower 4F", "South Item", "HP Up", rom_addresses["Missable_Pokemon_Tower_4F_Item_3"], + Missable(60)), + LocationData("Pokemon Tower 5F", "Southwest Item", "Nugget", rom_addresses["Missable_Pokemon_Tower_5F_Item"], + Missable(61)), + LocationData("Pokemon Tower 6F", "West Item", "Rare Candy", rom_addresses["Missable_Pokemon_Tower_6F_Item_1"], + Missable(62)), + LocationData("Pokemon Tower 6F", "Southeast Item", "X Accuracy", rom_addresses["Missable_Pokemon_Tower_6F_Item_2"], + Missable(63)), + LocationData("Fuchsia City", "Warden's House Item", "Rare Candy", rom_addresses["Missable_Wardens_House_Item"], + Missable(71)), + LocationData("Pokemon Mansion 1F", "North Item", "Escape Rope", + rom_addresses["Missable_Pokemon_Mansion_1F_Item_1"], Missable(72)), + LocationData("Pokemon Mansion 1F", "South Item", "Carbos", rom_addresses["Missable_Pokemon_Mansion_1F_Item_2"], + Missable(73)), + LocationData("Power Plant", "Southwest Item", "Carbos", rom_addresses["Missable_Power_Plant_Item_1"], Missable(86)), + LocationData("Power Plant", "North Item", "HP Up", rom_addresses["Missable_Power_Plant_Item_2"], Missable(87)), + LocationData("Power Plant", "Northeast Item", "Rare Candy", rom_addresses["Missable_Power_Plant_Item_3"], + Missable(88)), + LocationData("Power Plant", "Southeast Item", "TM25 Thunder", rom_addresses["Missable_Power_Plant_Item_4"], + Missable(89)), + LocationData("Power Plant", "South Item", "TM33 Reflect", rom_addresses["Missable_Power_Plant_Item_5"], + Missable(90)), + LocationData("Victory Road 2F", "Northeast Item", "TM17 Submission", rom_addresses["Missable_Victory_Road_2F_Item_1"], + Missable(92)), + LocationData("Victory Road 2F", "East Item", "Full Heal", rom_addresses["Missable_Victory_Road_2F_Item_2"], + Missable(93)), + LocationData("Victory Road 2F", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"], + Missable(94)), + LocationData("Victory Road 2F", "North Item Near Moltres", "Guard Spec", rom_addresses["Missable_Victory_Road_2F_Item_4"], + Missable(95)), + LocationData("Viridian Forest", "East Item", "Antidote", rom_addresses["Missable_Viridian_Forest_Item_1"], + Missable(100)), + LocationData("Viridian Forest", "Northwest Item", "Potion", rom_addresses["Missable_Viridian_Forest_Item_2"], + Missable(101)), + LocationData("Viridian Forest", "Southwest Item", "Poke Ball", + rom_addresses["Missable_Viridian_Forest_Item_3"], Missable(102)), + LocationData("Mt Moon 1F", "West Item", "Potion", rom_addresses["Missable_Mt_Moon_1F_Item_1"], Missable(103)), + LocationData("Mt Moon 1F", "Northwest Item", "Moon Stone", rom_addresses["Missable_Mt_Moon_1F_Item_2"], Missable(104)), + LocationData("Mt Moon 1F", "Southeast Item", "Rare Candy", rom_addresses["Missable_Mt_Moon_1F_Item_3"], Missable(105)), + LocationData("Mt Moon 1F", "East Item", "Escape Rope", rom_addresses["Missable_Mt_Moon_1F_Item_4"], + Missable(106)), + LocationData("Mt Moon 1F", "South Item", "Potion", rom_addresses["Missable_Mt_Moon_1F_Item_5"], Missable(107)), + LocationData("Mt Moon 1F", "Southwest Item", "TM12 Water Gun", rom_addresses["Missable_Mt_Moon_1F_Item_6"], + Missable(108)), + LocationData("Mt Moon B2F", "South Item", "HP Up", rom_addresses["Missable_Mt_Moon_B2F_Item_1"], Missable(111)), + LocationData("Mt Moon B2F", "North Item", "TM01 Mega Punch", rom_addresses["Missable_Mt_Moon_B2F_Item_2"], + Missable(112)), + LocationData("S.S. Anne 1F", "Item", "TM08 Body Slam", rom_addresses["Missable_SS_Anne_1F_Item"], + Missable(114)), + LocationData("S.S. Anne 2F", "Item 1", "Max Ether", rom_addresses["Missable_SS_Anne_2F_Item_1"], + Missable(115)), + LocationData("S.S. Anne 2F", "Item 2", "Rare Candy", rom_addresses["Missable_SS_Anne_2F_Item_2"], + Missable(116)), + LocationData("S.S. Anne B1F", "Item 1", "Ether", rom_addresses["Missable_SS_Anne_B1F_Item_1"], Missable(117)), + LocationData("S.S. Anne B1F", "Item 2", "TM44 Rest", rom_addresses["Missable_SS_Anne_B1F_Item_2"], + Missable(118)), + LocationData("S.S. Anne B1F", "Item 3", "Max Potion", rom_addresses["Missable_SS_Anne_B1F_Item_3"], + Missable(119)), + LocationData("Victory Road 3F", "Northeast Item", "Max Revive", rom_addresses["Missable_Victory_Road_3F_Item_1"], + Missable(120)), + LocationData("Victory Road 3F", "Northwest Item", "TM47 Explosion", rom_addresses["Missable_Victory_Road_3F_Item_2"], + Missable(121)), + LocationData("Rocket Hideout B1F", "West Item", "Escape Rope", + rom_addresses["Missable_Rocket_Hideout_B1F_Item_1"], Missable(123)), + LocationData("Rocket Hideout B1F", "Southwest Item", "Hyper Potion", + rom_addresses["Missable_Rocket_Hideout_B1F_Item_2"], Missable(124)), + LocationData("Rocket Hideout B2F", "Northwest Left Item", "Moon Stone", rom_addresses["Missable_Rocket_Hideout_B2F_Item_1"], + Missable(125)), + LocationData("Rocket Hideout B2F", "Northeast Item", "Nugget", rom_addresses["Missable_Rocket_Hideout_B2F_Item_2"], + Missable(126)), + LocationData("Rocket Hideout B2F", "Northwest Right Item", "TM07 Horn Drill", + rom_addresses["Missable_Rocket_Hideout_B2F_Item_3"], Missable(127)), + LocationData("Rocket Hideout B2F", "Southwest Item", "Super Potion", + rom_addresses["Missable_Rocket_Hideout_B2F_Item_4"], Missable(128)), + LocationData("Rocket Hideout B3F", "East Item", "TM10 Double Edge", + rom_addresses["Missable_Rocket_Hideout_B3F_Item_1"], Missable(129)), + LocationData("Rocket Hideout B3F", "Center Item", "Rare Candy", rom_addresses["Missable_Rocket_Hideout_B3F_Item_2"], + Missable(130)), + LocationData("Rocket Hideout B4F", "West Item", "HP Up", rom_addresses["Missable_Rocket_Hideout_B4F_Item_1"], + Missable(132)), + LocationData("Rocket Hideout B4F", "Northwest Item", "TM02 Razor Wind", + rom_addresses["Missable_Rocket_Hideout_B4F_Item_2"], Missable(133)), + LocationData("Rocket Hideout B4F", "Southwest Item (Lift Key)", "Iron", rom_addresses["Missable_Rocket_Hideout_B4F_Item_3"], + Missable(134)), + LocationData("Rocket Hideout B4F", "Giovanni Item (Lift Key)", "Silph Scope", + rom_addresses["Missable_Rocket_Hideout_B4F_Item_4"], [EventFlag(0x6A7), Missable(135)]), + LocationData("Rocket Hideout B4F", "Rocket Grunt Item", "Lift Key", rom_addresses["Missable_Rocket_Hideout_B4F_Item_5"], + [EventFlag(0x6A6), Missable(136)]), + LocationData("Silph Co 3F", "Item (Card Key)", "Hyper Potion", rom_addresses["Missable_Silph_Co_3F_Item"], Missable(144)), + LocationData("Silph Co 4F", "Left Item (Card Key)", "Full Heal", rom_addresses["Missable_Silph_Co_4F_Item_1"], + Missable(148)), + LocationData("Silph Co 4F", "Middle Item (Card Key)", "Max Revive", rom_addresses["Missable_Silph_Co_4F_Item_2"], + Missable(149)), + LocationData("Silph Co 4F", "Right Item (Card Key)", "Escape Rope", rom_addresses["Missable_Silph_Co_4F_Item_3"], + Missable(150)), + LocationData("Silph Co 5F", "Southwest Item", "TM09 Take Down", rom_addresses["Missable_Silph_Co_5F_Item_1"], + Missable(155)), + LocationData("Silph Co 5F", "Northwest Item (Card Key)", "Protein", rom_addresses["Missable_Silph_Co_5F_Item_2"], Missable(156)), + LocationData("Silph Co 5F", "Southeast Item", "Card Key", rom_addresses["Missable_Silph_Co_5F_Item_3"], Missable(157)), + LocationData("Silph Co 6F", "West Item (Card Key)", "HP Up", rom_addresses["Missable_Silph_Co_6F_Item_1"], Missable(161)), + LocationData("Silph Co 6F", "Southwest Item (Card Key)", "X Accuracy", rom_addresses["Missable_Silph_Co_6F_Item_2"], + Missable(162)), + LocationData("Silph Co 7F", "West Item", "Calcium", rom_addresses["Missable_Silph_Co_7F_Item_1"], Missable(168)), + LocationData("Silph Co 7F", "East Item (Card Key)", "TM03 Swords Dance", rom_addresses["Missable_Silph_Co_7F_Item_2"], + Missable(169)), + LocationData("Silph Co 10F", "Left Item", "TM26 Earthquake", rom_addresses["Missable_Silph_Co_10F_Item_1"], + Missable(180)), + LocationData("Silph Co 10F", "Bottom Item", "Rare Candy", rom_addresses["Missable_Silph_Co_10F_Item_2"], + Missable(181)), + LocationData("Silph Co 10F", "Right Item", "Carbos", rom_addresses["Missable_Silph_Co_10F_Item_3"], Missable(182)), + LocationData("Pokemon Mansion 2F", "Northeast Item", "Calcium", rom_addresses["Missable_Pokemon_Mansion_2F_Item"], + Missable(187)), + LocationData("Pokemon Mansion 3F", "Southwest Item", "Max Potion", rom_addresses["Missable_Pokemon_Mansion_3F_Item_1"], + Missable(188)), + LocationData("Pokemon Mansion 3F", "Northeast Item", "Iron", rom_addresses["Missable_Pokemon_Mansion_3F_Item_2"], + Missable(189)), + LocationData("Pokemon Mansion B1F", "North Item", "Rare Candy", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_1"], Missable(190)), + LocationData("Pokemon Mansion B1F", "Southwest Item", "Full Restore", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_2"], Missable(191)), + LocationData("Pokemon Mansion B1F", "South Item", "TM14 Blizzard", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_3"], Missable(192)), + LocationData("Pokemon Mansion B1F", "Northwest Item", "TM22 Solar Beam", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_4"], Missable(193)), + LocationData("Pokemon Mansion B1F", "West Item", "Secret Key", + rom_addresses["Missable_Pokemon_Mansion_B1F_Item_5"], Missable(194)), + LocationData("Safari Zone East", "Northeast Item", "Full Restore", rom_addresses["Missable_Safari_Zone_East_Item_1"], + Missable(195)), + LocationData("Safari Zone East", "West Item", "Max Potion", rom_addresses["Missable_Safari_Zone_East_Item_2"], + Missable(196)), + LocationData("Safari Zone East", "East Item", "Carbos", rom_addresses["Missable_Safari_Zone_East_Item_3"], + Missable(197)), + LocationData("Safari Zone East", "Center Item", "TM37 Egg Bomb", rom_addresses["Missable_Safari_Zone_East_Item_4"], + Missable(198)), + LocationData("Safari Zone North", "Northeast Item", "Protein", rom_addresses["Missable_Safari_Zone_North_Item_1"], + Missable(199)), + LocationData("Safari Zone North", "North Item", "TM40 Skull Bash", + rom_addresses["Missable_Safari_Zone_North_Item_2"], Missable(200)), + LocationData("Safari Zone West", "Southwest Item", "Max Potion", rom_addresses["Missable_Safari_Zone_West_Item_1"], + Missable(201)), + LocationData("Safari Zone West", "Northwest Item", "TM32 Double Team", + rom_addresses["Missable_Safari_Zone_West_Item_2"], Missable(202)), + LocationData("Safari Zone West", "Southeast Item", "Max Revive", rom_addresses["Missable_Safari_Zone_West_Item_3"], + Missable(203)), + LocationData("Safari Zone West", "Northeast Item", "Gold Teeth", rom_addresses["Missable_Safari_Zone_West_Item_4"], + Missable(204)), + LocationData("Safari Zone Center", "Island Item", "Nugget", rom_addresses["Missable_Safari_Zone_Center_Item"], + Missable(205)), + LocationData("Cerulean Cave 2F", "East Item", "PP Up", rom_addresses["Missable_Cerulean_Cave_2F_Item_1"], + Missable(206)), + LocationData("Cerulean Cave 2F", "Southwest Item", "Ultra Ball", rom_addresses["Missable_Cerulean_Cave_2F_Item_2"], + Missable(207)), + LocationData("Cerulean Cave 2F", "North Item", "Full Restore", rom_addresses["Missable_Cerulean_Cave_2F_Item_3"], + Missable(208)), + LocationData("Cerulean Cave B1F", "Center Item", "Ultra Ball", rom_addresses["Missable_Cerulean_Cave_B1F_Item_1"], + Missable(210)), + LocationData("Cerulean Cave B1F", "North Item", "Max Revive", rom_addresses["Missable_Cerulean_Cave_B1F_Item_2"], + Missable(211)), + LocationData("Victory Road 1F", "Top Item", "TM43 Sky Attack", rom_addresses["Missable_Victory_Road_1F_Item_1"], + Missable(212)), + LocationData("Victory Road 1F", "Left Item", "Rare Candy", rom_addresses["Missable_Victory_Road_1F_Item_2"], + Missable(213)), + LocationData("Rock Tunnel B1F", "Southwest Item", "Hideout Key", rom_addresses["Missable_Rock_Tunnel_B1F_Item_1"], + Missable(231)), + LocationData("Rock Tunnel B1F", "West Item", "Mansion Key", rom_addresses["Missable_Rock_Tunnel_B1F_Item_2"], + Missable(232)), + LocationData("Rock Tunnel B1F", "Northwest Item", "Plant Key", rom_addresses["Missable_Rock_Tunnel_B1F_Item_3"], + Missable(233)), + LocationData("Rock Tunnel B1F", "North Item", "Safari Pass", rom_addresses["Missable_Rock_Tunnel_B1F_Item_4"], + Missable(234)), + + LocationData("Pewter Gym", "Brock 1", "Boulder Badge", rom_addresses['Badge_Pewter_Gym'], EventFlag(0x8A0)), + LocationData("Cerulean Gym", "Misty 1", "Cascade Badge", rom_addresses['Badge_Cerulean_Gym'], EventFlag(0x8A1)), + LocationData("Vermilion Gym", "Lt. Surge 1", "Thunder Badge", rom_addresses['Badge_Vermilion_Gym'], EventFlag(0x8A2)), + LocationData("Celadon Gym", "Erika 1", "Rainbow Badge", rom_addresses['Badge_Celadon_Gym'], EventFlag(0x8A3)), + LocationData("Fuchsia Gym", "Koga 1", "Soul Badge", rom_addresses['Badge_Fuchsia_Gym'], EventFlag(0x8A4)), + LocationData("Saffron Gym", "Sabrina 1", "Marsh Badge", rom_addresses['Badge_Saffron_Gym'], EventFlag(0x8A5)), + LocationData("Cinnabar Gym", "Blaine 1", "Volcano Badge", rom_addresses['Badge_Cinnabar_Gym'], EventFlag(0x8A6)), + LocationData("Viridian Gym", "Giovanni 1", "Earth Badge", rom_addresses['Badge_Viridian_Gym'], EventFlag(0x8A7)), + + LocationData("Viridian Forest", "Hidden Item Northwest by Trainer", "Potion", rom_addresses['Hidden_Item_Viridian_Forest_1'], Hidden(0)), + LocationData("Viridian Forest", "Hidden Item Entrance Tree", "Antidote", rom_addresses['Hidden_Item_Viridian_Forest_2'], Hidden(1)), + LocationData("Mt Moon B2F", "Hidden Item Dead End Before Fossils", "Moon Stone", rom_addresses['Hidden_Item_MtMoonB2F_1'], Hidden(2)), + LocationData("Route 25", "Hidden Item Fence Outside Bill's House", "Ether", rom_addresses['Hidden_Item_Route_25_1'], Hidden(3)), + LocationData("Route 9", "Hidden Item Rock By Grass", "Ether", rom_addresses['Hidden_Item_Route_9'], Hidden(4)), + LocationData("S.S. Anne 1F", "Hidden Item Kitchen Trash", "Great Ball", rom_addresses['Hidden_Item_SS_Anne_Kitchen'], Hidden(5)), + LocationData("S.S. Anne B1F", "Hidden Item Under Pillow", "Hyper Potion", rom_addresses['Hidden_Item_SS_Anne_B1F'], Hidden(6)), + LocationData("Route 10 North", "Hidden Item Behind Rock Tunnel Entrance Tree", "Super Potion", rom_addresses['Hidden_Item_Route_10_1'], Hidden(7)), + LocationData("Route 10 South", "Hidden Item Rock", "Max Ether", rom_addresses['Hidden_Item_Route_10_2'], Hidden(8)), + LocationData("Rocket Hideout B1F", "Hidden Item Pot Plant", "PP Up", rom_addresses['Hidden_Item_Rocket_Hideout_B1F'], Hidden(9)), + LocationData("Rocket Hideout B3F", "Hidden Item Near East Item", "Nugget", rom_addresses['Hidden_Item_Rocket_Hideout_B3F'], Hidden(10)), + LocationData("Rocket Hideout B4F", "Hidden Item Behind Giovanni", "Super Potion", rom_addresses['Hidden_Item_Rocket_Hideout_B4F'], Hidden(11)), + LocationData("Pokemon Tower 5F", "Hidden Item Near West Staircase", "Elixir", rom_addresses['Hidden_Item_Pokemon_Tower_5F'], Hidden(12)), + LocationData("Route 13", "Hidden Item Dead End Boulder", "PP Up", rom_addresses['Hidden_Item_Route_13_1'], Hidden(13)), + LocationData("Route 13", "Hidden Item Dead End By Water Corner", "Calcium", rom_addresses['Hidden_Item_Route_13_2'], Hidden(14)), + LocationData("Pokemon Mansion B1F", "Hidden Item Secret Key Room Corner", "Rare Candy", rom_addresses['Hidden_Item_Pokemon_Mansion_B1F'], Hidden(15)), + LocationData("Safari Zone West", "Hidden Item Secret House Statue", "Revive", rom_addresses['Hidden_Item_Safari_Zone_West'], Hidden(17)), + LocationData("Silph Co 5F", "Hidden Item Pot Plant", "Elixir", rom_addresses['Hidden_Item_Silph_Co_5F'], Hidden(18)), + LocationData("Silph Co 9F", "Hidden Item Nurse Bed", "Max Potion", rom_addresses['Hidden_Item_Silph_Co_9F'], Hidden(19)), + LocationData("Copycat's House", "Hidden Item Desk", "Nugget", rom_addresses['Hidden_Item_Copycats_House'], Hidden(20)), + LocationData("Cerulean Cave 1F", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21)), + LocationData("Cerulean Cave B1F", "Hidden Item Northeast Rocks", "Ultra Ball", rom_addresses['Hidden_Item_Cerulean_Cave_B1F'], Hidden(22)), + LocationData("Power Plant", "Hidden Item Central Dead End", "Max Elixir", rom_addresses['Hidden_Item_Power_Plant_1'], Hidden(23)), + LocationData("Power Plant", "Hidden Item Before Zapdos", "PP Up", rom_addresses['Hidden_Item_Power_Plant_2'], Hidden(24)), + LocationData("Seafoam Islands B2F", "Hidden Item Rock", "Nugget", rom_addresses['Hidden_Item_Seafoam_Islands_B2F'], Hidden(25)), + LocationData("Seafoam Islands B4F", "Hidden Item Corner Island", "Ultra Ball", rom_addresses['Hidden_Item_Seafoam_Islands_B4F'], Hidden(26)), + LocationData("Pokemon Mansion 1F", "Hidden Item Block Near Entrance Carpet", "Moon Stone", rom_addresses['Hidden_Item_Pokemon_Mansion_1F'], Hidden(27)), + LocationData("Pokemon Mansion 3F", "Hidden Item Behind Burglar", "Max Revive", rom_addresses['Hidden_Item_Pokemon_Mansion_3F'], Hidden(28)), + LocationData("Route 23 North", "Hidden Item Rocks Before Final Guard", "Full Restore", rom_addresses['Hidden_Item_Route_23_1'], Hidden(29)), + LocationData("Route 23 North", "Hidden Item East Tree After Water", "Ultra Ball", rom_addresses['Hidden_Item_Route_23_2'], Hidden(30)), + LocationData("Route 23 South", "Hidden Item On Island", "Max Ether", rom_addresses['Hidden_Item_Route_23_3'], Hidden(31)), + LocationData("Victory Road 2F", "Hidden Item Rock Before Moltres", "Ultra Ball", rom_addresses['Hidden_Item_Victory_Road_2F_1'], Hidden(32)), + LocationData("Victory Road 2F", "Hidden Item Rock In Final Room", "Full Restore", rom_addresses['Hidden_Item_Victory_Road_2F_2'], Hidden(33)), + #LocationData("Vermilion City", "Hidden Item The Truck", "Max Elixir", rom_addresses['Hidden_Item_Unused_6F'], Hidden(34)), + LocationData("Viridian City", "Hidden Item Cuttable Tree", "Potion", rom_addresses['Hidden_Item_Viridian_City'], Hidden(35)), + LocationData("Route 11", "Hidden Item Isolated Tree Near Gate", "Potion", rom_addresses['Hidden_Item_Route_11'], Hidden(36)), + LocationData("Route 12 West", "Hidden Item Tree Near Gate", "Hyper Potion", rom_addresses['Hidden_Item_Route_12'], Hidden(37)), + LocationData("Route 17", "Hidden Item In Grass", "Rare Candy", rom_addresses['Hidden_Item_Route_17_1'], Hidden(38)), + LocationData("Route 17", "Hidden Item Near Northernmost Sign", "Full Restore", rom_addresses['Hidden_Item_Route_17_2'], Hidden(39)), + LocationData("Route 17", "Hidden Item East Center", "PP Up", rom_addresses['Hidden_Item_Route_17_3'], Hidden(40)), + LocationData("Route 17", "Hidden Item West Center", "Max Revive", rom_addresses['Hidden_Item_Route_17_4'], Hidden(41)), + LocationData("Route 17", "Hidden Item Before Final Bridge", "Max Elixir", rom_addresses['Hidden_Item_Route_17_5'], Hidden(42)), + LocationData("Underground Tunnel North-South", "Hidden Item Near Northern Stairs", "Full Restore", rom_addresses['Hidden_Item_Underground_Path_NS_1'], Hidden(43)), + LocationData("Underground Tunnel North-South", "Hidden Item Near Southern Stairs", "X Special", rom_addresses['Hidden_Item_Underground_Path_NS_2'], Hidden(44)), + LocationData("Underground Tunnel West-East", "Hidden Item West", "Nugget", rom_addresses['Hidden_Item_Underground_Path_WE_1'], Hidden(45)), + LocationData("Underground Tunnel West-East", "Hidden Item East", "Elixir", rom_addresses['Hidden_Item_Underground_Path_WE_2'], Hidden(46)), + LocationData("Celadon City", "Hidden Item Dead End Near Cuttable Tree", "PP Up", rom_addresses['Hidden_Item_Celadon_City'], Hidden(47)), + LocationData("Route 25", "Hidden Item Northeast Of Grass", "Elixir", rom_addresses['Hidden_Item_Route_25_2'], Hidden(48)), + LocationData("Mt Moon B2F", "Hidden Item Lone Rock", "Ether", rom_addresses['Hidden_Item_MtMoonB2F_2'], Hidden(49)), + LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50)), + LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51)), + LocationData("Cerulean City", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52)), + LocationData("Route 4", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53)), + + LocationData("Indigo Plateau", "Become Champion", "Become Champion", event=True), + LocationData("Pokemon Tower 7F", "Fuji Saved", "Fuji Saved", event=True), + LocationData("Silph Co 11F", "Silph Co Liberated", "Silph Co Liberated", event=True), + + LocationData("Pallet Town", "Super Rod Pokemon - 1", "Tentacool", rom_addresses["Wild_Super_Rod_A"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pallet Town", "Super Rod Pokemon - 2", "Poliwag", rom_addresses["Wild_Super_Rod_A"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 22", "Super Rod Pokemon - 1", "Goldeen", rom_addresses["Wild_Super_Rod_B"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 22", "Super Rod Pokemon - 2", "Poliwag", rom_addresses["Wild_Super_Rod_B"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Super Rod Pokemon - 1", "Psyduck", rom_addresses["Wild_Super_Rod_C"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Super Rod Pokemon - 2", "Goldeen", rom_addresses["Wild_Super_Rod_C"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Super Rod Pokemon - 3", "Krabby", rom_addresses["Wild_Super_Rod_C"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Super Rod Pokemon - 1", "Krabby", rom_addresses["Wild_Super_Rod_D"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Super Rod Pokemon - 2", "Shellder", rom_addresses["Wild_Super_Rod_D"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Super Rod Pokemon - 1", "Poliwhirl", rom_addresses["Wild_Super_Rod_E"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Super Rod Pokemon - 2", "Slowpoke", rom_addresses["Wild_Super_Rod_E"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 1", "Dratini", rom_addresses["Wild_Super_Rod_F"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 2", "Krabby", rom_addresses["Wild_Super_Rod_F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 3", "Psyduck", rom_addresses["Wild_Super_Rod_F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Super Rod Pokemon - 4", "Slowpoke", rom_addresses["Wild_Super_Rod_F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 1", "Tentacool", rom_addresses["Wild_Super_Rod_G"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 2", "Krabby", rom_addresses["Wild_Super_Rod_G"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 3", "Goldeen", rom_addresses["Wild_Super_Rod_G"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 North", "Super Rod Pokemon - 4", "Magikarp", rom_addresses["Wild_Super_Rod_G"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 1", "Staryu", rom_addresses["Wild_Super_Rod_H"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 2", "Horsea", rom_addresses["Wild_Super_Rod_H"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 3", "Shellder", rom_addresses["Wild_Super_Rod_H"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Route 19", "Super Rod Pokemon - 4", "Goldeen", rom_addresses["Wild_Super_Rod_H"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 1", "Slowbro", rom_addresses["Wild_Super_Rod_I"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 2", "Seaking", rom_addresses["Wild_Super_Rod_I"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 3", "Kingler", rom_addresses["Wild_Super_Rod_I"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 South", "Super Rod Pokemon - 4", "Seadra", rom_addresses["Wild_Super_Rod_I"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 1", "Seaking", rom_addresses["Wild_Super_Rod_J"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 2", "Krabby", rom_addresses["Wild_Super_Rod_J"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 3", "Goldeen", rom_addresses["Wild_Super_Rod_J"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Fuchsia City", "Super Rod Pokemon - 4", "Magikarp", rom_addresses["Wild_Super_Rod_J"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route1"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 2", "Rattata", rom_addresses["Wild_Route1"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route1"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 4", "Rattata", rom_addresses["Wild_Route1"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route1"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 6", "Pidgey", rom_addresses["Wild_Route1"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 7", "Pidgey", rom_addresses["Wild_Route1"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 8", "Rattata", rom_addresses["Wild_Route1"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 9", "Pidgey", rom_addresses["Wild_Route1"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 1", "Wild Pokemon - 10", "Pidgey", rom_addresses["Wild_Route1"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route2"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route2"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route2"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 4", "Rattata", rom_addresses["Wild_Route2"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route2"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 6", ["Weedle", "Caterpie"], rom_addresses["Wild_Route2"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route2"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 8", "Rattata", rom_addresses["Wild_Route2"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 9", ["Weedle", "Caterpie"], rom_addresses["Wild_Route2"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 2", "Wild Pokemon - 10", ["Weedle", "Caterpie"], rom_addresses["Wild_Route2"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route22"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 2", ["Nidoran M", "Nidoran F"], rom_addresses["Wild_Route22"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route22"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 4", ["Nidoran M", "Nidoran F"], rom_addresses["Wild_Route22"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 5", "Rattata", rom_addresses["Wild_Route22"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 6", ["Nidoran M", "Nidoran F"], rom_addresses["Wild_Route22"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 7", "Spearow", rom_addresses["Wild_Route22"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route22"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 9", ["Nidoran F", "Nidoran M"], rom_addresses["Wild_Route22"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Route 22", "Wild Pokemon - 10", ["Nidoran F", "Nidoran M"], rom_addresses["Wild_Route22"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 1", ["Weedle", "Caterpie"], + rom_addresses["Wild_ViridianForest"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 2", ["Kakuna", "Metapod"], + rom_addresses["Wild_ViridianForest"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 3", ["Weedle", "Caterpie"], + rom_addresses["Wild_ViridianForest"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 4", ["Weedle", "Caterpie"], + rom_addresses["Wild_ViridianForest"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 5", ["Kakuna", "Metapod"], + rom_addresses["Wild_ViridianForest"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 6", ["Kakuna", "Metapod"], + rom_addresses["Wild_ViridianForest"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 7", ["Metapod", "Kakuna"], + rom_addresses["Wild_ViridianForest"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 8", ["Caterpie", "Weedle"], + rom_addresses["Wild_ViridianForest"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 9", "Pikachu", rom_addresses["Wild_ViridianForest"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Viridian Forest", "Wild Pokemon - 10", "Pikachu", rom_addresses["Wild_ViridianForest"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route3"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route3"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route3"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 4", "Spearow", rom_addresses["Wild_Route3"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route3"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 6", "Pidgey", rom_addresses["Wild_Route3"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 7", "Spearow", rom_addresses["Wild_Route3"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 8", "Jigglypuff", rom_addresses["Wild_Route3"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 9", "Jigglypuff", rom_addresses["Wild_Route3"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 3", "Wild Pokemon - 10", "Jigglypuff", rom_addresses["Wild_Route3"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_MtMoon1F"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_MtMoon1F"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_MtMoon1F"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 4", "Geodude", rom_addresses["Wild_MtMoon1F"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 5", "Zubat", rom_addresses["Wild_MtMoon1F"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_MtMoon1F"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 7", "Geodude", rom_addresses["Wild_MtMoon1F"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 8", "Paras", rom_addresses["Wild_MtMoon1F"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 9", "Zubat", rom_addresses["Wild_MtMoon1F"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon 1F", "Wild Pokemon - 10", "Clefairy", rom_addresses["Wild_MtMoon1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 3", "Geodude", rom_addresses["Wild_MtMoonB1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 4", "Geodude", rom_addresses["Wild_MtMoonB1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 5", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 6", "Paras", rom_addresses["Wild_MtMoonB1F"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 7", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 8", "Zubat", rom_addresses["Wild_MtMoonB1F"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 9", "Clefairy", rom_addresses["Wild_MtMoonB1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B1F", "Wild Pokemon - 10", "Geodude", rom_addresses["Wild_MtMoonB1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_MtMoonB2F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 4", "Geodude", rom_addresses["Wild_MtMoonB2F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 5", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 6", "Paras", rom_addresses["Wild_MtMoonB2F"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 7", "Paras", rom_addresses["Wild_MtMoonB2F"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 8", "Clefairy", rom_addresses["Wild_MtMoonB2F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 9", "Zubat", rom_addresses["Wild_MtMoonB2F"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Mt Moon B2F", "Wild Pokemon - 10", "Clefairy", rom_addresses["Wild_MtMoonB2F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route4"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route4"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route4"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route4"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route4"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route4"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 4", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route4"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 1", ["Weedle", "Caterpie"], rom_addresses["Wild_Route24"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 2", ["Kakuna", "Metapod"], rom_addresses["Wild_Route24"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route24"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 4", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route24"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route24"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 6", "Abra", rom_addresses["Wild_Route24"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route24"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route24"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 9", "Abra", rom_addresses["Wild_Route24"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 24", "Wild Pokemon - 10", "Abra", rom_addresses["Wild_Route24"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 1", ["Weedle", "Caterpie"], rom_addresses["Wild_Route25"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 2", ["Kakuna", "Metapod"], rom_addresses["Wild_Route25"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route25"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 4", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route25"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route25"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 6", "Abra", rom_addresses["Wild_Route25"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route25"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 8", "Abra", rom_addresses["Wild_Route25"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 9", ["Metapod", "Kakuna"], rom_addresses["Wild_Route25"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 25", "Wild Pokemon - 10", ["Caterpie", "Weedle"], rom_addresses["Wild_Route25"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route9"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route9"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route9"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route9"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 7", "Rattata", rom_addresses["Wild_Route9"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route9"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 9", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route9"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route5"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route5"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route5"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 4", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 5", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 6", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route5"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route5"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route5"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 9", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 5", "Wild Pokemon - 10", ["Mankey", "Meowth"], rom_addresses["Wild_Route5"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route6"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route6"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route6"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 4", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 5", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 6", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route6"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route6"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route6"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 9", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 6", "Wild Pokemon - 10", ["Mankey", "Meowth"], rom_addresses["Wild_Route6"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 1", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route11"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route11"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 3", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route11"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 4", "Drowzee", rom_addresses["Wild_Route11"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route11"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 6", "Drowzee", rom_addresses["Wild_Route11"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 7", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route11"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route11"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 9", "Drowzee", rom_addresses["Wild_Route11"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 11", "Wild Pokemon - 10", "Drowzee", rom_addresses["Wild_Route11"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 3", "Geodude", rom_addresses["Wild_RockTunnel1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 4", "Machop", rom_addresses["Wild_RockTunnel1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 5", "Geodude", rom_addresses["Wild_RockTunnel1F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 7", "Zubat", rom_addresses["Wild_RockTunnel1F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 8", "Machop", rom_addresses["Wild_RockTunnel1F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 9", "Onix", rom_addresses["Wild_RockTunnel1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel 1F", "Wild Pokemon - 10", "Onix", rom_addresses["Wild_RockTunnel1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 1", "Zubat", rom_addresses["Wild_RockTunnelB1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 2", "Zubat", rom_addresses["Wild_RockTunnelB1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 3", "Geodude", rom_addresses["Wild_RockTunnelB1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 4", "Machop", rom_addresses["Wild_RockTunnelB1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 5", "Geodude", rom_addresses["Wild_RockTunnelB1F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_RockTunnelB1F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 7", "Machop", rom_addresses["Wild_RockTunnelB1F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 8", "Onix", rom_addresses["Wild_RockTunnelB1F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 9", "Onix", rom_addresses["Wild_RockTunnelB1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Rock Tunnel B1F", "Wild Pokemon - 10", "Geodude", rom_addresses["Wild_RockTunnelB1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 1", "Voltorb", rom_addresses["Wild_Route10"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route10"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 3", "Voltorb", rom_addresses["Wild_Route10"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 4", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 5", "Spearow", rom_addresses["Wild_Route10"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 6", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 7", "Voltorb", rom_addresses["Wild_Route10"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 8", "Spearow", rom_addresses["Wild_Route10"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 9", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Route 10 North", "Wild Pokemon - 10", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route10"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route12"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route12"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route12"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route12"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route12"] + 9, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route12"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route12"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 8", "Pidgey", rom_addresses["Wild_Route12"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 9", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route12"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Route 12 Grass", "Wild Pokemon - 10", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route12"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route8"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 2", ["Mankey", "Meowth"], rom_addresses["Wild_Route8"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 3", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route8"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 4", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route8"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 6", ["Mankey", "Meowth"], rom_addresses["Wild_Route8"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 7", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route8"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 8", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 9", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 8 Grass", "Wild Pokemon - 10", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route8"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 1", "Pidgey", rom_addresses["Wild_Route7"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 2", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route7"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 3", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 4", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route7"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route7"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 6", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 7", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route7"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 8", ["Growlithe", "Vulpix"], rom_addresses["Wild_Route7"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 9", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 7", "Wild Pokemon - 10", ["Mankey", "Meowth"], rom_addresses["Wild_Route7"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 7", "Gastly", rom_addresses["Wild_PokemonTower3F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower3F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower3F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 3F", "Wild Pokemon - 10", "Haunter", rom_addresses["Wild_PokemonTower3F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 7", "Haunter", rom_addresses["Wild_PokemonTower4F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower4F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower4F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 4F", "Wild Pokemon - 10", "Gastly", rom_addresses["Wild_PokemonTower4F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 7", "Haunter", rom_addresses["Wild_PokemonTower5F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower5F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower5F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 5F", "Wild Pokemon - 10", "Gastly", rom_addresses["Wild_PokemonTower5F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 6", "Gastly", rom_addresses["Wild_PokemonTower6F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 7", "Haunter", rom_addresses["Wild_PokemonTower6F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower6F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 9", "Cubone", rom_addresses["Wild_PokemonTower6F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 6F", "Wild Pokemon - 10", "Haunter", rom_addresses["Wild_PokemonTower6F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 1", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 2", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 3", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 4", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 5", "Gastly", rom_addresses["Wild_PokemonTower7F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 6", "Haunter", rom_addresses["Wild_PokemonTower7F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 7", "Cubone", rom_addresses["Wild_PokemonTower7F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 8", "Cubone", rom_addresses["Wild_PokemonTower7F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 9", "Haunter", rom_addresses["Wild_PokemonTower7F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Tower 7F", "Wild Pokemon - 10", "Haunter", rom_addresses["Wild_PokemonTower7F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route13"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route13"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route13"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route13"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route13"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route13"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route13"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 8", "Ditto", rom_addresses["Wild_Route13"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 9", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route13"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 13", "Wild Pokemon - 10", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route13"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route14"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route14"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 3", "Ditto", rom_addresses["Wild_Route14"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route14"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route14"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route14"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route14"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 8", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route14"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 9", "Pidgeotto", rom_addresses["Wild_Route14"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 14", "Wild Pokemon - 10", "Pidgeotto", rom_addresses["Wild_Route14"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 1", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route15"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 2", "Ditto", rom_addresses["Wild_Route15"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 3", "Pidgey", rom_addresses["Wild_Route15"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 4", "Venonat", rom_addresses["Wild_Route15"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 5", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route15"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 6", "Venonat", rom_addresses["Wild_Route15"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 7", ["Oddish", "Bellsprout"], rom_addresses["Wild_Route15"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 8", ["Gloom", "Weepinbell"], rom_addresses["Wild_Route15"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 9", "Pidgeotto", rom_addresses["Wild_Route15"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 15", "Wild Pokemon - 10", "Pidgeotto", rom_addresses["Wild_Route15"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 1", "Spearow", rom_addresses["Wild_Route16"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route16"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 3", "Rattata", rom_addresses["Wild_Route16"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 4", "Doduo", rom_addresses["Wild_Route16"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 5", "Rattata", rom_addresses["Wild_Route16"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 6", "Doduo", rom_addresses["Wild_Route16"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 7", "Doduo", rom_addresses["Wild_Route16"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 8", "Rattata", rom_addresses["Wild_Route16"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 9", "Raticate", rom_addresses["Wild_Route16"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 16 North", "Wild Pokemon - 10", "Raticate", rom_addresses["Wild_Route16"] + 19, None, + event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 1", "Spearow", rom_addresses["Wild_Route17"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route17"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 3", "Raticate", rom_addresses["Wild_Route17"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 4", "Doduo", rom_addresses["Wild_Route17"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 5", "Raticate", rom_addresses["Wild_Route17"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 6", "Doduo", rom_addresses["Wild_Route17"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 7", "Doduo", rom_addresses["Wild_Route17"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 8", "Raticate", rom_addresses["Wild_Route17"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 9", "Fearow", rom_addresses["Wild_Route17"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 17", "Wild Pokemon - 10", "Fearow", rom_addresses["Wild_Route17"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 1", "Spearow", rom_addresses["Wild_Route18"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 2", "Spearow", rom_addresses["Wild_Route18"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 3", "Raticate", rom_addresses["Wild_Route18"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 4", "Doduo", rom_addresses["Wild_Route18"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 5", "Fearow", rom_addresses["Wild_Route18"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 6", "Doduo", rom_addresses["Wild_Route18"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 7", "Doduo", rom_addresses["Wild_Route18"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 8", "Raticate", rom_addresses["Wild_Route18"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 9", "Fearow", rom_addresses["Wild_Route18"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 18", "Wild Pokemon - 10", "Fearow", rom_addresses["Wild_Route18"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneCenter"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 2", "Rhyhorn", rom_addresses["Wild_SafariZoneCenter"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 3", "Venonat", rom_addresses["Wild_SafariZoneCenter"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneCenter"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneCenter"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneCenter"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 7", ["Nidorina", "Nidorino"], + rom_addresses["Wild_SafariZoneCenter"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 8", "Parasect", rom_addresses["Wild_SafariZoneCenter"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 9", ["Scyther", "Pinsir"], + rom_addresses["Wild_SafariZoneCenter"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone Center", "Wild Pokemon - 10", "Chansey", rom_addresses["Wild_SafariZoneCenter"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneEast"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 2", "Doduo", rom_addresses["Wild_SafariZoneEast"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 3", "Paras", rom_addresses["Wild_SafariZoneEast"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneEast"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneEast"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneEast"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 7", ["Nidoran F", "Nidoran M"], + rom_addresses["Wild_SafariZoneEast"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 8", "Parasect", rom_addresses["Wild_SafariZoneEast"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 9", "Kangaskhan", rom_addresses["Wild_SafariZoneEast"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone East", "Wild Pokemon - 10", ["Scyther", "Pinsir"], + rom_addresses["Wild_SafariZoneEast"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneNorth"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 2", "Rhyhorn", rom_addresses["Wild_SafariZoneNorth"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 3", "Paras", rom_addresses["Wild_SafariZoneNorth"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneNorth"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneNorth"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneNorth"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 7", ["Nidorina", "Nidorino"], + rom_addresses["Wild_SafariZoneNorth"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 8", "Venomoth", rom_addresses["Wild_SafariZoneNorth"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 9", "Chansey", rom_addresses["Wild_SafariZoneNorth"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone North", "Wild Pokemon - 10", "Tauros", rom_addresses["Wild_SafariZoneNorth"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 1", ["Nidoran M", "Nidoran F"], + rom_addresses["Wild_SafariZoneWest"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 2", "Doduo", rom_addresses["Wild_SafariZoneWest"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 3", "Venonat", rom_addresses["Wild_SafariZoneWest"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 4", "Exeggcute", rom_addresses["Wild_SafariZoneWest"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 5", ["Nidorino", "Nidorina"], + rom_addresses["Wild_SafariZoneWest"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 6", "Exeggcute", rom_addresses["Wild_SafariZoneWest"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 7", ["Nidoran F", "Nidoran M"], + rom_addresses["Wild_SafariZoneWest"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 8", "Venomoth", rom_addresses["Wild_SafariZoneWest"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 9", "Tauros", rom_addresses["Wild_SafariZoneWest"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Safari Zone West", "Wild Pokemon - 10", "Kangaskhan", rom_addresses["Wild_SafariZoneWest"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 1", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 2", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 3", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 4", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 5", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 6", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 7", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 8", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 9", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 20 West", "Surf Pokemon - 10", "Tentacool", rom_addresses["Wild_SeaRoutes"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 1", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 2", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 3", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 4", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 5", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 6", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 7", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 8", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 9", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 21", "Surf Pokemon - 10", "Tentacool", rom_addresses["Wild_Surf_Route21"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 1", "Seel", rom_addresses["Wild_SeafoamIslands1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 2", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslands1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 3", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslands1F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 4", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslands1F"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 5", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslands1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 6", "Zubat", rom_addresses["Wild_SeafoamIslands1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 7", "Golbat", rom_addresses["Wild_SeafoamIslands1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 8", ["Psyduck", "Slowpoke"], + rom_addresses["Wild_SeafoamIslands1F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 9", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslands1F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands 1F", "Wild Pokemon - 10", ["Golduck", "Slowbro"], + rom_addresses["Wild_SeafoamIslands1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 1", ["Staryu", "Shellder"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 2", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 3", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 4", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 5", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 6", "Seel", rom_addresses["Wild_SeafoamIslandsB1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 7", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 8", "Seel", rom_addresses["Wild_SeafoamIslandsB1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 9", "Dewgong", rom_addresses["Wild_SeafoamIslandsB1F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B1F", "Wild Pokemon - 10", ["Seadra", "Kingler"], + rom_addresses["Wild_SeafoamIslandsB1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 1", "Seel", rom_addresses["Wild_SeafoamIslandsB2F"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 2", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 3", "Seel", rom_addresses["Wild_SeafoamIslandsB2F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 4", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 7, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 5", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 6", ["Staryu", "Shellder"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 7", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 8", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 9", "Golbat", rom_addresses["Wild_SeafoamIslandsB2F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B2F", "Wild Pokemon - 10", ["Slowbro", "Golduck"], + rom_addresses["Wild_SeafoamIslandsB2F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 1", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 2", "Seel", rom_addresses["Wild_SeafoamIslandsB3F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 3", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 4", "Seel", rom_addresses["Wild_SeafoamIslandsB3F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 5", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 6", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 7", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 8", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 9", ["Seadra", "Kingler"], + rom_addresses["Wild_SeafoamIslandsB3F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B3F", "Wild Pokemon - 10", "Dewgong", + rom_addresses["Wild_SeafoamIslandsB3F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 1", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 2", ["Shellder", "Staryu"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 3", ["Horsea", "Krabby"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 4", "Shellder", rom_addresses["Wild_SeafoamIslandsB4F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 5", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 6", "Seel", rom_addresses["Wild_SeafoamIslandsB4F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 7", ["Slowpoke", "Psyduck"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 8", "Seel", rom_addresses["Wild_SeafoamIslandsB4F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 9", ["Slowbro", "Golduck"], + rom_addresses["Wild_SeafoamIslandsB4F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Seafoam Islands B4F", "Wild Pokemon - 10", "Golbat", rom_addresses["Wild_SeafoamIslandsB4F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 1", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion1F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 2", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 3", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 5", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansion1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 6", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 7", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansion1F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 8", "Ponyta", rom_addresses["Wild_PokemonMansion1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 9", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion1F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 1F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansion1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 1", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansion2F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 2", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion2F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 3", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion2F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansion2F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 5", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion2F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 6", "Ponyta", rom_addresses["Wild_PokemonMansion2F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 7", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansion2F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 8", "Ponyta", rom_addresses["Wild_PokemonMansion2F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 9", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion2F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 2F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansion2F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 1", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion3F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 2", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansion3F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 3", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansion3F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansion3F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 5", ["Ponyta", "Magmar"], + rom_addresses["Wild_PokemonMansion3F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 6", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion3F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 7", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansion3F"] + 13, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 8", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansion3F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 9", "Ponyta", rom_addresses["Wild_PokemonMansion3F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion 3F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansion3F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 1", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansionB1F"] + 1, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 2", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansionB1F"] + 3, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 3", ["Growlithe", "Vulpix"], + rom_addresses["Wild_PokemonMansionB1F"] + 5, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 4", "Ponyta", rom_addresses["Wild_PokemonMansionB1F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 5", ["Koffing", "Grimer"], + rom_addresses["Wild_PokemonMansionB1F"] + 9, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 6", ["Weezing", "Muk"], + rom_addresses["Wild_PokemonMansionB1F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 7", "Ponyta", rom_addresses["Wild_PokemonMansionB1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 8", ["Grimer", "Koffing"], + rom_addresses["Wild_PokemonMansionB1F"] + 15, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 9", ["Weezing", "Magmar"], + rom_addresses["Wild_PokemonMansionB1F"] + 17, None, event=True, type="Wild Encounter"), + LocationData("Pokemon Mansion B1F", "Wild Pokemon - 10", ["Muk", "Weezing"], + rom_addresses["Wild_PokemonMansionB1F"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 1", "Rattata", rom_addresses["Wild_Route21"] + 1, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 2", "Pidgey", rom_addresses["Wild_Route21"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 3", "Raticate", rom_addresses["Wild_Route21"] + 5, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 4", "Rattata", rom_addresses["Wild_Route21"] + 7, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 5", "Pidgey", rom_addresses["Wild_Route21"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 6", "Pidgeotto", rom_addresses["Wild_Route21"] + 11, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 7", "Pidgeotto", rom_addresses["Wild_Route21"] + 13, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 8", "Tangela", rom_addresses["Wild_Route21"] + 15, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 9", "Tangela", rom_addresses["Wild_Route21"] + 17, None, event=True, + type="Wild Encounter"), + LocationData("Route 21", "Wild Pokemon - 10", "Tangela", rom_addresses["Wild_Route21"] + 19, None, event=True, + type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 1", "Golbat", rom_addresses["Wild_CeruleanCave1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 2", "Hypno", rom_addresses["Wild_CeruleanCave1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 3", "Magneton", rom_addresses["Wild_CeruleanCave1F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 4", "Dodrio", rom_addresses["Wild_CeruleanCave1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 5", "Venomoth", rom_addresses["Wild_CeruleanCave1F"] + 9, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 6", ["Arbok", "Sandslash"], + rom_addresses["Wild_CeruleanCave1F"] + 11, None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 7", "Kadabra", rom_addresses["Wild_CeruleanCave1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 8", "Parasect", rom_addresses["Wild_CeruleanCave1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 9", "Raichu", rom_addresses["Wild_CeruleanCave1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 1F", "Wild Pokemon - 10", "Ditto", rom_addresses["Wild_CeruleanCave1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 1", "Dodrio", rom_addresses["Wild_CeruleanCave2F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 2", "Venomoth", rom_addresses["Wild_CeruleanCave2F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 3", "Kadabra", rom_addresses["Wild_CeruleanCave2F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 4", "Rhydon", rom_addresses["Wild_CeruleanCave2F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 5", "Marowak", rom_addresses["Wild_CeruleanCave2F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 6", "Electrode", rom_addresses["Wild_CeruleanCave2F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 7", "Chansey", rom_addresses["Wild_CeruleanCave2F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 8", "Wigglytuff", rom_addresses["Wild_CeruleanCave2F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 9", "Ditto", rom_addresses["Wild_CeruleanCave2F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave 2F", "Wild Pokemon - 10", "Ditto", rom_addresses["Wild_CeruleanCave2F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 1", "Rhydon", rom_addresses["Wild_CeruleanCaveB1F"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 2", "Marowak", rom_addresses["Wild_CeruleanCaveB1F"] + 3, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 3", "Electrode", rom_addresses["Wild_CeruleanCaveB1F"] + 5, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 4", "Chansey", rom_addresses["Wild_CeruleanCaveB1F"] + 7, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 5", "Parasect", rom_addresses["Wild_CeruleanCaveB1F"] + 9, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 6", "Raichu", rom_addresses["Wild_CeruleanCaveB1F"] + 11, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 7", ["Arbok", "Sandslash"], + rom_addresses["Wild_CeruleanCaveB1F"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 8", "Ditto", rom_addresses["Wild_CeruleanCaveB1F"] + 15, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 9", "Ditto", rom_addresses["Wild_CeruleanCaveB1F"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Cerulean Cave B1F", "Wild Pokemon - 10", "Ditto", rom_addresses["Wild_CeruleanCaveB1F"] + 19, + None, event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 1", "Voltorb", rom_addresses["Wild_PowerPlant"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 2", "Magnemite", rom_addresses["Wild_PowerPlant"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 3", "Pikachu", rom_addresses["Wild_PowerPlant"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 4", "Pikachu", rom_addresses["Wild_PowerPlant"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 5", "Magnemite", rom_addresses["Wild_PowerPlant"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 6", "Voltorb", rom_addresses["Wild_PowerPlant"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 7", "Magneton", rom_addresses["Wild_PowerPlant"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 8", "Magneton", rom_addresses["Wild_PowerPlant"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 9", ["Electabuzz", "Raichu"], rom_addresses["Wild_PowerPlant"] + 17, + None, event=True, type="Wild Encounter"), + LocationData("Power Plant", "Wild Pokemon - 10", ["Electabuzz", "Raichu"], + rom_addresses["Wild_PowerPlant"] + 19, None, event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 1", ["Ekans", "Sandshrew"], rom_addresses["Wild_Route23"] + 1, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 2", "Ditto", rom_addresses["Wild_Route23"] + 3, None, event=True, + type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 3", "Spearow", rom_addresses["Wild_Route23"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 4", "Fearow", rom_addresses["Wild_Route23"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 5", "Ditto", rom_addresses["Wild_Route23"] + 9, None, event=True, + type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 6", "Fearow", rom_addresses["Wild_Route23"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 7", ["Arbok", "Sandslash"], rom_addresses["Wild_Route23"] + 13, + None, event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 8", "Ditto", rom_addresses["Wild_Route23"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 9", "Fearow", rom_addresses["Wild_Route23"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Route 23 North", "Wild Pokemon - 10", "Fearow", rom_addresses["Wild_Route23"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 1", "Machop", rom_addresses["Wild_VictoryRoad2F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_VictoryRoad2F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_VictoryRoad2F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 4", "Onix", rom_addresses["Wild_VictoryRoad2F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 5", "Onix", rom_addresses["Wild_VictoryRoad2F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 6", "Onix", rom_addresses["Wild_VictoryRoad2F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 7", "Machoke", rom_addresses["Wild_VictoryRoad2F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 8", "Golbat", rom_addresses["Wild_VictoryRoad2F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 9", "Marowak", rom_addresses["Wild_VictoryRoad2F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 2F", "Wild Pokemon - 10", "Graveler", rom_addresses["Wild_VictoryRoad2F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 1", "Machop", rom_addresses["Wild_VictoryRoad3F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_VictoryRoad3F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_VictoryRoad3F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 4", "Onix", rom_addresses["Wild_VictoryRoad3F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 5", "Venomoth", rom_addresses["Wild_VictoryRoad3F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 6", "Onix", rom_addresses["Wild_VictoryRoad3F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 7", "Graveler", rom_addresses["Wild_VictoryRoad3F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 8", "Golbat", rom_addresses["Wild_VictoryRoad3F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 9", "Machoke", rom_addresses["Wild_VictoryRoad3F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 3F", "Wild Pokemon - 10", "Machoke", rom_addresses["Wild_VictoryRoad3F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 1", "Machop", rom_addresses["Wild_VictoryRoad1F"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 2", "Geodude", rom_addresses["Wild_VictoryRoad1F"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 3", "Zubat", rom_addresses["Wild_VictoryRoad1F"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 4", "Onix", rom_addresses["Wild_VictoryRoad1F"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 5", "Onix", rom_addresses["Wild_VictoryRoad1F"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 6", "Onix", rom_addresses["Wild_VictoryRoad1F"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 7", "Graveler", rom_addresses["Wild_VictoryRoad1F"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 8", "Golbat", rom_addresses["Wild_VictoryRoad1F"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 9", "Machoke", rom_addresses["Wild_VictoryRoad1F"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Victory Road 1F", "Wild Pokemon - 10", "Marowak", rom_addresses["Wild_VictoryRoad1F"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 1", "Diglett", rom_addresses["Wild_DiglettsCave"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 2", "Diglett", rom_addresses["Wild_DiglettsCave"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 3", "Diglett", rom_addresses["Wild_DiglettsCave"] + 5, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 4", "Diglett", rom_addresses["Wild_DiglettsCave"] + 7, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 5", "Diglett", rom_addresses["Wild_DiglettsCave"] + 9, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 6", "Diglett", rom_addresses["Wild_DiglettsCave"] + 11, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 7", "Diglett", rom_addresses["Wild_DiglettsCave"] + 13, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 8", "Diglett", rom_addresses["Wild_DiglettsCave"] + 15, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 9", "Dugtrio", rom_addresses["Wild_DiglettsCave"] + 17, None, + event=True, type="Wild Encounter"), + LocationData("Diglett's Cave", "Wild Pokemon - 10", "Dugtrio", rom_addresses["Wild_DiglettsCave"] + 19, None, + event=True, type="Wild Encounter"), + LocationData("Anywhere", "Good Rod Pokemon - 1", "Goldeen", rom_addresses["Wild_Good_Rod"] + 1, None, + event=True, type="Wild Encounter"), + LocationData("Anywhere", "Good Rod Pokemon - 2", "Poliwag", rom_addresses["Wild_Good_Rod"] + 3, None, + event=True, type="Wild Encounter"), + LocationData("Anywhere", "Old Rod Pokemon", "Magikarp", rom_addresses["Wild_Old_Rod"] + 1, None, + event=True, type="Wild Encounter"), + + # "Wild encounters" means a Pokemon you cannot miss or release and lose - re-use it for these + LocationData("Celadon Prize Corner", "Pokemon Prize - 1", "Abra", [rom_addresses["Prize_Mon_A"], + rom_addresses["Prize_Mon_A2"]], None, event=True, + type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 2", "Clefairy", [rom_addresses["Prize_Mon_B"], + rom_addresses["Prize_Mon_B2"]], None, + event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 3", ["Nidorina", "Nidorino"], [rom_addresses["Prize_Mon_C"], + rom_addresses["Prize_Mon_C2"]], + None, + event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 4", ["Dratini", "Pinsir"], [rom_addresses["Prize_Mon_D"], + rom_addresses["Prize_Mon_D2"]], + None, event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 5", ["Scyther", "Dratini"], [rom_addresses["Prize_Mon_E"], + rom_addresses["Prize_Mon_E2"]], + None, event=True, type="Wild Encounter"), + LocationData("Celadon Prize Corner", "Pokemon Prize - 6", "Porygon", [rom_addresses["Prize_Mon_F"], + rom_addresses["Prize_Mon_F2"]], None, + event=True, type="Wild Encounter"), + + # counted for pokedex, because they cannot be permanently "missed" but not for HMs since they can be released + # or evolved forms may not be able to learn the same HMs + LocationData("Celadon City", "Celadon Mansion Pokemon", "Eevee", rom_addresses["Gift_Eevee"], None, + event=True, type="Static Pokemon"), + LocationData("Silph Co 7F", "Gift Pokemon", "Lapras", rom_addresses["Gift_Lapras"], None, + event=True, type="Static Pokemon"), + LocationData("Route 3", "Pokemon For Sale", "Magikarp", rom_addresses["Gift_Magikarp"], None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Old Amber Pokemon", "Aerodactyl", rom_addresses["Gift_Aerodactyl"], None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Helix Fossil Pokemon", "Omanyte", rom_addresses["Gift_Omanyte"], None, + event=True, type="Static Pokemon"), + LocationData("Cinnabar Island", "Dome Fossil Pokemon", "Kabuto", rom_addresses["Gift_Kabuto"], None, + event=True, type="Static Pokemon"), + + # not counted for logic currently. Could perhaps make static encounters resettable in the future? + LocationData("Power Plant", "Fake Pokeball Battle 1", "Voltorb", rom_addresses["Static_Encounter_Voltorb_A"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 2", "Voltorb", rom_addresses["Static_Encounter_Voltorb_B"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 3", "Voltorb", rom_addresses["Static_Encounter_Voltorb_C"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 4", "Voltorb", rom_addresses["Static_Encounter_Voltorb_D"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 5", "Voltorb", rom_addresses["Static_Encounter_Voltorb_E"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 6", "Voltorb", rom_addresses["Static_Encounter_Voltorb_F"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 7", "Voltorb", rom_addresses["Static_Encounter_Electrode_A"], + None, event=True, type="Missable Pokemon"), + LocationData("Power Plant", "Fake Pokeball Battle 8", "Voltorb", rom_addresses["Static_Encounter_Electrode_B"], + None, event=True, type="Missable Pokemon"), + + LocationData("Pokemon Tower 6F", "Restless Soul", "Marowak", [rom_addresses["Ghost_Battle1"], + rom_addresses["Ghost_Battle2"], + rom_addresses["Ghost_Battle3"], + rom_addresses["Ghost_Battle4"], + rom_addresses["Ghost_Battle5"], + rom_addresses["Ghost_Battle6"]], None, event=True, + type="Missable Pokemon"), + + LocationData("Route 12 West", "Sleeping Pokemon", "Snorlax", rom_addresses["Static_Encounter_Snorlax_A"], + None, event=True, type="Missable Pokemon"), + LocationData("Route 16", "Sleeping Pokemon", "Snorlax", rom_addresses["Static_Encounter_Snorlax_B"], + None, event=True, type="Missable Pokemon"), + + LocationData("Saffron City", "Fighting Dojo Gift 1", "Hitmonlee", rom_addresses["Gift_Hitmonlee"], + None, event=True, type="Missable Pokemon"), + LocationData("Saffron City", "Fighting Dojo Gift 2", "Hitmonchan", rom_addresses["Gift_Hitmonchan"], + None, event=True, type="Missable Pokemon"), + + LocationData("Pallet Town", "Starter 1", "Charmander", [rom_addresses["Starter1_A"], + rom_addresses["Starter1_B"], rom_addresses["Starter1_C"], + rom_addresses["Starter1_D"], + rom_addresses["Starter1_F"], rom_addresses["Starter1_H"]], + None, event=True, + type="Starter Pokemon"), + + LocationData("Pallet Town", "Starter 2", "Squirtle", [rom_addresses["Starter2_A"], + rom_addresses["Starter2_B"], rom_addresses["Starter2_E"], + rom_addresses["Starter2_F"], + rom_addresses["Starter2_G"], rom_addresses["Starter2_H"], + rom_addresses["Starter2_I"], + rom_addresses["Starter2_J"], rom_addresses["Starter2_K"], + rom_addresses["Starter2_L"], + rom_addresses["Starter2_M"], rom_addresses["Starter2_N"], + rom_addresses["Starter2_O"]], None, + event=True, type="Starter Pokemon"), + + LocationData("Pallet Town", "Starter 3", "Bulbasaur", [rom_addresses["Starter3_A"], + rom_addresses["Starter3_B"], rom_addresses["Starter3_C"], + rom_addresses["Starter3_D"], + rom_addresses["Starter3_E"], rom_addresses["Starter3_G"], + rom_addresses["Starter3_I"], + rom_addresses["Starter3_J"], rom_addresses["Starter3_K"], + rom_addresses["Starter3_L"], + rom_addresses["Starter3_M"], rom_addresses["Starter3_N"], + rom_addresses["Starter3_O"]], None, + event=True, type="Starter Pokemon"), + + LocationData("Power Plant", "Legendary Pokemon", "Zapdos", rom_addresses["Static_Encounter_Zapdos"], + None, event=True, type="Legendary Pokemon"), + LocationData("Seafoam Islands B4F", "Legendary Pokemon", "Articuno", rom_addresses["Static_Encounter_Articuno"], + None, event=True, type="Legendary Pokemon"), + LocationData("Victory Road 2F", "Legendary Pokemon", "Moltres", rom_addresses["Static_Encounter_Moltres"], + None, event=True, type="Legendary Pokemon"), + LocationData("Cerulean Cave B1F", "Legendary Pokemon", "Mewtwo", rom_addresses["Static_Encounter_Mewtwo"], + None, event=True, type="Legendary Pokemon"), + LocationData("Vermilion City", "Legendary Pokemon", "Mew", rom_addresses["Static_Encounter_Mew"], + None, event=True, type="Legendary Pokemon"), +] + +for i, location in enumerate(location_data): + if location.event or location.rom_address is None: + location.address = None + else: + location.address = loc_id_start + i + + + +class PokemonRBLocation(Location): + game = "Pokemon Red and Blue" + + def __init__(self, player, name, address, rom_address): + super(PokemonRBLocation, self).__init__( + player, name, + address + ) + self.rom_address = rom_address \ No newline at end of file diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py new file mode 100644 index 0000000000..3b1a3594bd --- /dev/null +++ b/worlds/pokemon_rb/logic.py @@ -0,0 +1,73 @@ +from ..AutoWorld import LogicMixin +import worlds.pokemon_rb.poke_data as poke_data + + +class PokemonLogic(LogicMixin): + def pokemon_rb_can_surf(self, player): + return (((self.has("HM03 Surf", player) and self.can_learn_hm("10000", player)) + or self.has("Flippers", player)) and (self.has("Soul Badge", player) or + self.has(self.world.worlds[player].extra_badges.get("Surf"), player) + or self.world.badges_needed_for_hm_moves[player].value == 0)) + + def pokemon_rb_can_cut(self, player): + return ((self.has("HM01 Cut", player) and self.can_learn_hm("100", player) or self.has("Master Sword", player)) + and (self.has("Cascade Badge", player) or + self.has(self.world.worlds[player].extra_badges.get("Cut"), player) or + self.world.badges_needed_for_hm_moves[player].value == 0)) + + def pokemon_rb_can_fly(self, player): + return (((self.has("HM02 Fly", player) and self.can_learn_hm("1000", player)) or self.has("Flute", player)) and + (self.has("Thunder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Fly"), player) + or self.world.badges_needed_for_hm_moves[player].value == 0)) + + def pokemon_rb_can_strength(self, player): + return ((self.has("HM04 Strength", player) and self.can_learn_hm("100000", player)) or + self.has("Titan's Mitt", player)) and (self.has("Rainbow Badge", player) or + self.has(self.world.worlds[player].extra_badges.get("Strength"), player) + or self.world.badges_needed_for_hm_moves[player].value == 0) + + def pokemon_rb_can_flash(self, player): + return (((self.has("HM05 Flash", player) and self.can_learn_hm("1000000", player)) or self.has("Lamp", player)) + and (self.has("Boulder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Flash"), + player) or self.world.badges_needed_for_hm_moves[player].value == 0)) + + def can_learn_hm(self, move, player): + for pokemon, data in self.world.worlds[player].local_poke_data.items(): + if self.has(pokemon, player) and data["tms"][6] & int(move, 2): + return True + return False + + def pokemon_rb_can_get_hidden_items(self, player): + return self.has("Item Finder", player) or not self.world.require_item_finder[player].value + + def pokemon_rb_cerulean_cave(self, count, player): + return len([item for item in + ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge", + "Volcano Badge", "Earth Badge", "Bicycle", "Silph Scope", "Item Finder", "Super Rod", "Good Rod", + "Old Rod", "Lift Key", "Card Key", "Town Map", "Coin Case", "S.S. Ticket", "Secret Key", + "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", "HM03 Surf", + "HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count + + def pokemon_rb_can_pass_guards(self, player): + if self.world.tea[player].value: + return self.has("Tea", player) + else: + # this could just be "True", but you never know what weird options I might introduce later ;) + return self.can_reach("Celadon City - Counter Man", "Location", player) + + def pokemon_rb_has_badges(self, count, player): + return len([item for item in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", + "Soul Badge", "Volcano Badge", "Earth Badge"] if self.has(item, player)]) >= count + + def pokemon_rb_has_pokemon(self, count, player): + obtained_pokemon = set() + for pokemon in poke_data.pokemon_data.keys(): + if self.has(pokemon, player) or self.has(f"Static {pokemon}", player): + obtained_pokemon.add(pokemon) + + return len(obtained_pokemon) >= count + + def pokemon_rb_fossil_checks(self, count, player): + return (self.can_reach('Mt Moon 1F - Southwest Item', 'Location', player) and + self.can_reach('Cinnabar Island - Lab Scientist', 'Location', player) and len( + [item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if self.has(item, player)]) >= count) diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py new file mode 100644 index 0000000000..37672c2501 --- /dev/null +++ b/worlds/pokemon_rb/options.py @@ -0,0 +1,481 @@ + +from Options import Toggle, Choice, Range, SpecialRange, FreeText, TextChoice + + +class GameVersion(Choice): + """Select Red or Blue version.""" + display_name = "Game Version" + option_red = 1 + option_blue = 0 + default = "random" + + +class TrainerName(FreeText): + """Your trainer name. Cannot exceed 7 characters. + See the setup guide on archipelago.gg for a list of allowed characters.""" + display_name = "Trainer Name" + default = "ASH" + + +class RivalName(FreeText): + """Your rival's name. Cannot exceed 7 characters. + See the setup guide on archipelago.gg for a list of allowed characters.""" + display_name = "Rival's Name" + default = "GARY" + + +class Goal(Choice): + """If Professor Oak is selected, your victory condition will require challenging and defeating Oak after becoming""" + """Champion and defeating or capturing the Pokemon at the end of Cerulean Cave.""" + display_name = "Goal" + option_pokemon_league = 0 + option_professor_oak = 1 + default = 0 + + +class EliteFourCondition(Range): + """Number of badges required to challenge the Elite Four once the Indigo Plateau has been reached. + Your rival will reveal the amount needed on the first Route 22 battle (after turning in Oak's Parcel).""" + display_name = "Elite Four Condition" + range_start = 0 + range_end = 8 + default = 8 + + +class VictoryRoadCondition(Range): + """Number of badges required to reach Victory Road.""" + display_name = "Victory Road Condition" + range_start = 0 + range_end = 8 + default = 8 + + +class ViridianGymCondition(Range): + """Number of badges required to enter Viridian Gym.""" + display_name = "Viridian Gym Condition" + range_start = 0 + range_end = 7 + default = 7 + + +class CeruleanCaveCondition(Range): + """Number of badges, HMs, and key items (not counting items you can lose) required to access Cerulean Cave.""" + """If extra_key_items is turned on, the number chosen will be increased by 4.""" + display_name = "Cerulean Cave Condition" + range_start = 0 + range_end = 25 + default = 20 + + +class SecondFossilCheckCondition(Range): + """After choosing one of the fossil location items, you can obtain the remaining item from the Cinnabar Lab + Scientist after reviving this number of fossils.""" + display_name = "Second Fossil Check Condition" + range_start = 0 + range_end = 3 + default = 3 + + +class BadgeSanity(Toggle): + """Shuffle gym badges into the general item pool. If turned off, badges will be shuffled across the 8 gyms.""" + display_name = "Badgesanity" + default = 0 + + +class BadgesNeededForHMMoves(Choice): + """Off will remove the requirement for badges to use HM moves. Extra will give the Marsh, Volcano, and Earth + Badges a random HM move to enable. Extra Plus will additionally pick two random badges to enable a second HM move. + A man in Cerulean City will reveal the moves enabled by each Badge.""" + display_name = "Badges Needed For HM Moves" + default = 1 + option_on = 1 + alias_true = 1 + option_off = 0 + alias_false = 0 + option_extra = 2 + option_extra_plus = 3 + + +class OldMan(Choice): + """With Open Viridian City, the Old Man will let you through without needing to turn in Oak's Parcel.""" + """Early Parcel will ensure Oak's Parcel is available at the beginning of your game.""" + display_name = "Old Man" + option_vanilla = 0 + option_early_parcel = 1 + option_open_viridian_city = 2 + default = 1 + + +class Tea(Toggle): + """Adds a Tea item to the item pool which the Saffron guards require instead of the vending machine drinks. + Adds a location check to the Celadon Mansion 1F, where Tea is acquired in FireRed and LeafGreen.""" + display_name = "Tea" + default = 0 + + +class ExtraKeyItems(Toggle): + """Adds key items that are required to access the Rocket Hideout, Cinnabar Mansion, Safari Zone, and Power Plant. + Adds four item pickups to Rock Tunnel B1F.""" + display_name = "Extra Key Items" + default = 0 + + +class ExtraStrengthBoulders(Toggle): + """Adds Strength Boulders blocking the Route 11 gate, and in Route 13 (can be bypassed with Surf). + This potentially increases the usefulness of Strength as well as the Bicycle.""" + display_name = "Extra Strength Boulders" + default = 0 + + +class RequireItemFinder(Toggle): + """Require Item Finder to pick up hidden items.""" + display_name = "Require Item Finder" + default = 0 + + +class RandomizeHiddenItems(Choice): + """Randomize hidden items. If you choose exclude, they will be randomized but will be guaranteed junk items.""" + display_name = "Randomize Hidden Items" + option_on = 1 + option_off = 0 + alias_true = 1 + alias_false = 0 + option_exclude = 2 + default = 0 + + +class FreeFlyLocation(Toggle): + """One random fly destination will be unlocked by default.""" + display_name = "Free Fly Location" + default = 1 + + +class OaksAidRt2(Range): + """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 2""" + display_name = "Oak's Aide Route 2" + range_start = 0 + range_end = 80 + default = 10 + + +class OaksAidRt11(Range): + """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 11""" + display_name = "Oak's Aide Route 11" + range_start = 0 + range_end = 80 + default = 30 + + +class OaksAidRt15(Range): + """Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 15""" + display_name = "Oak's Aide Route 15" + range_start = 0 + range_end = 80 + default = 50 + + +class ExpModifier(SpecialRange): + """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16.""" + display_name = "Exp Modifier" + range_start = 0 + range_end = 255 + default = 16 + special_range_names = { + "half": default / 2, + "normal": default, + "double": default * 2, + "triple": default * 3, + "quadruple": default * 4, + "quintuple": default * 5, + "sextuple": default * 6, + "septuple": default * 7, + "octuple": default * 8, + } + + +class RandomizeWildPokemon(Choice): + """Randomize all wild Pokemon and game corner prize Pokemon. match_types will select a Pokemon with at least one + type matching the original type of the original Pokemon. match_base_stats will prefer Pokemon with closer base stat + totals. match_types_and_base_stats will match types and will weight towards similar base stats, but there may not be + many to choose from.""" + display_name = "Randomize Wild Pokemon" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class RandomizeStarterPokemon(Choice): + """Randomize the starter Pokemon choices.""" + display_name = "Randomize Starter Pokemon" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class RandomizeStaticPokemon(Choice): + """Randomize all one-time gift and encountered Pokemon, except legendaries. + These will always be first evolution stage Pokemon.""" + display_name = "Randomize Static Pokemon" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class RandomizeLegendaryPokemon(Choice): + """Randomize Legendaries. Mew has been added as an encounter at the Vermilion dock truck. + Shuffle will shuffle the legendaries with each other. Static will shuffle them into other static Pokemon locations. + 'Any' will allow legendaries to appear anywhere based on wild and static randomization options, and their locations + will be randomized according to static Pokemon randomization options.""" + display_name = "Randomize Legendary Pokemon" + default = 0 + option_vanilla = 0 + option_shuffle = 1 + option_static = 2 + option_any = 3 + + +class CatchEmAll(Choice): + """Guarantee all first evolution stage Pokemon are available, or all Pokemon of all stages. + Currently only has an effect if wild Pokemon are randomized.""" + display_name = "Catch 'Em All" + default = 0 + option_off = 0 + alias_false = 0 + option_first_stage = 1 + option_all_pokemon = 2 + + +class RandomizeTrainerParties(Choice): + """Randomize enemy Pokemon encountered in trainer battles.""" + display_name = "Randomize Trainer Parties" + default = 0 + option_vanilla = 0 + option_match_types = 1 + option_match_base_stats = 2 + option_match_types_and_base_stats = 3 + option_completely_random = 4 + + +class TrainerLegendaries(Toggle): + """Allow legendary Pokemon in randomized trainer parties.""" + display_name = "Trainer Legendaries" + default = 0 + + +class BlindTrainers(Range): + """Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a + battle. If you move into and out of their line of sight without stopping, this chance will only trigger once.""" + display_name = "Blind Trainers" + range_start = 0 + range_end = 100 + default = 0 + + +class MinimumStepsBetweenEncounters(Range): + """Minimum number of steps between wild Pokemon encounters.""" + display_name = "Minimum Steps Between Encounters" + default = 3 + range_start = 0 + range_end = 255 + + +class RandomizePokemonStats(Choice): + """Randomize base stats for each Pokemon. Shuffle will shuffle the 5 base stat values amongst each other. Randomize + will completely randomize each stat, but will still add up to the same base stat total.""" + display_name = "Randomize Pokemon Stats" + default = 0 + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + + +class RandomizePokemonCatchRates(Toggle): + """Randomize the catch rate for each Pokemon.""" + display_name = "Randomize Catch Rates" + default = 0 + + +class MinimumCatchRate(Range): + """Minimum catch rate for each Pokemon. If randomize_catch_rates is on, this will be the minimum value that can be + chosen. Otherwise, it will raise any Pokemon's catch rate up to this value if its normal catch rate is lower.""" + display_name = "Minimum Catch Rate" + range_start = 1 + range_end = 255 + default = 3 + + +class RandomizePokemonMovesets(Choice): + """Randomize the moves learned by Pokemon. prefer_types will prefer moves that match the type of the Pokemon.""" + display_name = "Randomize Pokemon Movesets" + option_vanilla = 0 + option_prefer_types = 1 + option_completely_random = 2 + default = 0 + + +class StartWithFourMoves(Toggle): + """If movesets are randomized, this will give all Pokemon 4 starting moves.""" + display_name = "Start With Four Moves" + default = 0 + + +class TMCompatibility(Choice): + """Randomize which Pokemon can learn each TM. prefer_types: 90% chance if Pokemon's type matches the move, + 50% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same + TM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn + every TM.""" + display_name = "TM Compatibility" + default = 0 + option_vanilla = 0 + option_prefer_types = 1 + option_completely_random = 2 + option_full_compatibility = 3 + + +class HMCompatibility(Choice): + """Randomize which Pokemon can learn each HM. prefer_types: 100% chance if Pokemon's type matches the move, + 75% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same + HM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn + every HM.""" + display_name = "HM Compatibility" + default = 0 + option_vanilla = 0 + option_prefer_types = 1 + option_completely_random = 2 + option_full_compatibility = 3 + + +class RandomizePokemonTypes(Choice): + """Randomize the types of each Pokemon. Follow Evolutions will ensure Pokemon's types remain the same when evolving + (except possibly gaining a type).""" + display_name = "Pokemon Types" + option_vanilla = 0 + option_follow_evolutions = 1 + option_randomize = 2 + default = 0 + + +class SecondaryTypeChance(SpecialRange): + """If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions + is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types + to Pokemon that normally have a secondary type.""" + display_name = "Secondary Type Chance" + range_start = -1 + range_end = 100 + default = -1 + special_range_names = { + "vanilla": -1 + } + + +class RandomizeTypeChartTypes(Choice): + """The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness. + Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the attacking types + across the attacking type column and the defending types across the defending type column (so for example Normal + type will still have exactly 2 types that it receives non-regular damage from, and 2 types it deals non-regular + damage to). Randomize will randomize each type in both columns to any random type.""" + display_name = "Randomize Type Chart Types" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + default = 0 + + +class RandomizeTypeChartTypeEffectiveness(Choice): + """The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness. + Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the type effectiveness + across the type effectiveness column (so for example there will always be 6 type immunities). Randomize will + randomize each entry in the table to no effect, not very effective, or super effective; with no effect occurring + at a low chance. Chaos will randomize the values to anywhere between 0% and 200% damage, in 10% increments.""" + display_name = "Randomize Type Chart Type Effectiveness" + option_vanilla = 0 + option_shuffle = 1 + option_randomize = 2 + option_chaos = 3 + default = 0 + + +class SafariZoneNormalBattles(Toggle): + """Change the Safari Zone to have standard wild pokemon battles.""" + display_name = "Safari Zone Normal Battles" + default = 0 + + +class NormalizeEncounterChances(Toggle): + """Each wild encounter table has 10 slots for Pokemon. Normally the chance for each being chosen ranges from + 19.9% to 1.2%. Turn this on to normalize them all to 10% each.""" + display_name = "Normalize Encounter Chances" + default = 0 + + +class ReusableTMs(Toggle): + """Makes TMs reusable, so they will not be consumed upon use.""" + display_name = "Reusable TMs" + default = 0 + + +class StartingMoney(Range): + """The amount of money you start with.""" + display_name = "Starting Money" + default = 3000 + range_start = 0 + range_end = 999999 + + +pokemon_rb_options = { + "game_version": GameVersion, + "trainer_name": TrainerName, + "rival_name": RivalName, + #"goal": Goal, + "elite_four_condition": EliteFourCondition, + "victory_road_condition": VictoryRoadCondition, + "viridian_gym_condition": ViridianGymCondition, + "cerulean_cave_condition": CeruleanCaveCondition, + "second_fossil_check_condition": SecondFossilCheckCondition, + "badgesanity": BadgeSanity, + "old_man": OldMan, + "tea": Tea, + "extra_key_items": ExtraKeyItems, + "extra_strength_boulders": ExtraStrengthBoulders, + "require_item_finder": RequireItemFinder, + "randomize_hidden_items": RandomizeHiddenItems, + "badges_needed_for_hm_moves": BadgesNeededForHMMoves, + "free_fly_location": FreeFlyLocation, + "oaks_aide_rt_2": OaksAidRt2, + "oaks_aide_rt_11": OaksAidRt11, + "oaks_aide_rt_15": OaksAidRt15, + "blind_trainers": BlindTrainers, + "minimum_steps_between_encounters": MinimumStepsBetweenEncounters, + "exp_modifier": ExpModifier, + "randomize_wild_pokemon": RandomizeWildPokemon, + "randomize_starter_pokemon": RandomizeStarterPokemon, + "randomize_static_pokemon": RandomizeStaticPokemon, + "randomize_legendary_pokemon": RandomizeLegendaryPokemon, + "catch_em_all": CatchEmAll, + "randomize_pokemon_stats": RandomizePokemonStats, + "randomize_pokemon_catch_rates": RandomizePokemonCatchRates, + "minimum_catch_rate": MinimumCatchRate, + "randomize_trainer_parties": RandomizeTrainerParties, + "trainer_legendaries": TrainerLegendaries, + "randomize_pokemon_movesets": RandomizePokemonMovesets, + "start_with_four_moves": StartWithFourMoves, + "tm_compatibility": TMCompatibility, + "hm_compatibility": HMCompatibility, + "randomize_pokemon_types": RandomizePokemonTypes, + "secondary_type_chance": SecondaryTypeChance, + "randomize_type_matchup_types": RandomizeTypeChartTypes, + "randomize_type_matchup_type_effectiveness": RandomizeTypeChartTypeEffectiveness, + "safari_zone_normal_battles": SafariZoneNormalBattles, + "normalize_encounter_chances": NormalizeEncounterChances, + "reusable_tms": ReusableTMs, + "starting_money": StartingMoney, +} \ No newline at end of file diff --git a/worlds/pokemon_rb/poke_data.py b/worlds/pokemon_rb/poke_data.py new file mode 100644 index 0000000000..75222d570f --- /dev/null +++ b/worlds/pokemon_rb/poke_data.py @@ -0,0 +1,1212 @@ +id_to_mon = {1: 'Rhydon', 2: 'Kangaskhan', 3: 'Nidoran M', 4: 'Clefairy', 5: 'Spearow', 6: 'Voltorb', 7: 'Nidoking', + 8: 'Slowbro', 9: 'Ivysaur', 10: 'Exeggutor', 11: 'Lickitung', 12: 'Exeggcute', 13: 'Grimer', 14: 'Gengar', + 15: 'Nidoran F', 16: 'Nidoqueen', 17: 'Cubone', 18: 'Rhyhorn', 19: 'Lapras', 20: 'Arcanine', 21: 'Mew', + 22: 'Gyarados', 23: 'Shellder', 24: 'Tentacool', 25: 'Gastly', 26: 'Scyther', 27: 'Staryu', + 28: 'Blastoise', 29: 'Pinsir', 30: 'Tangela', 33: 'Growlithe', 34: 'Onix', 35: 'Fearow', 36: 'Pidgey', + 37: 'Slowpoke', 38: 'Kadabra', 39: 'Graveler', 40: 'Chansey', 41: 'Machoke', 42: 'Mr Mime', + 43: 'Hitmonlee', 44: 'Hitmonchan', 45: 'Arbok', 46: 'Parasect', 47: 'Psyduck', 48: 'Drowzee', 49: 'Golem', + 51: 'Magmar', 53: 'Electabuzz', 54: 'Magneton', 55: 'Koffing', 57: 'Mankey', 58: 'Seel', 59: 'Diglett', + 60: 'Tauros', 64: 'Farfetchd', 65: 'Venonat', 66: 'Dragonite', 70: 'Doduo', 71: 'Poliwag', 72: 'Jynx', + 73: 'Moltres', 74: 'Articuno', 75: 'Zapdos', 76: 'Ditto', 77: 'Meowth', 78: 'Krabby', 82: 'Vulpix', + 83: 'Ninetales', 84: 'Pikachu', 85: 'Raichu', 88: 'Dratini', 89: 'Dragonair', 90: 'Kabuto', 91: 'Kabutops', + 92: 'Horsea', 93: 'Seadra', 96: 'Sandshrew', 97: 'Sandslash', 98: 'Omanyte', 99: 'Omastar', + 100: 'Jigglypuff', 101: 'Wigglytuff', 102: 'Eevee', 103: 'Flareon', 104: 'Jolteon', 105: 'Vaporeon', + 106: 'Machop', 107: 'Zubat', 108: 'Ekans', 109: 'Paras', 110: 'Poliwhirl', 111: 'Poliwrath', 112: 'Weedle', + 113: 'Kakuna', 114: 'Beedrill', 116: 'Dodrio', 117: 'Primeape', 118: 'Dugtrio', 119: 'Venomoth', + 120: 'Dewgong', 123: 'Caterpie', 124: 'Metapod', 125: 'Butterfree', 126: 'Machamp', 128: 'Golduck', + 129: 'Hypno', 130: 'Golbat', 131: 'Mewtwo', 132: 'Snorlax', 133: 'Magikarp', 136: 'Muk', 138: 'Kingler', + 139: 'Cloyster', 141: 'Electrode', 142: 'Clefable', 143: 'Weezing', 144: 'Persian', 145: 'Marowak', + 147: 'Haunter', 148: 'Abra', 149: 'Alakazam', 150: 'Pidgeotto', 151: 'Pidgeot', 152: 'Starmie', + 153: 'Bulbasaur', 154: 'Venusaur', 155: 'Tentacruel', 157: 'Goldeen', 158: 'Seaking', 163: 'Ponyta', + 164: 'Rapidash', 165: 'Rattata', 166: 'Raticate', 167: 'Nidorino', 168: 'Nidorina', 169: 'Geodude', + 170: 'Porygon', 171: 'Aerodactyl', 173: 'Magnemite', 176: 'Charmander', 177: 'Squirtle', 178: 'Charmeleon', + 179: 'Wartortle', 180: 'Charizard', 185: 'Oddish', 186: 'Gloom', 187: 'Vileplume', 188: 'Bellsprout', + 189: 'Weepinbell', 190: 'Victreebel'} + + +pokemon_dex = { + 'Bulbasaur': 1, 'Ivysaur': 2, 'Venusaur': 3, 'Charmander': 4, 'Charmeleon': 5, 'Charizard': 6, 'Squirtle': 7, + 'Wartortle': 8, 'Blastoise': 9, 'Caterpie': 10, 'Metapod': 11, 'Butterfree': 12, 'Weedle': 13, 'Kakuna': 14, + 'Beedrill': 15, 'Pidgey': 16, 'Pidgeotto': 17, 'Pidgeot': 18, 'Rattata': 19, 'Raticate': 20, 'Spearow': 21, + 'Fearow': 22, 'Ekans': 23, 'Arbok': 24, 'Pikachu': 25, 'Raichu': 26, 'Sandshrew': 27, 'Sandslash': 28, + 'Nidoran F': 29, 'Nidorina': 30, 'Nidoqueen': 31, 'Nidoran M': 32, 'Nidorino': 33, 'Nidoking': 34, 'Clefairy': 35, + 'Clefable': 36, 'Vulpix': 37, 'Ninetales': 38, 'Jigglypuff': 39, 'Wigglytuff': 40, 'Zubat': 41, 'Golbat': 42, + 'Oddish': 43, 'Gloom': 44, 'Vileplume': 45, 'Paras': 46, 'Parasect': 47, 'Venonat': 48, 'Venomoth': 49, + 'Diglett': 50, 'Dugtrio': 51, 'Meowth': 52, 'Persian': 53, 'Psyduck': 54, 'Golduck': 55, 'Mankey': 56, + 'Primeape': 57, 'Growlithe': 58, 'Arcanine': 59, 'Poliwag': 60, 'Poliwhirl': 61, 'Poliwrath': 62, 'Abra': 63, + 'Kadabra': 64, 'Alakazam': 65, 'Machop': 66, 'Machoke': 67, 'Machamp': 68, 'Bellsprout': 69, 'Weepinbell': 70, + 'Victreebel': 71, 'Tentacool': 72, 'Tentacruel': 73, 'Geodude': 74, 'Graveler': 75, 'Golem': 76, 'Ponyta': 77, + 'Rapidash': 78, 'Slowpoke': 79, 'Slowbro': 80, 'Magnemite': 81, 'Magneton': 82, 'Farfetchd': 83, 'Doduo': 84, + 'Dodrio': 85, 'Seel': 86, 'Dewgong': 87, 'Grimer': 88, 'Muk': 89, 'Shellder': 90, 'Cloyster': 91, 'Gastly': 92, + 'Haunter': 93, 'Gengar': 94, 'Onix': 95, 'Drowzee': 96, 'Hypno': 97, 'Krabby': 98, 'Kingler': 99, 'Voltorb': 100, + 'Electrode': 101, 'Exeggcute': 102, 'Exeggutor': 103, 'Cubone': 104, 'Marowak': 105, 'Hitmonlee': 106, + 'Hitmonchan': 107, 'Lickitung': 108, 'Koffing': 109, 'Weezing': 110, 'Rhyhorn': 111, 'Rhydon': 112, 'Chansey': 113, + 'Tangela': 114, 'Kangaskhan': 115, 'Horsea': 116, 'Seadra': 117, 'Goldeen': 118, 'Seaking': 119, 'Staryu': 120, + 'Starmie': 121, 'Mr Mime': 122, 'Scyther': 123, 'Jynx': 124, 'Electabuzz': 125, 'Magmar': 126, 'Pinsir': 127, + 'Tauros': 128, 'Magikarp': 129, 'Gyarados': 130, 'Lapras': 131, 'Ditto': 132, 'Eevee': 133, 'Vaporeon': 134, + 'Jolteon': 135, 'Flareon': 136, 'Porygon': 137, 'Omanyte': 138, 'Omastar': 139, 'Kabuto': 140, 'Kabutops': 141, + 'Aerodactyl': 142, 'Snorlax': 143, 'Articuno': 144, 'Zapdos': 145, 'Moltres': 146, 'Dratini': 147, 'Dragonair': 148, + 'Dragonite': 149, 'Mewtwo': 150, 'Mew': 151 +} + + +type_ids = { + "Normal": 0x0, + "Fighting": 0x1, + "Flying": 0x2, + "Poison": 0x3, + "Ground": 0x4, + "Rock": 0x5, + "Bug": 0x7, + "Ghost": 0x8, + "Fire": 0x14, + "Water": 0x15, + "Grass": 0x16, + "Electric": 0x17, + "Psychic": 0x18, + "Ice": 0x19, + "Dragon": 0x1A +} +type_names = { + 0x0: "Normal", + 0x1: "Fighting", + 0x2: "Flying", + 0x3: "Poison", + 0x4: "Ground", + 0x5: "Rock", + 0x7: "Bug", + 0x8: "Ghost", + 0x14: "Fire", + 0x15: "Water", + 0x16: "Grass", + 0x17: "Electric", + 0x18: "Psychic", + 0x19: "Ice", + 0x1a: "Dragon" +} + +type_chart = [ + ["Water", "Fire", 20], + ["Fire", "Grass", 20], + ["Fire", "Ice", 20], + ["Grass", "Water", 20], + ["Electric", "Water", 20], + ["Water", "Rock", 20], + ["Ground", "Flying", 0], + ["Water", "Water", 5], + ["Fire", "Fire", 5], + ["Electric", "Electric", 5], + ["Ice", "Ice", 5], + ["Grass", "Grass", 5], + ["Psychic", "Psychic", 5], + ["Fire", "Water", 5], + ["Grass", "Fire", 5], + ["Water", "Grass", 5], + ["Electric", "Grass", 5], + ["Normal", "Rock", 5], + ["Normal", "Ghost", 0], + ["Ghost", "Ghost", 20], + ["Fire", "Bug", 20], + ["Fire", "Rock", 5], + ["Water", "Ground", 20], + ["Electric", "Ground", 0], + ["Electric", "Flying", 20], + ["Grass", "Ground", 20], + ["Grass", "Bug", 5], + ["Grass", "Poison", 5], + ["Grass", "Rock", 20], + ["Grass", "Flying", 5], + ["Ice", "Water", 5], + ["Ice", "Grass", 20], + ["Ice", "Ground", 20], + ["Ice", "Flying", 20], + ["Fighting", "Normal", 20], + ["Fighting", "Poison", 5], + ["Fighting", "Flying", 5], + ["Fighting", "Psychic", 5], + ["Fighting", "Bug", 5], + ["Fighting", "Rock", 20], + ["Fighting", "Ice", 20], + ["Fighting", "Ghost", 0], + ["Poison", "Grass", 20], + ["Poison", "Poison", 5], + ["Poison", "Ground", 5], + ["Poison", "Bug", 20], + ["Poison", "Rock", 5], + ["Poison", "Ghost", 5], + ["Ground", "Fire", 20], + ["Ground", "Electric", 20], + ["Ground", "Grass", 5], + ["Ground", "Bug", 5], + ["Ground", "Rock", 20], + ["Ground", "Poison", 20], + ["Flying", "Electric", 5], + ["Flying", "Fighting", 20], + ["Flying", "Bug", 20], + ["Flying", "Grass", 20], + ["Flying", "Rock", 5], + ["Psychic", "Fighting", 20], + ["Psychic", "Poison", 20], + ["Bug", "Fire", 5], + ["Bug", "Grass", 20], + ["Bug", "Fighting", 5], + ["Bug", "Flying", 5], + ["Bug", "Psychic", 20], + ["Bug", "Ghost", 5], + ["Bug", "Poison", 20], + ["Rock", "Fire", 20], + ["Rock", "Fighting", 5], + ["Rock", "Ground", 5], + ["Rock", "Flying", 20], + ["Rock", "Bug", 20], + ["Rock", "Ice", 20], + ["Ghost", "Normal", 0], + ["Ghost", "Psychic", 0], + ["Fire", "Dragon", 5], + ["Water", "Dragon", 5], + ["Electric", "Dragon", 5], + ["Grass", "Dragon", 5], + ["Ice", "Dragon", 20], + ["Dragon", "Dragon", 20] +] + +pokemon_data = { + 'Bulbasaur': {'id': 153, 'dex': 1, 'hp': 45, 'atk': 49, 'def': 49, 'spd': 45, 'spc': 65, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 64, 'start move 1': 'Tackle', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa4\x038\xc0\x03\x08\x06')}, + 'Ivysaur': {'id': 9, 'dex': 2, 'hp': 60, 'atk': 62, 'def': 63, 'spd': 60, 'spc': 80, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 141, 'start move 1': 'Tackle', 'start move 2': 'Growl', + 'start move 3': 'Leech Seed', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa4\x038\xc0\x03\x08\x06')}, + 'Venusaur': {'id': 154, 'dex': 3, 'hp': 80, 'atk': 82, 'def': 83, 'spd': 80, 'spc': 100, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 208, 'start move 1': 'Tackle', + 'start move 2': 'Growl', 'start move 3': 'Leech Seed', 'start move 4': 'Vine Whip', 'growth rate': 3, + 'tms': bytearray(b'\xa4C8\xc0\x03\x08\x06')}, + 'Charmander': {'id': 176, 'dex': 4, 'hp': 39, 'atk': 52, 'def': 43, 'spd': 65, 'spc': 50, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 65, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb5\x03O\xc8\xe3\x08&')}, + 'Charmeleon': {'id': 178, 'dex': 5, 'hp': 58, 'atk': 64, 'def': 58, 'spd': 80, 'spc': 65, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 142, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Ember', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb5\x03O\xc8\xe3\x08&')}, + 'Charizard': {'id': 180, 'dex': 6, 'hp': 78, 'atk': 84, 'def': 78, 'spd': 100, 'spc': 85, 'type1': 'Fire', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 209, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Ember', 'start move 4': 'Leer', 'growth rate': 3, + 'tms': bytearray(b'\xb5CO\xce\xe3\x08&')}, + 'Squirtle': {'id': 177, 'dex': 7, 'hp': 44, 'atk': 48, 'def': 65, 'spd': 43, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 66, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1?\x0f\xc8\x83\x082')}, + 'Wartortle': {'id': 179, 'dex': 8, 'hp': 59, 'atk': 63, 'def': 80, 'spd': 58, 'spc': 65, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 143, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'Bubble', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1?\x0f\xc8\x83\x082')}, + 'Blastoise': {'id': 28, 'dex': 9, 'hp': 79, 'atk': 83, 'def': 100, 'spd': 78, 'spc': 85, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 210, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'Bubble', 'start move 4': 'Water Gun', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x7f\x0f\xce\x83\x082')}, + 'Caterpie': {'id': 123, 'dex': 10, 'hp': 45, 'atk': 30, 'def': 35, 'spd': 45, 'spc': 20, 'type1': 'Bug', + 'type2': 'Bug', 'catch rate': 255, 'base exp': 53, 'start move 1': 'Tackle', + 'start move 2': 'String Shot', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Metapod': {'id': 124, 'dex': 11, 'hp': 50, 'atk': 20, 'def': 55, 'spd': 30, 'spc': 25, 'type1': 'Bug', + 'type2': 'Bug', 'catch rate': 120, 'base exp': 72, 'start move 1': 'Harden', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Butterfree': {'id': 125, 'dex': 12, 'hp': 60, 'atk': 45, 'def': 50, 'spd': 70, 'spc': 80, 'type1': 'Bug', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 160, 'start move 1': 'Confusion', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*C8\xf0C(\x02')}, + 'Weedle': {'id': 112, 'dex': 13, 'hp': 40, 'atk': 35, 'def': 30, 'spd': 50, 'spc': 20, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 52, 'start move 1': 'Poison Sting', + 'start move 2': 'String Shot', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Kakuna': {'id': 113, 'dex': 14, 'hp': 45, 'atk': 25, 'def': 50, 'spd': 35, 'spc': 25, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 71, 'start move 1': 'Harden', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Beedrill': {'id': 114, 'dex': 15, 'hp': 65, 'atk': 80, 'def': 40, 'spd': 75, 'spc': 45, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 159, 'start move 1': 'Fury Attack', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'$C\x18\xc0\xc3\x08\x06')}, + 'Pidgey': {'id': 36, 'dex': 16, 'hp': 40, 'atk': 45, 'def': 40, 'spd': 56, 'spc': 35, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 255, 'base exp': 55, 'start move 1': 'Gust', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'*\x03\x08\xc0C\x0c\n')}, + 'Pidgeotto': {'id': 150, 'dex': 17, 'hp': 63, 'atk': 60, 'def': 55, 'spd': 71, 'spc': 50, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 120, 'base exp': 113, 'start move 1': 'Gust', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'*\x03\x08\xc0C\x0c\n')}, + 'Pidgeot': {'id': 151, 'dex': 18, 'hp': 83, 'atk': 80, 'def': 75, 'spd': 91, 'spc': 70, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 172, 'start move 1': 'Gust', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b'*C\x08\xc0C\x0c\n')}, + 'Rattata': {'id': 165, 'dex': 19, 'hp': 30, 'atk': 56, 'def': 35, 'spd': 72, 'spc': 25, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 255, 'base exp': 57, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0/\x88\xc9\xc2\x08\x02')}, + 'Raticate': {'id': 166, 'dex': 20, 'hp': 55, 'atk': 81, 'def': 60, 'spd': 97, 'spc': 50, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 90, 'base exp': 116, 'start move 1': 'Tackle', + 'start move 2': 'Tail Whip', 'start move 3': 'Quick Attack', 'start move 4': 'No Move', + 'growth rate': 0, 'tms': bytearray(b'\xa0\x7f\x88\xc9\xc2\x08\x02')}, + 'Spearow': {'id': 5, 'dex': 21, 'hp': 40, 'atk': 60, 'def': 30, 'spd': 70, 'spc': 31, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 255, 'base exp': 58, 'start move 1': 'Peck', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*\x03\x08\xc0B\x0c\n')}, + 'Fearow': {'id': 35, 'dex': 22, 'hp': 65, 'atk': 90, 'def': 65, 'spd': 100, 'spc': 61, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 90, 'base exp': 162, 'start move 1': 'Peck', 'start move 2': 'Growl', + 'start move 3': 'Leer', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*C\x08\xc0B\x0c\n')}, + 'Ekans': {'id': 108, 'dex': 23, 'hp': 35, 'atk': 60, 'def': 44, 'spd': 55, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 62, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x18\xce\x82\x88"')}, + 'Arbok': {'id': 45, 'dex': 24, 'hp': 60, 'atk': 85, 'def': 69, 'spd': 80, 'spc': 65, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 90, 'base exp': 147, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'Poison Sting', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0C\x18\xce\x82\x88"')}, + 'Pikachu': {'id': 84, 'dex': 25, 'hp': 35, 'atk': 55, 'def': 30, 'spd': 90, 'spc': 50, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 190, 'base exp': 82, 'start move 1': 'Thundershock', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x83\x8d\xc1\xc3\x18B')}, + 'Raichu': {'id': 85, 'dex': 26, 'hp': 60, 'atk': 90, 'def': 55, 'spd': 100, 'spc': 90, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 75, 'base exp': 122, 'start move 1': 'Thundershock', + 'start move 2': 'Growl', 'start move 3': 'Thunder Wave', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xc3\x8d\xc1\xc3\x18B')}, + 'Sandshrew': {'id': 96, 'dex': 27, 'hp': 50, 'atk': 75, 'def': 85, 'spd': 40, 'spc': 30, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 255, 'base exp': 93, 'start move 1': 'Scratch', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4\x03\r\xce\xc2\x88&')}, + 'Sandslash': {'id': 97, 'dex': 28, 'hp': 75, 'atk': 100, 'def': 110, 'spd': 65, 'spc': 55, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 90, 'base exp': 163, 'start move 1': 'Scratch', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4C\r\xce\xc2\x88&')}, + 'Nidoran F': {'id': 15, 'dex': 29, 'hp': 55, 'atk': 47, 'def': 52, 'spd': 41, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 235, 'base exp': 59, 'start move 1': 'Growl', + 'start move 2': 'Tackle', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa0#\x88\xc1\x83\x08\x02')}, + 'Nidorina': {'id': 168, 'dex': 30, 'hp': 70, 'atk': 62, 'def': 67, 'spd': 56, 'spc': 55, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 117, 'start move 1': 'Growl', + 'start move 2': 'Tackle', 'start move 3': 'Scratch', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xe0?\x88\xc1\x83\x08\x02')}, + 'Nidoqueen': {'id': 16, 'dex': 31, 'hp': 90, 'atk': 82, 'def': 87, 'spd': 76, 'spc': 75, 'type1': 'Poison', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 194, 'start move 1': 'Tackle', + 'start move 2': 'Scratch', 'start move 3': 'Tail Whip', 'start move 4': 'Body Slam', 'growth rate': 3, + 'tms': bytearray(b'\xf1\xff\x8f\xc7\xa3\x882')}, + 'Nidoran M': {'id': 3, 'dex': 32, 'hp': 46, 'atk': 57, 'def': 40, 'spd': 50, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 235, 'base exp': 60, 'start move 1': 'Leer', + 'start move 2': 'Tackle', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xe0#\x88\xc1\x83\x08\x02')}, + 'Nidorino': {'id': 167, 'dex': 33, 'hp': 61, 'atk': 72, 'def': 57, 'spd': 65, 'spc': 55, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 118, 'start move 1': 'Leer', + 'start move 2': 'Tackle', 'start move 3': 'Horn Attack', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xe0?\x88\xc1\x83\x08\x02')}, + 'Nidoking': {'id': 7, 'dex': 34, 'hp': 81, 'atk': 92, 'def': 77, 'spd': 85, 'spc': 75, 'type1': 'Poison', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 195, 'start move 1': 'Tackle', + 'start move 2': 'Horn Attack', 'start move 3': 'Poison Sting', 'start move 4': 'Thrash', + 'growth rate': 3, 'tms': bytearray(b'\xf1\xff\x8f\xc7\xa3\x882')}, + 'Clefairy': {'id': 4, 'dex': 35, 'hp': 70, 'atk': 45, 'def': 48, 'spd': 35, 'spc': 60, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 150, 'base exp': 68, 'start move 1': 'Pound', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 4, + 'tms': bytearray(b'\xb1?\xaf\xf1\xa78c')}, + 'Clefable': {'id': 142, 'dex': 36, 'hp': 95, 'atk': 70, 'def': 73, 'spd': 60, 'spc': 85, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 25, 'base exp': 129, 'start move 1': 'Sing', + 'start move 2': 'Doubleslap', 'start move 3': 'Minimize', 'start move 4': 'Metronome', + 'growth rate': 4, 'tms': bytearray(b'\xb1\x7f\xaf\xf1\xa78c')}, + 'Vulpix': {'id': 82, 'dex': 37, 'hp': 38, 'atk': 41, 'def': 40, 'spd': 65, 'spc': 65, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 190, 'base exp': 63, 'start move 1': 'Ember', 'start move 2': 'Tail Whip', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xc8\xe3\x08\x02')}, + 'Ninetales': {'id': 83, 'dex': 38, 'hp': 73, 'atk': 76, 'def': 75, 'spd': 100, 'spc': 100, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 75, 'base exp': 178, 'start move 1': 'Ember', + 'start move 2': 'Tail Whip', 'start move 3': 'Quick Attack', 'start move 4': 'Roar', 'growth rate': 0, + 'tms': bytearray(b'\xa0C\x08\xc8\xe3\x08\x02')}, + 'Jigglypuff': {'id': 100, 'dex': 39, 'hp': 115, 'atk': 45, 'def': 20, 'spd': 20, 'spc': 25, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 170, 'base exp': 76, 'start move 1': 'Sing', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 4, + 'tms': bytearray(b'\xb1?\xaf\xf1\xa38c')}, + 'Wigglytuff': {'id': 101, 'dex': 40, 'hp': 140, 'atk': 70, 'def': 45, 'spd': 45, 'spc': 50, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 50, 'base exp': 109, 'start move 1': 'Sing', + 'start move 2': 'Disable', 'start move 3': 'Defense Curl', 'start move 4': 'Doubleslap', + 'growth rate': 4, 'tms': bytearray(b'\xb1\x7f\xaf\xf1\xa38c')}, + 'Zubat': {'id': 107, 'dex': 41, 'hp': 40, 'atk': 45, 'def': 35, 'spd': 55, 'spc': 40, 'type1': 'Poison', + 'type2': 'Flying', 'catch rate': 255, 'base exp': 54, 'start move 1': 'Leech Life', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*\x03\x18\xc0B\x08\x02')}, + 'Golbat': {'id': 130, 'dex': 42, 'hp': 75, 'atk': 80, 'def': 70, 'spd': 90, 'spc': 75, 'type1': 'Poison', + 'type2': 'Flying', 'catch rate': 90, 'base exp': 171, 'start move 1': 'Leech Life', + 'start move 2': 'Screech', 'start move 3': 'Bite', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'*C\x18\xc0B\x08\x02')}, + 'Oddish': {'id': 185, 'dex': 43, 'hp': 45, 'atk': 50, 'def': 55, 'spd': 30, 'spc': 75, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 78, 'start move 1': 'Absorb', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Gloom': {'id': 186, 'dex': 44, 'hp': 60, 'atk': 65, 'def': 70, 'spd': 40, 'spc': 85, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 132, 'start move 1': 'Absorb', + 'start move 2': 'Poisonpowder', 'start move 3': 'Stun Spore', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Vileplume': {'id': 187, 'dex': 45, 'hp': 75, 'atk': 80, 'def': 85, 'spd': 50, 'spc': 100, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 184, 'start move 1': 'Stun Spore', + 'start move 2': 'Sleep Powder', 'start move 3': 'Acid', 'start move 4': 'Petal Dance', + 'growth rate': 3, 'tms': bytearray(b'\xa4C8\xc0\x03\x08\x06')}, + 'Paras': {'id': 109, 'dex': 46, 'hp': 35, 'atk': 70, 'def': 55, 'spd': 25, 'spc': 55, 'type1': 'Bug', + 'type2': 'Grass', 'catch rate': 190, 'base exp': 70, 'start move 1': 'Scratch', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4\x038\xc8\x83\x08\x06')}, + 'Parasect': {'id': 46, 'dex': 47, 'hp': 60, 'atk': 95, 'def': 80, 'spd': 30, 'spc': 80, 'type1': 'Bug', + 'type2': 'Grass', 'catch rate': 75, 'base exp': 128, 'start move 1': 'Scratch', + 'start move 2': 'Stun Spore', 'start move 3': 'Leech Life', 'start move 4': 'No Move', + 'growth rate': 0, 'tms': bytearray(b'\xa4C8\xc8\x83\x08\x06')}, + 'Venonat': {'id': 65, 'dex': 48, 'hp': 60, 'atk': 55, 'def': 50, 'spd': 45, 'spc': 40, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 75, 'start move 1': 'Tackle', + 'start move 2': 'Disable', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x038\xd0\x03(\x02')}, + 'Venomoth': {'id': 119, 'dex': 49, 'hp': 70, 'atk': 65, 'def': 60, 'spd': 90, 'spc': 90, 'type1': 'Bug', + 'type2': 'Poison', 'catch rate': 75, 'base exp': 138, 'start move 1': 'Tackle', + 'start move 2': 'Disable', 'start move 3': 'Poisonpowder', 'start move 4': 'Leech Life', + 'growth rate': 0, 'tms': bytearray(b'*C8\xf0C(\x02')}, + 'Diglett': {'id': 59, 'dex': 50, 'hp': 10, 'atk': 55, 'def': 25, 'spd': 95, 'spc': 45, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 255, 'base exp': 81, 'start move 1': 'Scratch', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xce\x02\x88\x02')}, + 'Dugtrio': {'id': 118, 'dex': 51, 'hp': 35, 'atk': 80, 'def': 50, 'spd': 120, 'spc': 70, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 50, 'base exp': 153, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Dig', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0C\x08\xce\x02\x88\x02')}, + 'Meowth': {'id': 77, 'dex': 52, 'hp': 40, 'atk': 45, 'def': 35, 'spd': 90, 'spc': 40, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 255, 'base exp': 69, 'start move 1': 'Scratch', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x8f\x88\xc1\xc2\x08\x02')}, + 'Persian': {'id': 144, 'dex': 53, 'hp': 65, 'atk': 70, 'def': 60, 'spd': 115, 'spc': 65, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 90, 'base exp': 148, 'start move 1': 'Scratch', + 'start move 2': 'Growl', 'start move 3': 'Bite', 'start move 4': 'Screech', 'growth rate': 0, + 'tms': bytearray(b'\xa0\xcf\x88\xc1\xc2\x08\x02')}, + 'Psyduck': {'id': 47, 'dex': 54, 'hp': 50, 'atk': 52, 'def': 48, 'spd': 55, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 190, 'base exp': 80, 'start move 1': 'Scratch', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xbf\x0f\xc8\xc2\x082')}, + 'Golduck': {'id': 128, 'dex': 55, 'hp': 80, 'atk': 82, 'def': 78, 'spd': 85, 'spc': 80, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 75, 'base exp': 174, 'start move 1': 'Scratch', + 'start move 2': 'Tail Whip', 'start move 3': 'Disable', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xff\x0f\xc8\xc2\x082')}, + 'Mankey': {'id': 57, 'dex': 56, 'hp': 40, 'atk': 80, 'def': 35, 'spd': 70, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 190, 'base exp': 74, 'start move 1': 'Scratch', + 'start move 2': 'Leer', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x83\x8f\xc9\xc6\x88"')}, + 'Primeape': {'id': 117, 'dex': 57, 'hp': 65, 'atk': 105, 'def': 60, 'spd': 95, 'spc': 60, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 75, 'base exp': 149, 'start move 1': 'Scratch', + 'start move 2': 'Leer', 'start move 3': 'Karate Chop', 'start move 4': 'Fury Swipes', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xc3\x8f\xc9\xc6\x88"')}, + 'Growlithe': {'id': 33, 'dex': 58, 'hp': 55, 'atk': 70, 'def': 45, 'spd': 60, 'spc': 50, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 190, 'base exp': 91, 'start move 1': 'Bite', 'start move 2': 'Roar', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xa0\x03H\xc8\xe3\x08\x02')}, + 'Arcanine': {'id': 20, 'dex': 59, 'hp': 90, 'atk': 110, 'def': 80, 'spd': 95, 'spc': 80, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 75, 'base exp': 213, 'start move 1': 'Roar', 'start move 2': 'Ember', + 'start move 3': 'Leer', 'start move 4': 'Take Down', 'growth rate': 5, + 'tms': bytearray(b'\xa0CH\xe8\xe3\x08\x02')}, + 'Poliwag': {'id': 71, 'dex': 60, 'hp': 40, 'atk': 50, 'def': 40, 'spd': 90, 'spc': 40, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 255, 'base exp': 77, 'start move 1': 'Bubble', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa0?\x08\xd0\x82(\x12')}, + 'Poliwhirl': {'id': 110, 'dex': 61, 'hp': 65, 'atk': 65, 'def': 65, 'spd': 90, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 120, 'base exp': 131, 'start move 1': 'Bubble', + 'start move 2': 'Hypnosis', 'start move 3': 'Water Gun', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1?\x0f\xd6\x86(2')}, + 'Poliwrath': {'id': 111, 'dex': 62, 'hp': 90, 'atk': 85, 'def': 95, 'spd': 70, 'spc': 70, 'type1': 'Water', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 185, 'start move 1': 'Hypnosis', + 'start move 2': 'Water Gun', 'start move 3': 'Doubleslap', 'start move 4': 'Body Slam', + 'growth rate': 3, 'tms': bytearray(b'\xb1\x7f\x0f\xd6\x86(2')}, + 'Abra': {'id': 148, 'dex': 63, 'hp': 25, 'atk': 20, 'def': 15, 'spd': 90, 'spc': 105, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 200, 'base exp': 73, 'start move 1': 'Teleport', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xf0\x878C')}, + 'Kadabra': {'id': 38, 'dex': 64, 'hp': 40, 'atk': 35, 'def': 30, 'spd': 105, 'spc': 120, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 100, 'base exp': 145, 'start move 1': 'Teleport', + 'start move 2': 'Confusion', 'start move 3': 'Disable', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xf8\x878C')}, + 'Alakazam': {'id': 149, 'dex': 65, 'hp': 55, 'atk': 50, 'def': 45, 'spd': 120, 'spc': 135, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 50, 'base exp': 186, 'start move 1': 'Teleport', + 'start move 2': 'Confusion', 'start move 3': 'Disable', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1C\x0f\xf8\x878C')}, + 'Machop': {'id': 106, 'dex': 66, 'hp': 70, 'atk': 80, 'def': 50, 'spd': 35, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 180, 'base exp': 88, 'start move 1': 'Karate Chop', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xce\xa6\x88"')}, + 'Machoke': {'id': 41, 'dex': 67, 'hp': 80, 'atk': 100, 'def': 70, 'spd': 45, 'spc': 50, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 90, 'base exp': 146, 'start move 1': 'Karate Chop', + 'start move 2': 'Low Kick', 'start move 3': 'Leer', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1\x03\x0f\xce\xa6\x88"')}, + 'Machamp': {'id': 126, 'dex': 68, 'hp': 90, 'atk': 130, 'def': 80, 'spd': 55, 'spc': 65, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 193, 'start move 1': 'Karate Chop', + 'start move 2': 'Low Kick', 'start move 3': 'Leer', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1C\x0f\xce\xa6\x88"')}, + 'Bellsprout': {'id': 188, 'dex': 69, 'hp': 50, 'atk': 75, 'def': 35, 'spd': 40, 'spc': 70, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 255, 'base exp': 84, 'start move 1': 'Vine Whip', + 'start move 2': 'Growth', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Weepinbell': {'id': 189, 'dex': 70, 'hp': 65, 'atk': 90, 'def': 50, 'spd': 55, 'spc': 85, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 120, 'base exp': 151, 'start move 1': 'Vine Whip', + 'start move 2': 'Growth', 'start move 3': 'Wrap', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'$\x038\xc0\x03\x08\x06')}, + 'Victreebel': {'id': 190, 'dex': 71, 'hp': 80, 'atk': 105, 'def': 65, 'spd': 70, 'spc': 100, 'type1': 'Grass', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 191, 'start move 1': 'Sleep Powder', + 'start move 2': 'Stun Spore', 'start move 3': 'Acid', 'start move 4': 'Razor Leaf', 'growth rate': 3, + 'tms': bytearray(b'\xa4C8\xc0\x03\x08\x06')}, + 'Tentacool': {'id': 24, 'dex': 72, 'hp': 40, 'atk': 40, 'def': 35, 'spd': 70, 'spc': 100, 'type1': 'Water', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 105, 'start move 1': 'Acid', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'$?\x18\xc0\x83\x08\x16')}, + 'Tentacruel': {'id': 155, 'dex': 73, 'hp': 80, 'atk': 70, 'def': 65, 'spd': 100, 'spc': 120, 'type1': 'Water', + 'type2': 'Poison', 'catch rate': 60, 'base exp': 205, 'start move 1': 'Acid', + 'start move 2': 'Supersonic', 'start move 3': 'Wrap', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'$\x7f\x18\xc0\x83\x08\x16')}, + 'Geodude': {'id': 169, 'dex': 74, 'hp': 40, 'atk': 80, 'def': 100, 'spd': 20, 'spc': 30, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 255, 'base exp': 86, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa1\x03\x0f\xce.\xc8"')}, + 'Graveler': {'id': 39, 'dex': 75, 'hp': 55, 'atk': 95, 'def': 115, 'spd': 35, 'spc': 45, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 120, 'base exp': 134, 'start move 1': 'Tackle', + 'start move 2': 'Defense Curl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xa1\x03\x0f\xce.\xc8"')}, + 'Golem': {'id': 49, 'dex': 76, 'hp': 80, 'atk': 110, 'def': 130, 'spd': 45, 'spc': 55, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 177, 'start move 1': 'Tackle', + 'start move 2': 'Defense Curl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xb1C\x0f\xce.\xc8"')}, + 'Ponyta': {'id': 163, 'dex': 77, 'hp': 50, 'atk': 85, 'def': 55, 'spd': 90, 'spc': 65, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 190, 'base exp': 152, 'start move 1': 'Ember', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\x03\x08\xc0\xe3\x08\x02')}, + 'Rapidash': {'id': 164, 'dex': 78, 'hp': 65, 'atk': 100, 'def': 70, 'spd': 105, 'spc': 80, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 60, 'base exp': 192, 'start move 1': 'Ember', + 'start move 2': 'Tail Whip', 'start move 3': 'Stomp', 'start move 4': 'Growl', 'growth rate': 0, + 'tms': bytearray(b'\xe0C\x08\xc0\xe3\x08\x02')}, + 'Slowpoke': {'id': 37, 'dex': 79, 'hp': 90, 'atk': 65, 'def': 65, 'spd': 15, 'spc': 40, 'type1': 'Water', + 'type2': 'Psychic', 'catch rate': 190, 'base exp': 99, 'start move 1': 'Confusion', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\xbf\x08\xfe\xe38s')}, + 'Slowbro': {'id': 8, 'dex': 80, 'hp': 95, 'atk': 75, 'def': 110, 'spd': 30, 'spc': 80, 'type1': 'Water', + 'type2': 'Psychic', 'catch rate': 75, 'base exp': 164, 'start move 1': 'Confusion', + 'start move 2': 'Disable', 'start move 3': 'Headbutt', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\xff\x0f\xfe\xe38s')}, + 'Magnemite': {'id': 173, 'dex': 81, 'hp': 25, 'atk': 35, 'def': 70, 'spd': 45, 'spc': 95, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 190, 'base exp': 89, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x03\x88\xe1C\x18B')}, + 'Magneton': {'id': 54, 'dex': 82, 'hp': 50, 'atk': 60, 'def': 95, 'spd': 70, 'spc': 120, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 60, 'base exp': 161, 'start move 1': 'Tackle', + 'start move 2': 'Sonicboom', 'start move 3': 'Thundershock', 'start move 4': 'No Move', + 'growth rate': 0, 'tms': bytearray(b' C\x88\xe1C\x18B')}, + 'Farfetchd': {'id': 64, 'dex': 83, 'hp': 52, 'atk': 65, 'def': 55, 'spd': 60, 'spc': 58, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 94, 'start move 1': 'Peck', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xae\x03\x08\xc0\xc3\x08\x0e')}, + 'Doduo': {'id': 70, 'dex': 84, 'hp': 35, 'atk': 85, 'def': 45, 'spd': 75, 'spc': 35, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 190, 'base exp': 96, 'start move 1': 'Peck', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa8\x03\x08\xc0\x83\x0c\x0b')}, + 'Dodrio': {'id': 116, 'dex': 85, 'hp': 60, 'atk': 110, 'def': 70, 'spd': 100, 'spc': 60, 'type1': 'Normal', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 158, 'start move 1': 'Peck', 'start move 2': 'Growl', + 'start move 3': 'Fury Attack', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa8C\x08\xc0\x83\x0c\x0b')}, + 'Seel': {'id': 58, 'dex': 86, 'hp': 65, 'atk': 45, 'def': 55, 'spd': 45, 'spc': 70, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 190, 'base exp': 100, 'start move 1': 'Headbutt', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\xbf\x08\xc0\x82\x082')}, + 'Dewgong': {'id': 120, 'dex': 87, 'hp': 90, 'atk': 70, 'def': 80, 'spd': 70, 'spc': 95, 'type1': 'Water', + 'type2': 'Ice', 'catch rate': 75, 'base exp': 176, 'start move 1': 'Headbutt', 'start move 2': 'Growl', + 'start move 3': 'Aurora Beam', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\xff\x08\xc0\x82\x082')}, + 'Grimer': {'id': 13, 'dex': 88, 'hp': 80, 'atk': 80, 'def': 50, 'spd': 25, 'spc': 40, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 90, 'start move 1': 'Pound', 'start move 2': 'Disable', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x00\x98\xc1*H\x02')}, + 'Muk': {'id': 136, 'dex': 89, 'hp': 105, 'atk': 105, 'def': 75, 'spd': 50, 'spc': 65, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 75, 'base exp': 157, 'start move 1': 'Pound', 'start move 2': 'Disable', + 'start move 3': 'Poison Gas', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0@\x98\xc1*H\x02')}, + 'Shellder': {'id': 23, 'dex': 90, 'hp': 30, 'atk': 65, 'def': 100, 'spd': 40, 'spc': 45, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 190, 'base exp': 97, 'start move 1': 'Tackle', + 'start move 2': 'Withdraw', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' ?\x08\xe0KH\x13')}, + 'Cloyster': {'id': 139, 'dex': 91, 'hp': 50, 'atk': 95, 'def': 180, 'spd': 70, 'spc': 85, 'type1': 'Water', + 'type2': 'Ice', 'catch rate': 60, 'base exp': 203, 'start move 1': 'Withdraw', + 'start move 2': 'Supersonic', 'start move 3': 'Clamp', 'start move 4': 'Aurora Beam', 'growth rate': 5, + 'tms': bytearray(b' \x7f\x08\xe0KH\x13')}, + 'Gastly': {'id': 25, 'dex': 92, 'hp': 30, 'atk': 35, 'def': 30, 'spd': 80, 'spc': 100, 'type1': 'Ghost', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 95, 'start move 1': 'Lick', + 'start move 2': 'Confuse Ray', 'start move 3': 'Night Shade', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b' \x00\x98\xd1\nj\x02')}, + 'Haunter': {'id': 147, 'dex': 93, 'hp': 45, 'atk': 50, 'def': 45, 'spd': 95, 'spc': 115, 'type1': 'Ghost', + 'type2': 'Poison', 'catch rate': 90, 'base exp': 126, 'start move 1': 'Lick', + 'start move 2': 'Confuse Ray', 'start move 3': 'Night Shade', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b' \x00\x98\xd1\nj\x02')}, + 'Gengar': {'id': 14, 'dex': 94, 'hp': 60, 'atk': 65, 'def': 60, 'spd': 110, 'spc': 130, 'type1': 'Ghost', + 'type2': 'Poison', 'catch rate': 45, 'base exp': 190, 'start move 1': 'Lick', + 'start move 2': 'Confuse Ray', 'start move 3': 'Night Shade', 'start move 4': 'No Move', + 'growth rate': 3, 'tms': bytearray(b'\xb1C\x9f\xd1\x8ej"')}, + 'Onix': {'id': 34, 'dex': 95, 'hp': 35, 'atk': 45, 'def': 160, 'spd': 70, 'spc': 30, 'type1': 'Rock', + 'type2': 'Ground', 'catch rate': 45, 'base exp': 108, 'start move 1': 'Tackle', 'start move 2': 'Screech', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xce\x8a\xc8"')}, + 'Drowzee': {'id': 48, 'dex': 96, 'hp': 60, 'atk': 48, 'def': 45, 'spd': 42, 'spc': 90, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 190, 'base exp': 102, 'start move 1': 'Pound', + 'start move 2': 'Hypnosis', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x03\x0f\xf0\x87:C')}, + 'Hypno': {'id': 129, 'dex': 97, 'hp': 85, 'atk': 73, 'def': 70, 'spd': 67, 'spc': 115, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 75, 'base exp': 165, 'start move 1': 'Pound', + 'start move 2': 'Hypnosis', 'start move 3': 'Disable', 'start move 4': 'Confusion', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\x0f\xf0\x87:C')}, + 'Krabby': {'id': 78, 'dex': 98, 'hp': 30, 'atk': 105, 'def': 90, 'spd': 50, 'spc': 25, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 115, 'start move 1': 'Bubble', 'start move 2': 'Leer', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4?\x08\xc0\x02\x086')}, + 'Kingler': {'id': 138, 'dex': 99, 'hp': 55, 'atk': 130, 'def': 115, 'spd': 75, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 60, 'base exp': 206, 'start move 1': 'Bubble', 'start move 2': 'Leer', + 'start move 3': 'Vicegrip', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4\x7f\x08\xc0\x02\x086')}, + 'Voltorb': {'id': 6, 'dex': 100, 'hp': 40, 'atk': 30, 'def': 50, 'spd': 100, 'spc': 55, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 190, 'base exp': 103, 'start move 1': 'Tackle', + 'start move 2': 'Screech', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x01\x88\xe1KXB')}, + 'Electrode': {'id': 141, 'dex': 101, 'hp': 60, 'atk': 50, 'def': 70, 'spd': 140, 'spc': 80, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 60, 'base exp': 150, 'start move 1': 'Tackle', + 'start move 2': 'Screech', 'start move 3': 'Sonicboom', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' A\x88\xe1\xcbXB')}, + 'Exeggcute': {'id': 12, 'dex': 102, 'hp': 60, 'atk': 40, 'def': 80, 'spd': 40, 'spc': 60, 'type1': 'Grass', + 'type2': 'Psychic', 'catch rate': 90, 'base exp': 98, 'start move 1': 'Barrage', + 'start move 2': 'Hypnosis', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' \x03\x08\xf0\x1bh\x02')}, + 'Exeggutor': {'id': 10, 'dex': 103, 'hp': 95, 'atk': 95, 'def': 85, 'spd': 55, 'spc': 125, 'type1': 'Grass', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 212, 'start move 1': 'Barrage', + 'start move 2': 'Hypnosis', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' C8\xf0\x1bh"')}, + 'Cubone': {'id': 17, 'dex': 104, 'hp': 50, 'atk': 50, 'def': 95, 'spd': 35, 'spc': 40, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 190, 'base exp': 87, 'start move 1': 'Bone Club', + 'start move 2': 'Growl', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1?\x0f\xce\xa2\x08"')}, + 'Marowak': {'id': 145, 'dex': 105, 'hp': 60, 'atk': 80, 'def': 110, 'spd': 45, 'spc': 50, 'type1': 'Ground', + 'type2': 'Ground', 'catch rate': 75, 'base exp': 124, 'start move 1': 'Bone Club', + 'start move 2': 'Growl', 'start move 3': 'Leer', 'start move 4': 'Focus Energy', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x7f\x0f\xce\xa2\x08"')}, + 'Hitmonlee': {'id': 43, 'dex': 106, 'hp': 50, 'atk': 120, 'def': 53, 'spd': 87, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 139, 'start move 1': 'Double Kick', + 'start move 2': 'Meditate', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x03\x0f\xc0\xc6\x08"')}, + 'Hitmonchan': {'id': 44, 'dex': 107, 'hp': 50, 'atk': 105, 'def': 79, 'spd': 76, 'spc': 35, 'type1': 'Fighting', + 'type2': 'Fighting', 'catch rate': 45, 'base exp': 140, 'start move 1': 'Comet Punch', + 'start move 2': 'Agility', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x03\x0f\xc0\xc6\x08"')}, + 'Lickitung': {'id': 11, 'dex': 108, 'hp': 90, 'atk': 55, 'def': 75, 'spd': 30, 'spc': 60, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 127, 'start move 1': 'Wrap', + 'start move 2': 'Supersonic', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb5\x7f\x8f\xc7\xa2\x086')}, + 'Koffing': {'id': 55, 'dex': 109, 'hp': 40, 'atk': 65, 'def': 95, 'spd': 35, 'spc': 60, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 190, 'base exp': 114, 'start move 1': 'Tackle', 'start move 2': 'Smog', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x00\x88\xc1*H\x02')}, + 'Weezing': {'id': 143, 'dex': 110, 'hp': 65, 'atk': 90, 'def': 120, 'spd': 60, 'spc': 85, 'type1': 'Poison', + 'type2': 'Poison', 'catch rate': 60, 'base exp': 173, 'start move 1': 'Tackle', 'start move 2': 'Smog', + 'start move 3': 'Sludge', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' @\x88\xc1*H\x02')}, + 'Rhyhorn': {'id': 18, 'dex': 111, 'hp': 80, 'atk': 85, 'def': 95, 'spd': 25, 'spc': 30, 'type1': 'Ground', + 'type2': 'Rock', 'catch rate': 120, 'base exp': 135, 'start move 1': 'Horn Attack', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0\x03\x88\xcf\xa2\x88"')}, + 'Rhydon': {'id': 1, 'dex': 112, 'hp': 105, 'atk': 130, 'def': 120, 'spd': 40, 'spc': 45, 'type1': 'Ground', + 'type2': 'Rock', 'catch rate': 60, 'base exp': 204, 'start move 1': 'Horn Attack', + 'start move 2': 'Stomp', 'start move 3': 'Tail Whip', 'start move 4': 'Fury Attack', 'growth rate': 5, + 'tms': bytearray(b'\xf1\xff\x8f\xcf\xa2\x882')}, + 'Chansey': {'id': 40, 'dex': 113, 'hp': 250, 'atk': 5, 'def': 5, 'spd': 50, 'spc': 105, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 30, 'base exp': 255, 'start move 1': 'Pound', + 'start move 2': 'Doubleslap', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 4, + 'tms': bytearray(b'\xb1\x7f\xaf\xf1\xb79c')}, + 'Tangela': {'id': 30, 'dex': 114, 'hp': 65, 'atk': 55, 'def': 115, 'spd': 60, 'spc': 100, 'type1': 'Grass', + 'type2': 'Grass', 'catch rate': 45, 'base exp': 166, 'start move 1': 'Constrict', + 'start move 2': 'Bind', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa4C8\xc0\x82\x08\x06')}, + 'Kangaskhan': {'id': 2, 'dex': 115, 'hp': 105, 'atk': 95, 'def': 80, 'spd': 90, 'spc': 40, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 175, 'start move 1': 'Comet Punch', + 'start move 2': 'Rage', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x7f\x8f\xc7\xa2\x882')}, + 'Horsea': {'id': 92, 'dex': 116, 'hp': 30, 'atk': 40, 'def': 70, 'spd': 60, 'spc': 70, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 83, 'start move 1': 'Bubble', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' ?\x08\xc0\xc2\x08\x12')}, + 'Seadra': {'id': 93, 'dex': 117, 'hp': 55, 'atk': 65, 'def': 95, 'spd': 85, 'spc': 95, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 75, 'base exp': 155, 'start move 1': 'Bubble', + 'start move 2': 'Smokescreen', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' \x7f\x08\xc0\xc2\x08\x12')}, + 'Goldeen': {'id': 157, 'dex': 118, 'hp': 45, 'atk': 67, 'def': 60, 'spd': 63, 'spc': 50, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 111, 'start move 1': 'Peck', + 'start move 2': 'Tail Whip', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'`?\x08\xc0\xc2\x08\x12')}, + 'Seaking': {'id': 158, 'dex': 119, 'hp': 80, 'atk': 92, 'def': 65, 'spd': 68, 'spc': 80, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 60, 'base exp': 170, 'start move 1': 'Peck', + 'start move 2': 'Tail Whip', 'start move 3': 'Supersonic', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'`\x7f\x08\xc0\xc2\x08\x12')}, + 'Staryu': {'id': 27, 'dex': 120, 'hp': 30, 'atk': 45, 'def': 55, 'spd': 85, 'spc': 70, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 225, 'base exp': 106, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' ?\x88\xf1\xc38S')}, + 'Starmie': {'id': 152, 'dex': 121, 'hp': 60, 'atk': 75, 'def': 85, 'spd': 115, 'spc': 100, 'type1': 'Water', + 'type2': 'Psychic', 'catch rate': 60, 'base exp': 207, 'start move 1': 'Tackle', + 'start move 2': 'Water Gun', 'start move 3': 'Harden', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b' \x7f\x88\xf1\xc38S')}, + 'Mr Mime': {'id': 42, 'dex': 122, 'hp': 40, 'atk': 45, 'def': 65, 'spd': 90, 'spc': 100, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 136, 'start move 1': 'Confusion', + 'start move 2': 'Barrier', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\xaf\xf1\x878B')}, + 'Scyther': {'id': 26, 'dex': 123, 'hp': 70, 'atk': 110, 'def': 80, 'spd': 105, 'spc': 55, 'type1': 'Bug', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 187, 'start move 1': 'Quick Attack', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'$C\x08\xc0\xc2\x08\x06')}, + 'Jynx': {'id': 72, 'dex': 124, 'hp': 65, 'atk': 50, 'def': 35, 'spd': 95, 'spc': 95, 'type1': 'Ice', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 137, 'start move 1': 'Pound', + 'start move 2': 'Lovely Kiss', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1\x7f\x0f\xf0\x87(\x02')}, + 'Electabuzz': {'id': 53, 'dex': 125, 'hp': 65, 'atk': 83, 'def': 57, 'spd': 105, 'spc': 85, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 45, 'base exp': 156, 'start move 1': 'Quick Attack', + 'start move 2': 'Leer', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\x8f\xf1\xc78b')}, + 'Magmar': {'id': 51, 'dex': 126, 'hp': 65, 'atk': 95, 'def': 57, 'spd': 93, 'spc': 85, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 167, 'start move 1': 'Ember', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb1C\x0f\xf0\xa6("')}, + 'Pinsir': {'id': 29, 'dex': 127, 'hp': 65, 'atk': 125, 'def': 100, 'spd': 85, 'spc': 55, 'type1': 'Bug', + 'type2': 'Bug', 'catch rate': 45, 'base exp': 200, 'start move 1': 'Vicegrip', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xa4C\r\xc0\x02\x08&')}, + 'Tauros': {'id': 60, 'dex': 128, 'hp': 75, 'atk': 100, 'def': 95, 'spd': 110, 'spc': 70, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 211, 'start move 1': 'Tackle', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0s\x88\xc7\xa2\x08"')}, + 'Magikarp': {'id': 133, 'dex': 129, 'hp': 20, 'atk': 10, 'def': 55, 'spd': 80, 'spc': 20, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 255, 'base exp': 20, 'start move 1': 'Splash', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Gyarados': {'id': 22, 'dex': 130, 'hp': 95, 'atk': 125, 'def': 79, 'spd': 81, 'spc': 100, 'type1': 'Water', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 214, 'start move 1': 'Bite', + 'start move 2': 'Dragon Rage', 'start move 3': 'Leer', 'start move 4': 'Hydro Pump', 'growth rate': 5, + 'tms': bytearray(b'\xa0\x7f\xc8\xc1\xa3\x082')}, + 'Lapras': {'id': 19, 'dex': 131, 'hp': 130, 'atk': 85, 'def': 80, 'spd': 60, 'spc': 95, 'type1': 'Water', + 'type2': 'Ice', 'catch rate': 45, 'base exp': 219, 'start move 1': 'Water Gun', 'start move 2': 'Growl', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0\x7f\xe8\xd1\x83(2')}, + 'Ditto': {'id': 76, 'dex': 132, 'hp': 48, 'atk': 48, 'def': 48, 'spd': 48, 'spc': 48, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 35, 'base exp': 61, 'start move 1': 'Transform', + 'start move 2': 'No Move', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\x00\x00\x00\x00\x00\x00\x00')}, + 'Eevee': {'id': 102, 'dex': 133, 'hp': 55, 'atk': 55, 'def': 50, 'spd': 55, 'spc': 65, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 92, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0\x03\x08\xc0\xc3\x08\x02')}, + 'Vaporeon': {'id': 105, 'dex': 134, 'hp': 130, 'atk': 65, 'def': 60, 'spd': 65, 'spc': 110, 'type1': 'Water', + 'type2': 'Water', 'catch rate': 45, 'base exp': 196, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'Water Gun', + 'growth rate': 0, 'tms': bytearray(b'\xa0\x7f\x08\xc0\xc3\x08\x12')}, + 'Jolteon': {'id': 104, 'dex': 135, 'hp': 65, 'atk': 65, 'def': 60, 'spd': 130, 'spc': 110, 'type1': 'Electric', + 'type2': 'Electric', 'catch rate': 45, 'base exp': 197, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'Thundershock', + 'growth rate': 0, 'tms': bytearray(b'\xa0C\x88\xc1\xc3\x18B')}, + 'Flareon': {'id': 103, 'dex': 136, 'hp': 65, 'atk': 130, 'def': 60, 'spd': 65, 'spc': 110, 'type1': 'Fire', + 'type2': 'Fire', 'catch rate': 45, 'base exp': 198, 'start move 1': 'Tackle', + 'start move 2': 'Sand Attack', 'start move 3': 'Quick Attack', 'start move 4': 'Ember', + 'growth rate': 0, 'tms': bytearray(b'\xa0C\x08\xc0\xe3\x08\x02')}, + 'Porygon': {'id': 170, 'dex': 137, 'hp': 65, 'atk': 60, 'def': 70, 'spd': 40, 'spc': 75, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 45, 'base exp': 130, 'start move 1': 'Tackle', + 'start move 2': 'Sharpen', 'start move 3': 'Conversion', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b' s\x88\xf1\xc38C')}, + 'Omanyte': {'id': 98, 'dex': 138, 'hp': 35, 'atk': 40, 'def': 100, 'spd': 35, 'spc': 90, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 120, 'start move 1': 'Water Gun', + 'start move 2': 'Withdraw', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0?\x08\xc0\x03\x08\x12')}, + 'Omastar': {'id': 99, 'dex': 139, 'hp': 70, 'atk': 60, 'def': 125, 'spd': 55, 'spc': 115, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 199, 'start move 1': 'Water Gun', + 'start move 2': 'Withdraw', 'start move 3': 'Horn Attack', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xe0\x7f\r\xc0\x83\x08\x12')}, + 'Kabuto': {'id': 90, 'dex': 140, 'hp': 30, 'atk': 80, 'def': 90, 'spd': 55, 'spc': 45, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 119, 'start move 1': 'Scratch', 'start move 2': 'Harden', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xa0?\x08\xc0\x03\x08\x12')}, + 'Kabutops': {'id': 91, 'dex': 141, 'hp': 60, 'atk': 115, 'def': 105, 'spd': 80, 'spc': 70, 'type1': 'Rock', + 'type2': 'Water', 'catch rate': 45, 'base exp': 201, 'start move 1': 'Scratch', + 'start move 2': 'Harden', 'start move 3': 'Absorb', 'start move 4': 'No Move', 'growth rate': 0, + 'tms': bytearray(b'\xb6\x7f\r\xc0\x83\x08\x12')}, + 'Aerodactyl': {'id': 171, 'dex': 142, 'hp': 80, 'atk': 105, 'def': 65, 'spd': 130, 'spc': 60, 'type1': 'Rock', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 202, 'start move 1': 'Wing Attack', + 'start move 2': 'Agility', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*CH\xc0c\x0c\n')}, + 'Snorlax': {'id': 132, 'dex': 143, 'hp': 160, 'atk': 110, 'def': 65, 'spd': 30, 'spc': 65, 'type1': 'Normal', + 'type2': 'Normal', 'catch rate': 25, 'base exp': 154, 'start move 1': 'Headbutt', + 'start move 2': 'Amnesia', 'start move 3': 'Rest', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xb1\xff\xaf\xd7\xaf\xa82')}, + 'Articuno': {'id': 74, 'dex': 144, 'hp': 90, 'atk': 85, 'def': 100, 'spd': 85, 'spc': 125, 'type1': 'Ice', + 'type2': 'Flying', 'catch rate': 3, 'base exp': 215, 'start move 1': 'Peck', + 'start move 2': 'Ice Beam', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*\x7f\x08\xc0C\x0c\n')}, + 'Zapdos': {'id': 75, 'dex': 145, 'hp': 90, 'atk': 90, 'def': 85, 'spd': 100, 'spc': 125, 'type1': 'Electric', + 'type2': 'Flying', 'catch rate': 3, 'base exp': 216, 'start move 1': 'Thundershock', + 'start move 2': 'Drill Peck', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*C\x88\xc1C\x1cJ')}, + 'Moltres': {'id': 73, 'dex': 146, 'hp': 90, 'atk': 100, 'def': 90, 'spd': 90, 'spc': 125, 'type1': 'Fire', + 'type2': 'Flying', 'catch rate': 3, 'base exp': 217, 'start move 1': 'Peck', + 'start move 2': 'Fire Spin', 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'*C\x08\xc0c\x0c\n')}, + 'Dratini': {'id': 88, 'dex': 147, 'hp': 41, 'atk': 64, 'def': 45, 'spd': 50, 'spc': 50, 'type1': 'Dragon', + 'type2': 'Dragon', 'catch rate': 45, 'base exp': 67, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xa0?\xc8\xc1\xe3\x18\x12')}, + 'Dragonair': {'id': 89, 'dex': 148, 'hp': 61, 'atk': 84, 'def': 65, 'spd': 70, 'spc': 70, 'type1': 'Dragon', + 'type2': 'Dragon', 'catch rate': 45, 'base exp': 144, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'Thunder Wave', 'start move 4': 'No Move', 'growth rate': 5, + 'tms': bytearray(b'\xe0?\xc8\xc1\xe3\x18\x12')}, + 'Dragonite': {'id': 66, 'dex': 149, 'hp': 91, 'atk': 134, 'def': 95, 'spd': 80, 'spc': 100, 'type1': 'Dragon', + 'type2': 'Flying', 'catch rate': 45, 'base exp': 218, 'start move 1': 'Wrap', 'start move 2': 'Leer', + 'start move 3': 'Thunder Wave', 'start move 4': 'Agility', 'growth rate': 5, + 'tms': bytearray(b'\xe2\x7f\xc8\xc1\xe3\x182')}, + 'Mewtwo': {'id': 131, 'dex': 150, 'hp': 106, 'atk': 110, 'def': 90, 'spd': 130, 'spc': 154, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 3, 'base exp': 220, 'start move 1': 'Confusion', + 'start move 2': 'Disable', 'start move 3': 'Swift', 'start move 4': 'Psychic', 'growth rate': 5, + 'tms': bytearray(b'\xb1\xff\xaf\xf1\xaf8c')}, + 'Mew': {'id': 21, 'dex': 151, 'hp': 100, 'atk': 100, 'def': 100, 'spd': 100, 'spc': 100, 'type1': 'Psychic', + 'type2': 'Psychic', 'catch rate': 45, 'base exp': 64, 'start move 1': 'Pound', 'start move 2': 'No Move', + 'start move 3': 'No Move', 'start move 4': 'No Move', 'growth rate': 3, + 'tms': bytearray(b'\xff\xff\xff\xff\xff\xff\xff')}} + + + +evolves_from = { + "Ivysaur": "Bulbasaur", + "Venusaur": "Ivysaur", + "Charmeleon": "Charmander", + "Charizard": "Charmeleon", + "Wartortle": "Squirtle", + "Blastoise": "Wartortle", + "Metapod": "Caterpie", + "Butterfree": "Metapod", + "Kakuna": "Weedle", + "Beedrill": "Kakuna", + "Pidgeotto": "Pidgey", + "Pidgeot": "Pidgeotto", + "Raticate": "Rattata", + "Fearow": "Spearow", + "Arbok": "Ekans", + "Raichu": "Pikachu", + "Sandslash": "Sandshrew", + "Nidorina": "Nidoran F", + "Nidoqueen": "Nidorina", + "Nidorino": "Nidoran M", + "Nidoking": "Nidorino", + "Clefable": "Clefairy", + "Ninetales": "Vulpix", + "Wigglytuff": "Jigglypuff", + "Golbat": "Zubat", + "Gloom": "Oddish", + "Vileplume": "Gloom", + "Parasect": "Paras", + "Venomoth": "Venonat", + "Dugtrio": "Diglett", + "Persian": "Meowth", + "Golduck": "Psyduck", + "Primeape": "Mankey", + "Arcanine": "Growlithe", + "Poliwhirl": "Poliwag", + "Poliwrath": "Poliwhirl", + "Kadabra": "Abra", + "Alakazam": "Kadabra", + "Machoke": "Machop", + "Machamp": "Machoke", + "Weepinbell": "Bellsprout", + "Victreebel": "Weepinbell", + "Tentacruel": "Tentacool", + "Graveler": "Geodude", + "Golem": "Graveler", + "Rapidash": "Ponyta", + "Slowbro": "Slowpoke", + "Magneton": "Magnemite", + "Dodrio": "Doduo", + "Dewgong": "Seel", + "Muk": "Grimer", + "Cloyster": "Shellder", + "Haunter": "Gastly", + "Gengar": "Haunter", + "Hypno": "Drowzee", + "Kingler": "Krabby", + "Electrode": "Voltorb", + "Exeggutor": "Exeggcute", + "Marowak": "Cubone", + "Weezing": "Koffing", + "Rhydon": "Rhyhorn", + "Seadra": "Horsea", + "Seaking": "Goldeen", + "Starmie": "Staryu", + "Gyarados": "Magikarp", + "Vaporeon": "Eevee", + "Jolteon": "Eevee", + "Flareon": "Eevee", + "Omastar": "Omanyte", + "Kabutops": "Kabuto", + "Dragonair": "Dratini", + "Dragonite": "Dragonair" +} + +evolves_to = {} +for from_mon, to_mon in zip(evolves_from.values(), evolves_from.keys()): + if from_mon != "Eevee": + evolves_to[from_mon] = to_mon + +# basic_three_stage_pokemon = [] +# for mon in evolves_to.keys(): +# if evolves_to[mon] in evolves_to.keys(): +# basic_three_stage_pokemon.append(mon) +# print(basic_three_stage_pokemon) + +learnsets = { + 'Rhydon': ['Stomp', 'Tail Whip', 'Fury Attack', 'Horn Drill', 'Leer', 'Take Down'], + 'Kangaskhan': ['Bite', 'Tail Whip', 'Mega Punch', 'Leer', 'Dizzy Punch'], + 'Nidoran M': ['Horn Attack', 'Poison Sting', 'Focus Energy', 'Fury Attack', 'Horn Drill', 'Double Kick'], + 'Clefairy': ['Sing', 'Doubleslap', 'Minimize', 'Metronome', 'Defense Curl', 'Light Screen'], + 'Spearow': ['Leer', 'Fury Attack', 'Mirror Move', 'Drill Peck', 'Agility'], + 'Voltorb': ['Sonicboom', 'Selfdestruct', 'Light Screen', 'Swift', 'Explosion'], + 'Nidoking': ['Horn Attack', 'Poison Sting', 'Thrash'], + 'Slowbro': ['Disable', 'Headbutt', 'Growl', 'Water Gun', 'Withdraw', 'Amnesia', 'Psychic'], + 'Ivysaur': ['Leech Seed', 'Vine Whip', 'Poisonpowder', 'Razor Leaf', 'Growth', 'Sleep Powder', 'Solarbeam'], + 'Exeggutor': ['Stomp'], 'Lickitung': ['Stomp', 'Disable', 'Defense Curl', 'Slam', 'Screech'], + 'Exeggcute': ['Reflect', 'Leech Seed', 'Stun Spore', 'Poisonpowder', 'Solarbeam', 'Sleep Powder'], + 'Grimer': ['Poison Gas', 'Minimize', 'Sludge', 'Harden', 'Screech', 'Acid Armor'], + 'Gengar': ['Hypnosis', 'Dream Eater'], + 'Nidoran F': ['Scratch', 'Poison Sting', 'Tail Whip', 'Bite', 'Fury Swipes', 'Double Kick'], + 'Nidoqueen': ['Scratch', 'Poison Sting', 'Body Slam'], + 'Cubone': ['Leer', 'Focus Energy', 'Thrash', 'Bonemerang', 'Rage'], + 'Rhyhorn': ['Stomp', 'Tail Whip', 'Fury Attack', 'Horn Drill', 'Leer', 'Take Down'], + 'Lapras': ['Sing', 'Mist', 'Body Slam', 'Confuse Ray', 'Ice Beam', 'Hydro Pump'], + 'Mew': ['Transform', 'Mega Punch', 'Metronome', 'Psychic'], + 'Gyarados': ['Bite', 'Dragon Rage', 'Leer', 'Hydro Pump', 'Hyper Beam'], + 'Shellder': ['Supersonic', 'Clamp', 'Aurora Beam', 'Leer', 'Ice Beam'], + 'Tentacool': ['Supersonic', 'Wrap', 'Poison Sting', 'Water Gun', 'Constrict', 'Barrier', 'Screech', 'Hydro Pump'], + 'Gastly': ['Hypnosis', 'Dream Eater'], + 'Scyther': ['Leer', 'Focus Energy', 'Double Team', 'Slash', 'Swords Dance', 'Agility'], + 'Staryu': ['Water Gun', 'Harden', 'Recover', 'Swift', 'Minimize', 'Light Screen', 'Hydro Pump'], + 'Blastoise': ['Bubble', 'Water Gun', 'Bite', 'Withdraw', 'Skull Bash', 'Hydro Pump'], + 'Pinsir': ['Seismic Toss', 'Guillotine', 'Focus Energy', 'Harden', 'Slash', 'Swords Dance'], + 'Tangela': ['Absorb', 'Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Slam', 'Growth'], + 'Growlithe': ['Ember', 'Leer', 'Take Down', 'Agility', 'Flamethrower'], + 'Onix': ['Bind', 'Rock Throw', 'Rage', 'Slam', 'Harden'], + 'Fearow': ['Leer', 'Fury Attack', 'Mirror Move', 'Drill Peck', 'Agility'], + 'Pidgey': ['Sand Attack', 'Quick Attack', 'Whirlwind', 'Wing Attack', 'Agility', 'Mirror Move'], + 'Slowpoke': ['Disable', 'Headbutt', 'Growl', 'Water Gun', 'Amnesia', 'Psychic'], + 'Kadabra': ['Confusion', 'Disable', 'Psybeam', 'Recover', 'Psychic', 'Reflect'], + 'Graveler': ['Defense Curl', 'Rock Throw', 'Selfdestruct', 'Harden', 'Earthquake', 'Explosion'], + 'Chansey': ['Sing', 'Growl', 'Minimize', 'Defense Curl', 'Light Screen', 'Double Edge'], + 'Machoke': ['Low Kick', 'Leer', 'Focus Energy', 'Seismic Toss', 'Submission'], + 'Mr Mime': ['Confusion', 'Light Screen', 'Doubleslap', 'Meditate', 'Substitute'], + 'Hitmonlee': ['Rolling Kick', 'Jump Kick', 'Focus Energy', 'Hi Jump Kick', 'Mega Kick'], + 'Hitmonchan': ['Fire Punch', 'Ice Punch', 'Thunderpunch', 'Mega Punch', 'Counter'], + 'Arbok': ['Poison Sting', 'Bite', 'Glare', 'Screech', 'Acid'], + 'Parasect': ['Stun Spore', 'Leech Life', 'Spore', 'Slash', 'Growth'], + 'Psyduck': ['Tail Whip', 'Disable', 'Confusion', 'Fury Swipes', 'Hydro Pump'], + 'Drowzee': ['Disable', 'Confusion', 'Headbutt', 'Poison Gas', 'Psychic', 'Meditate'], + 'Golem': ['Defense Curl', 'Rock Throw', 'Selfdestruct', 'Harden', 'Earthquake', 'Explosion'], + 'Magmar': ['Leer', 'Confuse Ray', 'Fire Punch', 'Smokescreen', 'Smog', 'Flamethrower'], + 'Electabuzz': ['Thundershock', 'Screech', 'Thunderpunch', 'Light Screen', 'Thunder'], + 'Magneton': ['Sonicboom', 'Thundershock', 'Supersonic', 'Thunder Wave', 'Swift', 'Screech'], + 'Koffing': ['Sludge', 'Smokescreen', 'Selfdestruct', 'Haze', 'Explosion'], + 'Mankey': ['Karate Chop', 'Fury Swipes', 'Focus Energy', 'Seismic Toss', 'Thrash'], + 'Seel': ['Growl', 'Aurora Beam', 'Rest', 'Take Down', 'Ice Beam'], + 'Diglett': ['Growl', 'Dig', 'Sand Attack', 'Slash', 'Earthquake'], + 'Tauros': ['Stomp', 'Tail Whip', 'Leer', 'Rage', 'Take Down'], + 'Farfetchd': ['Leer', 'Fury Attack', 'Swords Dance', 'Agility', 'Slash'], + 'Venonat': ['Poisonpowder', 'Leech Life', 'Stun Spore', 'Psybeam', 'Sleep Powder', 'Psychic'], + 'Dragonite': ['Thunder Wave', 'Agility', 'Slam', 'Dragon Rage', 'Hyper Beam'], + 'Doduo': ['Growl', 'Fury Attack', 'Drill Peck', 'Rage', 'Tri Attack', 'Agility'], + 'Poliwag': ['Hypnosis', 'Water Gun', 'Doubleslap', 'Body Slam', 'Amnesia', 'Hydro Pump'], + 'Jynx': ['Lick', 'Doubleslap', 'Ice Punch', 'Body Slam', 'Thrash', 'Blizzard'], + 'Moltres': ['Leer', 'Agility', 'Sky Attack'], + 'Articuno': ['Blizzard', 'Agility', 'Mist'], + 'Zapdos': ['Thunder', 'Agility', 'Light Screen'], + 'Meowth': ['Bite', 'Pay Day', 'Screech', 'Fury Swipes', 'Slash'], + 'Krabby': ['Vicegrip', 'Guillotine', 'Stomp', 'Crabhammer', 'Harden'], + 'Vulpix': ['Quick Attack', 'Roar', 'Confuse Ray', 'Flamethrower', 'Fire Spin'], + 'Pikachu': ['Thunder Wave', 'Quick Attack', 'Swift', 'Agility', 'Thunder'], + 'Dratini': ['Thunder Wave', 'Agility', 'Slam', 'Dragon Rage', 'Hyper Beam'], + 'Dragonair': ['Thunder Wave', 'Agility', 'Slam', 'Dragon Rage', 'Hyper Beam'], + 'Kabuto': ['Absorb', 'Slash', 'Leer', 'Hydro Pump'], + 'Kabutops': ['Absorb', 'Slash', 'Leer', 'Hydro Pump'], + 'Horsea': ['Smokescreen', 'Leer', 'Water Gun', 'Agility', 'Hydro Pump'], + 'Seadra': ['Smokescreen', 'Leer', 'Water Gun', 'Agility', 'Hydro Pump'], + 'Sandshrew': ['Sand Attack', 'Slash', 'Poison Sting', 'Swift', 'Fury Swipes'], + 'Sandslash': ['Sand Attack', 'Slash', 'Poison Sting', 'Swift', 'Fury Swipes'], + 'Omanyte': ['Horn Attack', 'Leer', 'Spike Cannon', 'Hydro Pump'], + 'Omastar': ['Horn Attack', 'Leer', 'Spike Cannon', 'Hydro Pump'], + 'Jigglypuff': ['Pound', 'Disable', 'Defense Curl', 'Doubleslap', 'Rest', 'Body Slam', 'Double Edge'], + 'Eevee': ['Quick Attack', 'Tail Whip', 'Bite', 'Take Down'], + 'Flareon': ['Quick Attack', 'Ember', 'Tail Whip', 'Bite', 'Leer', 'Fire Spin', 'Rage', 'Flamethrower'], + 'Jolteon': ['Quick Attack', 'Thundershock', 'Tail Whip', 'Thunder Wave', 'Double Kick', 'Agility', 'Pin Missile', 'Thunder'], + 'Vaporeon': ['Quick Attack', 'Water Gun', 'Tail Whip', 'Bite', 'Acid Armor', 'Haze', 'Mist', 'Hydro Pump'], + 'Machop': ['Low Kick', 'Leer', 'Focus Energy', 'Seismic Toss', 'Submission'], + 'Zubat': ['Supersonic', 'Bite', 'Confuse Ray', 'Wing Attack', 'Haze'], + 'Ekans': ['Poison Sting', 'Bite', 'Glare', 'Screech', 'Acid'], + 'Paras': ['Stun Spore', 'Leech Life', 'Spore', 'Slash', 'Growth'], + 'Poliwhirl': ['Hypnosis', 'Water Gun', 'Doubleslap', 'Body Slam', 'Amnesia', 'Hydro Pump'], + 'Poliwrath': ['Hypnosis', 'Water Gun'], + 'Beedrill': ['Fury Attack', 'Focus Energy', 'Twineedle', 'Rage', 'Pin Missile', 'Agility'], + 'Dodrio': ['Growl', 'Fury Attack', 'Drill Peck', 'Rage', 'Tri Attack', 'Agility'], + 'Primeape': ['Karate Chop', 'Fury Swipes', 'Focus Energy', 'Seismic Toss', 'Thrash'], + 'Dugtrio': ['Growl', 'Dig', 'Sand Attack', 'Slash', 'Earthquake'], + 'Venomoth': ['Poisonpowder', 'Leech Life', 'Stun Spore', 'Psybeam', 'Sleep Powder', 'Psychic'], + 'Dewgong': ['Growl', 'Aurora Beam', 'Rest', 'Take Down', 'Ice Beam'], + 'Butterfree': ['Confusion', 'Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Supersonic', 'Whirlwind', 'Psybeam'], + 'Machamp': ['Low Kick', 'Leer', 'Focus Energy', 'Seismic Toss', 'Submission'], + 'Golduck': ['Tail Whip', 'Disable', 'Confusion', 'Fury Swipes', 'Hydro Pump'], + 'Hypno': ['Disable', 'Confusion', 'Headbutt', 'Poison Gas', 'Psychic', 'Meditate'], + 'Golbat': ['Supersonic', 'Bite', 'Confuse Ray', 'Wing Attack', 'Haze'], + 'Mewtwo': ['Barrier', 'Psychic', 'Recover', 'Mist', 'Amnesia'], + 'Snorlax': ['Body Slam', 'Harden', 'Double Edge', 'Hyper Beam'], + 'Magikarp': ['Tackle'], + 'Muk': ['Poison Gas', 'Minimize', 'Sludge', 'Harden', 'Screech', 'Acid Armor'], + 'Kingler': ['Vicegrip', 'Guillotine', 'Stomp', 'Crabhammer', 'Harden'], + 'Cloyster': ['Spike Cannon'], + 'Electrode': ['Sonicboom', 'Selfdestruct', 'Light Screen', 'Swift', 'Explosion'], + 'Weezing': ['Sludge', 'Smokescreen', 'Selfdestruct', 'Haze', 'Explosion'], + 'Persian': ['Bite', 'Pay Day', 'Screech', 'Fury Swipes', 'Slash'], + 'Marowak': ['Leer', 'Focus Energy', 'Thrash', 'Bonemerang', 'Rage'], + 'Haunter': ['Hypnosis', 'Dream Eater'], + 'Alakazam': ['Confusion', 'Disable', 'Psybeam', 'Recover', 'Psychic', 'Reflect'], + 'Pidgeotto': ['Sand Attack', 'Quick Attack', 'Whirlwind', 'Wing Attack', 'Agility', 'Mirror Move'], + 'Pidgeot': ['Sand Attack', 'Quick Attack', 'Whirlwind', 'Wing Attack', 'Agility', 'Mirror Move'], + 'Bulbasaur': ['Leech Seed', 'Vine Whip', 'Poisonpowder', 'Razor Leaf', 'Growth', 'Sleep Powder', 'Solarbeam'], + 'Venusaur': ['Leech Seed', 'Vine Whip', 'Poisonpowder', 'Razor Leaf', 'Growth', 'Sleep Powder', 'Solarbeam'], + 'Tentacruel': ['Supersonic', 'Wrap', 'Poison Sting', 'Water Gun', 'Constrict', 'Barrier', 'Screech', 'Hydro Pump'], + 'Goldeen': ['Supersonic', 'Horn Attack', 'Fury Attack', 'Waterfall', 'Horn Drill', 'Agility'], + 'Seaking': ['Supersonic', 'Horn Attack', 'Fury Attack', 'Waterfall', 'Horn Drill', 'Agility'], + 'Ponyta': ['Tail Whip', 'Stomp', 'Growl', 'Fire Spin', 'Take Down', 'Agility'], + 'Rapidash': ['Tail Whip', 'Stomp', 'Growl', 'Fire Spin', 'Take Down', 'Agility'], + 'Rattata': ['Quick Attack', 'Hyper Fang', 'Focus Energy', 'Super Fang'], + 'Raticate': ['Quick Attack', 'Hyper Fang', 'Focus Energy', 'Super Fang'], + 'Nidorino': ['Horn Attack', 'Poison Sting', 'Focus Energy', 'Fury Attack', 'Horn Drill', 'Double Kick'], + 'Nidorina': ['Scratch', 'Poison Sting', 'Tail Whip', 'Bite', 'Fury Swipes', 'Double Kick'], + 'Geodude': ['Defense Curl', 'Rock Throw', 'Selfdestruct', 'Harden', 'Earthquake', 'Explosion'], + 'Porygon': ['Psybeam', 'Recover', 'Agility', 'Tri Attack'], + 'Aerodactyl': ['Supersonic', 'Bite', 'Take Down', 'Hyper Beam'], + 'Magnemite': ['Sonicboom', 'Thundershock', 'Supersonic', 'Thunder Wave', 'Swift', 'Screech'], + 'Charmander': ['Ember', 'Leer', 'Rage', 'Slash', 'Flamethrower', 'Fire Spin'], + 'Squirtle': ['Bubble', 'Water Gun', 'Bite', 'Withdraw', 'Skull Bash', 'Hydro Pump'], + 'Charmeleon': ['Ember', 'Leer', 'Rage', 'Slash', 'Flamethrower', 'Fire Spin'], + 'Wartortle': ['Bubble', 'Water Gun', 'Bite', 'Withdraw', 'Skull Bash', 'Hydro Pump'], + 'Charizard': ['Ember', 'Leer', 'Rage', 'Slash', 'Flamethrower', 'Fire Spin'], + 'Oddish': ['Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Acid', 'Petal Dance', 'Solarbeam'], + 'Gloom': ['Poisonpowder', 'Stun Spore', 'Sleep Powder', 'Acid', 'Petal Dance', 'Solarbeam'], + 'Vileplume': ['Poisonpowder', 'Stun Spore', 'Sleep Powder'], + 'Bellsprout': ['Wrap', 'Poisonpowder', 'Sleep Powder', 'Stun Spore', 'Acid', 'Razor Leaf', 'Slam'], + 'Weepinbell': ['Wrap', 'Poisonpowder', 'Sleep Powder', 'Stun Spore', 'Acid', 'Razor Leaf', 'Slam'], + 'Victreebel': ['Wrap', 'Poisonpowder', 'Sleep Powder'] +} + +moves = { + 'No Move': {'id': 0, 'power': 0, 'type': 'Typeless', 'accuracy': 0, 'pp': 0}, + 'Pound': {'id': 1, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Karate Chop': {'id': 2, 'power': 50, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, + 'Doubleslap': {'id': 3, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 10}, + 'Comet Punch': {'id': 4, 'power': 18, 'type': 'Normal', 'accuracy': 85, 'pp': 15}, + 'Mega Punch': {'id': 5, 'power': 80, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Pay Day': {'id': 6, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Fire Punch': {'id': 7, 'power': 75, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, + 'Ice Punch': {'id': 8, 'power': 75, 'type': 'Ice', 'accuracy': 100, 'pp': 15}, + 'Thunderpunch': {'id': 9, 'power': 75, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, + 'Scratch': {'id': 10, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Vicegrip': {'id': 11, 'power': 55, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Guillotine': {'id': 12, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, + 'Razor Wind': {'id': 13, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, + 'Swords Dance': {'id': 14, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Cut': {'id': 15, 'power': 50, 'type': 'Normal', 'accuracy': 95, 'pp': 30}, + 'Gust': {'id': 16, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Wing Attack': {'id': 17, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, + 'Whirlwind': {'id': 18, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Fly': {'id': 19, 'power': 70, 'type': 'Flying', 'accuracy': 95, 'pp': 15}, + 'Bind': {'id': 20, 'power': 15, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, + 'Slam': {'id': 21, 'power': 80, 'type': 'Normal', 'accuracy': 75, 'pp': 20}, + 'Vine Whip': {'id': 22, 'power': 35, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, + 'Stomp': {'id': 23, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Double Kick': {'id': 24, 'power': 30, 'type': 'Fighting', 'accuracy': 100, 'pp': 30}, + 'Mega Kick': {'id': 25, 'power': 120, 'type': 'Normal', 'accuracy': 75, 'pp': 5}, + 'Jump Kick': {'id': 26, 'power': 70, 'type': 'Fighting', 'accuracy': 95, 'pp': 25}, + 'Rolling Kick': {'id': 27, 'power': 60, 'type': 'Fighting', 'accuracy': 85, 'pp': 15}, + 'Sand Attack': {'id': 28, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Headbutt': {'id': 29, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Horn Attack': {'id': 30, 'power': 65, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, + 'Fury Attack': {'id': 31, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Horn Drill': {'id': 32, 'power': 1, 'type': 'Normal', 'accuracy': 30, 'pp': 5}, + 'Tackle': {'id': 33, 'power': 35, 'type': 'Normal', 'accuracy': 95, 'pp': 35}, + 'Body Slam': {'id': 34, 'power': 85, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Wrap': {'id': 35, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Take Down': {'id': 36, 'power': 90, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Thrash': {'id': 37, 'power': 90, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Double Edge': {'id': 38, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Tail Whip': {'id': 39, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Poison Sting': {'id': 40, 'power': 15, 'type': 'Poison', 'accuracy': 100, 'pp': 35}, + 'Twineedle': {'id': 41, 'power': 25, 'type': 'Bug', 'accuracy': 100, 'pp': 20}, + 'Pin Missile': {'id': 42, 'power': 14, 'type': 'Bug', 'accuracy': 85, 'pp': 20}, + 'Leer': {'id': 43, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Bite': {'id': 44, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 25}, + 'Growl': {'id': 45, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Roar': {'id': 46, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Sing': {'id': 47, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 15}, + 'Supersonic': {'id': 48, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, + 'Sonicboom': {'id': 49, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 20}, + 'Disable': {'id': 50, 'power': 0, 'type': 'Normal', 'accuracy': 55, 'pp': 20}, + 'Acid': {'id': 51, 'power': 40, 'type': 'Poison', 'accuracy': 100, 'pp': 30}, + 'Ember': {'id': 52, 'power': 40, 'type': 'Fire', 'accuracy': 100, 'pp': 25}, + 'Flamethrower': {'id': 53, 'power': 95, 'type': 'Fire', 'accuracy': 100, 'pp': 15}, + 'Mist': {'id': 54, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, + 'Water Gun': {'id': 55, 'power': 40, 'type': 'Water', 'accuracy': 100, 'pp': 25}, + 'Hydro Pump': {'id': 56, 'power': 120, 'type': 'Water', 'accuracy': 80, 'pp': 5}, + 'Surf': {'id': 57, 'power': 95, 'type': 'Water', 'accuracy': 100, 'pp': 15}, + 'Ice Beam': {'id': 58, 'power': 95, 'type': 'Ice', 'accuracy': 100, 'pp': 10}, + 'Blizzard': {'id': 59, 'power': 120, 'type': 'Ice', 'accuracy': 90, 'pp': 5}, + 'Psybeam': {'id': 60, 'power': 65, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Bubblebeam': {'id': 61, 'power': 65, 'type': 'Water', 'accuracy': 100, 'pp': 20}, + 'Aurora Beam': {'id': 62, 'power': 65, 'type': 'Ice', 'accuracy': 100, 'pp': 20}, + 'Hyper Beam': {'id': 63, 'power': 150, 'type': 'Normal', 'accuracy': 90, 'pp': 5}, + 'Peck': {'id': 64, 'power': 35, 'type': 'Flying', 'accuracy': 100, 'pp': 35}, + 'Drill Peck': {'id': 65, 'power': 80, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, + 'Submission': {'id': 66, 'power': 80, 'type': 'Fighting', 'accuracy': 80, 'pp': 25}, + 'Low Kick': {'id': 67, 'power': 50, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, + 'Counter': {'id': 68, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, + 'Seismic Toss': {'id': 69, 'power': 1, 'type': 'Fighting', 'accuracy': 100, 'pp': 20}, + 'Strength': {'id': 70, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Absorb': {'id': 71, 'power': 20, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, + 'Mega Drain': {'id': 72, 'power': 40, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, + 'Leech Seed': {'id': 73, 'power': 0, 'type': 'Grass', 'accuracy': 90, 'pp': 10}, + 'Growth': {'id': 74, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Razor Leaf': {'id': 75, 'power': 55, 'type': 'Grass', 'accuracy': 95, 'pp': 25}, + 'Solarbeam': {'id': 76, 'power': 120, 'type': 'Grass', 'accuracy': 100, 'pp': 10}, + 'Poisonpowder': {'id': 77, 'power': 0, 'type': 'Poison', 'accuracy': 75, 'pp': 35}, + 'Stun Spore': {'id': 78, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 30}, + 'Sleep Powder': {'id': 79, 'power': 0, 'type': 'Grass', 'accuracy': 75, 'pp': 15}, + 'Petal Dance': {'id': 80, 'power': 70, 'type': 'Grass', 'accuracy': 100, 'pp': 20}, + 'String Shot': {'id': 81, 'power': 0, 'type': 'Bug', 'accuracy': 95, 'pp': 40}, + 'Dragon Rage': {'id': 82, 'power': 1, 'type': 'Dragon', 'accuracy': 100, 'pp': 10}, + 'Fire Spin': {'id': 83, 'power': 15, 'type': 'Fire', 'accuracy': 70, 'pp': 15}, + 'Thundershock': {'id': 84, 'power': 40, 'type': 'Electric', 'accuracy': 100, 'pp': 30}, + 'Thunderbolt': {'id': 85, 'power': 95, 'type': 'Electric', 'accuracy': 100, 'pp': 15}, + 'Thunder Wave': {'id': 86, 'power': 0, 'type': 'Electric', 'accuracy': 100, 'pp': 20}, + 'Thunder': {'id': 87, 'power': 120, 'type': 'Electric', 'accuracy': 70, 'pp': 10}, + 'Rock Throw': {'id': 88, 'power': 50, 'type': 'Rock', 'accuracy': 65, 'pp': 15}, + 'Earthquake': {'id': 89, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, + 'Fissure': {'id': 90, 'power': 1, 'type': 'Ground', 'accuracy': 30, 'pp': 5}, + 'Dig': {'id': 91, 'power': 100, 'type': 'Ground', 'accuracy': 100, 'pp': 10}, + 'Toxic': {'id': 92, 'power': 0, 'type': 'Poison', 'accuracy': 85, 'pp': 10}, + 'Confusion': {'id': 93, 'power': 50, 'type': 'Psychic', 'accuracy': 100, 'pp': 25}, + 'Psychic': {'id': 94, 'power': 90, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, + 'Hypnosis': {'id': 95, 'power': 0, 'type': 'Psychic', 'accuracy': 60, 'pp': 20}, + 'Meditate': {'id': 96, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 40}, + 'Agility': {'id': 97, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, + 'Quick Attack': {'id': 98, 'power': 40, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Rage': {'id': 99, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Teleport': {'id': 100, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Night Shade': {'id': 101, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 15}, + 'Mimic': {'id': 102, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Screech': {'id': 103, 'power': 0, 'type': 'Normal', 'accuracy': 85, 'pp': 40}, + 'Double Team': {'id': 104, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Recover': {'id': 105, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Harden': {'id': 106, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Minimize': {'id': 107, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Smokescreen': {'id': 108, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Confuse Ray': {'id': 109, 'power': 0, 'type': 'Ghost', 'accuracy': 100, 'pp': 10}, + 'Withdraw': {'id': 110, 'power': 0, 'type': 'Water', 'accuracy': 100, 'pp': 40}, + 'Defense Curl': {'id': 111, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Barrier': {'id': 112, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, + 'Light Screen': {'id': 113, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 30}, + 'Haze': {'id': 114, 'power': 0, 'type': 'Ice', 'accuracy': 100, 'pp': 30}, + 'Reflect': {'id': 115, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Focus Energy': {'id': 116, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Bide': {'id': 117, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Metronome': {'id': 118, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Mirror Move': {'id': 119, 'power': 0, 'type': 'Flying', 'accuracy': 100, 'pp': 20}, + 'Selfdestruct': {'id': 120, 'power': 130, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, + 'Egg Bomb': {'id': 121, 'power': 100, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, + 'Lick': {'id': 122, 'power': 20, 'type': 'Ghost', 'accuracy': 100, 'pp': 30}, + 'Smog': {'id': 123, 'power': 20, 'type': 'Poison', 'accuracy': 70, 'pp': 20}, + 'Sludge': {'id': 124, 'power': 65, 'type': 'Poison', 'accuracy': 100, 'pp': 20}, + 'Bone Club': {'id': 125, 'power': 65, 'type': 'Ground', 'accuracy': 85, 'pp': 20}, + 'Fire Blast': {'id': 126, 'power': 120, 'type': 'Fire', 'accuracy': 85, 'pp': 5}, + 'Waterfall': {'id': 127, 'power': 80, 'type': 'Water', 'accuracy': 100, 'pp': 15}, + 'Clamp': {'id': 128, 'power': 35, 'type': 'Water', 'accuracy': 75, 'pp': 10}, + 'Swift': {'id': 129, 'power': 60, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Skull Bash': {'id': 130, 'power': 100, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Spike Cannon': {'id': 131, 'power': 20, 'type': 'Normal', 'accuracy': 100, 'pp': 15}, + 'Constrict': {'id': 132, 'power': 10, 'type': 'Normal', 'accuracy': 100, 'pp': 35}, + 'Amnesia': {'id': 133, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 20}, + 'Kinesis': {'id': 134, 'power': 0, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, + 'Softboiled': {'id': 135, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Hi Jump Kick': {'id': 136, 'power': 85, 'type': 'Fighting', 'accuracy': 90, 'pp': 20}, + 'Glare': {'id': 137, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 30}, + 'Dream Eater': {'id': 138, 'power': 100, 'type': 'Psychic', 'accuracy': 100, 'pp': 15}, + 'Poison Gas': {'id': 139, 'power': 0, 'type': 'Poison', 'accuracy': 55, 'pp': 40}, + 'Barrage': {'id': 140, 'power': 15, 'type': 'Normal', 'accuracy': 85, 'pp': 20}, + 'Leech Life': {'id': 141, 'power': 20, 'type': 'Bug', 'accuracy': 100, 'pp': 15}, + 'Lovely Kiss': {'id': 142, 'power': 0, 'type': 'Normal', 'accuracy': 75, 'pp': 10}, + 'Sky Attack': {'id': 143, 'power': 140, 'type': 'Flying', 'accuracy': 90, 'pp': 5}, + 'Transform': {'id': 144, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Bubble': {'id': 145, 'power': 20, 'type': 'Water', 'accuracy': 100, 'pp': 30}, + 'Dizzy Punch': {'id': 146, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Spore': {'id': 147, 'power': 0, 'type': 'Grass', 'accuracy': 100, 'pp': 15}, + 'Flash': {'id': 148, 'power': 0, 'type': 'Normal', 'accuracy': 70, 'pp': 20}, + 'Psywave': {'id': 149, 'power': 1, 'type': 'Psychic', 'accuracy': 80, 'pp': 15}, + 'Splash': {'id': 150, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 40}, + 'Acid Armor': {'id': 151, 'power': 0, 'type': 'Poison', 'accuracy': 100, 'pp': 40}, + 'Crabhammer': {'id': 152, 'power': 90, 'type': 'Water', 'accuracy': 85, 'pp': 10}, + 'Explosion': {'id': 153, 'power': 170, 'type': 'Normal', 'accuracy': 100, 'pp': 5}, + 'Fury Swipes': {'id': 154, 'power': 18, 'type': 'Normal', 'accuracy': 80, 'pp': 15}, + 'Bonemerang': {'id': 155, 'power': 50, 'type': 'Ground', 'accuracy': 90, 'pp': 10}, + 'Rest': {'id': 156, 'power': 0, 'type': 'Psychic', 'accuracy': 100, 'pp': 10}, + 'Rock Slide': {'id': 157, 'power': 75, 'type': 'Rock', 'accuracy': 90, 'pp': 10}, + 'Hyper Fang': {'id': 158, 'power': 80, 'type': 'Normal', 'accuracy': 90, 'pp': 15}, + 'Sharpen': {'id': 159, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Conversion': {'id': 160, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 30}, + 'Tri Attack': {'id': 161, 'power': 80, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + 'Super Fang': {'id': 162, 'power': 1, 'type': 'Normal', 'accuracy': 90, 'pp': 10}, + 'Slash': {'id': 163, 'power': 70, 'type': 'Normal', 'accuracy': 100, 'pp': 20}, + 'Substitute': {'id': 164, 'power': 0, 'type': 'Normal', 'accuracy': 100, 'pp': 10}, + #'Struggle': {'id': 165, 'power': 50, 'type': 'Struggle_Type', 'accuracy': 100, 'pp': 10} +} + +encounter_tables = {'Wild_Super_Rod_A': 2, 'Wild_Super_Rod_B': 2, 'Wild_Super_Rod_C': 3, 'Wild_Super_Rod_D': 2, + 'Wild_Super_Rod_E': 2, 'Wild_Super_Rod_F': 4, 'Wild_Super_Rod_G': 4, 'Wild_Super_Rod_H': 4, + 'Wild_Super_Rod_I': 4, 'Wild_Super_Rod_J': 4, 'Wild_Route1': 10, 'Wild_Route2': 10, + 'Wild_Route22': 10, 'Wild_ViridianForest': 10, 'Wild_Route3': 10, 'Wild_MtMoon1F': 10, + 'Wild_MtMoonB1F': 10, 'Wild_MtMoonB2F': 10, 'Wild_Route4': 10, 'Wild_Route24': 10, + 'Wild_Route25': 10, 'Wild_Route9': 10, 'Wild_Route5': 10, 'Wild_Route6': 10, + 'Wild_Route11': 10, 'Wild_RockTunnel1F': 10, 'Wild_RockTunnelB1F': 10, 'Wild_Route10': 10, + 'Wild_Route12': 10, 'Wild_Route8': 10, 'Wild_Route7': 10, 'Wild_PokemonTower3F': 10, + 'Wild_PokemonTower4F': 10, 'Wild_PokemonTower5F': 10, 'Wild_PokemonTower6F': 10, + 'Wild_PokemonTower7F': 10, 'Wild_Route13': 10, 'Wild_Route14': 10, 'Wild_Route15': 10, + 'Wild_Route16': 10, 'Wild_Route17': 10, 'Wild_Route18': 10, 'Wild_SafariZoneCenter': 10, + 'Wild_SafariZoneEast': 10, 'Wild_SafariZoneNorth': 10, 'Wild_SafariZoneWest': 10, + 'Wild_SeaRoutes': 10, 'Wild_SeafoamIslands1F': 10, 'Wild_SeafoamIslandsB1F': 10, + 'Wild_SeafoamIslandsB2F': 10, 'Wild_SeafoamIslandsB3F': 10, 'Wild_SeafoamIslandsB4F': 10, + 'Wild_PokemonMansion1F': 10, 'Wild_PokemonMansion2F': 10, 'Wild_PokemonMansion3F': 10, + 'Wild_PokemonMansionB1F': 10, 'Wild_Route21': 10, 'Wild_CeruleanCave1F': 10, + 'Wild_CeruleanCave2F': 10, 'Wild_CeruleanCaveB1F': 10, 'Wild_PowerPlant': 10, + 'Wild_Route23': 10, 'Wild_VictoryRoad2F': 10, 'Wild_VictoryRoad3F': 10, + 'Wild_VictoryRoad1F': 10, 'Wild_DiglettsCave': 10, 'Wild_Good_Rod': 2} + +hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"] + +tm_moves = [ + 'Mega Punch', 'Razor Wind', 'Swords Dance', 'Whirlwind', 'Mega Kick', 'Toxic', 'Horn Drill', 'Body Slam', + 'Take Down', 'Double Edge', 'Bubblebeam', 'Water Gun', 'Ice Beam', 'Blizzard', 'Hyper Beam', 'Pay Day', + 'Submission', 'Counter', 'Seismic Toss', 'Rage', 'Mega Drain', 'Solarbeam', 'Dragon Rage', 'Thunderbolt', 'Thunder', + 'Earthquake', 'Fissure', 'Dig', 'Psychic', 'Teleport', 'Mimic', 'Double Team', 'Reflect', 'Bide', 'Metronome', + 'Selfdestruct', 'Egg Bomb', 'Fire Blast', 'Swift', 'Skull Bash', 'Softboiled', 'Dream Eater', 'Sky Attack', 'Rest', + 'Thunder Wave', 'Psywave', 'Explosion', 'Rock Slide', 'Tri Attack', 'Substitute' +] + + + +first_stage_pokemon = [pokemon for pokemon in pokemon_data.keys() if pokemon not in evolves_from] +legendary_pokemon = ["Articuno", "Zapdos", "Moltres", "Mewtwo", "Mew"] + diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py new file mode 100644 index 0000000000..1650e640cb --- /dev/null +++ b/worlds/pokemon_rb/regions.py @@ -0,0 +1,305 @@ + +from BaseClasses import MultiWorld, Region, Entrance, RegionType, LocationProgressType +from worlds.generic.Rules import add_item_rule +from .locations import location_data, PokemonRBLocation + + +def create_region(world: MultiWorld, player: int, name: str, locations_per_region=None, exits=None): + ret = Region(name, RegionType.Generic, name, player, world) + for location in locations_per_region.get(name, []): + if (world.randomize_hidden_items[player].value or "Hidden" not in location.name) and \ + (world.extra_key_items[player].value or name != "Rock Tunnel B1F" or "Item" not in location.name) and \ + (world.tea[player].value or location.name != "Celadon City - Mansion Lady"): + location.parent_region = ret + ret.locations.append(location) + if world.randomize_hidden_items[player].value == 2 and "Hidden" in location.name: + location.progress_type = LocationProgressType.EXCLUDED + add_item_rule(location, lambda i: not (i.advancement or i.useful)) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + locations_per_region[name] = [] + return ret + + +def create_regions(world: MultiWorld, player: int): + locations_per_region = {} + for location in location_data: + locations_per_region.setdefault(location.region, []) + locations_per_region[location.region].append(PokemonRBLocation(player, location.name, location.address, + location.rom_address)) + regions = [ + create_region(world, player, "Menu", locations_per_region), + create_region(world, player, "Anywhere", locations_per_region), + create_region(world, player, "Fossil", locations_per_region), + create_region(world, player, "Pallet Town", locations_per_region), + create_region(world, player, "Route 1", locations_per_region), + create_region(world, player, "Viridian City", locations_per_region), + create_region(world, player, "Viridian City North", locations_per_region), + create_region(world, player, "Viridian Gym", locations_per_region), + create_region(world, player, "Route 2", locations_per_region), + create_region(world, player, "Route 2 East", locations_per_region), + create_region(world, player, "Diglett's Cave", locations_per_region), + create_region(world, player, "Route 22", locations_per_region), + create_region(world, player, "Route 23 South", locations_per_region), + create_region(world, player, "Route 23 North", locations_per_region), + create_region(world, player, "Viridian Forest", locations_per_region), + create_region(world, player, "Pewter City", locations_per_region), + create_region(world, player, "Pewter Gym", locations_per_region), + create_region(world, player, "Route 3", locations_per_region), + create_region(world, player, "Mt Moon 1F", locations_per_region), + create_region(world, player, "Mt Moon B1F", locations_per_region), + create_region(world, player, "Mt Moon B2F", locations_per_region), + create_region(world, player, "Route 4", locations_per_region), + create_region(world, player, "Cerulean City", locations_per_region), + create_region(world, player, "Cerulean Gym", locations_per_region), + create_region(world, player, "Route 24", locations_per_region), + create_region(world, player, "Route 25", locations_per_region), + create_region(world, player, "Route 9", locations_per_region), + create_region(world, player, "Route 10 North", locations_per_region), + create_region(world, player, "Rock Tunnel 1F", locations_per_region), + create_region(world, player, "Rock Tunnel B1F", locations_per_region), + create_region(world, player, "Power Plant", locations_per_region), + create_region(world, player, "Route 10 South", locations_per_region), + create_region(world, player, "Lavender Town", locations_per_region), + create_region(world, player, "Pokemon Tower 1F", locations_per_region), + create_region(world, player, "Pokemon Tower 2F", locations_per_region), + create_region(world, player, "Pokemon Tower 3F", locations_per_region), + create_region(world, player, "Pokemon Tower 4F", locations_per_region), + create_region(world, player, "Pokemon Tower 5F", locations_per_region), + create_region(world, player, "Pokemon Tower 6F", locations_per_region), + create_region(world, player, "Pokemon Tower 7F", locations_per_region), + create_region(world, player, "Route 5", locations_per_region), + create_region(world, player, "Saffron City", locations_per_region), + create_region(world, player, "Saffron Gym", locations_per_region), + create_region(world, player, "Copycat's House", locations_per_region), + create_region(world, player, "Underground Tunnel North-South", locations_per_region), + create_region(world, player, "Route 6", locations_per_region), + create_region(world, player, "Vermilion City", locations_per_region), + create_region(world, player, "Vermilion Gym", locations_per_region), + create_region(world, player, "S.S. Anne 1F", locations_per_region), + create_region(world, player, "S.S. Anne B1F", locations_per_region), + create_region(world, player, "S.S. Anne 2F", locations_per_region), + create_region(world, player, "Route 11", locations_per_region), + create_region(world, player, "Route 11 East", locations_per_region), + create_region(world, player, "Route 12 North", locations_per_region), + create_region(world, player, "Route 12 South", locations_per_region), + create_region(world, player, "Route 12 Grass", locations_per_region), + create_region(world, player, "Route 12 West", locations_per_region), + create_region(world, player, "Route 7", locations_per_region), + create_region(world, player, "Underground Tunnel West-East", locations_per_region), + create_region(world, player, "Route 8", locations_per_region), + create_region(world, player, "Route 8 Grass", locations_per_region), + create_region(world, player, "Celadon City", locations_per_region), + create_region(world, player, "Celadon Prize Corner", locations_per_region), + create_region(world, player, "Celadon Gym", locations_per_region), + create_region(world, player, "Route 16", locations_per_region), + create_region(world, player, "Route 16 North", locations_per_region), + create_region(world, player, "Route 17", locations_per_region), + create_region(world, player, "Route 18", locations_per_region), + create_region(world, player, "Fuchsia City", locations_per_region), + create_region(world, player, "Fuchsia Gym", locations_per_region), + create_region(world, player, "Safari Zone Gate", locations_per_region), + create_region(world, player, "Safari Zone Center", locations_per_region), + create_region(world, player, "Safari Zone East", locations_per_region), + create_region(world, player, "Safari Zone North", locations_per_region), + create_region(world, player, "Safari Zone West", locations_per_region), + create_region(world, player, "Route 15", locations_per_region), + create_region(world, player, "Route 14", locations_per_region), + create_region(world, player, "Route 13", locations_per_region), + create_region(world, player, "Route 19", locations_per_region), + create_region(world, player, "Route 20 East", locations_per_region), + create_region(world, player, "Route 20 West", locations_per_region), + create_region(world, player, "Seafoam Islands 1F", locations_per_region), + create_region(world, player, "Seafoam Islands B1F", locations_per_region), + create_region(world, player, "Seafoam Islands B2F", locations_per_region), + create_region(world, player, "Seafoam Islands B3F", locations_per_region), + create_region(world, player, "Seafoam Islands B4F", locations_per_region), + create_region(world, player, "Cinnabar Island", locations_per_region), + create_region(world, player, "Cinnabar Gym", locations_per_region), + create_region(world, player, "Route 21", locations_per_region), + create_region(world, player, "Silph Co 1F", locations_per_region), + create_region(world, player, "Silph Co 2F", locations_per_region), + create_region(world, player, "Silph Co 3F", locations_per_region), + create_region(world, player, "Silph Co 4F", locations_per_region), + create_region(world, player, "Silph Co 5F", locations_per_region), + create_region(world, player, "Silph Co 6F", locations_per_region), + create_region(world, player, "Silph Co 7F", locations_per_region), + create_region(world, player, "Silph Co 8F", locations_per_region), + create_region(world, player, "Silph Co 9F", locations_per_region), + create_region(world, player, "Silph Co 10F", locations_per_region), + create_region(world, player, "Silph Co 11F", locations_per_region), + create_region(world, player, "Rocket Hideout B1F", locations_per_region), + create_region(world, player, "Rocket Hideout B2F", locations_per_region), + create_region(world, player, "Rocket Hideout B3F", locations_per_region), + create_region(world, player, "Rocket Hideout B4F", locations_per_region), + create_region(world, player, "Pokemon Mansion 1F", locations_per_region), + create_region(world, player, "Pokemon Mansion 2F", locations_per_region), + create_region(world, player, "Pokemon Mansion 3F", locations_per_region), + create_region(world, player, "Pokemon Mansion B1F", locations_per_region), + create_region(world, player, "Victory Road 1F", locations_per_region), + create_region(world, player, "Victory Road 2F", locations_per_region), + create_region(world, player, "Victory Road 3F", locations_per_region), + create_region(world, player, "Indigo Plateau", locations_per_region), + create_region(world, player, "Cerulean Cave 1F", locations_per_region), + create_region(world, player, "Cerulean Cave 2F", locations_per_region), + create_region(world, player, "Cerulean Cave B1F", locations_per_region), + create_region(world, player, "Evolution", locations_per_region), + ] + world.regions += regions + connect(world, player, "Menu", "Anywhere", one_way=True) + connect(world, player, "Menu", "Pallet Town", one_way=True) + connect(world, player, "Menu", "Fossil", lambda state: state.pokemon_rb_fossil_checks( + state.world.second_fossil_check_condition[player].value, player), one_way=True) + connect(world, player, "Pallet Town", "Route 1") + connect(world, player, "Route 1", "Viridian City") + connect(world, player, "Viridian City", "Route 22") + connect(world, player, "Route 22", "Route 23 South", + lambda state: state.pokemon_rb_has_badges(state.world.victory_road_condition[player].value, player)) + connect(world, player, "Route 23 South", "Route 23 North", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Viridian City North", "Viridian Gym", lambda state: + state.pokemon_rb_has_badges(state.world.viridian_gym_condition[player].value, player), one_way=True) + connect(world, player, "Route 2", "Route 2 East", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 2 East", "Diglett's Cave", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 2", "Viridian City North") + connect(world, player, "Route 2", "Viridian Forest") + connect(world, player, "Route 2", "Pewter City") + connect(world, player, "Pewter City", "Pewter Gym", one_way=True) + connect(world, player, "Pewter City", "Route 3") + connect(world, player, "Route 4", "Route 3", one_way=True) + connect(world, player, "Mt Moon 1F", "Mt Moon B1F", one_way=True) + connect(world, player, "Mt Moon B1F", "Mt Moon B2F", one_way=True) + connect(world, player, "Mt Moon B1F", "Route 4", one_way=True) + connect(world, player, "Route 4", "Cerulean City") + connect(world, player, "Cerulean City", "Cerulean Gym", one_way=True) + connect(world, player, "Cerulean City", "Route 24", one_way=True) + connect(world, player, "Route 24", "Route 25", one_way=True) + connect(world, player, "Cerulean City", "Route 9", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 9", "Route 10 North") + connect(world, player, "Route 10 North", "Rock Tunnel 1F", lambda state: state.pokemon_rb_can_flash(player)) + connect(world, player, "Route 10 North", "Power Plant", lambda state: state.pokemon_rb_can_surf(player) and + (state.has("Plant Key", player) or not state.world.extra_key_items[player].value), one_way=True) + connect(world, player, "Rock Tunnel 1F", "Route 10 South", lambda state: state.pokemon_rb_can_flash(player)) + connect(world, player, "Rock Tunnel 1F", "Rock Tunnel B1F") + connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True) + connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True) + connect(world, player, "Pokemon Tower 1F", "Pokemon Tower 2F", one_way=True) + connect(world, player, "Pokemon Tower 2F", "Pokemon Tower 3F", one_way=True) + connect(world, player, "Pokemon Tower 3F", "Pokemon Tower 4F", one_way=True) + connect(world, player, "Pokemon Tower 4F", "Pokemon Tower 5F", one_way=True) + connect(world, player, "Pokemon Tower 5F", "Pokemon Tower 6F", one_way=True) + connect(world, player, "Pokemon Tower 6F", "Pokemon Tower 7F", lambda state: state.has("Silph Scope", player)) + connect(world, player, "Cerulean City", "Route 5") + connect(world, player, "Route 5", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Route 5", "Underground Tunnel North-South") + connect(world, player, "Route 6", "Underground Tunnel North-South") + connect(world, player, "Route 6", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Route 7", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Route 8", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player)) + connect(world, player, "Saffron City", "Copycat's House", lambda state: state.has("Silph Co Liberated", player), one_way=True) + connect(world, player, "Saffron City", "Saffron Gym", lambda state: state.has("Silph Co Liberated", player), one_way=True) + connect(world, player, "Route 6", "Vermilion City") + connect(world, player, "Vermilion City", "Vermilion Gym", lambda state: state.pokemon_rb_can_surf(player) or state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Vermilion City", "S.S. Anne 1F", lambda state: state.has("S.S. Ticket", player), one_way=True) + connect(world, player, "S.S. Anne 1F", "S.S. Anne 2F", one_way=True) + connect(world, player, "S.S. Anne 1F", "S.S. Anne B1F", one_way=True) + connect(world, player, "Vermilion City", "Route 11") + connect(world, player, "Vermilion City", "Diglett's Cave") + connect(world, player, "Route 12 West", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player) or not state.world.extra_strength_boulders[player].value) + connect(world, player, "Route 12 North", "Route 12 South", lambda state: state.has("Poke Flute", player) or state.pokemon_rb_can_surf( player)) + connect(world, player, "Route 12 West", "Route 12 North", lambda state: state.has("Poke Flute", player)) + connect(world, player, "Route 12 West", "Route 12 South", lambda state: state.has("Poke Flute", player)) + connect(world, player, "Route 12 South", "Route 12 Grass", lambda state: state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 12 North", "Lavender Town") + connect(world, player, "Route 7", "Lavender Town") + connect(world, player, "Route 10 South", "Lavender Town") + connect(world, player, "Route 7", "Underground Tunnel West-East") + connect(world, player, "Route 8", "Underground Tunnel West-East") + connect(world, player, "Route 8", "Celadon City") + connect(world, player, "Route 8", "Route 8 Grass", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Route 7", "Celadon City") + connect(world, player, "Celadon City", "Celadon Gym", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Celadon City", "Celadon Prize Corner") + connect(world, player, "Celadon City", "Route 16") + connect(world, player, "Route 16", "Route 16 North", lambda state: state.pokemon_rb_can_cut(player), one_way=True) + connect(world, player, "Route 16", "Route 17", lambda state: state.has("Poke Flute", player) and state.has("Bicycle", player)) + connect(world, player, "Route 17", "Route 18", lambda state: state.has("Bicycle", player)) + connect(world, player, "Fuchsia City", "Fuchsia Gym", one_way=True) + connect(world, player, "Fuchsia City", "Route 18") + connect(world, player, "Fuchsia City", "Safari Zone Gate", one_way=True) + connect(world, player, "Safari Zone Gate", "Safari Zone Center", lambda state: state.has("Safari Pass", player) or not state.world.extra_key_items[player].value, one_way=True) + connect(world, player, "Safari Zone Center", "Safari Zone East", one_way=True) + connect(world, player, "Safari Zone Center", "Safari Zone West", one_way=True) + connect(world, player, "Safari Zone Center", "Safari Zone North", one_way=True) + connect(world, player, "Fuchsia City", "Route 15") + connect(world, player, "Route 15", "Route 14") + connect(world, player, "Route 14", "Route 13") + connect(world, player, "Route 13", "Route 12 South", lambda state: state.pokemon_rb_can_strength(player) or state.pokemon_rb_can_surf(player) or not state.world.extra_strength_boulders[player].value) + connect(world, player, "Fuchsia City", "Route 19", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Route 20 East", "Route 19") + connect(world, player, "Route 20 West", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Route 20 West", "Seafoam Islands 1F") + connect(world, player, "Route 20 East", "Seafoam Islands 1F", one_way=True) + connect(world, player, "Seafoam Islands 1F", "Route 20 East", lambda state: state.pokemon_rb_can_strength(player), one_way=True) + connect(world, player, "Viridian City", "Viridian City North", lambda state: state.has("Oak's Parcel", player) or state.world.old_man[player].value == 2 or state.pokemon_rb_can_cut(player)) + connect(world, player, "Route 3", "Mt Moon 1F", one_way=True) + connect(world, player, "Route 11", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player)) + connect(world, player, "Cinnabar Island", "Cinnabar Gym", lambda state: state.has("Secret Key", player), one_way=True) + connect(world, player, "Cinnabar Island", "Pokemon Mansion 1F", lambda state: state.has("Mansion Key", player) or not state.world.extra_key_items[player].value, one_way=True) + connect(world, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True) + connect(world, player, "Seafoam Islands B1F", "Seafoam Islands B2F", one_way=True) + connect(world, player, "Seafoam Islands B2F", "Seafoam Islands B3F", one_way=True) + connect(world, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True) + connect(world, player, "Route 21", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Pallet Town", "Route 21", lambda state: state.pokemon_rb_can_surf(player)) + connect(world, player, "Saffron City", "Silph Co 1F", lambda state: state.has("Fuji Saved", player), one_way=True) + connect(world, player, "Silph Co 1F", "Silph Co 2F", one_way=True) + connect(world, player, "Silph Co 2F", "Silph Co 3F", one_way=True) + connect(world, player, "Silph Co 3F", "Silph Co 4F", one_way=True) + connect(world, player, "Silph Co 4F", "Silph Co 5F", one_way=True) + connect(world, player, "Silph Co 5F", "Silph Co 6F", one_way=True) + connect(world, player, "Silph Co 6F", "Silph Co 7F", one_way=True) + connect(world, player, "Silph Co 7F", "Silph Co 8F", one_way=True) + connect(world, player, "Silph Co 8F", "Silph Co 9F", one_way=True) + connect(world, player, "Silph Co 9F", "Silph Co 10F", one_way=True) + connect(world, player, "Silph Co 10F", "Silph Co 11F", one_way=True) + connect(world, player, "Celadon City", "Rocket Hideout B1F", lambda state: state.has("Hideout Key", player) or not state.world.extra_key_items[player].value, one_way=True) + connect(world, player, "Rocket Hideout B1F", "Rocket Hideout B2F", one_way=True) + connect(world, player, "Rocket Hideout B2F", "Rocket Hideout B3F", one_way=True) + connect(world, player, "Rocket Hideout B3F", "Rocket Hideout B4F", one_way=True) + connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion 2F", one_way=True) + connect(world, player, "Pokemon Mansion 2F", "Pokemon Mansion 3F", one_way=True) + connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion B1F", one_way=True) + connect(world, player, "Route 23 North", "Victory Road 1F", lambda state: state.pokemon_rb_can_strength(player), one_way=True) + connect(world, player, "Victory Road 1F", "Victory Road 2F", one_way=True) + connect(world, player, "Victory Road 2F", "Victory Road 3F", one_way=True) + connect(world, player, "Victory Road 2F", "Indigo Plateau", lambda state: state.pokemon_rb_has_badges(state.world.elite_four_condition[player], player), one_way=True) + connect(world, player, "Cerulean City", "Cerulean Cave 1F", lambda state: + state.pokemon_rb_cerulean_cave(state.world.cerulean_cave_condition[player].value + (state.world.extra_key_items[player].value * 4), player) and + state.pokemon_rb_can_surf(player), one_way=True) + connect(world, player, "Cerulean Cave 1F", "Cerulean Cave 2F", one_way=True) + connect(world, player, "Cerulean Cave 1F", "Cerulean Cave B1F", lambda state: state.pokemon_rb_can_surf(player), one_way=True) + if world.worlds[player].fly_map != "Pallet Town": + connect(world, player, "Menu", world.worlds[player].fly_map, lambda state: state.pokemon_rb_can_fly(player), one_way=True, + name="Fly to " + world.worlds[player].fly_map) + + +def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None): + source_region = world.get_region(source, player) + target_region = world.get_region(target, player) + + if name is None: + name = source + " to " + target + + connection = Entrance( + player, + name, + source_region + ) + + connection.access_rule = rule + + source_region.exits.append(connection) + connection.connect(target_region) + if not one_way: + connect(world, player, target, source, rule, True) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py new file mode 100644 index 0000000000..370bd6afb7 --- /dev/null +++ b/worlds/pokemon_rb/rom.py @@ -0,0 +1,614 @@ +import os +import hashlib +import Utils +import bsdiff4 +from copy import deepcopy +from Patch import APDeltaPatch +from .text import encode_text +from .rom_addresses import rom_addresses +from .locations import location_data +import worlds.pokemon_rb.poke_data as poke_data + + +def choose_forced_type(chances, random): + n = random.randint(1, 100) + for chance in chances: + if chance[0] >= n: + return chance[1] + return None + + +def filter_moves(moves, type, random): + ret = [] + for move in moves: + if poke_data.moves[move]["type"] == type or type is None: + ret.append(move) + random.shuffle(ret) + return ret + + +def get_move(moves, chances, random, starting_move=False): + type = choose_forced_type(chances, random) + filtered_moves = filter_moves(moves, type, random) + for move in filtered_moves: + if poke_data.moves[move]["accuracy"] > 80 and poke_data.moves[move]["power"] > 0 or not starting_move: + moves.remove(move) + return move + else: + return get_move(moves, [], random, starting_move) + + +def get_encounter_slots(self): + encounter_slots = [location for location in location_data if location.type == "Wild Encounter"] + + for location in encounter_slots: + if isinstance(location.original_item, list): + location.original_item = location.original_item[not self.world.game_version[self.player].value] + return encounter_slots + + +def get_base_stat_total(mon): + return (poke_data.pokemon_data[mon]["atk"] + poke_data.pokemon_data[mon]["def"] + + poke_data.pokemon_data[mon]["hp"] + poke_data.pokemon_data[mon]["spd"] + + poke_data.pokemon_data[mon]["spc"]) + + +def randomize_pokemon(self, mon, mons_list, randomize_type): + if randomize_type in [1, 3]: + type_mons = [pokemon for pokemon in mons_list if any([poke_data.pokemon_data[mon][ + "type1"] in [self.local_poke_data[pokemon]["type1"], self.local_poke_data[pokemon]["type2"]], + poke_data.pokemon_data[mon]["type2"] in [self.local_poke_data[pokemon]["type1"], + self.local_poke_data[pokemon]["type2"]]])] + if not type_mons: + type_mons = mons_list.copy() + if randomize_type == 3: + stat_base = get_base_stat_total(mon) + type_mons.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base)) + mon = type_mons[round(self.world.random.triangular(0, len(type_mons) - 1, 0))] + if randomize_type == 2: + stat_base = get_base_stat_total(mon) + mons_list.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base)) + mon = mons_list[round(self.world.random.triangular(0, 50, 0))] + elif randomize_type == 4: + mon = self.world.random.choice(mons_list) + return mon + + +def process_trainer_data(self, data): + mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon + or self.world.trainer_legendaries[self.player].value] + address = rom_addresses["Trainer_Data"] + while address < rom_addresses["Trainer_Data_End"]: + if data[address] == 255: + mode = 1 + else: + mode = 0 + while True: + address += 1 + if data[address] == 0: + address += 1 + break + address += mode + mon = None + for i in range(1, 4): + for l in ["A", "B", "C", "D", "E", "F", "G", "H"]: + if rom_addresses[f"Rival_Starter{i}_{l}"] == address: + mon = " ".join(self.world.get_location(f"Pallet Town - Starter {i}", self.player).item.name.split()[1:]) + if l in ["D", "E", "F", "G", "H"] and mon in poke_data.evolves_to: + mon = poke_data.evolves_to[mon] + if l in ["F", "G", "H"] and mon in poke_data.evolves_to: + mon = poke_data.evolves_to[mon] + if mon is None and self.world.randomize_trainer_parties[self.player].value: + mon = poke_data.id_to_mon[data[address]] + mon = randomize_pokemon(self, mon, mons_list, self.world.randomize_trainer_parties[self.player].value) + if mon is not None: + data[address] = poke_data.pokemon_data[mon]["id"] + + +def process_static_pokemon(self): + starter_slots = [location for location in location_data if location.type == "Starter Pokemon"] + legendary_slots = [location for location in location_data if location.type == "Legendary Pokemon"] + static_slots = [location for location in location_data if location.type in + ["Static Pokemon", "Missable Pokemon"]] + legendary_mons = [slot.original_item for slot in legendary_slots] + + mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon + or self.world.randomize_legendary_pokemon[self.player].value == 3] + if self.world.randomize_legendary_pokemon[self.player].value == 0: + for slot in legendary_slots: + location = self.world.get_location(slot.name, self.player) + location.place_locked_item(self.create_item("Missable " + slot.original_item)) + elif self.world.randomize_legendary_pokemon[self.player].value == 1: + self.world.random.shuffle(legendary_mons) + for slot in legendary_slots: + location = self.world.get_location(slot.name, self.player) + location.place_locked_item(self.create_item("Missable " + legendary_mons.pop())) + elif self.world.randomize_legendary_pokemon[self.player].value == 2: + static_slots = static_slots + legendary_slots + self.world.random.shuffle(static_slots) + while legendary_slots: + swap_slot = legendary_slots.pop() + slot = static_slots.pop() + slot_type = slot.type.split()[0] + if slot_type == "Legendary": + slot_type = "Missable" + location = self.world.get_location(slot.name, self.player) + location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item)) + swap_slot.original_item = slot.original_item + elif self.world.randomize_legendary_pokemon[self.player].value == 3: + static_slots = static_slots + legendary_slots + + for slot in static_slots: + location = self.world.get_location(slot.name, self.player) + randomize_type = self.world.randomize_static_pokemon[self.player].value + slot_type = slot.type.split()[0] + if slot_type == "Legendary": + slot_type = "Missable" + if not randomize_type: + location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) + else: + location.place_locked_item(self.create_item(slot_type + " " + + randomize_pokemon(self, slot.original_item, mons_list, randomize_type))) + + for slot in starter_slots: + location = self.world.get_location(slot.name, self.player) + randomize_type = self.world.randomize_starter_pokemon[self.player].value + slot_type = "Missable" + if not randomize_type: + location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) + else: + location.place_locked_item(self.create_item(slot_type + " " + + randomize_pokemon(self, slot.original_item, mons_list, randomize_type))) + +def process_wild_pokemon(self): + + encounter_slots = get_encounter_slots(self) + + placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()} + if self.world.randomize_wild_pokemon[self.player].value: + mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon + or self.world.randomize_legendary_pokemon[self.player].value == 3] + self.world.random.shuffle(encounter_slots) + locations = [] + for slot in encounter_slots: + mon = randomize_pokemon(self, slot.original_item, mons_list, self.world.randomize_wild_pokemon[self.player].value) + placed_mons[mon] += 1 + location = self.world.get_location(slot.name, self.player) + location.item = self.create_item(mon) + location.event = True + location.locked = True + location.item.location = location + locations.append(location) + + mons_to_add = [] + remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and + (pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)] + if self.world.catch_em_all[self.player].value == 1: + mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and + (pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)] + elif self.world.catch_em_all[self.player].value == 2: + mons_to_add = remaining_pokemon.copy() + logic_needed_mons = max(self.world.oaks_aide_rt_2[self.player].value, + self.world.oaks_aide_rt_11[self.player].value, + self.world.oaks_aide_rt_15[self.player].value) + if self.world.accessibility[self.player] == "minimal": + logic_needed_mons = 0 + + self.world.random.shuffle(remaining_pokemon) + while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0]) + + len(mons_to_add) < logic_needed_mons): + mons_to_add.append(remaining_pokemon.pop()) + for mon in mons_to_add: + stat_base = get_base_stat_total(mon) + candidate_locations = get_encounter_slots(self) + if self.world.randomize_wild_pokemon[self.player].value in [1, 3]: + candidate_locations = [slot for slot in candidate_locations if any([poke_data.pokemon_data[slot.original_item][ + "type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], + poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"], + self.local_poke_data[mon]["type2"]]])] + if not candidate_locations: + candidate_locations = location_data + candidate_locations = [self.world.get_location(location.name, self.player) for location in candidate_locations] + candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base)) + for location in candidate_locations: + if placed_mons[location.item.name] > 1 or location.item.name not in poke_data.first_stage_pokemon: + placed_mons[location.item.name] -= 1 + location.item = self.create_item(mon) + location.item.location = location + placed_mons[mon] += 1 + break + + else: + for slot in encounter_slots: + location = self.world.get_location(slot.name, self.player) + location.item = self.create_item(slot.original_item) + location.event = True + location.locked = True + location.item.location = location + placed_mons[location.item.name] += 1 + + +def process_pokemon_data(self): + + local_poke_data = deepcopy(poke_data.pokemon_data) + learnsets = deepcopy(poke_data.learnsets) + + for mon, mon_data in local_poke_data.items(): + if self.world.randomize_pokemon_stats[self.player].value == 1: + stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]] + self.world.random.shuffle(stats) + mon_data["hp"] = stats[0] + mon_data["atk"] = stats[1] + mon_data["def"] = stats[2] + mon_data["spd"] = stats[3] + mon_data["spc"] = stats[4] + elif self.world.randomize_pokemon_stats[self.player].value == 2: + old_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 5 + stats = [1, 1, 1, 1, 1] + while old_stats > 0: + stat = self.world.random.randint(0, 4) + if stats[stat] < 255: + old_stats -= 1 + stats[stat] += 1 + mon_data["hp"] = stats[0] + mon_data["atk"] = stats[1] + mon_data["def"] = stats[2] + mon_data["spd"] = stats[3] + mon_data["spc"] = stats[4] + if self.world.randomize_pokemon_types[self.player].value: + if self.world.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from: + type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"] + type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"] + if type1 == type2: + if self.world.secondary_type_chance[self.player].value == -1: + if mon_data["type1"] != mon_data["type2"]: + while type2 == type1: + type2 = self.world.random.choice(list(poke_data.type_names.values())) + elif self.world.random.randint(1, 100) <= self.world.secondary_type_chance[self.player].value: + type2 = self.world.random.choice(list(poke_data.type_names.values())) + else: + type1 = self.world.random.choice(list(poke_data.type_names.values())) + type2 = type1 + if ((self.world.secondary_type_chance[self.player].value == -1 and mon_data["type1"] + != mon_data["type2"]) or self.world.random.randint(1, 100) + <= self.world.secondary_type_chance[self.player].value): + while type2 == type1: + type2 = self.world.random.choice(list(poke_data.type_names.values())) + + mon_data["type1"] = type1 + mon_data["type2"] = type2 + if self.world.randomize_pokemon_movesets[self.player].value: + if self.world.randomize_pokemon_movesets[self.player].value == 1: + if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal": + chances = [[75, "Normal"]] + elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal": + if mon_data["type1"] == "Normal": + second_type = mon_data["type2"] + else: + second_type = mon_data["type1"] + chances = [[30, "Normal"], [85, second_type]] + elif mon_data["type1"] == mon_data["type2"]: + chances = [[60, mon_data["type1"]], [80, "Normal"]] + else: + chances = [[50, mon_data["type1"]], [80, mon_data["type2"]], [85, "Normal"]] + else: + chances = [] + moves = set(poke_data.moves.keys()) + moves -= set(["No Move"] + poke_data.hm_moves) + mon_data["start move 1"] = get_move(moves, chances, self.world.random, True) + for i in range(2, 5): + if mon_data[f"start move {i}"] != "No Move" or self.world.start_with_four_moves[ + self.player].value == 1: + mon_data[f"start move {i}"] = get_move(moves, chances, self.world.random) + if mon in learnsets: + for move_num in range(0, len(learnsets[mon])): + learnsets[mon][move_num] = get_move(moves, chances, self.world.random) + if self.world.randomize_pokemon_catch_rates[self.player].value: + mon_data["catch rate"] = self.world.random.randint(self.world.minimum_catch_rate[self.player], 255) + else: + mon_data["catch rate"] = max(self.world.minimum_catch_rate[self.player], mon_data["catch rate"]) + + if mon in poke_data.evolves_from.keys() and mon_data["type1"] == local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == local_poke_data[poke_data.evolves_from[mon]]["type2"]: + mon_data["tms"] = local_poke_data[poke_data.evolves_from[mon]]["tms"] + elif mon != "Mew": + tms_hms = poke_data.tm_moves + poke_data.hm_moves + for flag, tm_move in enumerate(tms_hms): + if (flag < 50 and self.world.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 1): + type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]] + bit = int(self.world.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2]) + elif (flag < 50 and self.world.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 2): + bit = [0, 1][self.world.random.randint(0, 1)] + elif (flag < 50 and self.world.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 3): + bit = 1 + else: + continue + if bit: + mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8) + else: + mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) + + self.local_poke_data = local_poke_data + self.learnsets = learnsets + + + +def generate_output(self, output_directory: str): + random = self.world.slot_seeds[self.player] + game_version = self.world.game_version[self.player].current_key + data = bytearray(get_base_rom_bytes(game_version)) + + basemd5 = hashlib.md5() + basemd5.update(data) + + for location in self.world.get_locations(): + if location.player != self.player or location.rom_address is None: + continue + if location.item and location.item.player == self.player: + if location.rom_address: + rom_address = location.rom_address + if not isinstance(rom_address, list): + rom_address = [rom_address] + for address in rom_address: + if location.item.name in poke_data.pokemon_data.keys(): + data[address] = poke_data.pokemon_data[location.item.name]["id"] + elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): + data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] + else: + data[address] = self.item_name_to_id[location.item.name] - 172000000 + + else: + data[location.rom_address] = 0x2C # AP Item + data[rom_addresses['Fly_Location']] = self.fly_map_code + + if self.world.tea[self.player].value: + data[rom_addresses["Option_Tea"]] = 1 + data[rom_addresses["Guard_Drink_List"]] = 0x54 + data[rom_addresses["Guard_Drink_List"] + 1] = 0 + data[rom_addresses["Guard_Drink_List"] + 2] = 0 + + if self.world.extra_key_items[self.player].value: + data[rom_addresses['Options']] |= 4 + data[rom_addresses["Option_Blind_Trainers"]] = round(self.world.blind_trainers[self.player].value * 2.55) + data[rom_addresses['Option_Cerulean_Cave_Condition']] = self.world.cerulean_cave_condition[self.player].value + data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.world.minimum_steps_between_encounters[self.player].value + data[rom_addresses['Option_Victory_Road_Badges']] = self.world.victory_road_condition[self.player].value + data[rom_addresses['Option_Pokemon_League_Badges']] = self.world.elite_four_condition[self.player].value + data[rom_addresses['Option_Viridian_Gym_Badges']] = self.world.viridian_gym_condition[self.player].value + data[rom_addresses['Option_EXP_Modifier']] = self.world.exp_modifier[self.player].value + if not self.world.require_item_finder[self.player].value: + data[rom_addresses['Option_Itemfinder']] = 0 + if self.world.extra_strength_boulders[self.player].value: + for i in range(0, 3): + data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15 + if self.world.extra_key_items[self.player].value: + for i in range(0, 4): + data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15 + if self.world.old_man[self.player].value == 2: + data[rom_addresses['Option_Old_Man']] = 0x11 + data[rom_addresses['Option_Old_Man_Lying']] = 0x15 + money = str(self.world.starting_money[self.player].value) + while len(money) < 6: + money = "0" + money + data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) + data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) + data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16) + data[rom_addresses["Text_Badges_Needed"]] = encode_text( + str(max(self.world.victory_road_condition[self.player].value, + self.world.elite_four_condition[self.player].value)))[0] + if self.world.badges_needed_for_hm_moves[self.player].value == 0: + for hm_move in poke_data.hm_moves: + write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + rom_addresses["HM_" + hm_move + "_Badge_a"]) + elif self.extra_badges: + written_badges = {} + for hm_move, badge in self.extra_badges.items(): + data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F, + "Thunder Badge": 0x57, "Rainbow Badge": 0x5F, + "Soul Badge": 0x67, "Marsh Badge": 0x6F, + "Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge] + move_text = hm_move + if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]: + move_text = ", " + move_text + rom_address = rom_addresses["Badge_Text_" + badge.replace(" ", "_")] + if badge in written_badges: + rom_address += len(written_badges[badge]) + move_text = ", " + move_text + write_bytes(data, encode_text(move_text.upper()), rom_address) + written_badges[badge] = move_text + for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]: + if badge not in written_badges: + write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) + + chart = deepcopy(poke_data.type_chart) + if self.world.randomize_type_matchup_types[self.player].value == 1: + attacking_types = [] + defending_types = [] + for matchup in chart: + attacking_types.append(matchup[0]) + defending_types.append(matchup[1]) + random.shuffle(attacking_types) + random.shuffle(defending_types) + matchups = [] + while len(attacking_types) > 0: + if [attacking_types[0], defending_types[0]] not in matchups: + matchups.append([attacking_types.pop(0), defending_types.pop(0)]) + else: + matchup = matchups.pop(0) + attacking_types.append(matchup[0]) + defending_types.append(matchup[1]) + random.shuffle(attacking_types) + random.shuffle(defending_types) + for matchup, chart_row in zip(matchups, chart): + chart_row[0] = matchup[0] + chart_row[1] = matchup[1] + elif self.world.randomize_type_matchup_types[self.player].value == 2: + used_matchups = [] + for matchup in chart: + matchup[0] = random.choice(list(poke_data.type_names.values())) + matchup[1] = random.choice(list(poke_data.type_names.values())) + while [matchup[0], matchup[1]] in used_matchups: + matchup[0] = random.choice(list(poke_data.type_names.values())) + matchup[1] = random.choice(list(poke_data.type_names.values())) + used_matchups.append([matchup[0], matchup[1]]) + if self.world.randomize_type_matchup_type_effectiveness[self.player].value == 1: + effectiveness_list = [] + for matchup in chart: + effectiveness_list.append(matchup[2]) + random.shuffle(effectiveness_list) + for (matchup, effectiveness) in zip(chart, effectiveness_list): + matchup[2] = effectiveness + elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 2: + for matchup in chart: + matchup[2] = random.choice([0] + ([5, 20] * 5)) + elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 3: + for matchup in chart: + matchup[2] = self.world.random.choice([i for i in range(0, 21) if i != 10]) + type_loc = rom_addresses["Type_Chart"] + for matchup in chart: + data[type_loc] = poke_data.type_ids[matchup[0]] + data[type_loc + 1] = poke_data.type_ids[matchup[1]] + data[type_loc + 2] = matchup[2] + type_loc += 3 + # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" + # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to + # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes + # to the way effectiveness messages are generated. + self.type_chart = sorted(chart, key=lambda matchup: 0 - matchup[2]) + + if self.world.normalize_encounter_chances[self.player].value: + chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] + for i, chance in enumerate(chances): + data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance + + for mon, mon_data in self.local_poke_data.items(): + if mon == "Mew": + address = rom_addresses["Base_Stats_Mew"] + else: + address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1)) + data[address + 1] = self.local_poke_data[mon]["hp"] + data[address + 2] = self.local_poke_data[mon]["atk"] + data[address + 3] = self.local_poke_data[mon]["def"] + data[address + 4] = self.local_poke_data[mon]["spd"] + data[address + 5] = self.local_poke_data[mon]["spc"] + data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]] + data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]] + data[address + 8] = self.local_poke_data[mon]["catch rate"] + data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"] + data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"] + data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"] + data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"] + write_bytes(data, self.local_poke_data[mon]["tms"], address + 20) + if mon in self.learnsets: + address = rom_addresses["Learnset_" + mon.replace(" ", "")] + for i, move in enumerate(self.learnsets[mon]): + data[(address + 1) + i * 2] = poke_data.moves[move]["id"] + + data[rom_addresses["Option_Aide_Rt2"]] = self.world.oaks_aide_rt_2[self.player] + data[rom_addresses["Option_Aide_Rt11"]] = self.world.oaks_aide_rt_11[self.player] + data[rom_addresses["Option_Aide_Rt15"]] = self.world.oaks_aide_rt_15[self.player] + + if self.world.safari_zone_normal_battles[self.player].value == 1: + data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255 + + if self.world.reusable_tms[self.player].value: + data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 + + process_trainer_data(self, data) + + mons = [mon["id"] for mon in poke_data.pokemon_data.values()] + random.shuffle(mons) + data[rom_addresses['Title_Mon_First']] = mons.pop() + for mon in range(0, 16): + data[rom_addresses['Title_Mons'] + mon] = mons.pop() + if self.world.game_version[self.player].value: + mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name + else 1 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name else + 2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3) + else: + mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name + else 1 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name else + 2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3) + write_bytes(data, encode_text(self.world.seed_name, 20, True), rom_addresses['Title_Seed']) + + slot_name = self.world.player_name[self.player] + slot_name.replace("@", " ") + slot_name.replace("<", " ") + slot_name.replace(">", " ") + write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name']) + + write_bytes(data, self.trainer_name, rom_addresses['Player_Name']) + write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) + + write_bytes(data, basemd5.digest(), 0xFFCC) + write_bytes(data, self.world.seed_name.encode(), 0xFFDC) + write_bytes(data, self.world.player_name[self.player].encode(), 0xFFF0) + + + + outfilepname = f'_P{self.player}' + outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \ + if self.world.player_name[self.player] != 'Player%d' % self.player else '' + rompath = os.path.join(output_directory, f'AP_{self.world.seed_name}{outfilepname}.gb') + with open(rompath, 'wb') as outfile: + outfile.write(data) + if self.world.game_version[self.player].current_key == "red": + patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player, + player_name=self.world.player_name[self.player], patched_path=rompath) + else: + patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player, + player_name=self.world.player_name[self.player], patched_path=rompath) + + patch.write() + os.unlink(rompath) + + +def write_bytes(data, byte_array, address): + for byte in byte_array: + data[address] = byte + address += 1 + + +def get_base_rom_bytes(game_version: str, hash: str="") -> bytes: + file_name = get_base_rom_path(game_version) + with open(file_name, "rb") as file: + base_rom_bytes = bytes(file.read()) + if hash: + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if hash != basemd5.hexdigest(): + raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. ' + 'Get the correct game and version, then dump it') + with open(os.path.join(os.path.dirname(__file__), f'basepatch_{game_version}.bsdiff4'), 'rb') as stream: + base_patch = bytes(stream.read()) + base_rom_bytes = bsdiff4.patch(base_rom_bytes, base_patch) + return base_rom_bytes + + +def get_base_rom_path(game_version: str) -> str: + options = Utils.get_options() + file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name + + +class BlueDeltaPatch(APDeltaPatch): + patch_file_ending = ".apblue" + hash = "50927e843568814f7ed45ec4f944bd8b" + game_version = "blue" + game = "Pokemon Red and Blue" + result_file_ending = ".gb" + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes(cls.game_version, cls.hash) + + +class RedDeltaPatch(APDeltaPatch): + patch_file_ending = ".apred" + hash = "3d45c1ee9abd5738df46d2bdda8b57dc" + game_version = "red" + game = "Pokemon Red and Blue" + result_file_ending = ".gb" + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes(cls.game_version, cls.hash) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py new file mode 100644 index 0000000000..c04a75bcc4 --- /dev/null +++ b/worlds/pokemon_rb/rom_addresses.py @@ -0,0 +1,588 @@ +rom_addresses = { + "Option_Encounter_Minimum_Steps": 0x3c3, + "Option_Blind_Trainers": 0x317e, + "Base_Stats_Mew": 0x425b, + "Title_Mon_First": 0x436e, + "Title_Mons": 0x4547, + "Player_Name": 0x4569, + "Rival_Name": 0x4571, + "Title_Seed": 0x5dfe, + "Title_Slot_Name": 0x5e1e, + "PC_Item": 0x61ec, + "PC_Item_Quantity": 0x61f1, + "Options": 0x61f9, + "Fly_Location": 0x61fe, + "Option_Old_Man": 0xcaef, + "Option_Old_Man_Lying": 0xcaf2, + "Option_Boulders": 0xcd98, + "Option_Rock_Tunnel_Extra_Items": 0xcda1, + "Wild_Route1": 0xd0fb, + "Wild_Route2": 0xd111, + "Wild_Route22": 0xd127, + "Wild_ViridianForest": 0xd13d, + "Wild_Route3": 0xd153, + "Wild_MtMoon1F": 0xd169, + "Wild_MtMoonB1F": 0xd17f, + "Wild_MtMoonB2F": 0xd195, + "Wild_Route4": 0xd1ab, + "Wild_Route24": 0xd1c1, + "Wild_Route25": 0xd1d7, + "Wild_Route9": 0xd1ed, + "Wild_Route5": 0xd203, + "Wild_Route6": 0xd219, + "Wild_Route11": 0xd22f, + "Wild_RockTunnel1F": 0xd245, + "Wild_RockTunnelB1F": 0xd25b, + "Wild_Route10": 0xd271, + "Wild_Route12": 0xd287, + "Wild_Route8": 0xd29d, + "Wild_Route7": 0xd2b3, + "Wild_PokemonTower3F": 0xd2cd, + "Wild_PokemonTower4F": 0xd2e3, + "Wild_PokemonTower5F": 0xd2f9, + "Wild_PokemonTower6F": 0xd30f, + "Wild_PokemonTower7F": 0xd325, + "Wild_Route13": 0xd33b, + "Wild_Route14": 0xd351, + "Wild_Route15": 0xd367, + "Wild_Route16": 0xd37d, + "Wild_Route17": 0xd393, + "Wild_Route18": 0xd3a9, + "Wild_SafariZoneCenter": 0xd3bf, + "Wild_SafariZoneEast": 0xd3d5, + "Wild_SafariZoneNorth": 0xd3eb, + "Wild_SafariZoneWest": 0xd401, + "Wild_SeaRoutes": 0xd418, + "Wild_SeafoamIslands1F": 0xd42d, + "Wild_SeafoamIslandsB1F": 0xd443, + "Wild_SeafoamIslandsB2F": 0xd459, + "Wild_SeafoamIslandsB3F": 0xd46f, + "Wild_SeafoamIslandsB4F": 0xd485, + "Wild_PokemonMansion1F": 0xd49b, + "Wild_PokemonMansion2F": 0xd4b1, + "Wild_PokemonMansion3F": 0xd4c7, + "Wild_PokemonMansionB1F": 0xd4dd, + "Wild_Route21": 0xd4f3, + "Wild_Surf_Route21": 0xd508, + "Wild_CeruleanCave1F": 0xd51d, + "Wild_CeruleanCave2F": 0xd533, + "Wild_CeruleanCaveB1F": 0xd549, + "Wild_PowerPlant": 0xd55f, + "Wild_Route23": 0xd575, + "Wild_VictoryRoad2F": 0xd58b, + "Wild_VictoryRoad3F": 0xd5a1, + "Wild_VictoryRoad1F": 0xd5b7, + "Wild_DiglettsCave": 0xd5cd, + "Ghost_Battle5": 0xd723, + "HM_Surf_Badge_a": 0xda11, + "HM_Surf_Badge_b": 0xda16, + "Wild_Old_Rod": 0xe313, + "Wild_Good_Rod": 0xe340, + "Option_Reusable_TMs": 0xe60c, + "Wild_Super_Rod_A": 0xea40, + "Wild_Super_Rod_B": 0xea45, + "Wild_Super_Rod_C": 0xea4a, + "Wild_Super_Rod_D": 0xea51, + "Wild_Super_Rod_E": 0xea56, + "Wild_Super_Rod_F": 0xea5b, + "Wild_Super_Rod_G": 0xea64, + "Wild_Super_Rod_H": 0xea6d, + "Wild_Super_Rod_I": 0xea76, + "Wild_Super_Rod_J": 0xea7f, + "Starting_Money_High": 0xf949, + "Starting_Money_Middle": 0xf94c, + "Starting_Money_Low": 0xf94f, + "HM_Fly_Badge_a": 0x1318e, + "HM_Fly_Badge_b": 0x13193, + "HM_Cut_Badge_a": 0x131c4, + "HM_Cut_Badge_b": 0x131c9, + "HM_Strength_Badge_a": 0x131f4, + "HM_Strength_Badge_b": 0x131f9, + "HM_Flash_Badge_a": 0x13208, + "HM_Flash_Badge_b": 0x1320d, + "Encounter_Chances": 0x13911, + "Option_Viridian_Gym_Badges": 0x1901d, + "Event_Sleepy_Guy": 0x191bc, + "Starter2_K": 0x195a8, + "Starter3_K": 0x195b0, + "Event_Rocket_Thief": 0x196cc, + "Option_Cerulean_Cave_Condition": 0x1986c, + "Event_Stranded_Man": 0x19b2b, + "Event_Rivals_Sister": 0x19cf9, + "Option_Pokemon_League_Badges": 0x19e16, + "Missable_Silph_Co_4F_Item_1": 0x1a0d7, + "Missable_Silph_Co_4F_Item_2": 0x1a0de, + "Missable_Silph_Co_4F_Item_3": 0x1a0e5, + "Missable_Silph_Co_5F_Item_1": 0x1a337, + "Missable_Silph_Co_5F_Item_2": 0x1a33e, + "Missable_Silph_Co_5F_Item_3": 0x1a345, + "Missable_Silph_Co_6F_Item_1": 0x1a5ad, + "Missable_Silph_Co_6F_Item_2": 0x1a5b4, + "Event_Free_Sample": 0x1cade, + "Starter1_F": 0x1cca5, + "Starter2_F": 0x1cca9, + "Starter2_G": 0x1cde2, + "Starter3_G": 0x1cdea, + "Starter2_H": 0x1d0e5, + "Starter1_H": 0x1d0ef, + "Starter3_I": 0x1d0f6, + "Starter2_I": 0x1d100, + "Starter1_D": 0x1d107, + "Starter3_D": 0x1d111, + "Starter2_E": 0x1d2eb, + "Starter3_E": 0x1d2f3, + "Event_Oaks_Gift": 0x1d373, + "Event_Pokemart_Quest": 0x1d566, + "Event_Bicycle_Shop": 0x1d805, + "Text_Bicycle": 0x1d898, + "Event_Fuji": 0x1d9cd, + "Static_Encounter_Mew": 0x1dc4e, + "Gift_Eevee": 0x1dcc7, + "Event_Mr_Psychic": 0x1ddcf, + "Static_Encounter_Voltorb_A": 0x1e397, + "Static_Encounter_Voltorb_B": 0x1e39f, + "Static_Encounter_Voltorb_C": 0x1e3a7, + "Static_Encounter_Electrode_A": 0x1e3af, + "Static_Encounter_Voltorb_D": 0x1e3b7, + "Static_Encounter_Voltorb_E": 0x1e3bf, + "Static_Encounter_Electrode_B": 0x1e3c7, + "Static_Encounter_Voltorb_F": 0x1e3cf, + "Static_Encounter_Zapdos": 0x1e3d7, + "Missable_Power_Plant_Item_1": 0x1e3df, + "Missable_Power_Plant_Item_2": 0x1e3e6, + "Missable_Power_Plant_Item_3": 0x1e3ed, + "Missable_Power_Plant_Item_4": 0x1e3f4, + "Missable_Power_Plant_Item_5": 0x1e3fb, + "Event_Rt16_House_Woman": 0x1e5d4, + "Option_Victory_Road_Badges": 0x1e6a5, + "Event_Bill": 0x1e8d6, + "Starter1_O": 0x372b0, + "Starter2_O": 0x372b4, + "Starter3_O": 0x372b8, + "Base_Stats": 0x383de, + "Starter3_C": 0x39cf2, + "Starter1_C": 0x39cf8, + "Trainer_Data": 0x39d99, + "Rival_Starter2_A": 0x3a1e5, + "Rival_Starter3_A": 0x3a1e8, + "Rival_Starter1_A": 0x3a1eb, + "Rival_Starter2_B": 0x3a1f1, + "Rival_Starter3_B": 0x3a1f7, + "Rival_Starter1_B": 0x3a1fd, + "Rival_Starter2_C": 0x3a207, + "Rival_Starter3_C": 0x3a211, + "Rival_Starter1_C": 0x3a21b, + "Rival_Starter2_D": 0x3a409, + "Rival_Starter3_D": 0x3a413, + "Rival_Starter1_D": 0x3a41d, + "Rival_Starter2_E": 0x3a429, + "Rival_Starter3_E": 0x3a435, + "Rival_Starter1_E": 0x3a441, + "Rival_Starter2_F": 0x3a44d, + "Rival_Starter3_F": 0x3a459, + "Rival_Starter1_F": 0x3a465, + "Rival_Starter2_G": 0x3a473, + "Rival_Starter3_G": 0x3a481, + "Rival_Starter1_G": 0x3a48f, + "Rival_Starter2_H": 0x3a49d, + "Rival_Starter3_H": 0x3a4ab, + "Rival_Starter1_H": 0x3a4b9, + "Trainer_Data_End": 0x3a52e, + "Learnset_Rhydon": 0x3b1d9, + "Learnset_Kangaskhan": 0x3b1e7, + "Learnset_NidoranM": 0x3b1f6, + "Learnset_Clefairy": 0x3b208, + "Learnset_Spearow": 0x3b219, + "Learnset_Voltorb": 0x3b228, + "Learnset_Nidoking": 0x3b234, + "Learnset_Slowbro": 0x3b23c, + "Learnset_Ivysaur": 0x3b24f, + "Learnset_Exeggutor": 0x3b25f, + "Learnset_Lickitung": 0x3b263, + "Learnset_Exeggcute": 0x3b273, + "Learnset_Grimer": 0x3b284, + "Learnset_Gengar": 0x3b292, + "Learnset_NidoranF": 0x3b29b, + "Learnset_Nidoqueen": 0x3b2a9, + "Learnset_Cubone": 0x3b2b4, + "Learnset_Rhyhorn": 0x3b2c3, + "Learnset_Lapras": 0x3b2d1, + "Learnset_Mew": 0x3b2e1, + "Learnset_Gyarados": 0x3b2eb, + "Learnset_Shellder": 0x3b2fb, + "Learnset_Tentacool": 0x3b30a, + "Learnset_Gastly": 0x3b31f, + "Learnset_Scyther": 0x3b325, + "Learnset_Staryu": 0x3b337, + "Learnset_Blastoise": 0x3b347, + "Learnset_Pinsir": 0x3b355, + "Learnset_Tangela": 0x3b363, + "Learnset_Growlithe": 0x3b379, + "Learnset_Onix": 0x3b385, + "Learnset_Fearow": 0x3b391, + "Learnset_Pidgey": 0x3b3a0, + "Learnset_Slowpoke": 0x3b3b1, + "Learnset_Kadabra": 0x3b3c9, + "Learnset_Graveler": 0x3b3e1, + "Learnset_Chansey": 0x3b3ef, + "Learnset_Machoke": 0x3b407, + "Learnset_MrMime": 0x3b413, + "Learnset_Hitmonlee": 0x3b41f, + "Learnset_Hitmonchan": 0x3b42b, + "Learnset_Arbok": 0x3b437, + "Learnset_Parasect": 0x3b443, + "Learnset_Psyduck": 0x3b452, + "Learnset_Drowzee": 0x3b461, + "Learnset_Golem": 0x3b46f, + "Learnset_Magmar": 0x3b47f, + "Learnset_Electabuzz": 0x3b48f, + "Learnset_Magneton": 0x3b49b, + "Learnset_Koffing": 0x3b4ac, + "Learnset_Mankey": 0x3b4bd, + "Learnset_Seel": 0x3b4cc, + "Learnset_Diglett": 0x3b4db, + "Learnset_Tauros": 0x3b4e7, + "Learnset_Farfetchd": 0x3b4f9, + "Learnset_Venonat": 0x3b508, + "Learnset_Dragonite": 0x3b516, + "Learnset_Doduo": 0x3b52b, + "Learnset_Poliwag": 0x3b53c, + "Learnset_Jynx": 0x3b54a, + "Learnset_Moltres": 0x3b558, + "Learnset_Articuno": 0x3b560, + "Learnset_Zapdos": 0x3b568, + "Learnset_Meowth": 0x3b575, + "Learnset_Krabby": 0x3b584, + "Learnset_Vulpix": 0x3b59a, + "Learnset_Pikachu": 0x3b5ac, + "Learnset_Dratini": 0x3b5c1, + "Learnset_Dragonair": 0x3b5d0, + "Learnset_Kabuto": 0x3b5df, + "Learnset_Kabutops": 0x3b5e9, + "Learnset_Horsea": 0x3b5f6, + "Learnset_Seadra": 0x3b602, + "Learnset_Sandshrew": 0x3b615, + "Learnset_Sandslash": 0x3b621, + "Learnset_Omanyte": 0x3b630, + "Learnset_Omastar": 0x3b63a, + "Learnset_Jigglypuff": 0x3b648, + "Learnset_Eevee": 0x3b666, + "Learnset_Flareon": 0x3b670, + "Learnset_Jolteon": 0x3b682, + "Learnset_Vaporeon": 0x3b694, + "Learnset_Machop": 0x3b6a9, + "Learnset_Zubat": 0x3b6b8, + "Learnset_Ekans": 0x3b6c7, + "Learnset_Paras": 0x3b6d6, + "Learnset_Poliwhirl": 0x3b6e6, + "Learnset_Poliwrath": 0x3b6f4, + "Learnset_Beedrill": 0x3b704, + "Learnset_Dodrio": 0x3b714, + "Learnset_Primeape": 0x3b722, + "Learnset_Dugtrio": 0x3b72e, + "Learnset_Venomoth": 0x3b73a, + "Learnset_Dewgong": 0x3b748, + "Learnset_Butterfree": 0x3b762, + "Learnset_Machamp": 0x3b772, + "Learnset_Golduck": 0x3b780, + "Learnset_Hypno": 0x3b78c, + "Learnset_Golbat": 0x3b79a, + "Learnset_Mewtwo": 0x3b7a6, + "Learnset_Snorlax": 0x3b7b2, + "Learnset_Magikarp": 0x3b7bf, + "Learnset_Muk": 0x3b7c7, + "Learnset_Kingler": 0x3b7d7, + "Learnset_Cloyster": 0x3b7e3, + "Learnset_Electrode": 0x3b7e9, + "Learnset_Weezing": 0x3b7f7, + "Learnset_Persian": 0x3b803, + "Learnset_Marowak": 0x3b80f, + "Learnset_Haunter": 0x3b827, + "Learnset_Alakazam": 0x3b832, + "Learnset_Pidgeotto": 0x3b843, + "Learnset_Pidgeot": 0x3b851, + "Learnset_Bulbasaur": 0x3b864, + "Learnset_Venusaur": 0x3b874, + "Learnset_Tentacruel": 0x3b884, + "Learnset_Goldeen": 0x3b89b, + "Learnset_Seaking": 0x3b8a9, + "Learnset_Ponyta": 0x3b8c2, + "Learnset_Rapidash": 0x3b8d0, + "Learnset_Rattata": 0x3b8e1, + "Learnset_Raticate": 0x3b8eb, + "Learnset_Nidorino": 0x3b8f9, + "Learnset_Nidorina": 0x3b90b, + "Learnset_Geodude": 0x3b91c, + "Learnset_Porygon": 0x3b92a, + "Learnset_Aerodactyl": 0x3b934, + "Learnset_Magnemite": 0x3b942, + "Learnset_Charmander": 0x3b957, + "Learnset_Squirtle": 0x3b968, + "Learnset_Charmeleon": 0x3b979, + "Learnset_Wartortle": 0x3b98a, + "Learnset_Charizard": 0x3b998, + "Learnset_Oddish": 0x3b9b1, + "Learnset_Gloom": 0x3b9c3, + "Learnset_Vileplume": 0x3b9d1, + "Learnset_Bellsprout": 0x3b9dc, + "Learnset_Weepinbell": 0x3b9f0, + "Learnset_Victreebel": 0x3ba00, + "Type_Chart": 0x3e4b6, + "Type_Chart_Divider": 0x3e5ac, + "Ghost_Battle3": 0x3efd9, + "Missable_Pokemon_Mansion_1F_Item_1": 0x443d6, + "Missable_Pokemon_Mansion_1F_Item_2": 0x443dd, + "Map_Rock_TunnelF": 0x44676, + "Missable_Victory_Road_3F_Item_1": 0x44b07, + "Missable_Victory_Road_3F_Item_2": 0x44b0e, + "Missable_Rocket_Hideout_B1F_Item_1": 0x44d2d, + "Missable_Rocket_Hideout_B1F_Item_2": 0x44d34, + "Missable_Rocket_Hideout_B2F_Item_1": 0x4511d, + "Missable_Rocket_Hideout_B2F_Item_2": 0x45124, + "Missable_Rocket_Hideout_B2F_Item_3": 0x4512b, + "Missable_Rocket_Hideout_B2F_Item_4": 0x45132, + "Missable_Rocket_Hideout_B3F_Item_1": 0x4536f, + "Missable_Rocket_Hideout_B3F_Item_2": 0x45376, + "Missable_Rocket_Hideout_B4F_Item_1": 0x45627, + "Missable_Rocket_Hideout_B4F_Item_2": 0x4562e, + "Missable_Rocket_Hideout_B4F_Item_3": 0x45635, + "Missable_Rocket_Hideout_B4F_Item_4": 0x4563c, + "Missable_Rocket_Hideout_B4F_Item_5": 0x45643, + "Missable_Safari_Zone_East_Item_1": 0x458b2, + "Missable_Safari_Zone_East_Item_2": 0x458b9, + "Missable_Safari_Zone_East_Item_3": 0x458c0, + "Missable_Safari_Zone_East_Item_4": 0x458c7, + "Missable_Safari_Zone_North_Item_1": 0x45a12, + "Missable_Safari_Zone_North_Item_2": 0x45a19, + "Missable_Safari_Zone_Center_Item": 0x45bf9, + "Missable_Cerulean_Cave_2F_Item_1": 0x45e36, + "Missable_Cerulean_Cave_2F_Item_2": 0x45e3d, + "Missable_Cerulean_Cave_2F_Item_3": 0x45e44, + "Static_Encounter_Mewtwo": 0x45f44, + "Missable_Cerulean_Cave_B1F_Item_1": 0x45f4c, + "Missable_Cerulean_Cave_B1F_Item_2": 0x45f53, + "Missable_Rock_Tunnel_B1F_Item_1": 0x4619f, + "Missable_Rock_Tunnel_B1F_Item_2": 0x461a6, + "Missable_Rock_Tunnel_B1F_Item_3": 0x461ad, + "Missable_Rock_Tunnel_B1F_Item_4": 0x461b4, + "Static_Encounter_Articuno": 0x4690c, + "Hidden_Item_Viridian_Forest_1": 0x46e6d, + "Hidden_Item_Viridian_Forest_2": 0x46e73, + "Hidden_Item_MtMoonB2F_1": 0x46e7a, + "Hidden_Item_MtMoonB2F_2": 0x46e80, + "Hidden_Item_Route_25_1": 0x46e94, + "Hidden_Item_Route_25_2": 0x46e9a, + "Hidden_Item_Route_9": 0x46ea1, + "Hidden_Item_SS_Anne_Kitchen": 0x46eb4, + "Hidden_Item_SS_Anne_B1F": 0x46ebb, + "Hidden_Item_Route_10_1": 0x46ec2, + "Hidden_Item_Route_10_2": 0x46ec8, + "Hidden_Item_Rocket_Hideout_B1F": 0x46ecf, + "Hidden_Item_Rocket_Hideout_B3F": 0x46ed6, + "Hidden_Item_Rocket_Hideout_B4F": 0x46edd, + "Hidden_Item_Pokemon_Tower_5F": 0x46ef1, + "Hidden_Item_Route_13_1": 0x46ef8, + "Hidden_Item_Route_13_2": 0x46efe, + "Hidden_Item_Safari_Zone_West": 0x46f0c, + "Hidden_Item_Silph_Co_5F": 0x46f13, + "Hidden_Item_Silph_Co_9F": 0x46f1a, + "Hidden_Item_Copycats_House": 0x46f21, + "Hidden_Item_Cerulean_Cave_1F": 0x46f28, + "Hidden_Item_Cerulean_Cave_B1F": 0x46f2f, + "Hidden_Item_Power_Plant_1": 0x46f36, + "Hidden_Item_Power_Plant_2": 0x46f3c, + "Hidden_Item_Seafoam_Islands_B2F": 0x46f43, + "Hidden_Item_Seafoam_Islands_B4F": 0x46f4a, + "Hidden_Item_Pokemon_Mansion_1F": 0x46f51, + "Hidden_Item_Pokemon_Mansion_3F": 0x46f65, + "Hidden_Item_Pokemon_Mansion_B1F": 0x46f72, + "Hidden_Item_Route_23_1": 0x46f85, + "Hidden_Item_Route_23_2": 0x46f8b, + "Hidden_Item_Route_23_3": 0x46f91, + "Hidden_Item_Victory_Road_2F_1": 0x46f98, + "Hidden_Item_Victory_Road_2F_2": 0x46f9e, + "Hidden_Item_Unused_6F": 0x46fa5, + "Hidden_Item_Viridian_City": 0x46fb3, + "Hidden_Item_Route_11": 0x47060, + "Hidden_Item_Route_12": 0x47067, + "Hidden_Item_Route_17_1": 0x47075, + "Hidden_Item_Route_17_2": 0x4707b, + "Hidden_Item_Route_17_3": 0x47081, + "Hidden_Item_Route_17_4": 0x47087, + "Hidden_Item_Route_17_5": 0x4708d, + "Hidden_Item_Underground_Path_NS_1": 0x47094, + "Hidden_Item_Underground_Path_NS_2": 0x4709a, + "Hidden_Item_Underground_Path_WE_1": 0x470a1, + "Hidden_Item_Underground_Path_WE_2": 0x470a7, + "Hidden_Item_Celadon_City": 0x470ae, + "Hidden_Item_Seafoam_Islands_B3F": 0x470b5, + "Hidden_Item_Vermilion_City": 0x470bc, + "Hidden_Item_Cerulean_City": 0x470c3, + "Hidden_Item_Route_4": 0x470ca, + "Event_Counter": 0x482d3, + "Event_Thirsty_Girl_Lemonade": 0x484f9, + "Event_Thirsty_Girl_Soda": 0x4851d, + "Event_Thirsty_Girl_Water": 0x48541, + "Option_Tea": 0x4871d, + "Event_Mansion_Lady": 0x4872a, + "Badge_Celadon_Gym": 0x48a1b, + "Event_Celadon_Gym": 0x48a2f, + "Event_Gambling_Addict": 0x49293, + "Gift_Magikarp": 0x49430, + "Option_Aide_Rt11": 0x4958d, + "Event_Rt11_Oaks_Aide": 0x49591, + "Event_Mourning_Girl": 0x4968b, + "Option_Aide_Rt15": 0x49776, + "Event_Rt_15_Oaks_Aide": 0x4977a, + "Missable_Mt_Moon_1F_Item_1": 0x49c75, + "Missable_Mt_Moon_1F_Item_2": 0x49c7c, + "Missable_Mt_Moon_1F_Item_3": 0x49c83, + "Missable_Mt_Moon_1F_Item_4": 0x49c8a, + "Missable_Mt_Moon_1F_Item_5": 0x49c91, + "Missable_Mt_Moon_1F_Item_6": 0x49c98, + "Dome_Fossil_Text": 0x4a001, + "Event_Dome_Fossil": 0x4a021, + "Helix_Fossil_Text": 0x4a05d, + "Event_Helix_Fossil": 0x4a07d, + "Missable_Mt_Moon_B2F_Item_1": 0x4a166, + "Missable_Mt_Moon_B2F_Item_2": 0x4a16d, + "Missable_Safari_Zone_West_Item_1": 0x4a34f, + "Missable_Safari_Zone_West_Item_2": 0x4a356, + "Missable_Safari_Zone_West_Item_3": 0x4a35d, + "Missable_Safari_Zone_West_Item_4": 0x4a364, + "Event_Safari_Zone_Secret_House": 0x4a469, + "Missable_Route_24_Item": 0x506e6, + "Missable_Route_25_Item": 0x5080b, + "Starter2_B": 0x50fce, + "Starter3_B": 0x50fd0, + "Starter1_B": 0x50fd2, + "Starter2_A": 0x510f1, + "Starter3_A": 0x510f3, + "Starter1_A": 0x510f5, + "Option_Badge_Goal": 0x51317, + "Event_Nugget_Bridge": 0x5148f, + "Static_Encounter_Moltres": 0x51939, + "Missable_Victory_Road_2F_Item_1": 0x51941, + "Missable_Victory_Road_2F_Item_2": 0x51948, + "Missable_Victory_Road_2F_Item_3": 0x5194f, + "Missable_Victory_Road_2F_Item_4": 0x51956, + "Starter2_L": 0x51c85, + "Starter3_L": 0x51c8d, + "Gift_Lapras": 0x51d83, + "Missable_Silph_Co_7F_Item_1": 0x51f0d, + "Missable_Silph_Co_7F_Item_2": 0x51f14, + "Missable_Pokemon_Mansion_2F_Item": 0x520c9, + "Missable_Pokemon_Mansion_3F_Item_1": 0x522e2, + "Missable_Pokemon_Mansion_3F_Item_2": 0x522e9, + "Missable_Pokemon_Mansion_B1F_Item_1": 0x5248c, + "Missable_Pokemon_Mansion_B1F_Item_2": 0x52493, + "Missable_Pokemon_Mansion_B1F_Item_3": 0x5249a, + "Missable_Pokemon_Mansion_B1F_Item_4": 0x524a1, + "Missable_Pokemon_Mansion_B1F_Item_5": 0x524ae, + "Option_Safari_Zone_Battle_Type": 0x525c3, + "Prize_Mon_A2": 0x5282f, + "Prize_Mon_B2": 0x52830, + "Prize_Mon_C2": 0x52831, + "Prize_Mon_D2": 0x5283a, + "Prize_Mon_E2": 0x5283b, + "Prize_Mon_F2": 0x5283c, + "Prize_Mon_A": 0x52960, + "Prize_Mon_B": 0x52962, + "Prize_Mon_C": 0x52964, + "Prize_Mon_D": 0x52966, + "Prize_Mon_E": 0x52968, + "Prize_Mon_F": 0x5296a, + "Missable_Route_2_Item_1": 0x5404a, + "Missable_Route_2_Item_2": 0x54051, + "Missable_Route_4_Item": 0x543df, + "Missable_Route_9_Item": 0x546fd, + "Option_EXP_Modifier": 0x552c5, + "Rod_Vermilion_City_Fishing_Guru": 0x560df, + "Rod_Fuchsia_City_Fishing_Brother": 0x561eb, + "Rod_Route12_Fishing_Brother": 0x564ee, + "Missable_Route_12_Item_1": 0x58704, + "Missable_Route_12_Item_2": 0x5870b, + "Missable_Route_15_Item": 0x589c7, + "Ghost_Battle6": 0x58df0, + "Static_Encounter_Snorlax_A": 0x5969b, + "Static_Encounter_Snorlax_B": 0x599db, + "Event_Pokemon_Fan_Club": 0x59c8b, + "Event_Scared_Woman": 0x59e1f, + "Missable_Silph_Co_3F_Item": 0x5a0cb, + "Missable_Silph_Co_10F_Item_1": 0x5a281, + "Missable_Silph_Co_10F_Item_2": 0x5a288, + "Missable_Silph_Co_10F_Item_3": 0x5a28f, + "Guard_Drink_List": 0x5a600, + "Event_Museum": 0x5c266, + "Badge_Pewter_Gym": 0x5c3ed, + "Event_Pewter_Gym": 0x5c401, + "Badge_Cerulean_Gym": 0x5c716, + "Event_Cerulean_Gym": 0x5c72a, + "Badge_Vermilion_Gym": 0x5caba, + "Event_Vermillion_Gym": 0x5cace, + "Event_Copycat": 0x5cca9, + "Gift_Hitmonlee": 0x5cf1a, + "Gift_Hitmonchan": 0x5cf62, + "Badge_Saffron_Gym": 0x5d079, + "Event_Saffron_Gym": 0x5d08d, + "Option_Aide_Rt2": 0x5d5f2, + "Event_Route_2_Oaks_Aide": 0x5d5f6, + "Missable_Victory_Road_1F_Item_1": 0x5dae6, + "Missable_Victory_Road_1F_Item_2": 0x5daed, + "Starter2_J": 0x6060e, + "Starter3_J": 0x60616, + "Missable_Pokemon_Tower_3F_Item": 0x60787, + "Missable_Pokemon_Tower_4F_Item_1": 0x608b5, + "Missable_Pokemon_Tower_4F_Item_2": 0x608bc, + "Missable_Pokemon_Tower_4F_Item_3": 0x608c3, + "Missable_Pokemon_Tower_5F_Item": 0x60a80, + "Ghost_Battle1": 0x60b33, + "Ghost_Battle2": 0x60c0a, + "Missable_Pokemon_Tower_6F_Item_1": 0x60c85, + "Missable_Pokemon_Tower_6F_Item_2": 0x60c8c, + "Gift_Aerodactyl": 0x61064, + "Gift_Omanyte": 0x61068, + "Gift_Kabuto": 0x6106c, + "Missable_Viridian_Forest_Item_1": 0x6122c, + "Missable_Viridian_Forest_Item_2": 0x61233, + "Missable_Viridian_Forest_Item_3": 0x6123a, + "Starter2_M": 0x61450, + "Starter3_M": 0x61458, + "Event_SS_Anne_Captain": 0x618c3, + "Missable_SS_Anne_1F_Item": 0x61ac0, + "Missable_SS_Anne_2F_Item_1": 0x61ced, + "Missable_SS_Anne_2F_Item_2": 0x61d00, + "Missable_SS_Anne_B1F_Item_1": 0x61ee3, + "Missable_SS_Anne_B1F_Item_2": 0x61eea, + "Missable_SS_Anne_B1F_Item_3": 0x61ef1, + "Event_Silph_Co_President": 0x622ed, + "Ghost_Battle4": 0x708e1, + "Badge_Viridian_Gym": 0x749ca, + "Event_Viridian_Gym": 0x749de, + "Missable_Viridian_Gym_Item": 0x74c63, + "Missable_Cerulean_Cave_1F_Item_1": 0x74d68, + "Missable_Cerulean_Cave_1F_Item_2": 0x74d6f, + "Missable_Cerulean_Cave_1F_Item_3": 0x74d76, + "Event_Warden": 0x7512a, + "Missable_Wardens_House_Item": 0x751b7, + "Badge_Fuchsia_Gym": 0x755cd, + "Event_Fuschia_Gym": 0x755e1, + "Badge_Cinnabar_Gym": 0x75995, + "Event_Cinnabar_Gym": 0x759a9, + "Event_Lab_Scientist": 0x75dd6, + "Fossils_Needed_For_Second_Item": 0x75ea3, + "Event_Dome_Fossil_B": 0x75f20, + "Event_Helix_Fossil_B": 0x75f40, + "Starter2_N": 0x76169, + "Starter3_N": 0x76171, + "Option_Itemfinder": 0x76864, + "Text_Badges_Needed": 0x92304, + "Badge_Text_Boulder_Badge": 0x990b3, + "Badge_Text_Cascade_Badge": 0x990cb, + "Badge_Text_Thunder_Badge": 0x99111, + "Badge_Text_Rainbow_Badge": 0x9912e, + "Badge_Text_Soul_Badge": 0x99177, + "Badge_Text_Marsh_Badge": 0x9918c, + "Badge_Text_Volcano_Badge": 0x991d6, + "Badge_Text_Earth_Badge": 0x991f3, +} diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py new file mode 100644 index 0000000000..0e97b705db --- /dev/null +++ b/worlds/pokemon_rb/rules.py @@ -0,0 +1,165 @@ +from ..generic.Rules import add_item_rule, add_rule + +def set_rules(world, player): + + add_item_rule(world.get_location("Pallet Town - Player's PC", player), + lambda i: i.player == player and "Badge" not in i.name) + + access_rules = { + + "Pallet Town - Rival's Sister": lambda state: state.has("Oak's Parcel", player), + "Pallet Town - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player), + "Viridian City - Sleepy Guy": lambda state: state.pokemon_rb_can_cut(player) or state.pokemon_rb_can_surf(player), + "Route 2 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_2[player].value + 5, player), + "Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player), + "Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player), + "Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player), + "Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), + "Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)), + "Route 11 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_11[player].value + 5, player), + "Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player), + "Silph Co 11F - Silph Co President": lambda state: state.has("Card Key", player), + "Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player), + "Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player), + "Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player), + "Route 15 - Item": lambda state: state.pokemon_rb_can_cut(player), + "Route 25 - Item": lambda state: state.pokemon_rb_can_cut(player), + "Fuchsia City - Warden's House Item": lambda state: state.pokemon_rb_can_strength(player), + "Rocket Hideout B4F - Southwest Item (Lift Key)": lambda state: state.has("Lift Key", player), + "Rocket Hideout B4F - Giovanni Item (Lift Key)": lambda state: state.has("Lift Key", player), + "Silph Co 3F - Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 4F - Left Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 4F - Middle Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 4F - Right Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 5F - Northwest Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 6F - West Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 6F - Southwest Item (Card Key)": lambda state: state.has("Card Key", player), + "Silph Co 7F - East Item (Card Key)": lambda state: state.has("Card Key", player), + "Safari Zone Center - Island Item": lambda state: state.pokemon_rb_can_surf(player), + + "Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player), + + "Pallet Town - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Pallet Town - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 22 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 22 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 24 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 24 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 24 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 6 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 6 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 10 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 10 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Safari Zone Center - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 12 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 19 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Route 23 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player), + "Fuchsia City - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player), + "Anywhere - Good Rod Pokemon - 1": lambda state: state.has("Good Rod", player), + "Anywhere - Good Rod Pokemon - 2": lambda state: state.has("Good Rod", player), + "Anywhere - Old Rod Pokemon": lambda state: state.has("Old Rod", player), + "Celadon Prize Corner - Pokemon Prize - 1": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 2": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 3": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 4": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 5": lambda state: state.has("Coin Case", player), + "Celadon Prize Corner - Pokemon Prize - 6": lambda state: state.has("Coin Case", player), + "Cinnabar Island - Old Amber Pokemon": lambda state: state.has("Old Amber", player), + "Cinnabar Island - Helix Fossil Pokemon": lambda state: state.has("Helix Fossil", player), + "Cinnabar Island - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player), + "Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), + "Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), + "Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player), + "Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player) + } + + hidden_item_access_rules = { + "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Viridian Forest - Hidden Item Entrance Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 9 - Hidden Item Rock By Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "S.S. Anne 1F - Hidden Item Kitchen Trash": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "S.S. Anne B1F - Hidden Item Under Pillow": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 10 - Hidden Item Behind Rock Tunnel Entrance Tree": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Route 10 - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 13 - Hidden Item Dead End Boulder": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 13 - Hidden Item Dead End By Water Corner": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Safari Zone West - Hidden Item Secret House Statue": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Silph Co 5F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Silph Co 9F - Hidden Item Nurse Bed": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Copycat's House - Hidden Item Desk": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Power Plant - Hidden Item Central Dead End": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Power Plant - Hidden Item Before Zapdos": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Seafoam Islands B2F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 23 - Hidden Item Rocks Before Final Guard": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 23 - Hidden Item East Tree After Water": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 23 - Hidden Item On Island": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Viridian City - Hidden Item Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 11 - Hidden Item Isolated Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 12 - Hidden Item Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item In Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item Near Northernmost Sign": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item East Center": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item West Center": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Route 17 - Hidden Item Before Final Bridge": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel North-South - Hidden Item Near Northern Stairs": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel North-South - Hidden Item Near Southern Stairs": lambda + state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel West-East - Hidden Item West": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Underground Tunnel West-East - Hidden Item East": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 25 - Hidden Item Northeast Of Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Mt Moon B2F - Hidden Item Lone Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Seafoam Islands B3F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player), + "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: state.pokemon_rb_can_get_hidden_items( + player), + "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: state.pokemon_rb_can_get_hidden_items(player), + } + for loc, rule in access_rules.items(): + add_rule(world.get_location(loc, player), rule) + if world.randomize_hidden_items[player].value != 0: + for loc, rule in hidden_item_access_rules.items(): + add_rule(world.get_location(loc, player), rule) diff --git a/worlds/pokemon_rb/text.py b/worlds/pokemon_rb/text.py new file mode 100644 index 0000000000..e15623d4b8 --- /dev/null +++ b/worlds/pokemon_rb/text.py @@ -0,0 +1,147 @@ +special_chars = { + "PKMN": 0x4A, + "'d": 0xBB, + "'l": 0xBC, + "'t": 0xBE, + "'v": 0xBF, + "PK": 0xE1, + "MN": 0xE2, + "'r": 0xE4, + "'m": 0xE5, + "MALE": 0xEF, + "FEMALE": 0xF5, +} + +char_map = { + "@": 0x50, # String terminator + "#": 0x54, # Poké + "‘": 0x70, + "’": 0x71, + "“": 0x72, + "”": 0x73, + "·": 0x74, + "…": 0x75, + " ": 0x7F, + "A": 0x80, + "B": 0x81, + "C": 0x82, + "D": 0x83, + "E": 0x84, + "F": 0x85, + "G": 0x86, + "H": 0x87, + "I": 0x88, + "J": 0x89, + "K": 0x8A, + "L": 0x8B, + "M": 0x8C, + "N": 0x8D, + "O": 0x8E, + "P": 0x8F, + "Q": 0x90, + "R": 0x91, + "S": 0x92, + "T": 0x93, + "U": 0x94, + "V": 0x95, + "W": 0x96, + "X": 0x97, + "Y": 0x98, + "Z": 0x99, + "(": 0x9A, + ")": 0x9B, + ":": 0x9C, + ";": 0x9D, + "[": 0x9E, + "]": 0x9F, + "a": 0xA0, + "b": 0xA1, + "c": 0xA2, + "d": 0xA3, + "e": 0xA4, + "f": 0xA5, + "g": 0xA6, + "h": 0xA7, + "i": 0xA8, + "j": 0xA9, + "k": 0xAA, + "l": 0xAB, + "m": 0xAC, + "n": 0xAD, + "o": 0xAE, + "p": 0xAF, + "q": 0xB0, + "r": 0xB1, + "s": 0xB2, + "t": 0xB3, + "u": 0xB4, + "v": 0xB5, + "w": 0xB6, + "x": 0xB7, + "y": 0xB8, + "z": 0xB9, + "é": 0xBA, + "'": 0xE0, + "-": 0xE3, + "?": 0xE6, + "!": 0xE7, + ".": 0xE8, + "♂": 0xEF, + "¥": 0xF0, + "$": 0xF0, + "×": 0xF1, + "/": 0xF3, + ",": 0xF4, + "♀": 0xF5, + "0": 0xF6, + "1": 0xF7, + "2": 0xF8, + "3": 0xF9, + "4": 0xFA, + "5": 0xFB, + "6": 0xFC, + "7": 0xFD, + "8": 0xFE, + "9": 0xFF, +} + +unsafe_chars = ["@", "#", "PKMN"] + + +def encode_text(text: str, length: int=0, whitespace=False, force=False, safety=False): + encoded_text = bytearray() + spec_char = "" + special = False + for char in text: + if char == ">": + if spec_char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'") + try: + encoded_text.append(special_chars[spec_char]) + except KeyError: + if force: + encoded_text.append(char_map[" "]) + else: + raise KeyError(f"Invalid Pokemon text special character '<{spec_char}>'") + spec_char = "" + special = False + elif char == "<": + spec_char = "" + special = True + elif special is True: + spec_char += char + else: + if char in unsafe_chars and safety: + raise KeyError(f"Disallowed Pokemon text character '{char}'") + try: + encoded_text.append(char_map[char]) + except KeyError: + if force: + encoded_text.append(char_map[" "]) + else: + raise KeyError(f"Invalid Pokemon text character '{char}'") + if length > 0: + encoded_text = encoded_text[:length] + while whitespace and len(encoded_text) < length: + encoded_text.append(char_map[" " if whitespace is True else whitespace]) + return encoded_text From b014ce082b266e4fdaa3622975f6b6800d5b1fdd Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Wed, 12 Oct 2022 23:51:25 -0600 Subject: [PATCH 052/105] Hylics 2: Implement new game (#1058) --- worlds/hylics2/Exits.py | 94 +++++++ worlds/hylics2/Items.py | 243 ++++++++++++++++ worlds/hylics2/Locations.py | 383 +++++++++++++++++++++++++ worlds/hylics2/Options.py | 41 +++ worlds/hylics2/Rules.py | 433 +++++++++++++++++++++++++++++ worlds/hylics2/__init__.py | 246 ++++++++++++++++ worlds/hylics2/docs/en_Hylics 2.md | 17 ++ worlds/hylics2/docs/setup_en.md | 35 +++ 8 files changed, 1492 insertions(+) create mode 100644 worlds/hylics2/Exits.py create mode 100644 worlds/hylics2/Items.py create mode 100644 worlds/hylics2/Locations.py create mode 100644 worlds/hylics2/Options.py create mode 100644 worlds/hylics2/Rules.py create mode 100644 worlds/hylics2/__init__.py create mode 100644 worlds/hylics2/docs/en_Hylics 2.md create mode 100644 worlds/hylics2/docs/setup_en.md diff --git a/worlds/hylics2/Exits.py b/worlds/hylics2/Exits.py new file mode 100644 index 0000000000..99ebeba277 --- /dev/null +++ b/worlds/hylics2/Exits.py @@ -0,0 +1,94 @@ +from typing import List, Dict + + +region_exit_table: Dict[int, List[str]] = { + 0: ["New Game"], + + 1: ["To Waynehouse", + "To New Muldul", + "To Viewax", + "To TV Island", + "To Shield Facility", + "To Worm Pod", + "To Foglast", + "To Sage Labyrinth", + "To Hylemxylem"], + + 2: ["To World", + "To Afterlife",], + + 3: ["To Airship", + "To Waynehouse", + "To New Muldul", + "To Drill Castle", + "To Viewax", + "To Arcade Island", + "To TV Island", + "To Juice Ranch", + "To Shield Facility", + "To Worm Pod", + "To Foglast", + "To Sage Airship", + "To Hylemxylem"], + + 4: ["To World", + "To Afterlife", + "To New Muldul Vault"], + + 5: ["To New Muldul"], + + 6: ["To World", + "To Afterlife"], + + 7: ["To World"], + + 8: ["To World"], + + 9: ["To World", + "To Afterlife"], + + 10: ["To World"], + + 11: ["To World", + "To Afterlife", + "To Worm Pod"], + + 12: ["To Shield Facility", + "To Afterlife"], + + 13: ["To World", + "To Afterlife"], + + 14: ["To World", + "To Sage Labyrinth"], + + 15: ["To Drill Castle", + "To Afterlife"], + + 16: ["To World"], + + 17: ["To World", + "To Afterlife"] +} + + +exit_lookup_table: Dict[str, int] = { + "New Game": 2, + "To Waynehouse": 2, + "To Afterlife": 1, + "To World": 3, + "To New Muldul": 4, + "To New Muldul Vault": 5, + "To Viewax": 6, + "To Airship": 7, + "To Arcade Island": 8, + "To TV Island": 9, + "To Juice Ranch": 10, + "To Shield Facility": 11, + "To Worm Pod": 12, + "To Foglast": 13, + "To Drill Castle": 14, + "To Sage Labyrinth": 15, + "To Sage Airship": 16, + "To Hylemxylem": 17 +} \ No newline at end of file diff --git a/worlds/hylics2/Items.py b/worlds/hylics2/Items.py new file mode 100644 index 0000000000..9e36b3c393 --- /dev/null +++ b/worlds/hylics2/Items.py @@ -0,0 +1,243 @@ +from BaseClasses import ItemClassification +from typing import TypedDict, Dict + + +class ItemDict(TypedDict): + classification: ItemClassification + count: int + name: str + + +item_table: Dict[int, ItemDict] = { + # Things + 200622: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'DUBIOUS BERRY'}, + 200623: {'classification': ItemClassification.filler, + 'count': 11, + 'name': 'BURRITO'}, + 200624: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'COFFEE'}, + 200625: {'classification': ItemClassification.filler, + 'count': 6, + 'name': 'SOUL SPONGE'}, + 200626: {'classification': ItemClassification.useful, + 'count': 6, + 'name': 'MUSCLE APPLIQUE'}, + 200627: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'POOLWINE'}, + 200628: {'classification': ItemClassification.filler, + 'count': 3, + 'name': 'CUPCAKE'}, + 200629: {'classification': ItemClassification.filler, + 'count': 3, + 'name': 'COOKIE'}, + 200630: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'HOUSE KEY'}, + 200631: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'MEAT'}, + 200632: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'PNEUMATOPHORE'}, + 200633: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'CAVE KEY'}, + 200634: {'classification': ItemClassification.filler, + 'count': 6, + 'name': 'JUICE'}, + 200635: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'DOCK KEY'}, + 200636: {'classification': ItemClassification.filler, + 'count': 14, + 'name': 'BANANA'}, + 200637: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'PAPER CUP'}, + 200638: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'JAIL KEY'}, + 200639: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'PADDLE'}, + 200640: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'WORM ROOM KEY'}, + 200641: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'BRIDGE KEY'}, + 200642: {'classification': ItemClassification.filler, + 'count': 2, + 'name': 'STEM CELL'}, + 200643: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'UPPER CHAMBER KEY'}, + 200644: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'VESSEL ROOM KEY'}, + 200645: {'classification': ItemClassification.filler, + 'count': 3, + 'name': 'CLOUD GERM'}, + 200646: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'SKULL BOMB'}, + 200647: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'TOWER KEY'}, + 200648: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'DEEP KEY'}, + 200649: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'MULTI-COFFEE'}, + 200650: {'classification': ItemClassification.filler, + 'count': 4, + 'name': 'MULTI-JUICE'}, + 200651: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'MULTI STEM CELL'}, + 200652: {'classification': ItemClassification.filler, + 'count': 6, + 'name': 'MULTI SOUL SPONGE'}, + #200653: {'classification': ItemClassification.filler, + # 'count': 1, + # 'name': 'ANTENNA'}, + 200654: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'UPPER HOUSE KEY'}, + 200655: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BOTTOMLESS JUICE'}, + 200656: {'classification': ItemClassification.progression, + 'count': 3, + 'name': 'SAGE TOKEN'}, + 200657: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'CLICKER'}, + + # Garbs > Gloves + 200658: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'CURSED GLOVES'}, + 200659: {'classification': ItemClassification.useful, + 'count': 5, + 'name': 'LONG GLOVES'}, + 200660: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BRAIN DIGITS'}, + 200661: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'MATERIEL MITTS'}, + 200662: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'PLEATHER GAGE'}, + 200663: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'PEPTIDE BODKINS'}, + 200664: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TELESCOPIC SLEEVE'}, + 200665: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TENDRIL HAND'}, + 200666: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'PSYCHIC KNUCKLE'}, + 200667: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'SINGLE GLOVE'}, + + # Garbs > Accessories + 200668: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'FADED PONCHO'}, + 200669: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'JUMPSUIT'}, + 200670: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BOOTS'}, + 200671: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'CONVERTER WORM'}, + 200672: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'COFFEE CHIP'}, + 200673: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'RANCHER PONCHO'}, + 200674: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'ORGAN FORT'}, + 200675: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'LOOPED DOME'}, + 200676: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'DUCTILE HABIT'}, + 200677: {'classification': ItemClassification.useful, + 'count': 2, + 'name': 'TARP'}, + + # Bones + 200686: {'classification': ItemClassification.filler, + 'count': 1, + 'name': '100 Bones'}, + 200687: {'classification': ItemClassification.filler, + 'count': 1, + 'name': '50 Bones'} +} + + +gesture_item_table: Dict[int, ItemDict] = { + 200678: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'POROMER BLEB'}, + 200679: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'SOUL CRISPER'}, + 200680: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TIME SIGIL'}, + 200681: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'CHARGE UP'}, + 200682: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'FATE SANDBOX'}, + 200683: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'TELEDENUDATE'}, + 200684: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'LINK MOLLUSC'}, + 200685: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'BOMBO - GENESIS'}, + 200688: {'classification': ItemClassification.useful, + 'count': 1, + 'name': 'NEMATODE INTERFACE'}, +} + + +party_item_table: Dict[int, ItemDict] = { + 200689: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Pongorma'}, + 200690: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dedusmuln'}, + 200691: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Somsnosa'} +} + +medallion_item_table: Dict[int, ItemDict] = { + 200692: {'classification': ItemClassification.filler, + 'count': 30, + 'name': '10 Bones'} +} \ No newline at end of file diff --git a/worlds/hylics2/Locations.py b/worlds/hylics2/Locations.py new file mode 100644 index 0000000000..0f8bbb9b99 --- /dev/null +++ b/worlds/hylics2/Locations.py @@ -0,0 +1,383 @@ +from typing import Dict, TypedDict + + +class LocationDict(TypedDict, total=False): + name: str + region: int + + +location_table: Dict[int, LocationDict] = { + # Waynehouse + 200622: {'name': "Waynehouse: Toilet", + 'region': 2}, + 200623: {'name': "Waynehouse: Basement Pot 1", + 'region': 2}, + 200624: {'name': "Waynehouse: Basement Pot 2", + 'region': 2}, + 200625: {'name': "Waynehouse: Basement Pot 3", + 'region': 2}, + 200626: {'name': "Waynehouse: Sarcophagus", + 'region': 2}, + + # Afterlife + 200628: {'name': "Afterlife: Mangled Wayne", + 'region': 1}, + 200629: {'name': "Afterlife: Jar near Mangled Wayne", + 'region': 1}, + 200630: {'name': "Afterlife: Jar under Pool", + 'region': 1}, + + # New Muldul + 200632: {'name': "New Muldul: Shop Ceiling Pot 1", + 'region': 4}, + 200633: {'name': "New Muldul: Shop Ceiling Pot 2", + 'region': 4}, + 200634: {'name': "New Muldul: Flag Banana", + 'region': 4}, + 200635: {'name': "New Muldul: Pot near Vault", + 'region': 4}, + 200636: {'name': "New Muldul: Underground Pot", + 'region': 4}, + 200637: {'name': "New Muldul: Underground Chest", + 'region': 4}, + 200638: {'name': "New Muldul: Juice Trade", + 'region': 4}, + 200639: {'name': "New Muldul: Basement Suitcase", + 'region': 4}, + 200640: {'name': "New Muldul: Upper House Chest 1", + 'region': 4}, + 200641: {'name': "New Muldul: Upper House Chest 2", + 'region': 4}, + + # New Muldul Vault + 200643: {'name': "New Muldul: Talk to Pongorma", + 'region': 4}, + 200645: {'name': "New Muldul: Rescued Blerol 1", + 'region': 4}, + 200646: {'name': "New Muldul: Rescued Blerol 2", + 'region': 4}, + 200647: {'name': "New Muldul: Vault Left Chest", + 'region': 5}, + 200648: {'name': "New Muldul: Vault Right Chest", + 'region': 5}, + 200649: {'name': "New Muldul: Vault Bomb", + 'region': 5}, + + # Viewax's Edifice + 200650: {'name': "Viewax's Edifice: Fountain Banana", + 'region': 6}, + 200651: {'name': "Viewax's Edifice: Dedusmuln's Suitcase", + 'region': 6}, + 200652: {'name': "Viewax's Edifice: Dedusmuln's Campfire", + 'region': 6}, + 200653: {'name': "Viewax's Edifice: Talk to Dedusmuln", + 'region': 6}, + 200655: {'name': "Viewax's Edifice: Canopic Jar", + 'region': 6}, + 200656: {'name': "Viewax's Edifice: Cave Sarcophagus", + 'region': 6}, + 200657: {'name': "Viewax's Edifice: Shielded Key", + 'region': 6}, + 200658: {'name': "Viewax's Edifice: Tower Pot", + 'region': 6}, + 200659: {'name': "Viewax's Edifice: Tower Jar", + 'region': 6}, + 200660: {'name': "Viewax's Edifice: Tower Chest", + 'region': 6}, + 200661: {'name': "Viewax's Edifice: Sage Fridge", + 'region': 6}, + 200662: {'name': "Viewax's Edifice: Sage Item 1", + 'region': 6}, + 200663: {'name': "Viewax's Edifice: Sage Item 2", + 'region': 6}, + 200664: {'name': "Viewax's Edifice: Viewax Pot", + 'region': 6}, + 200665: {'name': "Viewax's Edifice: Defeat Viewax", + 'region': 6}, + + # Viewax Arcade Minigame + 200667: {'name': "Arcade 1: Key", + 'region': 6}, + 200668: {'name': "Arcade 1: Coin Dash", + 'region': 6}, + 200669: {'name': "Arcade 1: Burrito Alcove 1", + 'region': 6}, + 200670: {'name': "Arcade 1: Burrito Alcove 2", + 'region': 6}, + 200671: {'name': "Arcade 1: Behind Spikes Banana", + 'region': 6}, + 200672: {'name': "Arcade 1: Pyramid Banana", + 'region': 6}, + 200673: {'name': "Arcade 1: Moving Platforms Muscle Applique", + 'region': 6}, + 200674: {'name': "Arcade 1: Bed Banana", + 'region': 6}, + + # Airship + 200675: {'name': "Airship: Talk to Somsnosa", + 'region': 7}, + + # Arcade Island + 200676: {'name': "Arcade Island: Shielded Key", + 'region': 8}, + 200677: {'name': "Arcade 2: Flying Machine Banana", + 'region': 8}, + 200678: {'name': "Arcade 2: Paper Cup Detour", + 'region': 8}, + 200679: {'name': "Arcade 2: Peak Muscle Applique", + 'region': 8}, + 200680: {'name': "Arcade 2: Double Banana 1", + 'region': 8}, + 200681: {'name': "Arcade 2: Double Banana 2", + 'region': 8}, + 200682: {'name': "Arcade 2: Cave Burrito", + 'region': 8}, + + # Juice Ranch + 200684: {'name': "Juice Ranch: Juice 1", + 'region': 10}, + 200685: {'name': "Juice Ranch: Juice 2", + 'region': 10}, + 200686: {'name': "Juice Ranch: Juice 3", + 'region': 10}, + 200687: {'name': "Juice Ranch: Ledge Rancher", + 'region': 10}, + 200688: {'name': "Juice Ranch: Battle with Somsnosa", + 'region': 10}, + 200690: {'name': "Juice Ranch: Fridge", + 'region': 10}, + + # Worm Pod + 200692: {'name': "Worm Pod: Key", + 'region': 12}, + + # Foglast + 200693: {'name': "Foglast: West Sarcophagus", + 'region': 13}, + 200694: {'name': "Foglast: Underground Sarcophagus", + 'region': 13}, + 200695: {'name': "Foglast: Shielded Key", + 'region': 13}, + 200696: {'name': "Foglast: Buy Clicker", + 'region': 13}, + 200698: {'name': "Foglast: Shielded Chest", + 'region': 13}, + 200699: {'name': "Foglast: Cave Fridge", + 'region': 13}, + 200700: {'name': "Foglast: Roof Sarcophagus", + 'region': 13}, + 200701: {'name': "Foglast: Under Lair Sarcophagus 1", + 'region': 13}, + 200702: {'name': "Foglast: Under Lair Sarcophagus 2", + 'region': 13}, + 200703: {'name': "Foglast: Under Lair Sarcophagus 3", + 'region': 13}, + 200704: {'name': "Foglast: Sage Sarcophagus", + 'region': 13}, + 200705: {'name': "Foglast: Sage Item 1", + 'region': 13}, + 200706: {'name': "Foglast: Sage Item 2", + 'region': 13}, + + # Drill Castle + 200707: {'name': "Drill Castle: Ledge Banana", + 'region': 14}, + 200708: {'name': "Drill Castle: Island Banana", + 'region': 14}, + 200709: {'name': "Drill Castle: Island Pot", + 'region': 14}, + 200710: {'name': "Drill Castle: Cave Sarcophagus", + 'region': 14}, + 200711: {'name': "Drill Castle: Roof Banana", + 'region': 14}, + + # Sage Labyrinth + 200713: {'name': "Sage Labyrinth: 1F Chest Near Fountain", + 'region': 15}, + 200714: {'name': "Sage Labyrinth: 1F Hidden Sarcophagus", + 'region': 15}, + 200715: {'name': "Sage Labyrinth: 1F Four Statues Chest 1", + 'region': 15}, + 200716: {'name': "Sage Labyrinth: 1F Four Statues Chest 2", + 'region': 15}, + 200717: {'name': "Sage Labyrinth: B1 Double Chest 1", + 'region': 15}, + 200718: {'name': "Sage Labyrinth: B1 Double Chest 2", + 'region': 15}, + 200719: {'name': "Sage Labyrinth: B1 Single Chest", + 'region': 15}, + 200720: {'name': "Sage Labyrinth: B1 Enemy Chest", + 'region': 15}, + 200721: {'name': "Sage Labyrinth: B1 Hidden Sarcophagus", + 'region': 15}, + 200722: {'name': "Sage Labyrinth: B1 Hole Chest", + 'region': 15}, + 200723: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 1", + 'region': 15}, + 200724: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 2", + 'region': 15}, + 200754: {'name': "Sage Labyrinth: 2F Sarcophagus", + 'region': 15}, + 200725: {'name': "Sage Labyrinth: Motor Hunter Sarcophagus", + 'region': 15}, + 200726: {'name': "Sage Labyrinth: Sage Item 1", + 'region': 15}, + 200727: {'name': "Sage Labyrinth: Sage Item 2", + 'region': 15}, + 200728: {'name': "Sage Labyrinth: Sage Left Arm", + 'region': 15}, + 200729: {'name': "Sage Labyrinth: Sage Right Arm", + 'region': 15}, + 200730: {'name': "Sage Labyrinth: Sage Left Leg", + 'region': 15}, + 200731: {'name': "Sage Labyrinth: Sage Right Leg", + 'region': 15}, + + # Sage Airship + 200732: {'name': "Sage Airship: Bottom Level Pot", + 'region': 16}, + 200733: {'name': "Sage Airship: Flesh Pot", + 'region': 16}, + 200734: {'name': "Sage Airship: Top Jar", + 'region': 16}, + + # Hylemxylem + 200736: {'name': "Hylemxylem: Jar", + 'region': 17}, + 200737: {'name': "Hylemxylem: Lower Reservoir Key", + 'region': 17}, + 200738: {'name': "Hylemxylem: Fountain Banana", + 'region': 17}, + 200739: {'name': "Hylemxylem: East Island Banana", + 'region': 17}, + 200740: {'name': "Hylemxylem: East Island Chest", + 'region': 17}, + 200741: {'name': "Hylemxylem: Upper Chamber Banana", + 'region': 17}, + 200742: {'name': "Hylemxylem: Across Upper Reservoir Chest", + 'region': 17}, + 200743: {'name': "Hylemxylem: Drained Lower Reservoir Chest", + 'region': 17}, + 200744: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 1", + 'region': 17}, + 200745: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 2", + 'region': 17}, + 200746: {'name': "Hylemxylem: Lower Reservoir Hole Pot 1", + 'region': 17}, + 200747: {'name': "Hylemxylem: Lower Reservoir Hole Pot 2", + 'region': 17}, + 200748: {'name': "Hylemxylem: Lower Reservoir Hole Pot 3", + 'region': 17}, + 200749: {'name': "Hylemxylem: Lower Reservoir Hole Sarcophagus", + 'region': 17}, + 200750: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 1", + 'region': 17}, + 200751: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 2", + 'region': 17}, + 200752: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 3", + 'region': 17}, + 200753: {'name': "Hylemxylem: Upper Reservoir Hole Key", + 'region': 17} +} + + +tv_location_table: Dict[int, LocationDict] = { + 200627: {'name': "Waynehouse: TV", + 'region': 2}, + 200631: {'name': "Afterlife: TV", + 'region': 1}, + 200642: {'name': "New Muldul: TV", + 'region': 4}, + 200666: {'name': "Viewax's Edifice: TV", + 'region': 6}, + 200683: {'name': "TV Island: TV", + 'region': 9}, + 200691: {'name': "Juice Ranch: TV", + 'region': 10}, + 200697: {'name': "Foglast: TV", + 'region': 13}, + 200712: {'name': "Drill Castle: TV", + 'region': 14}, + 200735: {'name': "Sage Airship: TV", + 'region': 16} +} + + +party_location_table: Dict[int, LocationDict] = { + 200644: {'name': "New Muldul: Pongorma Joins", + 'region': 4}, + 200654: {'name': "Viewax's Edifice: Dedusmuln Joins", + 'region': 6}, + 200689: {'name': "Juice Ranch: Somsnosa Joins", + 'region': 10} +} + + +medallion_location_table: Dict[int, LocationDict] = { + 200755: {'name': "New Muldul: Upper House Medallion", + 'region': 4}, + + 200756: {'name': "New Muldul: Vault Rear Left Medallion", + 'region': 5}, + 200757: {'name': "New Muldul: Vault Rear Right Medallion", + 'region': 5}, + 200758: {'name': "New Muldul: Vault Center Medallion", + 'region': 5}, + 200759: {'name': "New Muldul: Vault Front Left Medallion", + 'region': 5}, + 200760: {'name': "New Muldul: Vault Front Right Medallion", + 'region': 5}, + + 200761: {'name': "Viewax's Edifice: Fort Wall Medallion", + 'region': 6}, + 200762: {'name': "Viewax's Edifice: Jar Medallion", + 'region': 6}, + 200763: {'name': "Viewax's Edifice: Sage Chair Medallion", + 'region': 6}, + 200764: {'name': "Arcade 1: Lonely Medallion", + 'region': 6}, + 200765: {'name': "Arcade 1: Alcove Medallion", + 'region': 6}, + 200766: {'name': "Arcade 1: Lava Medallion", + 'region': 6}, + + 200767: {'name': "Arcade 2: Flying Machine Medallion", + 'region': 8}, + 200768: {'name': "Arcade 2: Guarded Medallion", + 'region': 8}, + 200769: {'name': "Arcade 2: Spinning Medallion", + 'region': 8}, + 200770: {'name': "Arcade 2: Hook Medallion", + 'region': 8}, + 200771: {'name': "Arcade 2: Flag Medallion", + 'region': 8}, + + 200772: {'name': "Foglast: Under Lair Medallion", + 'region': 13}, + 200773: {'name': "Foglast: Mid-Air Medallion", + 'region': 13}, + 200774: {'name': "Foglast: Top of Tower Medallion", + 'region': 13}, + + 200775: {'name': "Sage Airship: Walkway Medallion", + 'region': 16}, + 200776: {'name': "Sage Airship: Flesh Medallion", + 'region': 16}, + 200777: {'name': "Sage Airship: Top of Ship Medallion", + 'region': 16}, + 200778: {'name': "Sage Airship: Hidden Medallion 1", + 'region': 16}, + 200779: {'name': "Sage Airship: Hidden Medallion 2", + 'region': 16}, + 200780: {'name': "Sage Airship: Hidden Medallion 3", + 'region': 16}, + + 200781: {'name': "Hylemxylem: Lower Reservoir Medallion", + 'region': 17}, + 200782: {'name': "Hylemxylem: Lower Reservoir Hole Medallion", + 'region': 17}, + 200783: {'name': "Hylemxylem: Drain Switch Medallion", + 'region': 17}, + 200784: {'name': "Hylemxylem: Warpo Medallion", + 'region': 17} +} \ No newline at end of file diff --git a/worlds/hylics2/Options.py b/worlds/hylics2/Options.py new file mode 100644 index 0000000000..ac57e666a1 --- /dev/null +++ b/worlds/hylics2/Options.py @@ -0,0 +1,41 @@ +from Options import Choice, Toggle, DefaultOnToggle, DeathLink + +class PartyShuffle(Toggle): + """Shuffles party members into the pool. + Note that enabling this can potentially increase both the difficulty and length of a run.""" + display_name = "Shuffle Party Members" + +class GestureShuffle(Choice): + """Choose where gestures will appear in the item pool.""" + display_name = "Shuffle Gestures" + option_anywhere = 0 + option_tvs_only = 1 + option_default_locations = 2 + default = 0 + +class MedallionShuffle(Toggle): + """Shuffles red medallions into the pool.""" + display_name = "Shuffle Red Medallions" + +class RandomStart(Toggle): + """Start the randomizer in 1 of 4 positions. + (Waynehouse, Viewax's Edifice, TV Island, Shield Facility)""" + display_name = "Randomize Start Location" + +class ExtraLogic(DefaultOnToggle): + """Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult.""" + display_name = "Extra Items in Logic" + +class Hylics2DeathLink(DeathLink): + """When you die, everyone dies. The reverse is also true. + Note that this also includes death by using the PERISH gesture. + Can be toggled via in-game console command "/deathlink".""" + +hylics2_options = { + "party_shuffle": PartyShuffle, + "gesture_shuffle" : GestureShuffle, + "medallion_shuffle" : MedallionShuffle, + "random_start" : RandomStart, + "extra_items_in_logic": ExtraLogic, + "death_link": Hylics2DeathLink +} \ No newline at end of file diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py new file mode 100644 index 0000000000..be38e102ea --- /dev/null +++ b/worlds/hylics2/Rules.py @@ -0,0 +1,433 @@ +from worlds.generic.Rules import add_rule +from ..AutoWorld import LogicMixin + + +class Hylics2Logic(LogicMixin): + + def _hylics2_can_air_dash(self, player): + return self.has("PNEUMATOPHORE", player) + + def _hylics2_has_airship(self, player): + return self.has("DOCK KEY", player) + + def _hylics2_has_jail_key(self, player): + return self.has("JAIL KEY", player) + + def _hylics2_has_paddle(self, player): + return self.has("PADDLE", player) + + def _hylics2_has_worm_room_key(self, player): + return self.has("WORM ROOM KEY", player) + + def _hylics2_has_bridge_key(self, player): + return self.has("BRIDGE KEY", player) + + def _hylics2_has_upper_chamber_key(self, player): + return self.has("UPPER CHAMBER KEY", player) + + def _hylics2_has_vessel_room_key(self, player): + return self.has("VESSEL ROOM KEY", player) + + def _hylics2_has_house_key(self, player): + return self.has("HOUSE KEY", player) + + def _hylics2_has_cave_key(self, player): + return self.has("CAVE KEY", player) + + def _hylics2_has_skull_bomb(self, player): + return self.has("SKULL BOMB", player) + + def _hylics2_has_tower_key(self, player): + return self.has("TOWER KEY", player) + + def _hylics2_has_deep_key(self, player): + return self.has("DEEP KEY", player) + + def _hylics2_has_upper_house_key(self, player): + return self.has("UPPER HOUSE KEY", player) + + def _hylics2_has_clicker(self, player): + return self.has("CLICKER", player) + + def _hylics2_has_tokens(self, player): + return self.has("SAGE TOKEN", player, 3) + + def _hylics2_has_charge_up(self, player): + return self.has("CHARGE UP", player) + + def _hylics2_has_cup(self, player): + return self.has("PAPER CUP", player, 1) + + def _hylics2_has_1_member(self, player): + return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player) + + def _hylics2_has_2_members(self, player): + return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\ + (self.has("Pongorma", player) and self.has("Somsnosa", player)) or\ + (self.has("Dedusmuln", player) and self.has("Somsnosa", player)) + + def _hylics2_has_3_members(self, player): + return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player) + + def _hylics2_enter_arcade2(self, player): + return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player) + + def _hylics2_enter_wormpod(self, player): + return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\ + self._hylics2_has_paddle(player) + + def _hylics2_enter_sageship(self, player): + return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\ + self._hylics2_has_paddle(player) + + def _hylics2_enter_foglast(self, player): + return self._hylics2_enter_wormpod(player) + + def _hylics2_enter_hylemxylem(self, player): + return self._hylics2_can_air_dash(player) and self._hylics2_enter_wormpod(player) and\ + self._hylics2_has_bridge_key(player) + + +def set_rules(hylics2world): + world = hylics2world.world + player = hylics2world.player + + # Afterlife + add_rule(world.get_location("Afterlife: TV", player), + lambda state: state._hylics2_has_cave_key(player)) + + # New Muldul + add_rule(world.get_location("New Muldul: Underground Chest", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("New Muldul: TV", player), + lambda state: state._hylics2_has_house_key(player)) + add_rule(world.get_location("New Muldul: Upper House Chest 1", player), + lambda state: state._hylics2_has_upper_house_key(player)) + add_rule(world.get_location("New Muldul: Upper House Chest 2", player), + lambda state: state._hylics2_has_upper_house_key(player)) + + # New Muldul Vault + add_rule(world.get_location("New Muldul: Rescued Blerol 1", player), + lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\ + ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\ + (state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player)))) + add_rule(world.get_location("New Muldul: Rescued Blerol 2", player), + lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\ + ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\ + (state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player)))) + add_rule(world.get_location("New Muldul: Vault Left Chest", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Right Chest", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Bomb", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + + # Viewax's Edifice + add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Shielded Key", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Shielded Key", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Tower Pot", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Tower Jar", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Tower Chest", player), + lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player)) + add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: TV", player), + lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player), + lambda state: state._hylics2_can_air_dash(player)) + + # Arcade 1 + add_rule(world.get_location("Arcade 1: Key", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Coin Dash", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Pyramid Banana", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Bed Banana", player), + lambda state: state._hylics2_has_paddle(player)) + + # Airship + add_rule(world.get_location("Airship: Talk to Somsnosa", player), + lambda state: state._hylics2_has_worm_room_key(player)) + + # Foglast + add_rule(world.get_location("Foglast: Underground Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Shielded Key", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: TV", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player)) + add_rule(world.get_location("Foglast: Buy Clicker", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Shielded Chest", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Cave Fridge", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Roof Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Sage Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Sage Item 1", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Sage Item 2", player), + lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player)) + + # Drill Castle + add_rule(world.get_location("Drill Castle: Island Banana", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Drill Castle: Island Pot", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Drill Castle: TV", player), + lambda state: state._hylics2_can_air_dash(player)) + + # Sage Labyrinth + add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player), + lambda state: state._hylics2_has_deep_key(player)) + add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player), + lambda state: state._hylics2_has_deep_key(player)) + + # Sage Airship + add_rule(world.get_location("Sage Airship: TV", player), + lambda state: state._hylics2_has_tokens(player)) + + # Hylemxylem + add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + + # extra rules if Extra Items in Logic is enabled + if world.extra_items_in_logic[player]: + for i in world.get_region("Foglast", player).entrances: + add_rule(i, lambda state: state._hylics2_has_charge_up(player)) + for i in world.get_region("Sage Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\ + state._hylics2_has_worm_room_key(player)) + for i in world.get_region("Hylemxylem", player).entrances: + add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player)) + + add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player), + lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player)) + + # extra rules if Shuffle Party Members is enabled + if world.party_shuffle[player]: + for i in world.get_region("Arcade Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player)) + for i in world.get_region("Foglast", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player) or\ + (state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player))) + for i in world.get_region("Sage Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player)) + for i in world.get_region("Hylemxylem", player).entrances: + add_rule(i, lambda state: state._hylics2_has_3_members(player)) + + add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("New Muldul: Rescued Blerol 1", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("New Muldul: Rescued Blerol 2", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("New Muldul: Vault Left Chest", player), + lambda state: state._hylics2_has_3_members(player)) + add_rule(world.get_location("New Muldul: Vault Right Chest", player), + lambda state: state._hylics2_has_3_members(player)) + add_rule(world.get_location("New Muldul: Vault Bomb", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player), + lambda state: state._hylics2_has_2_members(player)) + add_rule(world.get_location("Airship: Talk to Somsnosa", player), + lambda state: state._hylics2_has_3_members(player)) + add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player), + lambda state: state._hylics2_has_3_members(player)) + + # extra rules if Shuffle Red Medallions is enabled + if world.medallion_shuffle[player]: + add_rule(world.get_location("New Muldul: Upper House Medallion", player), + lambda state: state._hylics2_has_upper_house_key(player)) + add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Center Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player), + lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Arcade 1: Lonely Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Arcade 1: Alcove Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Foglast: Under Lair Medallion", player), + lambda state: state._hylics2_has_bridge_key(player)) + add_rule(world.get_location("Foglast: Mid-Air Medallion", player), + lambda state: state._hylics2_can_air_dash(player)) + add_rule(world.get_location("Foglast: Top of Tower Medallion", player), + lambda state: state._hylics2_has_paddle(player)) + add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player), + lambda state: state._hylics2_has_upper_chamber_key(player)) + + # extra rules is Shuffle Red Medallions and Party Shuffle are enabled + if world.party_shuffle[player] and world.medallion_shuffle[player]: + add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Center Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player), + lambda state: state._hylics2_has_jail_key(player)) + + # entrances + for i in world.get_region("Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Arcade Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player)) + for i in world.get_region("Worm Pod", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_wormpod(player)) + for i in world.get_region("Foglast", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_foglast(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_skull_bomb(player)) + for i in world.get_region("Sage Airship", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_sageship(player)) + for i in world.get_region("Hylemxylem", player).entrances: + add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player)) + + # random start logic (default) + if ((not world.random_start[player]) or \ + (world.random_start[player] and hylics2world.start_location == "Waynehouse")): + # entrances + for i in world.get_region("Viewax", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("TV Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Shield Facility", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Juice Ranch", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + + # random start logic (Viewax's Edifice) + elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"): + for i in world.get_region("Waynehouse", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul Vault", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("Drill Castle", player).entrances: + add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) + for i in world.get_region("TV Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Shield Facility", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Juice Ranch", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + + # random start logic (TV Island) + elif (world.random_start[player] and hylics2world.start_location == "TV Island"): + for i in world.get_region("Waynehouse", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul Vault", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Drill Castle", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Viewax", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Shield Facility", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Juice Ranch", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + + # random start logic (Shield Facility) + elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"): + for i in world.get_region("Waynehouse", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("New Muldul Vault", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Drill Castle", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Viewax", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("TV Island", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) + for i in world.get_region("Sage Labyrinth", player).entrances: + add_rule(i, lambda state: state._hylics2_has_airship(player)) \ No newline at end of file diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py new file mode 100644 index 0000000000..b429eb6a44 --- /dev/null +++ b/worlds/hylics2/__init__.py @@ -0,0 +1,246 @@ +import random +from typing import Dict, Any +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, RegionType +from worlds.generic.Rules import set_rule +from ..AutoWorld import World, WebWorld +from . import Items, Locations, Options, Rules, Exits + + +class Hylics2Web(WebWorld): + theme = "ocean" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to settings up the Hylics 2 randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["TRPG"] + )] + + +class Hylics2World(World): + """ + Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne, + travel the world, and gather your allies to defeat the nefarious Gibby in his Hylemxylem! + """ + game: str = "Hylics 2" + web = Hylics2Web() + + all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table, + **Items.medallion_item_table} + all_locations = {**Locations.location_table, **Locations.tv_location_table, **Locations.party_location_table, + **Locations.medallion_location_table} + + item_name_to_id = {data["name"]: item_id for item_id, data in all_items.items()} + location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()} + option_definitions = Options.hylics2_options + + topology_present: bool = True + remote_items: bool = True + remote_start_inventory: bool = True + + data_version: 1 + + start_location = "Waynehouse" + + + def set_rules(self): + Rules.set_rules(self) + + + def create_item(self, name: str) -> "Hylics2Item": + item_id: int = self.item_name_to_id[name] + + return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player) + + + def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item": + return Hylics2Item(name, classification, code, self.player) + + + def create_event(self, event: str): + return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player) + + + # set random starting location if option is enabled + def generate_early(self): + if self.world.random_start[self.player]: + i = self.world.random.randint(0, 3) + if i == 0: + self.start_location = "Waynehouse" + elif i == 1: + self.start_location = "Viewax's Edifice" + elif i == 2: + self.start_location = "TV Island" + elif i == 3: + self.start_location = "Shield Facility" + + def generate_basic(self): + # create location for beating the game and place Victory event there + loc = Location(self.player, "Defeat Gibby", None, self.world.get_region("Hylemxylem", self.player)) + loc.place_locked_item(self.create_event("Victory")) + set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player) + and state._hylics2_has_vessel_room_key(self.player)) + self.world.get_region("Hylemxylem", self.player).locations.append(loc) + self.world.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + # create item pool + pool = [] + + # add regular items + for i, data in Items.item_table.items(): + if data["count"] > 0: + for j in range(data["count"]): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add party members if option is enabled + if self.world.party_shuffle[self.player]: + for i, data in Items.party_item_table.items(): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # handle gesture shuffle options + if self.world.gesture_shuffle[self.player] == 2: # vanilla locations + gestures = Items.gesture_item_table + self.world.get_location("Waynehouse: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678)) + self.world.get_location("Afterlife: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683)) + self.world.get_location("New Muldul: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679)) + self.world.get_location("Viewax's Edifice: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680)) + self.world.get_location("TV Island: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681)) + self.world.get_location("Juice Ranch: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682)) + self.world.get_location("Foglast: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684)) + self.world.get_location("Drill Castle: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688)) + self.world.get_location("Sage Airship: TV", self.player)\ + .place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685)) + + elif self.world.gesture_shuffle[self.player] == 1: # TVs only + gestures = list(Items.gesture_item_table.items()) + tvs = list(Locations.tv_location_table.items()) + + # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get + # placed at Sage Airship: TV + if self.world.extra_items_in_logic[self.player]: + tv = self.world.random.choice(tvs) + gest = gestures.index((200681, Items.gesture_item_table[200681])) + while tv[1]["name"] == "Sage Airship: TV": + tv = self.world.random.choice(tvs) + self.world.get_location(tv[1]["name"], self.player)\ + .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], + gestures[gest])) + gestures.remove(gestures[gest]) + tvs.remove(tv) + + for i in range(len(gestures)): + gest = self.world.random.choice(gestures) + tv = self.world.random.choice(tvs) + self.world.get_location(tv[1]["name"], self.player)\ + .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1])) + gestures.remove(gest) + tvs.remove(tv) + + else: # add gestures to pool like normal + for i, data in Items.gesture_item_table.items(): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add '10 Bones' items if medallion shuffle is enabled + if self.world.medallion_shuffle[self.player]: + for i, data in Items.medallion_item_table.items(): + for j in range(data["count"]): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add to world's pool + self.world.itempool += pool + + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data: Dict[str, Any] = { + "party_shuffle": self.world.party_shuffle[self.player].value, + "medallion_shuffle": self.world.medallion_shuffle[self.player].value, + "random_start" : self.world.random_start[self.player].value, + "start_location" : self.start_location, + "death_link": self.world.death_link[self.player].value + } + return slot_data + + + def create_regions(self) -> None: + + region_table: Dict[int, Region] = { + 0: Region("Menu", RegionType.Generic, "Menu", self.player, self.world), + 1: Region("Afterlife", RegionType.Generic, "Afterlife", self.player, self.world), + 2: Region("Waynehouse", RegionType.Generic, "Waynehouse", self.player, self.world), + 3: Region("World", RegionType.Generic, "World", self.player, self.world), + 4: Region("New Muldul", RegionType.Generic, "New Muldul", self.player, self.world), + 5: Region("New Muldul Vault", RegionType.Generic, "New Muldul Vault", self.player, self.world), + 6: Region("Viewax", RegionType.Generic, "Viewax's Edifice", self.player, self.world), + 7: Region("Airship", RegionType.Generic, "Airship", self.player, self.world), + 8: Region("Arcade Island", RegionType.Generic, "Arcade Island", self.player, self.world), + 9: Region("TV Island", RegionType.Generic, "TV Island", self.player, self.world), + 10: Region("Juice Ranch", RegionType.Generic, "Juice Ranch", self.player, self.world), + 11: Region("Shield Facility", RegionType.Generic, "Shield Facility", self.player, self.world), + 12: Region("Worm Pod", RegionType.Generic, "Worm Pod", self.player, self.world), + 13: Region("Foglast", RegionType.Generic, "Foglast", self.player, self.world), + 14: Region("Drill Castle", RegionType.Generic, "Drill Castle", self.player, self.world), + 15: Region("Sage Labyrinth", RegionType.Generic, "Sage Labyrinth", self.player, self.world), + 16: Region("Sage Airship", RegionType.Generic, "Sage Airship", self.player, self.world), + 17: Region("Hylemxylem", RegionType.Generic, "Hylemxylem", self.player, self.world) + } + + # create regions from table + for i, reg in region_table.items(): + self.world.regions.append(reg) + # get all exits per region + for j, exits in Exits.region_exit_table.items(): + if j == i: + for k in exits: + # create entrance and connect it to parent and destination regions + ent = Entrance(self.player, k, reg) + reg.exits.append(ent) + if k == "New Game" and self.world.random_start[self.player]: + if self.start_location == "Waynehouse": + ent.connect(region_table[2]) + elif self.start_location == "Viewax's Edifice": + ent.connect(region_table[6]) + elif self.start_location == "TV Island": + ent.connect(region_table[9]) + elif self.start_location == "Shield Facility": + ent.connect(region_table[11]) + else: + for name, num in Exits.exit_lookup_table.items(): + if k == name: + ent.connect(region_table[num]) + + # add regular locations + for i, data in Locations.location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + for i, data in Locations.tv_location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + + # add party member locations if option is enabled + if self.world.party_shuffle[self.player]: + for i, data in Locations.party_location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + + # add medallion locations if option is enabled + if self.world.medallion_shuffle[self.player]: + for i, data in Locations.medallion_location_table.items(): + region_table[data["region"]].locations\ + .append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]])) + + +class Hylics2Location(Location): + game: str = "Hylics 2" + + +class Hylics2Item(Item): + game: str = "Hylics 2" \ No newline at end of file diff --git a/worlds/hylics2/docs/en_Hylics 2.md b/worlds/hylics2/docs/en_Hylics 2.md new file mode 100644 index 0000000000..cb201a52bb --- /dev/null +++ b/worlds/hylics2/docs/en_Hylics 2.md @@ -0,0 +1,17 @@ +# Hylics 2 + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +In Hylics 2, all unique items, equipment and skills are randomized. This includes items in chests, items that are freely standing in the world, items recieved from talking to certain characters, gestures learned from TVs, and so on. Items recieved from completing battles are not randomized, with the exception of the Jail Key recieved from defeating Viewax. + +## What Hylics 2 items can appear in other players' worlds? + +Consumable items, key items, gloves, accessories, and gestures can appear in other players' worlds. + +## What does another world's item look like in Hylics 2? + +All items retain their original appearance. You won't know if an item belongs to another player until you collect it. \ No newline at end of file diff --git a/worlds/hylics2/docs/setup_en.md b/worlds/hylics2/docs/setup_en.md new file mode 100644 index 0000000000..1e1ac49790 --- /dev/null +++ b/worlds/hylics2/docs/setup_en.md @@ -0,0 +1,35 @@ +# Hylics 2 Randomizer Setup Guide + +## Required Software + +- Hylics 2 from: [Steam](https://store.steampowered.com/app/1286710/Hylics_2/) or [itch.io](https://mason-lindroth.itch.io/hylics-2) +- BepInEx from: [GitHub](https://github.com/BepInEx/BepInEx/releases) +- Archipelago Mod for Hylics 2 from: [GitHub](https://github.com/TRPG0/ArchipelagoHylics2) + +## Instructions (Windows) + +1. Download and install BepInEx 5 (32-bit, version 5.4.20 or newer) to your Hylics 2 root folder. *Do not use any pre-release versions of BepInEx 6.* + +2. Start Hylics 2 once so that BepInEx can create its required configuration files. + +3. Download the latest version of ArchipelagoHylics2 from the [Releases](https://github.com/TRPG0/ArchipelagoHylics2/releases) page and extract the contents of the zip file into `BepInEx\plugins`. + +4. Start Hylics 2 again. To verify that the mod is working, begin a new game or load a save file. + +## Connecting + +To connect to an Archipelago server, open the in-game console (default key: `/`) and use the command `/connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used. +**Make sure that you have connected to a server at least once before attempting to check any locations.** + +## Other Commands + +There are a few additional commands that can be used while playing Hylics 2 randomizer: + +- `/disconnect` - Disconnect from an Archipelago server. +- `/popups` - Enables or disables in-game messages when an item is found or recieved. +- `/airship` - Resummons the airship at the dock above New Muldul and teleports Wayne to it, in case the player gets stuck. Player must have the DOCK KEY to use this command. +- `/respawn` - Moves Wayne back to the spawn position of the current area in case you get stuck. `/respawn home` will teleport Wayne back to his original starting position. +- `/checked [region]` - States how many locations have been checked in a given region. If no region is given, then the player's location will be used. +- `/deathlink` - Enables or disables DeathLink. +- `/help [command]` - Lists a command, it's description, and it's required arguments (if any). If no command is given, all commands will be displayed. +- `![command]` - Entering any command with an `!` at the beginning allows for remotely sending commands to the server. \ No newline at end of file From e708bea8197915ac664a6c3bd615e595dd0f164c Mon Sep 17 00:00:00 2001 From: Jarno Date: Thu, 13 Oct 2022 07:55:00 +0200 Subject: [PATCH 053/105] [Sudoku] Added new BK mode game (#910) Co-authored-by: Hussein Farran Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> --- Main.py | 2 +- test/general/TestImplemented.py | 2 +- test/general/TestReachability.py | 2 +- worlds/bk_sudoku/__init__.py | 33 ++++++++++++++++++++++++++++++ worlds/bk_sudoku/docs/en_Sudoku.md | 13 ++++++++++++ worlds/bk_sudoku/docs/setup_en.md | 24 ++++++++++++++++++++++ 6 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 worlds/bk_sudoku/__init__.py create mode 100644 worlds/bk_sudoku/docs/en_Sudoku.md create mode 100644 worlds/bk_sudoku/docs/setup_en.md diff --git a/Main.py b/Main.py index bbd0c805df..b26dcf7986 100644 --- a/Main.py +++ b/Main.py @@ -82,7 +82,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) numlength = 8 for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): - if not cls.hidden: + if not cls.hidden and len(cls.item_names) > 0: logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} " f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - " f"{max(cls.item_id_to_name):{numlength}}) | " diff --git a/test/general/TestImplemented.py b/test/general/TestImplemented.py index b2e696ab0d..15e099ff09 100644 --- a/test/general/TestImplemented.py +++ b/test/general/TestImplemented.py @@ -8,7 +8,7 @@ class TestImplemented(unittest.TestCase): def testCompletionCondition(self): """Ensure a completion condition is set that has requirements.""" for gamename, world_type in AutoWorldRegister.world_types.items(): - if not world_type.hidden and gamename not in {"ArchipIDLE", "Final Fantasy"}: + if not world_type.hidden and gamename not in {"ArchipIDLE", "Final Fantasy", "Sudoku"}: with self.subTest(gamename): world = setup_default_world(world_type) self.assertFalse(world.completion_condition[1](world.state)) diff --git a/test/general/TestReachability.py b/test/general/TestReachability.py index d638b56e8d..13d56d740a 100644 --- a/test/general/TestReachability.py +++ b/test/general/TestReachability.py @@ -28,7 +28,7 @@ class TestBase(unittest.TestCase): def testEmptyStateCanReachSomething(self): for game_name, world_type in AutoWorldRegister.world_types.items(): # Final Fantasy logic is controlled by finalfantasyrandomizer.com - if game_name != "Archipelago" and game_name != "Final Fantasy": + if game_name not in {"Archipelago", "Final Fantasy", "Sudoku"}: with self.subTest("Game", game=game_name): world = setup_default_world(world_type) state = CollectionState(world) diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py new file mode 100644 index 0000000000..7183b0ec18 --- /dev/null +++ b/worlds/bk_sudoku/__init__.py @@ -0,0 +1,33 @@ +from BaseClasses import Tutorial +from ..AutoWorld import World, WebWorld +from typing import Dict + + +class Bk_SudokuWebWorld(WebWorld): + settings_page = "games/Sudoku/info/en" + theme = 'partyTime' + tutorials = [ + Tutorial( + tutorial_name='Setup Guide', + description='A guide to playing BK Sudoku', + language='English', + file_name='setup_en.md', + link='guide/en', + authors=['Jarno'] + ) + ] + + +class Bk_SudokuWorld(World): + """ + Play a little Sudoku while you're in BK mode to maybe get some useful hints + """ + game = "Sudoku" + web = Bk_SudokuWebWorld() + + item_name_to_id: Dict[str, int] = {} + location_name_to_id: Dict[str, int] = {} + + @classmethod + def stage_assert_generate(cls, world): + raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world") diff --git a/worlds/bk_sudoku/docs/en_Sudoku.md b/worlds/bk_sudoku/docs/en_Sudoku.md new file mode 100644 index 0000000000..072e43a980 --- /dev/null +++ b/worlds/bk_sudoku/docs/en_Sudoku.md @@ -0,0 +1,13 @@ +# Bk Sudoku + +## What is this game? + +BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku client that can connect to any existing multiworld. When connected, you can play Sudoku to unlock random hints for your game. While slow, it will give you something to do when you can't reach the checks in your game. + +## What hints are unlocked? + +After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. It is possible to hint the same location repeatedly if that location is still unchecked. + +## Where is the settings page? + +There is no settings page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. diff --git a/worlds/bk_sudoku/docs/setup_en.md b/worlds/bk_sudoku/docs/setup_en.md new file mode 100644 index 0000000000..5e93dce873 --- /dev/null +++ b/worlds/bk_sudoku/docs/setup_en.md @@ -0,0 +1,24 @@ +# BK Sudoku Setup Guide + +## Required Software +- [Bk Sudoku](https://github.com/Jarno458/sudoku) +- [.Net 6](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net60) + +## General Concept + +This is a client that can connect to any multiworld slot, and lets you play Sudoku to unlock random hints for that slot's locations. + +Due to the fact that the Sudoku client may connect to any slot, it is not necessary to generate a YAML for this game as it does not generate any new slots in the multiworld session. + +## Installation Procedures + +Go to the latest release on [BK Sudoku Releases](https://github.com/Jarno458/sudoku/releases). Download and extract the `Bk_Sudoku.zip` file. + +## Joining a MultiWorld Game + +1. Run Bk_Sudoku.exe +2. Enter the name of the slot you wish to connect to +3. Enter the server url & port number +4. Press connect +5. Choose difficulty +6. Try to solve the Sudoku From 6b9073acd7bf16dbebff32f99b4ae7cf49127bcd Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 13 Oct 2022 10:46:44 +0200 Subject: [PATCH 054/105] sm64ex: Update min client version --- worlds/sm64ex/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 8cf2f74350..b1eef4ff2c 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -36,7 +36,7 @@ class SM64World(World): location_name_to_id = location_table data_version = 8 - required_client_version = (0, 3, 0) + required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] From 3bd4ef3f3dc1d67bfa3a9ca9184fe46fa42beb3b Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 13 Oct 2022 13:55:21 -0400 Subject: [PATCH 055/105] =?UTF-8?q?[Pok=C3=A9mon=20=20R/B]=20Fixes=20(#109?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prevent legendaries from being shuffled into restless soul encounter * Prevent Poke Tower 6F wild mons from being same as restless soul * fix non-deterministic generation --- worlds/pokemon_rb/__init__.py | 2 -- worlds/pokemon_rb/options.py | 3 +-- worlds/pokemon_rb/rom.py | 29 +++++++++++++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index d4ca79ff9a..4c37db8450 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -130,8 +130,6 @@ class PokemonRedBlueWorld(World): locations.append(location) self.world.random.choice(locations).place_locked_item(item) - - if not self.world.badgesanity[self.player].value: self.world.non_local_items[self.player].value -= self.item_name_groups["Badges"] for i in range(5): diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 37672c2501..e7e972f3b8 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -219,8 +219,7 @@ class RandomizeStarterPokemon(Choice): class RandomizeStaticPokemon(Choice): - """Randomize all one-time gift and encountered Pokemon, except legendaries. - These will always be first evolution stage Pokemon.""" + """Randomize one-time gift and encountered Pokemon. These will always be first evolution stage Pokemon.""" display_name = "Randomize Static Pokemon" default = 0 option_vanilla = 0 diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 370bd6afb7..0487387363 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -112,6 +112,10 @@ def process_static_pokemon(self): ["Static Pokemon", "Missable Pokemon"]] legendary_mons = [slot.original_item for slot in legendary_slots] + tower_6F_mons = set() + for i in range(1, 11): + tower_6F_mons.add(self.world.get_location(f"Pokemon Tower 6F - Wild Pokemon - {i}", self.player).item.name) + mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3] if self.world.randomize_legendary_pokemon[self.player].value == 0: @@ -126,6 +130,7 @@ def process_static_pokemon(self): elif self.world.randomize_legendary_pokemon[self.player].value == 2: static_slots = static_slots + legendary_slots self.world.random.shuffle(static_slots) + static_slots.sort(key=lambda s: 0 if s.name == "Pokemon Tower 6F - Restless Soul" else 1) while legendary_slots: swap_slot = legendary_slots.pop() slot = static_slots.pop() @@ -147,8 +152,12 @@ def process_static_pokemon(self): if not randomize_type: location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) else: - location.place_locked_item(self.create_item(slot_type + " " + - randomize_pokemon(self, slot.original_item, mons_list, randomize_type))) + mon = self.create_item(slot_type + " " + + randomize_pokemon(self, slot.original_item, mons_list, randomize_type)) + while location.name == "Pokemon Tower 6F - Restless Soul" and mon in tower_6F_mons: + mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, + randomize_type)) + location.place_locked_item(mon) for slot in starter_slots: location = self.world.get_location(slot.name, self.player) @@ -158,7 +167,8 @@ def process_static_pokemon(self): location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) else: location.place_locked_item(self.create_item(slot_type + " " + - randomize_pokemon(self, slot.original_item, mons_list, randomize_type))) + randomize_pokemon(self, slot.original_item, mons_list, randomize_type))) + def process_wild_pokemon(self): @@ -172,6 +182,12 @@ def process_wild_pokemon(self): locations = [] for slot in encounter_slots: mon = randomize_pokemon(self, slot.original_item, mons_list, self.world.randomize_wild_pokemon[self.player].value) + # if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak + # if static Pokemon are randomized we deal with that during static encounter randomization + while (self.world.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak" + and "Pokemon Tower 6F" in slot.name): + # to account for the possibility that only one ground type Pokemon exists, match only stats for this fix + mon = randomize_pokemon(self, slot.original_item, mons_list, 2) placed_mons[mon] += 1 location = self.world.get_location(slot.name, self.player) location.item = self.create_item(mon) @@ -293,8 +309,9 @@ def process_pokemon_data(self): chances = [[50, mon_data["type1"]], [80, mon_data["type2"]], [85, "Normal"]] else: chances = [] - moves = set(poke_data.moves.keys()) - moves -= set(["No Move"] + poke_data.hm_moves) + moves = list(poke_data.moves.keys()) + for move in ["No Move"] + poke_data.hm_moves: + moves.remove(move) mon_data["start move 1"] = get_move(moves, chances, self.world.random, True) for i in range(2, 5): if mon_data[f"start move {i}"] != "No Move" or self.world.start_with_four_moves[ @@ -462,7 +479,7 @@ def generate_output(self, output_directory: str): matchup[2] = random.choice([0] + ([5, 20] * 5)) elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 3: for matchup in chart: - matchup[2] = self.world.random.choice([i for i in range(0, 21) if i != 10]) + matchup[2] = random.choice([i for i in range(0, 21) if i != 10]) type_loc = rom_addresses["Type_Chart"] for matchup in chart: data[type_loc] = poke_data.type_ids[matchup[0]] From 7f3f886e41653e2df2c6f56500672146cc05c2db Mon Sep 17 00:00:00 2001 From: toasterparty Date: Thu, 13 Oct 2022 10:57:50 -0700 Subject: [PATCH 056/105] Overcooked! 2: Implementation (#1046) Overcooked! 2 is a couch co-op arcade game with a very high skill ceiling. It has a small but occult following, and the community craves a reason to keep coming back besides just grinding high scores. as such, this PR represents 3 major milestones in one: * The launch of OC2 Modding, a modding framework which is the first public mod for the game beyond simple RAM trainers * The launch of OC2 Randomizer * The integration of OC2 Randomizer in Archipelago --- README.md | 1 + test/overcooked2/TestOvercooked2.py | 142 + test/overcooked2/__init__.py | 0 worlds/overcooked2/Items.py | 152 + worlds/overcooked2/Locations.py | 15 + worlds/overcooked2/Logic.py | 3899 +++++++++++++++++++ worlds/overcooked2/Options.py | 110 + worlds/overcooked2/Overcooked2Levels.py | 349 ++ worlds/overcooked2/__init__.py | 510 +++ worlds/overcooked2/docs/en_Overcooked! 2.md | 86 + worlds/overcooked2/docs/setup_en.md | 84 + 11 files changed, 5348 insertions(+) create mode 100644 test/overcooked2/TestOvercooked2.py create mode 100644 test/overcooked2/__init__.py create mode 100644 worlds/overcooked2/Items.py create mode 100644 worlds/overcooked2/Locations.py create mode 100644 worlds/overcooked2/Logic.py create mode 100644 worlds/overcooked2/Options.py create mode 100644 worlds/overcooked2/Overcooked2Levels.py create mode 100644 worlds/overcooked2/__init__.py create mode 100644 worlds/overcooked2/docs/en_Overcooked! 2.md create mode 100644 worlds/overcooked2/docs/setup_en.md diff --git a/README.md b/README.md index 9615b0fba4..65f1792d92 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Currently, the following games are supported: * Dark Souls 3 * Super Mario World * Pokémon Red and Blue +* Overcooked! 2 For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/test/overcooked2/TestOvercooked2.py b/test/overcooked2/TestOvercooked2.py new file mode 100644 index 0000000000..ec9efd1f67 --- /dev/null +++ b/test/overcooked2/TestOvercooked2.py @@ -0,0 +1,142 @@ +import unittest +import json + +from random import Random + +from worlds.overcooked2.Items import * +from worlds.overcooked2.Overcooked2Levels import Overcooked2Level, level_id_to_shortname +from worlds.overcooked2.Logic import level_logic, level_shuffle_factory +from worlds.overcooked2.Locations import oc2_location_name_to_id + + +class Overcooked2Test(unittest.TestCase): + def testItems(self): + self.assertEqual(len(item_name_to_id), len(item_id_to_name)) + self.assertEqual(len(item_name_to_id), len(item_table)) + + previous_item = None + for item_name in item_table.keys(): + item: Item = item_table[item_name] + self.assertGreaterEqual(item.code, oc2_base_id, "Overcooked Item ID out of range") + self.assertLessEqual(item.code, item_table["Calmer Unbread"].code, "Overcooked Item ID out of range") + + if previous_item is not None: + self.assertEqual(item.code, previous_item + 1, + f"Overcooked Item ID noncontinguous: {item.code-oc2_base_id}") + previous_item = item.code + + self.assertEqual(item_table["Ok Emote"].code - item_table["Cooking Emote"].code, + 5, "Overcooked Emotes noncontigious") + + for item_name in item_frequencies: + self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in item_frequencies") + + for item_name in item_name_to_config_name.keys(): + self.assertIn(item_name, item_table.keys(), "Unexpected Overcooked Item in config mapping") + + for config_name in item_name_to_config_name.values(): + self.assertIn(config_name, vanilla_values.keys(), "Unexpected Overcooked Item in default config mapping") + + for config_name in vanilla_values.keys(): + self.assertIn(config_name, item_name_to_config_name.values(), + "Unexpected Overcooked Item in default config mapping") + + events = [ + ("Kevin-2", {"action": "UNLOCK_LEVEL", "payload": "38"}), + ("Curse Emote", {"action": "UNLOCK_EMOTE", "payload": "1"}), + ("Larger Tip Jar", {"action": "INC_TIP_COMBO", "payload": ""}), + ("Order Lookahead", {"action": "INC_ORDERS_ON_SCREEN", "payload": ""}), + ("Control Stick Batteries", {"action": "SET_VALUE", "payload": "DisableControlStick=False"}), + ] + for (item_name, expected_event) in events: + expected_event["message"] = f"{item_name} Acquired!" + event = item_to_unlock_event(item_name) + self.assertEqual(event, expected_event) + + self.assertFalse(is_progression("Preparing Emote")) + + for item_name in item_table: + item_to_unlock_event(item_name) + + def testOvercooked2Levels(self): + level_count = 0 + for _ in Overcooked2Level(): + level_count += 1 + self.assertEqual(level_count, 44) + + def testOvercooked2ShuffleFactory(self): + previous_runs = set() + for seed in range(0, 5): + levels = level_shuffle_factory(Random(seed), True, False) + self.assertEqual(len(levels), 44) + previous_level_id = None + for level_id in levels.keys(): + if previous_level_id is not None: + self.assertEqual(previous_level_id+1, level_id) + previous_level_id = level_id + + self.assertNotIn(levels[15], previous_runs) + previous_runs.add(levels[15]) + + levels = level_shuffle_factory(Random(123), False, True) + self.assertEqual(len(levels), 44) + + def testLevelNameRepresentation(self): + shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()] + + for shortname in shortnames: + self.assertIn(shortname, level_logic.keys()) + + self.assertEqual(len(level_logic), len(level_id_to_shortname)) + + for level_name in level_logic.keys(): + if level_name != "*": + self.assertIn(level_name, level_id_to_shortname.values()) + + for level_name in level_id_to_shortname.values(): + if level_name != "Tutorial": + self.assertIn(level_name, level_logic.keys()) + + region_names = [level.level_name for level in Overcooked2Level()] + for location_name in oc2_location_name_to_id.keys(): + level_name = location_name.split(" ")[0] + self.assertIn(level_name, region_names) + + def testLogic(self): + for level_name in level_logic.keys(): + logic = level_logic[level_name] + self.assertEqual(len(logic), 3, "Levels must provide logic for 1, 2, and 3 stars") + + for l in logic: + self.assertEqual(len(l), 2) + (exclusive, additive) = l + + for req in exclusive: + self.assertEqual(type(req), str) + self.assertIn(req, item_table.keys()) + + if len(additive) != 0: + self.assertGreater(len(additive), 1) + total_weight = 0.0 + for req in additive: + self.assertEqual(len(req), 2) + (item_name, weight) = req + self.assertEqual(type(item_name), str) + self.assertEqual(type(weight), float) + total_weight += weight + self.assertIn(item_name, item_table.keys()) + + self.assertGreaterEqual(total_weight, 0.99, "Additive requirements must add to 1.0 or greater to have any effect") + + def testItemLocationMapping(self): + number_of_items = 0 + for item_name in item_frequencies: + freq = item_frequencies[item_name] + self.assertGreaterEqual(freq, 0) + number_of_items += freq + + for item_name in item_table: + if item_name not in item_frequencies.keys(): + number_of_items += 1 + + self.assertLessEqual(number_of_items, len(oc2_location_name_to_id), "Too many items (before fillers placed)") diff --git a/test/overcooked2/__init__.py b/test/overcooked2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/overcooked2/Items.py b/worlds/overcooked2/Items.py new file mode 100644 index 0000000000..8cbe071140 --- /dev/null +++ b/worlds/overcooked2/Items.py @@ -0,0 +1,152 @@ +from BaseClasses import Item +from typing import NamedTuple, Dict + + +class ItemData(NamedTuple): + code: int + + +class Overcooked2Item(Item): + game: str = "Overcooked! 2" + + +oc2_base_id = 213700 + +item_table: Dict[str, ItemData] = { + "Wood" : ItemData(oc2_base_id + 1 ), + "Coal Bucket" : ItemData(oc2_base_id + 2 ), + "Spare Plate" : ItemData(oc2_base_id + 3 ), + "Fire Extinguisher" : ItemData(oc2_base_id + 4 ), + "Bellows" : ItemData(oc2_base_id + 5 ), + "Clean Dishes" : ItemData(oc2_base_id + 6 ), + "Larger Tip Jar" : ItemData(oc2_base_id + 7 ), + "Progressive Dash" : ItemData(oc2_base_id + 8 ), + "Progressive Throw/Catch" : ItemData(oc2_base_id + 9 ), + "Coin Purse" : ItemData(oc2_base_id + 10), + "Control Stick Batteries" : ItemData(oc2_base_id + 11), + "Wok Wheels" : ItemData(oc2_base_id + 12), + "Dish Scrubber" : ItemData(oc2_base_id + 13), + "Burn Leniency" : ItemData(oc2_base_id + 14), + "Sharp Knife" : ItemData(oc2_base_id + 15), + "Order Lookahead" : ItemData(oc2_base_id + 16), + "Lightweight Backpack" : ItemData(oc2_base_id + 17), + "Faster Respawn Time" : ItemData(oc2_base_id + 18), + "Faster Condiment/Drink Switch" : ItemData(oc2_base_id + 19), + "Guest Patience" : ItemData(oc2_base_id + 20), + "Kevin-1" : ItemData(oc2_base_id + 21), + "Kevin-2" : ItemData(oc2_base_id + 22), + "Kevin-3" : ItemData(oc2_base_id + 23), + "Kevin-4" : ItemData(oc2_base_id + 24), + "Kevin-5" : ItemData(oc2_base_id + 25), + "Kevin-6" : ItemData(oc2_base_id + 26), + "Kevin-7" : ItemData(oc2_base_id + 27), + "Kevin-8" : ItemData(oc2_base_id + 28), + "Cooking Emote" : ItemData(oc2_base_id + 29), + "Curse Emote" : ItemData(oc2_base_id + 30), + "Serving Emote" : ItemData(oc2_base_id + 31), + "Preparing Emote" : ItemData(oc2_base_id + 32), + "Washing Up Emote" : ItemData(oc2_base_id + 33), + "Ok Emote" : ItemData(oc2_base_id + 34), + "Ramp Button" : ItemData(oc2_base_id + 35), + "Bonus Star" : ItemData(oc2_base_id + 36), + "Calmer Unbread" : ItemData(oc2_base_id + 37), +} + +item_frequencies = { + "Progressive Throw/Catch": 2, + "Larger Tip Jar": 2, + "Order Lookahead": 2, + "Progressive Dash": 2, + "Bonus Star": 0, # Filler Item + # default: 1 +} + +item_name_to_config_name = { + "Wood" : "DisableWood" , + "Coal Bucket" : "DisableCoal" , + "Spare Plate" : "DisableOnePlate" , + "Fire Extinguisher" : "DisableFireExtinguisher" , + "Bellows" : "DisableBellows" , + "Clean Dishes" : "PlatesStartDirty" , + "Control Stick Batteries" : "DisableControlStick" , + "Wok Wheels" : "DisableWokDrag" , + "Dish Scrubber" : "WashTimeMultiplier" , + "Burn Leniency" : "BurnSpeedMultiplier" , + "Sharp Knife" : "ChoppingTimeScale" , + "Lightweight Backpack" : "BackpackMovementScale" , + "Faster Respawn Time" : "RespawnTime" , + "Faster Condiment/Drink Switch": "CarnivalDispenserRefactoryTime", + "Guest Patience" : "CustomOrderLifetime" , + "Ramp Button" : "DisableRampButton" , + "Calmer Unbread" : "AggressiveHorde" , + "Coin Purse" : "DisableEarnHordeMoney" , +} + +vanilla_values = { + "DisableWood": False, + "DisableCoal": False, + "DisableOnePlate": False, + "DisableFireExtinguisher": False, + "DisableBellows": False, + "PlatesStartDirty": False, + "DisableControlStick": False, + "DisableWokDrag": False, + "DisableRampButton": False, + "WashTimeMultiplier": 1.0, + "BurnSpeedMultiplier": 1.0, + "ChoppingTimeScale": 1.0, + "BackpackMovementScale": 1.0, + "RespawnTime": 5.0, + "CarnivalDispenserRefactoryTime": 0.0, + "CustomOrderLifetime": 100.0, + "AggressiveHorde": False, + "DisableEarnHordeMoney": False, +} + +item_id_to_name: Dict[int, str] = { + data.code: item_name for item_name, data in item_table.items() if data.code +} + +item_name_to_id: Dict[str, int] = { + item_name: data.code for item_name, data in item_table.items() if data.code +} + + +def is_progression(item_name: str) -> bool: + return not item_name.endswith("Emote") + + +def item_to_unlock_event(item_name: str) -> Dict[str, str]: + message = f"{item_name} Acquired!" + action = "" + payload = "" + if item_name.startswith("Kevin"): + kevin_num = int(item_name.split("-")[-1]) + action = "UNLOCK_LEVEL" + payload = str(kevin_num + 36) + elif "Emote" in item_name: + action = "UNLOCK_EMOTE" + payload = str(item_table[item_name].code - item_table["Cooking Emote"].code) + elif item_name == "Larger Tip Jar": + action = "INC_TIP_COMBO" + elif item_name == "Order Lookahead": + action = "INC_ORDERS_ON_SCREEN" + elif item_name == "Bonus Star": + action = "INC_STAR_COUNT" + payload = "1" + elif item_name == "Progressive Dash": + action = "INC_DASH" + elif item_name == "Progressive Throw/Catch": + action = "INC_THROW" + else: + config_name = item_name_to_config_name[item_name] + vanilla_value = vanilla_values[config_name] + + action = "SET_VALUE" + payload = f"{config_name}={vanilla_value}" + + return { + "message": message, + "action": action, + "payload": payload, + } diff --git a/worlds/overcooked2/Locations.py b/worlds/overcooked2/Locations.py new file mode 100644 index 0000000000..1b73b74e94 --- /dev/null +++ b/worlds/overcooked2/Locations.py @@ -0,0 +1,15 @@ +from BaseClasses import Location +from .Overcooked2Levels import Overcooked2Level + + +class Overcooked2Location(Location): + game: str = "Overcooked! 2" + + +oc2_location_name_to_id = dict() +oc2_location_id_to_name = dict() +for level in Overcooked2Level(): + if level.level_id == 36: + continue # level 6-6 does not have an item location + oc2_location_name_to_id[level.location_name_item] = level.level_id + oc2_location_id_to_name[level.level_id] = level.location_name_item diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py new file mode 100644 index 0000000000..6fb1a50a41 --- /dev/null +++ b/worlds/overcooked2/Logic.py @@ -0,0 +1,3899 @@ +from BaseClasses import CollectionState +from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level +from typing import Dict +from random import Random + + +def has_requirements_for_level_access(state: CollectionState, level_name: str, previous_level_completed_event_name: str, + required_star_count: int, player: int) -> bool: + # Check if the ramps in the overworld are set correctly + if level_name in ramp_logic: + if not state.has("Ramp Button", player): + return False # need the item to use ramps + + for req in ramp_logic[level_name]: + if not state.has(req + " Level Complete", player): + return False # This level needs another to be beaten first + + # Kevin Levels Need to have the corresponding items + if level_name.startswith("K"): + return state.has(level_name, player) + + # Must have enough stars to purchase level + star_count = state.item_count("Star", player) + state.item_count("Bonus Star", player) + if star_count < required_star_count: + return False + + # If this isn't the first level in a world, it needs the previous level to be unlocked first + if previous_level_completed_event_name is not None: + if not state.has(previous_level_completed_event_name, player): + return False + + # If we made it this far we have all requirements + return True + + +def has_requirements_for_level_star( + state: CollectionState, level: Overcooked2GenericLevel, stars: int, player: int) -> bool: + assert 0 <= stars <= 3 + + # First ensure that previous stars are obtainable + if stars > 1: + if not has_requirements_for_level_star(state, level, stars-1, player): + return False + + # Second, ensure that global requirements are met + if not meets_requirements(state, "*", stars, player): + return False + + # Finally, return success only if this level's requirements are met + return meets_requirements(state, level.shortname, stars, player) + + +def meets_requirements(state: CollectionState, name: str, stars: int, player: int): + # Get requirements for level + (exclusive_reqs, additive_reqs) = level_logic[name][stars-1] + + # print(f"{name} ({stars}-Stars): {exclusive_reqs}|{additive_reqs}") + + # Check if we meet exclusive requirements + if len(exclusive_reqs) > 0 and not state.has_all(exclusive_reqs, player): + return False + + # Check if we meet additive requirements + if len(additive_reqs) == 0: + return True + + total: float = 0.0 + for (item_name, weight) in additive_reqs: + for _ in range(0, state.item_count(item_name, player)): + total += weight + if total >= 0.99: # be nice to rounding errors :) + return True + + return False + + +def is_item_progression(item_name, level_mapping, include_kevin): + if item_name.endswith("Emote"): + return False + + if "Kevin" in item_name or item_name in ["Ramp Button"]: + return True # always progression + + def item_in_logic(shortname, _item_name): + for star in range(0, 3): + (exclusive, additive) = level_logic[shortname][star] + + if _item_name in exclusive: + return True + + for req in additive: + if req[0] == _item_name: + if req[1] > 0.3: # this bit smells of a deal with the devil, but it seems to be for the better + return True + break + + return False + + if item_in_logic("*", item_name): + return True + + for level in Overcooked2Level(): + if not include_kevin and level.level_id > 36: + break + + if level_mapping is None: + unmapped_level = Overcooked2GenericLevel(level.level_id) + else: + unmapped_level = level_mapping[level.level_id] + + if item_in_logic(unmapped_level.shortname, item_name): + return True + + return False + + +def is_useful(item_name): + return item_name in [ + "Faster Respawn Time", + "Fire Extinguisher", + "Clean Dishes", + "Larger Tip Jar", + "Dish Scrubber", + "Burn Leniency", + "Sharp Knife", + "Order Lookahead", + "Guest Patience", + "Bonus Star", + ] + + +def level_shuffle_factory( + rng: Random, + shuffle_prep_levels: bool, + shuffle_horde_levels: bool, +) -> Dict[int, Overcooked2GenericLevel]: # return + # Create a list of all valid levels for selection + # (excludes tutorial, throne, kevin and sometimes horde levels) + pool = list() + for dlc in Overcooked2Dlc: + for level_id in range(dlc.start_level_id(), dlc.end_level_id()): + if level_id in dlc.excluded_levels(): + continue + + if not shuffle_horde_levels and level_id in dlc.horde_levels(): + continue + + if not shuffle_prep_levels and level_id in dlc.prep_levels(): + continue + + pool.append( + Overcooked2GenericLevel(level_id, dlc) + ) + + # Sort the pool to eliminate risk + pool.sort(key=lambda x: int(x.dlc)*1000 + x.level_id) + + result: Dict[int, Overcooked2GenericLevel] = dict() + story = Overcooked2Dlc.STORY + + while len(result) == 0 or not meets_minimum_sphere_one_requirements(result): + result.clear() + + # Shuffle the pool, using the provided RNG + rng.shuffle(pool) + + # Return the first 44 levels and assign those to each level + for level_id in range(story.start_level_id(), story.end_level_id()): + if level_id not in story.excluded_levels(): + result[level_id] = pool[level_id-1] + else: + result[level_id] = Overcooked2GenericLevel(level_id) # This is just 6-6 right now + + return result + + +def meets_minimum_sphere_one_requirements( + levels: Dict[int, Overcooked2GenericLevel], +) -> bool: + + # 1-1, 2-1, and 4-1 are garunteed to be accessible on + # the overworld without requiring a ramp or additional stars + sphere_one = [1, 7, 19] + + # 1-2, 2-2, 3-1 and 5-1 are almost always the next thing unlocked + sphere_twoish = [2, 8, 13, 25] + + # Peek the logic for sphere one and see how many are possible + # with no items + sphere_one_count = 0 + for level_id in sphere_one: + if (is_completable_no_items(levels[level_id])): + sphere_one_count += 1 + + sphere_twoish_count = 0 + for level_id in sphere_twoish: + if (is_completable_no_items(levels[level_id])): + sphere_twoish_count += 1 + + return sphere_one_count >= 2 and \ + sphere_twoish_count >= 2 and \ + sphere_one_count + sphere_twoish_count >= 6 + + +def is_completable_no_items(level: Overcooked2GenericLevel) -> bool: + one_star_logic = level_logic[level.shortname][0] + (exclusive, additive) = one_star_logic + + # print(f"\n{level.shortname}: {exclusive} / {additive}") + + return len(exclusive) == 0 and len(additive) == 0 + + +# If key missing, doesn't require a ramp to access (or the logic is handled by a preceeding level) +# +# If empty, a ramp is required to access, but the ramp button is garunteed accessible +# +# If populated, a ramp is required to access and the button requires all levels in the +# list to be compelted before it can be pressed +# +ramp_logic = { + "1-5": [], + "2-2": [], + "3-1": [], + "5-2": [], + "6-1": [], + "6-2": ["5-1"], # 5-1 spawns blue button, blue button gets you to red button + "Kevin-1": [], + "Kevin-7": ["5-1"], # 5-1 spawns blue button, + # press blue button, + # climb blue ramp, + # jump the gap, + # climb wood ramps + "Kevin-8": ["5-1", "6-2"], # Same as above, but 6-2 spawns the ramp to K8 +} + +horde_logic = { # Additive + ("Coin Purse", 0.7), + ("Calmer Unbread", 0.35), + ("Progressive Dash", 0.2), + ("Progressive Throw/Catch", 0.15), + ("Sharp Knife", 0.15), + ("Dish Scrubber", 0.125), + ("Burn Leniency", 0.1), + ("Spare Plate", 0.075), + ("Clean Dishes", 0.025), +} + +# Level 1 - dict keyed by friendly level names +# Level 2 - tuple with 3 elements, one for each star requirement +# Level 3 - tuple with 2 elements, one for exclusive requirements and one for additive requirements +# Level 4 (exclusive) - set of item name strings of items which MUST be in the inventory to allow logical completion +# Level 4 (additive) - list of tuples containing item name and item weight where the sum of which are in the player's inventory +# must be 1.0+ to allow logical completion +# +# Each Star's logical requirements imply any previous requirements +# +level_logic = { + # "Tutorial": [], + "*": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 0.4), + ("Progressive Dash", 0.35), + ("Sharp Knife", 0.3), + ("Dish Scrubber", 0.25), + ("Larger Tip Jar", 0.2), + ("Spare Plate", 0.2), + ("Burn Leniency", 0.15), + ("Order Lookahead", 0.15), + ("Clean Dishes", 0.1), + ("Guest Patience", 0.1), + }, + ), + ( # 3-star + [ # Exclusive + "Progressive Dash", + "Spare Plate", + "Larger Tip Jar", + "Progressive Throw/Catch", + ], + { # Additive + }, + ) + ), + "Story 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 1-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-1": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-3": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 1.0), + ("Progressive Dash", 1.0), + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Fire Extinguisher", + }, + [ # Additive + + ] + ) + ), + "Story 2-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 2-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 1.0), + ("Progressive Dash", 0.5), + ("Sharp Knife", 0.5), + ("Larger Tip Jar", 0.25), + ("Dish Scrubber", 0.25), + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 3-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 4-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-1": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-5": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 5-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ) + ), + "Story 6-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-4": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story 6-6": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + "Progressive Dash", + "Spare Plate", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-4": ( + ( # 1-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-7": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Story K-8": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-3": ( + ( # 1-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-2": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf 3-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Bellows", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Surf K-1": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-1": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-2": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 2-3": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 2-4": ( + ( # 1-star + { # Exclusive + "Wood" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Lightweight Backpack" + }, + { # Additive + + }, + ) + ), + "Campfire 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-3": ( + ( # 1-star + { # Exclusive + "Wood", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Lightweight Backpack", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire 3-4": ( + ( # 1-star + { # Exclusive + "Wood", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Campfire K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 2-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 2-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival 3-4": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "Carnival K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival K-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Carnival K-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + } + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 2-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde 3-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + ("Progressive Throw/Catch", 0.5), + ("Progressive Dash", 0.5), + ("Coal Bucket", 0.5), + }, + ), + ( # 2-star + { # Exclusive + "Progressive Throw/Catch", + "Coal Bucket", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-2": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde K-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-1": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-3": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-4": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-5": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-6": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-7": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Horde H-8": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Christmas 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-6": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Chinese 1-7": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter H-2": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter H-4": ( + ( # 1-star + { # Exclusive + + }, + horde_logic, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Winter 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Wok Wheels" + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Spring 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "SOBO 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-3": ( + ( # 1-star + { # Exclusive + "Control Stick Batteries" + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "SOBO 1-4": ( + ( # 1-star + { # Exclusive + "Fire Extinguisher", + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + "Faster Condiment/Drink Switch" + }, + { # Additive + + }, + ) + ), + "SOBO 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + "Fire Extinguisher", + "Faster Condiment/Drink Switch", + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + }, + { # Additive + + }, + ) + ), + "Moon 1-1": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-2": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-3": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-4": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), + "Moon 1-5": ( + ( # 1-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 2-star + { # Exclusive + + }, + { # Additive + + }, + ), + ( # 3-star + { # Exclusive + + }, + { # Additive + + }, + ) + ), +} diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py new file mode 100644 index 0000000000..78e0fd6e90 --- /dev/null +++ b/worlds/overcooked2/Options.py @@ -0,0 +1,110 @@ +from typing import TypedDict +from Options import DefaultOnToggle, Range, Choice + + +class OC2OnToggle(DefaultOnToggle): + @property + def result(self) -> bool: + return bool(self.value) + + +class AlwaysServeOldestOrder(OC2OnToggle): + """Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps players dig out of a broken tip combo faster.""" + display_name = "Always Serve Oldest Order" + + +class AlwaysPreserveCookingProgress(OC2OnToggle): + """Modifies the game to behave more like AYCE, where adding an item to an in-progress container doesn't reset the entire progress bar.""" + display_name = "Preserve Cooking/Mixing Progress" + + +class DisplayLeaderboardScores(OC2OnToggle): + """Modifies the Overworld map to fetch and display the current world records for each level. Press number keys 1-4 to view leaderboard scores for that number of players.""" + display_name = "Display Leaderboard Scores" + + +class ShuffleLevelOrder(OC2OnToggle): + """Shuffles the order of kitchens on the overworld map. Also draws from DLC maps.""" + display_name = "Shuffle Level Order" + + +class IncludeHordeLevels(OC2OnToggle): + """Includes "Horde Defence" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool.""" + display_name = "Include Horde Levels" + + +class KevinLevels(OC2OnToggle): + """Includes the 8 Kevin level locations on the map as unlockables. Turn off to make games shorter.""" + display_name = "Kevin Level Checks" + + +class FixBugs(OC2OnToggle): + """Fixes Bugs Present in the base game: + - Double Serving Exploit + - Sink Bug + - Control Stick Cancel/Throw Bug + - Can't Throw Near Empty Burner Bug""" + display_name = "Fix Bugs" + + +class ShorterLevelDuration(OC2OnToggle): + """Modifies level duration to be about 1/3rd shorter than in the original game, thus bringing the item discovery pace in line with other popular Archipelago games. + + Points required to earn stars are scaled accordingly. ("Boss Levels" which change scenery mid-game are not affected.)""" + display_name = "Shorter Level Duration" + + +class PrepLevels(Choice): + """Choose How "Prep Levels" are handled (levels where the timer does not start until the first order is served): + + - Original: Prep Levels may appear + + - Excluded: Prep Levels are excluded from the pool during level shuffling + + - All You Can Eat: Prep Levels may appear, but the timer automatically starts. The star score requirements are also adjusted to use the All You Can Eat World Record (if it exists)""" + auto_display_name = True + display_name = "Prep Level Behavior" + option_original = 0 + option_excluded = 1 + option_all_you_can_eat = 2 + default = 1 + + +class StarsToWin(Range): + """Number of stars required to unlock 6-6. + + Level purchase requirements between 1-1 and 6-6 will be spread between these two numbers. Using too high of a number may result in more frequent generation failures, especially when horde levels are enabled.""" + display_name = "Stars to Win" + range_start = 0 + range_end = 100 + default = 66 + + +class StarThresholdScale(Range): + """How difficult should the third star for each level be on a scale of 1-100%, where 100% is the current world record score and 45% is the average vanilla 4-star score.""" + display_name = "Star Difficulty %" + range_start = 1 + range_end = 100 + default = 45 + + +overcooked_options = { + # randomization options + "shuffle_level_order": ShuffleLevelOrder, + "include_horde_levels": IncludeHordeLevels, + "prep_levels": PrepLevels, + "kevin_levels": KevinLevels, + + # quality of life options + "fix_bugs": FixBugs, + "shorter_level_duration": ShorterLevelDuration, + "always_preserve_cooking_progress": AlwaysPreserveCookingProgress, + "always_serve_oldest_order": AlwaysServeOldestOrder, + "display_leaderboard_scores": DisplayLeaderboardScores, + + # difficulty settings + "stars_to_win": StarsToWin, + "star_threshold_scale": StarThresholdScale, +} + +OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()}) diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py new file mode 100644 index 0000000000..aac9ea0cbe --- /dev/null +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -0,0 +1,349 @@ +from enum import Enum +from typing import List + + +class Overcooked2Dlc(Enum): + STORY = "Story" + SURF_N_TURF = "Surf 'n' Turf" + CAMPFIRE_COOK_OFF = "Campfire Cook Off" + NIGHT_OF_THE_HANGRY_HORDE = "Night of the Hangry Horde" + CARNIVAL_OF_CHAOS = "Carnival of Chaos" + SEASONAL = "Seasonal" + # CHRISTMAS = "Christmas" + # CHINESE_NEW_YEAR = "Chinese New Year" + # WINTER_WONDERLAND = "Winter Wonderland" + # MOON_HARVEST = "Moon Harvest" + # SPRING_FRESTIVAL = "Spring Festival" + # SUNS_OUT_BUNS_OUT = "Sun's Out Buns Out" + + def __int__(self) -> int: + if self == Overcooked2Dlc.STORY: + return 0 + if self == Overcooked2Dlc.SURF_N_TURF: + return 1 + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + return 2 + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return 3 + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + return 4 + if self == Overcooked2Dlc.SEASONAL: + return 5 + assert False + + # inclusive + def start_level_id(self) -> int: + if self == Overcooked2Dlc.STORY: + return 1 + return 0 + + # exclusive + def end_level_id(self) -> int: + id = None + if self == Overcooked2Dlc.STORY: + id = 6*6 + 8 # world_count*level_count + kevin count + if self == Overcooked2Dlc.SURF_N_TURF: + id = 3*4 + 1 + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + id = 3*4 + 3 + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + id = 3*3 + 3 + 8 + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + id = 3*4 + 3 + if self == Overcooked2Dlc.SEASONAL: + id = 31 + + return self.start_level_id() + id + + # Tutorial + Horde Levels + Endgame + def excluded_levels(self) -> List[int]: + if self == Overcooked2Dlc.STORY: + return [0, 36] + + return [] + + def horde_levels(self) -> List[int]: + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return [12, 13, 14, 15, 16, 17, 18, 19] + if self == Overcooked2Dlc.SEASONAL: + return [13, 15] + + return [] + + def prep_levels(self) -> List[int]: + if self == Overcooked2Dlc.STORY: + return [1, 2, 5, 10, 12, 13, 28, 31] + if self == Overcooked2Dlc.SURF_N_TURF: + return [0, 4] + if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: + return [0, 2, 4, 9] + if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: + return [0, 1, 4] + if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: + return [0, 1, 3, 4, 5] + if self == Overcooked2Dlc.SEASONAL: + # moon 1-1 is a prep level for 1P only, but we can't make that assumption here + return [0, 1, 5, 6, 12, 14, 16, 17, 18, 22, 23, 24, 27, 29] + + return [] + + +class Overcooked2GameWorld(Enum): + ONE = 1 + TWO = 2 + THREE = 3 + FOUR = 4 + FIVE = 5 + SIX = 6 + KEVIN = 7 + + @property + def as_str(self) -> str: + if self == Overcooked2GameWorld.KEVIN: + return "Kevin" + + return str(int(self.value)) + + @property + def sublevel_count(self) -> int: + if self == Overcooked2GameWorld.KEVIN: + return 8 + + return 6 + + @property + def base_id(self) -> int: + if self == Overcooked2GameWorld.ONE: + return 1 + + prev = Overcooked2GameWorld(self.value - 1) + return prev.base_id + prev.sublevel_count + + @property + def name(self) -> str: + if self == Overcooked2GameWorld.KEVIN: + return "Kevin" + + return "World " + self.as_str + + +class Overcooked2GenericLevel(): + dlc: Overcooked2Dlc + level_id: int + + def __init__(self, level_id: int, dlc: Overcooked2Dlc = Overcooked2Dlc("Story")): + self.dlc = dlc + self.level_id = level_id + + def __str__(self) -> str: + return f"{self.dlc.value}|{self.level_id}" + + def __repr__(self) -> str: + return f"{self}" + + @property + def shortname(self) -> str: + return level_id_to_shortname[(self.dlc, self.level_id)] + + @property + def is_horde(self) -> bool: + return self.level_id in self.dlc.horde_levels() + + +class Overcooked2Level: + """ + Abstraction for a playable levels in Overcooked 2. By default constructor + it can be used as an iterator for all locations in the Story map. + """ + world: Overcooked2GameWorld + sublevel: int + + def __init__(self): + self.world = Overcooked2GameWorld.ONE + self.sublevel = 0 + + def __iter__(self): + return self + + def __next__(self): + self.sublevel += 1 + if self.sublevel > self.world.sublevel_count: + if self.world == Overcooked2GameWorld.KEVIN: + raise StopIteration + self.world = Overcooked2GameWorld(self.world.value + 1) + self.sublevel = 1 + + return self + + @property + def level_id(self) -> int: + return self.world.base_id + (self.sublevel - 1) + + @property + def level_name(self) -> str: + return self.world.as_str + "-" + str(self.sublevel) + + @property + def location_name_item(self) -> str: + return self.level_name + " Completed" + + @property + def location_name_level_complete(self) -> str: + return self.level_name + " Level Completed" + + @property + def event_name_level_complete(self) -> str: + return self.level_name + " Level Complete" + + def location_name_star_event(self, stars: int) -> str: + return "%s (%d-Star)" % (self.level_name, stars) + + @property + def as_generic_level(self) -> Overcooked2GenericLevel: + return Overcooked2GenericLevel(self.level_id) + + +# Note that there are valid levels beyond what is listed here, but they are all +# Onion King Dialogs +level_id_to_shortname = { + (Overcooked2Dlc.STORY , 0 ): "Tutorial" , + (Overcooked2Dlc.STORY , 1 ): "Story 1-1" , + (Overcooked2Dlc.STORY , 2 ): "Story 1-2" , + (Overcooked2Dlc.STORY , 3 ): "Story 1-3" , + (Overcooked2Dlc.STORY , 4 ): "Story 1-4" , + (Overcooked2Dlc.STORY , 5 ): "Story 1-5" , + (Overcooked2Dlc.STORY , 6 ): "Story 1-6" , + (Overcooked2Dlc.STORY , 7 ): "Story 2-1" , + (Overcooked2Dlc.STORY , 8 ): "Story 2-2" , + (Overcooked2Dlc.STORY , 9 ): "Story 2-3" , + (Overcooked2Dlc.STORY , 10 ): "Story 2-4" , + (Overcooked2Dlc.STORY , 11 ): "Story 2-5" , + (Overcooked2Dlc.STORY , 12 ): "Story 2-6" , + (Overcooked2Dlc.STORY , 13 ): "Story 3-1" , + (Overcooked2Dlc.STORY , 14 ): "Story 3-2" , + (Overcooked2Dlc.STORY , 15 ): "Story 3-3" , + (Overcooked2Dlc.STORY , 16 ): "Story 3-4" , + (Overcooked2Dlc.STORY , 17 ): "Story 3-5" , + (Overcooked2Dlc.STORY , 18 ): "Story 3-6" , + (Overcooked2Dlc.STORY , 19 ): "Story 4-1" , + (Overcooked2Dlc.STORY , 20 ): "Story 4-2" , + (Overcooked2Dlc.STORY , 21 ): "Story 4-3" , + (Overcooked2Dlc.STORY , 22 ): "Story 4-4" , + (Overcooked2Dlc.STORY , 23 ): "Story 4-5" , + (Overcooked2Dlc.STORY , 24 ): "Story 4-6" , + (Overcooked2Dlc.STORY , 25 ): "Story 5-1" , + (Overcooked2Dlc.STORY , 26 ): "Story 5-2" , + (Overcooked2Dlc.STORY , 27 ): "Story 5-3" , + (Overcooked2Dlc.STORY , 28 ): "Story 5-4" , + (Overcooked2Dlc.STORY , 29 ): "Story 5-5" , + (Overcooked2Dlc.STORY , 30 ): "Story 5-6" , + (Overcooked2Dlc.STORY , 31 ): "Story 6-1" , + (Overcooked2Dlc.STORY , 32 ): "Story 6-2" , + (Overcooked2Dlc.STORY , 33 ): "Story 6-3" , + (Overcooked2Dlc.STORY , 34 ): "Story 6-4" , + (Overcooked2Dlc.STORY , 35 ): "Story 6-5" , + (Overcooked2Dlc.STORY , 36 ): "Story 6-6" , + (Overcooked2Dlc.STORY , 37 ): "Story K-1" , + (Overcooked2Dlc.STORY , 38 ): "Story K-2" , + (Overcooked2Dlc.STORY , 39 ): "Story K-3" , + (Overcooked2Dlc.STORY , 40 ): "Story K-4" , + (Overcooked2Dlc.STORY , 41 ): "Story K-5" , + (Overcooked2Dlc.STORY , 42 ): "Story K-6" , + (Overcooked2Dlc.STORY , 43 ): "Story K-7" , + (Overcooked2Dlc.STORY , 44 ): "Story K-8" , + (Overcooked2Dlc.SURF_N_TURF , 0 ): "Surf 1-1" , + (Overcooked2Dlc.SURF_N_TURF , 1 ): "Surf 1-2" , + (Overcooked2Dlc.SURF_N_TURF , 2 ): "Surf 1-3" , + (Overcooked2Dlc.SURF_N_TURF , 3 ): "Surf 1-4" , + (Overcooked2Dlc.SURF_N_TURF , 4 ): "Surf 2-1" , + (Overcooked2Dlc.SURF_N_TURF , 5 ): "Surf 2-2" , + (Overcooked2Dlc.SURF_N_TURF , 6 ): "Surf 2-3" , + (Overcooked2Dlc.SURF_N_TURF , 7 ): "Surf 2-4" , + (Overcooked2Dlc.SURF_N_TURF , 8 ): "Surf 3-1" , + (Overcooked2Dlc.SURF_N_TURF , 9 ): "Surf 3-2" , + (Overcooked2Dlc.SURF_N_TURF , 10 ): "Surf 3-3" , + (Overcooked2Dlc.SURF_N_TURF , 11 ): "Surf 3-4" , + (Overcooked2Dlc.SURF_N_TURF , 12 ): "Surf K-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 0 ): "Campfire 1-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 1 ): "Campfire 1-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 2 ): "Campfire 1-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 3 ): "Campfire 1-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 4 ): "Campfire 2-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 5 ): "Campfire 2-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 6 ): "Campfire 2-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 7 ): "Campfire 2-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 8 ): "Campfire 3-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 9 ): "Campfire 3-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 10 ): "Campfire 3-3" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 11 ): "Campfire 3-4" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 12 ): "Campfire K-1" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 13 ): "Campfire K-2" , + (Overcooked2Dlc.CAMPFIRE_COOK_OFF , 14 ): "Campfire K-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 0 ): "Carnival 1-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 1 ): "Carnival 1-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 2 ): "Carnival 1-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 3 ): "Carnival 1-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 4 ): "Carnival 2-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 5 ): "Carnival 2-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 6 ): "Carnival 2-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 7 ): "Carnival 2-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 8 ): "Carnival 3-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 9 ): "Carnival 3-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 10 ): "Carnival 3-3" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 11 ): "Carnival 3-4" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 12 ): "Carnival K-1" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 13 ): "Carnival K-2" , + (Overcooked2Dlc.CARNIVAL_OF_CHAOS , 14 ): "Carnival K-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 0 ): "Horde 1-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 1 ): "Horde 1-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 2 ): "Horde 1-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 3 ): "Horde 2-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 4 ): "Horde 2-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 5 ): "Horde 2-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 6 ): "Horde 3-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 7 ): "Horde 3-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 8 ): "Horde 3-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 9 ): "Horde K-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 10 ): "Horde K-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 11 ): "Horde K-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 12 ): "Horde H-1" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 13 ): "Horde H-2" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 14 ): "Horde H-3" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 15 ): "Horde H-4" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 16 ): "Horde H-5" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 17 ): "Horde H-6" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 18 ): "Horde H-7" , + (Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 19 ): "Horde H-8" , + (Overcooked2Dlc.SEASONAL , 0 ): "Christmas 1-1" , + (Overcooked2Dlc.SEASONAL , 1 ): "Christmas 1-2" , + (Overcooked2Dlc.SEASONAL , 2 ): "Christmas 1-3" , + (Overcooked2Dlc.SEASONAL , 3 ): "Christmas 1-4" , + (Overcooked2Dlc.SEASONAL , 4 ): "Christmas 1-5" , + (Overcooked2Dlc.SEASONAL , 5 ): "Chinese 1-1" , + (Overcooked2Dlc.SEASONAL , 6 ): "Chinese 1-2" , + (Overcooked2Dlc.SEASONAL , 7 ): "Chinese 1-3" , + (Overcooked2Dlc.SEASONAL , 8 ): "Chinese 1-4" , + (Overcooked2Dlc.SEASONAL , 9 ): "Chinese 1-5" , + (Overcooked2Dlc.SEASONAL , 10 ): "Chinese 1-6" , + (Overcooked2Dlc.SEASONAL , 11 ): "Chinese 1-7" , + (Overcooked2Dlc.SEASONAL , 12 ): "Winter 1-1" , + (Overcooked2Dlc.SEASONAL , 13 ): "Winter H-2" , + (Overcooked2Dlc.SEASONAL , 14 ): "Winter 1-3" , + (Overcooked2Dlc.SEASONAL , 15 ): "Winter H-4" , + (Overcooked2Dlc.SEASONAL , 16 ): "Winter 1-5" , + (Overcooked2Dlc.SEASONAL , 17 ): "Spring 1-1" , + (Overcooked2Dlc.SEASONAL , 18 ): "Spring 1-2" , + (Overcooked2Dlc.SEASONAL , 19 ): "Spring 1-3" , + (Overcooked2Dlc.SEASONAL , 20 ): "Spring 1-4" , + (Overcooked2Dlc.SEASONAL , 21 ): "Spring 1-5" , + (Overcooked2Dlc.SEASONAL , 22 ): "SOBO 1-1" , + (Overcooked2Dlc.SEASONAL , 23 ): "SOBO 1-2" , + (Overcooked2Dlc.SEASONAL , 24 ): "SOBO 1-3" , + (Overcooked2Dlc.SEASONAL , 25 ): "SOBO 1-4" , + (Overcooked2Dlc.SEASONAL , 26 ): "SOBO 1-5" , + (Overcooked2Dlc.SEASONAL , 27 ): "Moon 1-1" , + (Overcooked2Dlc.SEASONAL , 28 ): "Moon 1-2" , + (Overcooked2Dlc.SEASONAL , 29 ): "Moon 1-3" , + (Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" , + (Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" , +} diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py new file mode 100644 index 0000000000..c47b755fbf --- /dev/null +++ b/worlds/overcooked2/__init__.py @@ -0,0 +1,510 @@ +from enum import Enum +from typing import Callable, Dict, Any, List, Optional + +from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, RegionType, Tutorial +from worlds.AutoWorld import World, WebWorld + +from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel +from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name +from .Options import overcooked_options, OC2Options, OC2OnToggle +from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies +from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful + + +class Overcooked2Web(WebWorld): + theme = "partyTime" + + bug_report_page = "https://github.com/toasterparty/oc2-modding/issues" + setup_en = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up the Overcooked! 2 randomizer on your computer.", + "English", + "setup_en.md", + "setup/en", + ["toasterparty"] + ) + + tutorials = [setup_en] + + +class PrepLevelMode(Enum): + original = 0 + excluded = 1 + ayce = 2 + + +class Overcooked2World(World): + """ + Overcooked! 2 is a franticly paced arcade cooking game where + players race against the clock to complete orders for points. Bring + peace to the Onion Kingdom once again by recovering lost items and abilities, + earning stars to unlock levels, and defeating the unbread horde. Levels are + randomized to increase gameplay variety. Play with up to 4 friends. + """ + + # Autoworld API + + game = "Overcooked! 2" + web = Overcooked2Web() + required_client_version = (0, 3, 4) + option_definitions = overcooked_options + topology_present: bool = False + remote_items: bool = True + remote_start_inventory: bool = False + data_version = 2 + + item_name_to_id = item_name_to_id + item_id_to_name = item_id_to_name + + location_id_to_name = oc2_location_id_to_name + location_name_to_id = oc2_location_name_to_id + + options: Dict[str, Any] + itempool: List[Overcooked2Item] + + + # Helper Functions + + def is_level_horde(self, level_id: int) -> bool: + return self.options["IncludeHordeLevels"] and \ + (self.level_mapping is not None) and \ + level_id in self.level_mapping.keys() and \ + self.level_mapping[level_id].is_horde + + def create_item(self, item: str, classification: ItemClassification = ItemClassification.progression) -> Overcooked2Item: + return Overcooked2Item(item, classification, self.item_name_to_id[item], self.player) + + def create_event(self, event: str, classification: ItemClassification) -> Overcooked2Item: + return Overcooked2Item(event, classification, None, self.player) + + def place_event(self, location_name: str, item_name: str, + classification: ItemClassification = ItemClassification.progression_skip_balancing): + location: Location = self.world.get_location(location_name, self.player) + location.place_locked_item(self.create_event(item_name, classification)) + + def add_region(self, region_name: str): + region = Region( + region_name, + RegionType.Generic, + region_name, + self.player, + self.world, + ) + self.world.regions.append(region) + + def connect_regions(self, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None): + sourceRegion = self.world.get_region(source, self.player) + targetRegion = self.world.get_region(target, self.player) + + connection = Entrance(self.player, '', sourceRegion) + if rule: + connection.access_rule = rule + + sourceRegion.exits.append(connection) + connection.connect(targetRegion) + + def add_level_location( + self, + region_name: str, + location_name: str, + level_id: int, + stars: int, + is_event: bool = False, + ) -> None: + + if is_event: + location_id = None + else: + location_id = level_id + + region = self.world.get_region(region_name, self.player) + location = Overcooked2Location( + self.player, + location_name, + location_id, + region, + ) + + location.event = is_event + + # if level_id is none, then it's the 6-6 edge case + if level_id is None: + level_id = 36 + if self.level_mapping is not None and level_id in self.level_mapping: + level = self.level_mapping[level_id] + else: + level = Overcooked2GenericLevel(level_id) + + completion_condition: Callable[[CollectionState], bool] = \ + lambda state, level=level, stars=stars: \ + has_requirements_for_level_star(state, level, stars, self.player) + location.access_rule = completion_condition + + region.locations.append( + location + ) + + def get_options(self) -> Dict[str, Any]: + return OC2Options({option.__name__: getattr(self.world, name)[self.player].result + if issubclass(option, OC2OnToggle) else getattr(self.world, name)[self.player].value + for name, option in overcooked_options.items()}) + + # Helper Data + + level_unlock_counts: Dict[int, int] # level_id, stars to purchase + level_mapping: Dict[int, Overcooked2GenericLevel] # level_id, level + + # Autoworld Hooks + + def generate_early(self): + self.options = self.get_options() + + # 0.0 to 1.0 where 1.0 is World Record + self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0 + + # Generate level unlock requirements such that the levels get harder to unlock + # the further the game has progressed, and levels progress radially rather than linearly + self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"]) + + # Assign new kitchens to each spot on the overworld using pure random chance and nothing else + if self.options["ShuffleLevelOrder"]: + self.level_mapping = \ + level_shuffle_factory( + self.world.random, + self.options["PrepLevels"] != PrepLevelMode.excluded.value, + self.options["IncludeHordeLevels"], + ) + else: + self.level_mapping = None + + def create_regions(self) -> None: + # Menu -> Overworld + self.add_region("Menu") + self.add_region("Overworld") + self.connect_regions("Menu", "Overworld") + + for level in Overcooked2Level(): + if not self.options["KevinLevels"] and level.level_id > 36: + break + + # Create Region (e.g. "1-1") + self.add_region(level.level_name) + + # Add Location to house progression item (1-star) + if level.level_id == 36: + # 6-6 doesn't have progression, but it does have victory condition which is placed later + self.add_level_location( + level.level_name, + level.location_name_item, + None, + 1, + ) + else: + # Location to house progression item + self.add_level_location( + level.level_name, + level.location_name_item, + level.level_id, + 1, + ) + + # Location to house level completed event + self.add_level_location( + level.level_name, + level.location_name_level_complete, + level.level_id, + 1, + is_event=True, + ) + + # Add Locations to house star aquisition events, except for horde levels + if not self.is_level_horde(level.level_id): + for n in [1, 2, 3]: + self.add_level_location( + level.level_name, + level.location_name_star_event(n), + level.level_id, + n, + is_event=True, + ) + + # Overworld -> Level + required_star_count: int = self.level_unlock_counts[level.level_id] + if level.level_id % 6 != 1 and level.level_id <= 36: + previous_level_completed_event_name: str = Overcooked2GenericLevel( + level.level_id - 1).shortname.split(" ")[1] + " Level Complete" + else: + previous_level_completed_event_name = None + + level_access_rule: Callable[[CollectionState], bool] = \ + lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \ + has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.player) + self.connect_regions("Overworld", level.level_name, level_access_rule) + + # Level --> Overworld + self.connect_regions(level.level_name, "Overworld") + + completion_condition: Callable[[CollectionState], bool] = lambda state: \ + state.has("Victory", self.player) + self.world.completion_condition[self.player] = completion_condition + + def create_items(self): + self.itempool = [] + + # Make Items + # useful = list() + # filler = list() + # progression = list() + for item_name in item_table: + if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]: + # skip items which are irrelevant to the seed + continue + + if not self.options["KevinLevels"] and item_name.startswith("Kevin"): + continue + + if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]): + # print(f"{item_name} is progression") + # progression.append(item_name) + classification = ItemClassification.progression + else: + # print(f"{item_name} is filler") + if (is_useful(item_name)): + # useful.append(item_name) + classification = ItemClassification.useful + else: + # filler.append(item_name) + classification = ItemClassification.filler + + if item_name in item_frequencies: + freq = item_frequencies[item_name] + + while freq > 0: + self.itempool.append(self.create_item(item_name, classification)) + classification = ItemClassification.useful # only the first progressive item can be progression + freq -= 1 + else: + self.itempool.append(self.create_item(item_name, classification)) + + # print(f"progression: {progression}") + # print(f"useful: {useful}") + # print(f"filler: {filler}") + + # Fill any free space with filler + pool_count = len(oc2_location_name_to_id) + if not self.options["KevinLevels"]: + pool_count -= 8 + + while len(self.itempool) < pool_count: + self.itempool.append(self.create_item("Bonus Star", ItemClassification.useful)) + + self.world.itempool += self.itempool + + def set_rules(self): + pass + + def generate_basic(self) -> None: + # Add Events (Star Acquisition) + for level in Overcooked2Level(): + if not self.options["KevinLevels"] and level.level_id > 36: + break + + if level.level_id != 36: + self.place_event(level.location_name_level_complete, level.event_name_level_complete) + + if self.is_level_horde(level.level_id): + continue # horde levels don't have star rewards + + for n in [1, 2, 3]: + self.place_event(level.location_name_star_event(n), "Star") + + # Add Victory Condition + self.place_event("6-6 Completed", "Victory") + + # Items get distributed to locations + + def fill_json_data(self) -> Dict[str, Any]: + mod_name = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}" + + # Serialize Level Order + story_level_order = dict() + + if self.options["ShuffleLevelOrder"]: + for level_id in self.level_mapping: + level: Overcooked2GenericLevel = self.level_mapping[level_id] + story_level_order[str(level_id)] = { + "DLC": level.dlc.value, + "LevelID": level.level_id, + } + + custom_level_order = dict() + custom_level_order["Story"] = story_level_order + + # Serialize Unlock Requirements + level_purchase_requirements = dict() + for level_id in self.level_unlock_counts: + level_purchase_requirements[str(level_id)] = self.level_unlock_counts[level_id] + + # Override Vanilla Unlock Chain Behavior + # (all worlds accessible from the start and progressible in any order) + level_unlock_requirements = dict() + level_force_reveal = [ + 1, # 1-1 + 7, # 2-1 + 13, # 3-1 + 19, # 4-1 + 25, # 5-1 + 31, # 6-1 + ] + for level_id in range(1, 37): + if (level_id not in level_force_reveal): + level_unlock_requirements[str(level_id)] = level_id - 1 + + # Set Kevin Unlock Requirements + if self.options["KevinLevels"]: + def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]: + location = self.world.find_item(f"Kevin-{level_id-36}", self.player) + if location.player != self.player: + return None # This kevin level will be unlocked by the server at runtime + level_id = oc2_location_name_to_id[location.name] + return level_id + + for level_id in range(37, 45): + keyholder_level_id = kevin_level_to_keyholder_level_id(level_id) + if keyholder_level_id is not None: + level_unlock_requirements[str(level_id)] = keyholder_level_id + + # Place Items at Level Completion Screens (local only) + on_level_completed: Dict[str, list[Dict[str, str]]] = dict() + regions = self.world.get_regions(self.player) + for region in regions: + for location in region.locations: + if location.item is None: + continue + if location.item.code is None: + continue # it's an event + if location.item.player != self.player: + continue # not for us + level_id = str(oc2_location_name_to_id[location.name]) + on_level_completed[level_id] = [item_to_unlock_event(location.item.name)] + + # Put it all together + star_threshold_scale = self.options["StarThresholdScale"] / 100 + + base_data = { + # Changes Inherent to rando + "DisableAllMods": False, + "UnlockAllChefs": True, + "UnlockAllDLC": True, + "DisplayFPS": True, + "SkipTutorial": True, + "SkipAllOnionKing": True, + "SkipTutorialPopups": True, + "RevealAllLevels": False, + "PurchaseAllLevels": False, + "CheatsEnabled": False, + "ImpossibleTutorial": True, + "ForbidDLC": True, + "ForceSingleSaveSlot": True, + "DisableNGP": True, + "LevelForceReveal": level_force_reveal, + "SaveFolderName": mod_name, + "CustomOrderTimeoutPenalty": 10, + "LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44], + + # Game Modifications + "LevelPurchaseRequirements": level_purchase_requirements, + "Custom66TimerScale": max(0.4, (1.0 - star_threshold_scale)), + + "CustomLevelOrder": custom_level_order, + + # Items (Starting Inventory) + "CustomOrderLifetime": 70.0, # 100 is original + "DisableWood": True, + "DisableCoal": True, + "DisableOnePlate": True, + "DisableFireExtinguisher": True, + "DisableBellows": True, + "PlatesStartDirty": True, + "MaxTipCombo": 2, + "DisableDash": True, + "WeakDash": True, + "DisableThrow": True, + "DisableCatch": True, + "DisableControlStick": True, + "DisableWokDrag": True, + "DisableRampButton": True, + "WashTimeMultiplier": 1.4, + "BurnSpeedMultiplier": 1.43, + "MaxOrdersOnScreenOffset": -2, + "ChoppingTimeScale": 1.4, + "BackpackMovementScale": 0.75, + "RespawnTime": 10.0, + "CarnivalDispenserRefactoryTime": 4.0, + "LevelUnlockRequirements": level_unlock_requirements, + "LockedEmotes": [0, 1, 2, 3, 4, 5], + "StarOffset": 0, + "AggressiveHorde": True, + "DisableEarnHordeMoney": True, + + # Item Unlocking + "OnLevelCompleted": on_level_completed, + } + + # Set remaining data in the options dict + bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"] + for bug in bugs: + self.options[bug] = self.options["FixBugs"] + self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"] + self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce.value + self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0 + self.options["LeaderboardScoreScale"] = { + "FourStars": 1.0, + "ThreeStars": star_threshold_scale, + "TwoStars": star_threshold_scale * 0.75, + "OneStar": star_threshold_scale * 0.35, + } + + base_data.update(self.options) + return base_data + + def fill_slot_data(self) -> Dict[str, Any]: + return self.fill_json_data() + + +def level_unlock_requirement_factory(stars_to_win: int) -> Dict[int, int]: + level_unlock_counts = dict() + level = 1 + sublevel = 1 + for n in range(1, 37): + progress: float = float(n)/36.0 + progress *= progress # x^2 curve + + star_count = int(progress*float(stars_to_win)) + min = (n-1)*3 + if (star_count > min): + star_count = min + + level_id = (level-1)*6 + sublevel + + # print("%d-%d (%d) = %d" % (level, sublevel, level_id, star_count)) + + level_unlock_counts[level_id] = star_count + + level += 1 + if level > 6: + level = 1 + sublevel += 1 + + # force sphere 1 to 0 stars to help keep our promises to the item fill algo + level_unlock_counts[1] = 0 # 1-1 + level_unlock_counts[7] = 0 # 2-1 + level_unlock_counts[19] = 0 # 4-1 + + # Force 5-1 into sphere 1 to help things out + level_unlock_counts[25] = 1 # 5-1 + + for n in range(37, 46): + level_unlock_counts[n] = 0 + + return level_unlock_counts diff --git a/worlds/overcooked2/docs/en_Overcooked! 2.md b/worlds/overcooked2/docs/en_Overcooked! 2.md new file mode 100644 index 0000000000..d6de25f3e9 --- /dev/null +++ b/worlds/overcooked2/docs/en_Overcooked! 2.md @@ -0,0 +1,86 @@ +# Overcooked! 2 + +## Quick Links +- [Setup Guide](../../../../tutorial/Overcooked!%202/setup/en) +- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) + +## How Does Randomizer Work in the Kitchen? + +The *Overcooked! 2* Randomizer completely transforms the game into a metroidvania with items and item locations. Many of the Chefs' inherent abilities have been temporarily removed such that your scoring potential is limited at the start of the game. The more your inventory grows, the easier it will be to earn 2 and 3 Stars on each level. + +The game takes place entirely in the "Story" campaign on a fresh save file. The ultimate goal is to reach and complete level 6-6. In order to do this you must regain enough of your abilities to complete all levels in World 6 and obtain enough stars to purchase 6-6*. + +Randomizer can be played alone (one player switches between controlling two chefs) or up to 4 local/online friends. Player count can be changed at any time during the Archipelago game. + +**Note: 6-6 is excluded from "Shuffle Level Order", so it will always be the standard final boss stage.* + +## Items + +The first time a level is completed, a random item is given to the chef(s). If playing in a MultiWorld, completing a level may instead give another Archipelago user their item. The item found is displayed as text at the top of the results screen. + +Once all items have been obtained, the game will play like the original experience. + +The following items were invented for Randomizer: + +### Player Abilities +- Dash/Dash Cooldown +- Throw/Catch +- Sharp Knife +- Dish Scrubber +- Control Stick Batteries +- Lightweight Backpack +- Faster Respawn Time +- Emote (x6) + +### Objects +- Spare Plate +- Clean Dishes +- Wood +- Coal Bucket +- Bellows +- Fire Extinguisher + +### Kitchen/Environment +- Larger Tip Jar +- Guest Patience +- Burn Leniency +- Faster Condiment & Drink Switch +- Wok Wheels +- Coin Purse +- Calmer Unbread + +### Overworld +- Unlock Kevin Level (x8) +- Ramp Button +- Bonus Star (Filler Item*) + +**Note: Bonus star count varies with settings* + +## Other Game Modifications + +In addition to shuffling items, the following changes are applied to the game: + +### Quality of Life +- Tutorial is skipped +- Non-linear level order +- "Auto-Complete" feature to finish a level early when a target score is obtained +- Bugfixes for issues present in the base game (including "Sink Bug" and "Double Serving") +- All chef avatars automatically unlocked +- Optionally, level time can be reduced to make progression faster paced + +### Randomization Options + +- *Shuffle Level Order* + - Replaces each level on the overworld with a random level + - DLC levels can show up on the Story Overworld + - Optionally exclude "Horde" Levels + - Optionally exclude "Prep" Levels + +### Difficulty Adjustments +- Stars required to unlock levels have been rebalanced +- Points required to earn stars have been rebalanced + - Based off of the current World Record on the game's [Leaderboard](https://overcooked.greeny.dev) + - 1-Star/2-Star scores are much closer to the 3-Star Score +- Significantly reduced the time allotted to beat the final level +- Reduced penalty for expired order diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md new file mode 100644 index 0000000000..d724f02f7f --- /dev/null +++ b/worlds/overcooked2/docs/setup_en.md @@ -0,0 +1,84 @@ +# Overcooked! 2 Randomizer Setup Guide + +## Quick Links +- [Main Page](../../../../games/Overcooked!%202/info/en) +- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) + +## Required Software + +- Windows 10+ +- [Overcooked! 2](https://store.steampowered.com/bundle/13608/Overcooked_2___Gourmet_Edition/) for PC + - **Steam: Recommended** + - Steam (Beta Branch): Supported + - Epic Games: Supported + - GOG: Not officially supported - Adventurous users may choose to experiment at their own risk + - Windows Store (aka GamePass): Not Supported + - Xbox/PS/Switch: Not Supported +- [OC2-Modding Client](https://github.com/toasterparty/oc2-modding/releases) (instructions below) + +## Overview + +*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the orignal game files need to be modified in any way. + +When connecting to an Archipelago session using the in-game login screen, a modfile containing all relevant game modifications is automatically downloaded and applied. + +From this point, the game will communicate with the Archipelago service directly to manage sending/receiving items. Notifications of important events will appear through an in-game console at the top of the screen. + +## Overcooked! 2 Modding Guide + +### Install + +1. Download and extract the contents of the latest [OC2-Modding Release](https://github.com/toasterparty/oc2-modding/releases) anywhere on your PC + +2. Double-Click **oc2-modding-install.bat** follow the instructions. + +Once *OC2-Modding* is installed, you have successfully installed everything you need to play/participate in Archipelago MultiWorld games. + +### Disable + +To temporarily turn off *OC2-Modding* and return to the original game, open **...\Overcooked! 2\BepInEx\config\OC2Modding.cfg** in a text editor like notepad and edit the following: + +`DisableAllMods = true` + +To re-enable, simply change the word **true** back to a **false**. + +### Uninstall + +To completely remove *OC2-Modding*, navigate to your game's installation folder and run **oc2-modding-uninstall.bat**. + +## Generate a MultiWorld Game + +1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste + +2. Export your yaml file and use it to generate a new randomized game +- (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en)) + +## Joining a MultiWorld Game + +1. Launch the game + +2. When attempting to enter the main menu from the title screen, the game will freeze and prompt you to sign in: + +![Sign-In Screen](https://i.imgur.com/goMy7o2.png) + +3. Sign-in with server address, username and password of the corresponding room you would like to join. +- Otherwise, if you just want to play the vanilla game without any modifications, you may press "Continue without Archipelago" button. + +4. Upon successful connection to the Archipelago service, you will be granted access to the main menu. The game will act as though you are playing for the first time. ***DO NOT FEAR*** — your original save data has not been overwritten; the Overcooked Randomizer just uses a temporary directory for it's save game data. + +## Playing Co-Op + +- To play local multiplayer (or Parsec/"Steam Play Together"), simply add the additional player to your game session as you would in the base game + +- To play online multiplayer, the guest *must* also have the same version of OC2-Modding installed. In order for the game to work, the guest must sign in using the same information the host used to connect to the Archipelago session. Once both host and client are both connected, they may join one another in-game and proceed as normal. It does not matter who hosts the game, and the game's hosts may be changed at any point. You may notice some things are different when playing this way: + + - Guests will still receive Archipelago messages about sent/received items the same as the host + + - When the host loads the campaign, any connected guests are forced to select "Don't Save" when prompted to pick which save slot to use. This is because randomizer uses the Archipelago service as a pseudo "cloud save", so progress will always be synchronized between all participants of that randomized *Overcooked! 2* instance. + +## Auto-Complete + +Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved. + +To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting. From 097ac189e428d38b47cb876393731fa4d488d384 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 14 Oct 2022 19:35:53 +0200 Subject: [PATCH 057/105] SoE: add tests ... (#1097) * SoE: add tests ... ... for goals, bronze axe and bronze spear+ * SoE: fix tests --- test/soe/TestAccess.py | 22 +++++++++++ test/soe/TestGoal.py | 53 ++++++++++++++++++++++++++ test/soe/__init__.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 test/soe/TestAccess.py create mode 100644 test/soe/TestGoal.py create mode 100644 test/soe/__init__.py diff --git a/test/soe/TestAccess.py b/test/soe/TestAccess.py new file mode 100644 index 0000000000..c7da7b8896 --- /dev/null +++ b/test/soe/TestAccess.py @@ -0,0 +1,22 @@ +import typing +from . import SoETestBase + + +class AccessTest(SoETestBase): + @staticmethod + def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]): + return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers] + + def testBronzeAxe(self): + gourds = { + "Pyramid bottom": (118, 121, 122, 123, 124, 125), + "Pyramid top": (140,) + } + locations = ["Rimsala"] + self._resolveGourds(gourds) + items = [["Bronze Axe"]] + self.assertAccessDependency(locations, items) + + def testBronzeSpearPlus(self): + locations = ["Megataur"] + items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]] + self.assertAccessDependency(locations, items) diff --git a/test/soe/TestGoal.py b/test/soe/TestGoal.py new file mode 100644 index 0000000000..d127d38998 --- /dev/null +++ b/test/soe/TestGoal.py @@ -0,0 +1,53 @@ +from . import SoETestBase + + +class TestFragmentGoal(SoETestBase): + options = { + "energy_core": "fragments", + "available_fragments": 21, + "required_fragments": 20, + } + + def testFragments(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) + self.assertBeatable(False) # 0 fragments + fragments = self.get_items_by_name("Energy Core Fragment") + victory = self.get_item_by_name("Victory") + self.collect(fragments[:-2]) # 1 too few + self.assertEqual(self.count("Energy Core Fragment"), 19) + self.assertBeatable(False) + self.collect(fragments[-2:-1]) # exact + self.assertEqual(self.count("Energy Core Fragment"), 20) + self.assertBeatable(True) + self.remove([victory]) # reset + self.collect(fragments[-1:]) # 1 extra + self.assertEqual(self.count("Energy Core Fragment"), 21) + self.assertBeatable(True) + + def testNoWeapon(self): + self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"]) + self.assertBeatable(False) + + def testNoRocket(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"]) + self.assertBeatable(False) + + +class TestShuffleGoal(SoETestBase): + options = { + "energy_core": "shuffle", + } + + def testCore(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) + self.assertBeatable(False) + self.collect_by_name(["Energy Core"]) + self.assertBeatable(True) + + def testNoWeapon(self): + self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"]) + self.assertBeatable(False) + + def testNoRocket(self): + self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"]) + self.assertBeatable(False) diff --git a/test/soe/__init__.py b/test/soe/__init__.py new file mode 100644 index 0000000000..0161a6c32f --- /dev/null +++ b/test/soe/__init__.py @@ -0,0 +1,84 @@ +import typing +import unittest +from argparse import Namespace +from test.general import gen_steps +from BaseClasses import MultiWorld, Item +from worlds import AutoWorld +from worlds.AutoWorld import call_all + + +class SoETestBase(unittest.TestCase): + options: typing.Dict[str, typing.Any] = {} + world: MultiWorld + game = "Secret of Evermore" + + def setUp(self): + self.world = MultiWorld(1) + self.world.game[1] = self.game + self.world.player_name = {1: "Tester"} + self.world.set_seed() + args = Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items(): + setattr(args, name, {1: option.from_any(self.options.get(name, option.default))}) + self.world.set_options(args) + self.world.set_default_common_options() + for step in gen_steps: + call_all(self.world, step) + + def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]): + if isinstance(item_names, str): + item_names = (item_names,) + for item in self.world.get_items(): + if item.name not in item_names: + self.world.state.collect(item) + + def get_item_by_name(self, item_name: str): + for item in self.world.get_items(): + if item.name == item_name: + return item + raise ValueError("No such item") + + def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]): + if isinstance(item_names, str): + item_names = (item_names,) + return [item for item in self.world.itempool if item.name in item_names] + + def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]): + items = self.get_items_by_name(item_names) + self.collect(items) + return items + + def collect(self, items: typing.Union[Item, typing.Iterable[Item]]): + if isinstance(items, Item): + items = (items,) + for item in items: + self.world.state.collect(item) + + def remove(self, items: typing.Union[Item, typing.Iterable[Item]]): + if isinstance(items, Item): + items = (items,) + for item in items: + if item.location and item.location.event and item.location in self.world.state.events: + self.world.state.events.remove(item.location) + self.world.state.remove(item) + + def can_reach_location(self, location): + return self.world.state.can_reach(location, "Location", 1) + + def count(self, item_name): + return self.world.state.count(item_name, 1) + + def assertAccessDependency(self, locations, possible_items): + all_items = [item_name for item_names in possible_items for item_name in item_names] + + self.collect_all_but(all_items) + for location in self.world.get_locations(): + self.assertEqual(self.world.state.can_reach(location), location.name not in locations) + for item_names in possible_items: + items = self.collect_by_name(item_names) + for location in locations: + self.assertTrue(self.can_reach_location(location)) + self.remove(items) + + def assertBeatable(self, beatable: bool): + self.assertEqual(self.world.can_beat_game(self.world.state), beatable) From 722b3c53690703295d08a5a6a99d8bc974d37f72 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 14 Oct 2022 22:52:45 +0200 Subject: [PATCH 058/105] Core: make add_rule set if it finds an empty rule (#1093) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/generic/Rules.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 9b338e4d70..6f70e1b584 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -53,17 +53,25 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], spot.access_rule = rule -def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], 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) + # empty rule, replace instead of add + if old_rule is spot.__class__.access_rule: + spot.access_rule = rule if combine == "and" else old_rule else: - spot.access_rule = lambda state: rule(state) and old_rule(state) + if combine == "and": + spot.access_rule = lambda state: rule(state) and old_rule(state) + else: + spot.access_rule = lambda state: rule(state) or old_rule(state) 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) + # empty rule + if old_rule is location.__class__.item_rule: + location.item_rule = lambda i: i.name != item or i.player != player + else: + location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int): @@ -77,9 +85,16 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): location.item_rule = lambda i: i.name not in items and old_rule(i) -def add_item_rule(location: "BaseClasses.Location", rule: ItemRule): +def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): old_rule = location.item_rule - location.item_rule = lambda item: rule(item) and old_rule(item) + # empty rule, replace instead of add + if old_rule is location.__class__.item_rule: + location.item_rule = rule if combine == "and" else old_rule + else: + if combine == "and": + location.item_rule = lambda item: rule(item) and old_rule(item) + else: + location.item_rule = lambda item: rule(item) or old_rule(item) def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, From 0aea1e780f48c400f6299ee3d89db8f42aa9e7f9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 14 Oct 2022 03:56:03 +0200 Subject: [PATCH 059/105] Fill: create minimal excluded location rule only once --- Fill.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index 4b095eb108..0bbbf01413 100644 --- a/Fill.py +++ b/Fill.py @@ -233,9 +233,12 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): maximum_exploration_state = sweep_from_pool(state, []) unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] - for location in unreachable_locations: - add_item_rule(location, lambda item: not ((item.classification & 0b0011) and - world.accessibility[item.player] != 'minimal')) + if unreachable_locations: + def forbid_important_item_rule(item: Item): + return not (item.classification & 0b0011) and world.accessibility[item.player] != 'minimal' + + for location in unreachable_locations: + add_item_rule(location, forbid_important_item_rule) def distribute_items_restrictive(world: MultiWorld) -> None: From bbb6ee89cfe5b7b97fade141e53965a52229f18c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 14 Oct 2022 22:53:49 +0200 Subject: [PATCH 060/105] Hylics 2: add to readme (#1094) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 65f1792d92..51e46d1c6e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Currently, the following games are supported: * Dark Souls 3 * Super Mario World * Pokémon Red and Blue +* Hylics 2 * Overcooked! 2 For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). From 1f01404ca40c7c07856e8212dd5b0497a95dc7bd Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Fri, 14 Oct 2022 23:09:17 +0200 Subject: [PATCH 061/105] WebHost: Fixed scrolling to anchors (#1085) * WebHost: Modernized anchor code * WebHost: Fixed scrolling to anchors * WebHost: Fixed scrolling to anchors when fonts are being loaded * WebHost: Anchor PR changes requested by LegendaryLinux --- WebHostLib/static/assets/faq.js | 26 +++++++++++------------- WebHostLib/static/assets/gameInfo.js | 26 +++++++++++------------- WebHostLib/static/assets/glossary.js | 26 +++++++++++------------- WebHostLib/static/assets/tutorial.js | 26 +++++++++++------------- WebHostLib/static/styles/themes/base.css | 2 ++ 5 files changed, 50 insertions(+), 56 deletions(-) diff --git a/WebHostLib/static/assets/faq.js b/WebHostLib/static/assets/faq.js index 35f46e1628..1bf5e5a659 100644 --- a/WebHostLib/static/assets/faq.js +++ b/WebHostLib/static/assets/faq.js @@ -26,24 +26,22 @@ window.addEventListener('load', () => { adjustHeaderWidth(); // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); tutorialWrapper.innerHTML = diff --git a/WebHostLib/static/assets/gameInfo.js b/WebHostLib/static/assets/gameInfo.js index 8a9c8b3f22..b8c56905a5 100644 --- a/WebHostLib/static/assets/gameInfo.js +++ b/WebHostLib/static/assets/gameInfo.js @@ -26,24 +26,22 @@ window.addEventListener('load', () => { adjustHeaderWidth(); // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); gameInfo.innerHTML = diff --git a/WebHostLib/static/assets/glossary.js b/WebHostLib/static/assets/glossary.js index 44012d699f..04a2920086 100644 --- a/WebHostLib/static/assets/glossary.js +++ b/WebHostLib/static/assets/glossary.js @@ -26,24 +26,22 @@ window.addEventListener('load', () => { adjustHeaderWidth(); // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); tutorialWrapper.innerHTML = diff --git a/WebHostLib/static/assets/tutorial.js b/WebHostLib/static/assets/tutorial.js index 39fc356913..1db08d85b3 100644 --- a/WebHostLib/static/assets/tutorial.js +++ b/WebHostLib/static/assets/tutorial.js @@ -33,24 +33,22 @@ window.addEventListener('load', () => { } // Reset the id of all header divs to something nicer - const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')); - const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/); - for (let i=0; i < headers.length; i++){ - const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase() - headers[i].setAttribute('id', headerId); - headers[i].addEventListener('click', () => - window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`); + for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); + header.setAttribute('id', headerId); + header.addEventListener('click', () => { + window.location.hash = `#${headerId}`; + header.scrollIntoView(); + }); } // Manually scroll the user to the appropriate header if anchor navigation is used - if (scrollTargetIndex > -1) { - try{ - const scrollTarget = window.location.href.substring(scrollTargetIndex + 1); - document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" }); - } catch(error) { - console.error(error); + document.fonts.ready.finally(() => { + if (window.location.hash) { + const scrollTarget = document.getElementById(window.location.hash.substring(1)); + scrollTarget?.scrollIntoView(); } - } + }); }).catch((error) => { console.error(error); tutorialWrapper.innerHTML = diff --git a/WebHostLib/static/styles/themes/base.css b/WebHostLib/static/styles/themes/base.css index 0dbdf5f6ea..fca65a51c1 100644 --- a/WebHostLib/static/styles/themes/base.css +++ b/WebHostLib/static/styles/themes/base.css @@ -1,5 +1,7 @@ html{ padding-top: 110px; + scroll-padding-top: 100px; + scroll-behavior: smooth; } #base-header{ From 51f65f4b9e68e9653d06e99044fb7a50e8b5a604 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sat, 15 Oct 2022 03:39:04 -0700 Subject: [PATCH 062/105] OoT: ER algorithm improvements (#1103) * OoT: ER improvements Include dungeon rewards in itempool to allow for ER improvement Better validate_world function by checking for multi-entrance incompatibility more efficiently Fix some generation failures by ensuring all entrances placed with logic Introduce bias to some interior entrance placement to improve generation rate * OoT: fix overworld ER spoiler information * OoT: rewrite dungeon item placement algorithm in particular, no longer assumes that exactly the number of vanilla keys is present, which lets it place more or fewer items. --- worlds/oot/EntranceShuffle.py | 68 ++++++++++++++++------ worlds/oot/__init__.py | 106 ++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 67 deletions(-) diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 08d1e3ff79..bd06a3d81b 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -343,6 +343,27 @@ priority_entrance_table = { } +# These hint texts have more than one entrance, so they are OK for impa's house and potion shop +multi_interior_regions = { + 'Kokiri Forest', + 'Lake Hylia', + 'the Market', + 'Kakariko Village', + 'Lon Lon Ranch', +} + +interior_entrance_bias = { + 'Kakariko Village -> Kak Potion Shop Front': 4, + 'Kak Backyard -> Kak Potion Shop Back': 4, + 'Kakariko Village -> Kak Impas House': 3, + 'Kak Impas Ledge -> Kak Impas House Back': 3, + 'Goron City -> GC Shop': 2, + 'Zoras Domain -> ZD Shop': 2, + 'Market Entrance -> Market Guard House': 2, + 'ToT Entrance -> Temple of Time': 1, +} + + class EntranceShuffleError(Exception): pass @@ -500,7 +521,7 @@ def shuffle_random_entrances(ootworld): delete_target_entrance(remaining_target) for pool_type, entrance_pool in one_way_entrance_pools.items(): - shuffle_entrance_pool(ootworld, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5) + shuffle_entrance_pool(ootworld, pool_type, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5) replaced_entrances = [entrance.replaces for entrance in entrance_pool] for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()): if remaining_target.replaces in replaced_entrances: @@ -510,7 +531,7 @@ def shuffle_random_entrances(ootworld): # Shuffle all entrance pools, in order for pool_type, entrance_pool in entrance_pools.items(): - shuffle_entrance_pool(ootworld, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state) + shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True) # Multiple checks after shuffling to ensure everything is OK # Check that all entrances hook up correctly @@ -596,7 +617,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}') -def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): +def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -604,11 +625,11 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t retry_count -= 1 rollbacks = [] try: - shuffle_entrances(ootworld, restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) + shuffle_entrances(ootworld, pool_type+'Rest', restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) if check_all: - shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) + shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state) else: - shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, set(), all_state, none_state) + shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, set(), all_state, none_state) validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state) for entrance, target in rollbacks: @@ -621,12 +642,16 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}') -def shuffle_entrances(ootworld, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state): +def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state): ootworld.world.random.shuffle(entrances) for entrance in entrances: if entrance.connected_region != None: continue ootworld.world.random.shuffle(target_entrances) + # Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems. + # success rate over randomization + if pool_type in {'InteriorSoft', 'MixedSoft'}: + target_entrances.sort(reverse=True, key=lambda entrance: interior_entrance_bias.get(entrance.replaces.name, 0)) for target in target_entrances: if target.connected_region == None: continue @@ -715,25 +740,33 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all # Check if all locations are reachable if not beatable-only or game is not yet complete if locations_to_ensure_reachable: - if world.accessibility[player].current_key != 'minimal' or not world.can_beat_game(all_state): - for loc in locations_to_ensure_reachable: - if not all_state.can_reach(loc, 'Location', player): - raise EntranceShuffleError(f'{loc} is unreachable') + for loc in locations_to_ensure_reachable: + if not all_state.can_reach(loc, 'Location', player): + raise EntranceShuffleError(f'{loc} is unreachable') if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']): # Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints - potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) - potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) - if potion_front_entrance is not None and potion_back_entrance is not None and not same_hint_area(potion_front_entrance, potion_back_entrance): + potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) + potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) + if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back): raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area') + elif (potion_front and not potion_back) or (not potion_front and potion_back): + # Check the hint area and ensure it's one of the ones with more than one entrance + potion_placed_entrance = potion_front if potion_front else potion_back + if get_hint_area(potion_placed_entrance) not in multi_interior_regions: + raise EntranceShuffleError('Kak Potion Shop entrances can never be in the same hint area') # When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides if ootworld.shuffle_cows: - impas_front_entrance = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) - impas_back_entrance = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) - if impas_front_entrance is not None and impas_back_entrance is not None and not same_hint_area(impas_front_entrance, impas_back_entrance): + impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) + impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) + if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back): raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area') + elif (impas_front and not impas_back) or (not impas_front and impas_back): + impas_placed_entrance = impas_front if impas_front else impas_back + if get_hint_area(impas_placed_entrance) not in multi_interior_regions: + raise EntranceShuffleError('Kak Impas House entrances can never be in the same hint area') # Check basic refills, time passing, return to ToT if (ootworld.shuffle_special_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions) and \ @@ -845,3 +878,4 @@ def delete_target_entrance(target): if target.parent_region != None: target.parent_region.exits.remove(target) target.parent_region = None + del target diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 2536c3d4c9..c985ea13f0 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1,7 +1,7 @@ import logging import threading import copy -from collections import Counter +from collections import Counter, deque logger = logging.getLogger("Ocarina of Time") @@ -412,17 +412,6 @@ class OOTWorld(World): self.shop_prices[location.name] = int(self.world.random.betavariate(1.5, 2) * 60) * 5 def fill_bosses(self, bossCount=9): - rewardlist = ( - 'Kokiri Emerald', - 'Goron Ruby', - 'Zora Sapphire', - 'Forest Medallion', - 'Fire Medallion', - 'Water Medallion', - 'Spirit Medallion', - 'Shadow Medallion', - 'Light Medallion' - ) boss_location_names = ( 'Queen Gohma', 'King Dodongo', @@ -434,7 +423,7 @@ class OOTWorld(World): 'Twinrova', 'Links Pocket' ) - boss_rewards = [self.create_item(reward) for reward in rewardlist] + boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward'] boss_locations = [self.world.get_location(loc, self.player) for loc in boss_location_names] placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None] @@ -447,9 +436,8 @@ class OOTWorld(World): self.world.random.shuffle(prize_locs) item = prizepool.pop() loc = prize_locs.pop() - self.world.push_item(loc, item, collect=False) - loc.locked = True - loc.event = True + loc.place_locked_item(item) + self.world.itempool.remove(item) def create_item(self, name: str): if name in item_table: @@ -496,6 +484,10 @@ class OOTWorld(World): # Generate itempool generate_itempool(self) add_dungeon_items(self) + # Add dungeon rewards + rewardlist = sorted(list(self.item_name_groups['rewards'])) + self.itempool += map(self.create_item, rewardlist) + junk_pool = get_junk_pool(self) removed_items = [] # Determine starting items @@ -621,61 +613,64 @@ class OOTWorld(World): "Gerudo Training Ground Maze Path Final Chest", "Gerudo Training Ground MQ Ice Arrows Chest", ] + def get_names(items): + for item in items: + yield item.name + # Place/set rules for dungeon items itempools = { - 'dungeon': [], - 'overworld': [], - 'any_dungeon': [], + 'dungeon': set(), + 'overworld': set(), + 'any_dungeon': set(), } any_dungeon_locations = [] for dungeon in self.dungeons: - itempools['dungeon'] = [] + itempools['dungeon'] = set() # Put the dungeon items into their appropriate pools. # Build in reverse order since we need to fill boss key first and pop() returns the last element if self.shuffle_mapcompass in itempools: - itempools[self.shuffle_mapcompass].extend(dungeon.dungeon_items) + itempools[self.shuffle_mapcompass].update(get_names(dungeon.dungeon_items)) if self.shuffle_smallkeys in itempools: - itempools[self.shuffle_smallkeys].extend(dungeon.small_keys) + itempools[self.shuffle_smallkeys].update(get_names(dungeon.small_keys)) shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey if shufflebk in itempools: - itempools[shufflebk].extend(dungeon.boss_key) + itempools[shufflebk].update(get_names(dungeon.boss_key)) # We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it. dungeon_locations = [loc for region in dungeon.regions for loc in region.locations if loc.item is None and ( self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)] if itempools['dungeon']: # only do this if there's anything to shuffle - for item in itempools['dungeon']: + dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['dungeon']] + for item in dungeon_itempool: self.world.itempool.remove(item) self.world.random.shuffle(dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), dungeon_locations, - itempools['dungeon'], True, True) + dungeon_itempool, True, True) any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations # Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary if self.shuffle_fortresskeys == 'any_dungeon': - fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey', - self.world.itempool) - itempools['any_dungeon'].extend(fortresskeys) + itempools['any_dungeon'].add('Small Key (Thieves Hideout)') if itempools['any_dungeon']: - for item in itempools['any_dungeon']: + any_dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['any_dungeon']] + for item in any_dungeon_itempool: self.world.itempool.remove(item) - itempools['any_dungeon'].sort(key=lambda item: - {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) + any_dungeon_itempool.sort(key=lambda item: + {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) self.world.random.shuffle(any_dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations, - itempools['any_dungeon'], True, True) + any_dungeon_itempool, True, True) # If anything is overworld-only, fill into local non-dungeon locations if self.shuffle_fortresskeys == 'overworld': - fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey', - self.world.itempool) - itempools['overworld'].extend(fortresskeys) + itempools['overworld'].add('Small Key (Thieves Hideout)') if itempools['overworld']: - for item in itempools['overworld']: + overworld_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['overworld']] + for item in overworld_itempool: self.world.itempool.remove(item) - itempools['overworld'].sort(key=lambda item: - {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) + overworld_itempool.sort(key=lambda item: + {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0)) non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations and (loc.type != 'Shop' or loc.name in self.shop_prices) and @@ -683,7 +678,7 @@ class OOTWorld(World): (loc.name not in dungeon_song_locations or self.shuffle_song_items != 'dungeon')] self.world.random.shuffle(non_dungeon_locations) fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations, - itempools['overworld'], True, True) + overworld_itempool, True, True) # Place songs # 5 built-in retries because this section can fail sometimes @@ -805,6 +800,10 @@ class OOTWorld(World): or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): loc.address = None + # Handle item-linked dungeon items and songs + def stage_pre_fill(cls): + pass + def generate_output(self, output_directory: str): if self.hints != 'none': self.hint_data_available.wait() @@ -831,18 +830,25 @@ class OOTWorld(World): # Write entrances to spoiler log all_entrances = self.get_shuffled_entrances() - all_entrances.sort(key=lambda x: x.name) - all_entrances.sort(key=lambda x: x.type) + all_entrances.sort(reverse=True, key=lambda x: x.name) + all_entrances.sort(reverse=True, key=lambda x: x.type) if not self.decouple_entrances: - for loadzone in all_entrances: - if loadzone.primary: - entrance = loadzone + while all_entrances: + loadzone = all_entrances.pop() + if loadzone.type != 'Overworld': + if loadzone.primary: + entrance = loadzone + else: + entrance = loadzone.reverse + if entrance.reverse is not None: + self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) + else: + self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) else: - entrance = loadzone.reverse - if entrance.reverse is not None: - self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) - else: - self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + reverse = loadzone.replaces.reverse + if reverse in all_entrances: + all_entrances.remove(reverse) + self.world.spoiler.set_entrance(loadzone, reverse, 'both', self.player) else: for entrance in all_entrances: self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) @@ -1027,7 +1033,7 @@ class OOTWorld(World): all_state = self.world.get_all_state(use_cache=False) # Remove event progression items for item, player in all_state.prog_items: - if (item not in item_table or item_table[item][2] is None) and player == self.player: + if player == self.player and (item not in item_table or (item_table[item][2] is None and item_table[item][0] != 'DungeonReward')): all_state.prog_items[(item, player)] = 0 # Remove all events and checked locations all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player} From ca9c3d05d65293843590126b4eb02e2ed9ab5c91 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sat, 15 Oct 2022 04:44:39 -0700 Subject: [PATCH 063/105] Docs: information on Retrieved packet (#1101) --- docs/network protocol.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/network protocol.md b/docs/network protocol.md index 0e7a53f3cf..84587ab237 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -234,6 +234,8 @@ Sent to clients as a response the a [Get](#Get) package. | ---- | ---- | ----- | | keys | dict\[str\, any] | A key-value collection containing all the values for the keys requested in the [Get](#Get) package. | +If a requested key was not present in the server's data, the associated value will be `null`. + Additional arguments added to the [Get](#Get) package that triggered this [Retrieved](#Retrieved) will also be passed along. ### SetReply From 5e97463bdc61d64f2329b0c6018fe383db5504d1 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 15 Oct 2022 20:53:07 -0400 Subject: [PATCH 064/105] [Pokemon R/B] Fix inno_setup mistake (#1105) --- inno_setup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inno_setup.iss b/inno_setup.iss index e097798ca4..578add59f6 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -527,7 +527,7 @@ begin RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb'); bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b'); - if Length(redrom) = 0 then + if Length(bluerom) = 0 then BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb'); end; From f7fc6fa7aa3c0a0fdecf3937850ac0ec49af9495 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 16 Oct 2022 01:32:17 -0400 Subject: [PATCH 065/105] Core: Fix forbid_important_item_rule with parenthesis (#1107) --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 0bbbf01413..41a096769f 100644 --- a/Fill.py +++ b/Fill.py @@ -235,7 +235,7 @@ def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locat unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] if unreachable_locations: def forbid_important_item_rule(item: Item): - return not (item.classification & 0b0011) and world.accessibility[item.player] != 'minimal' + return not ((item.classification & 0b0011) and world.accessibility[item.player] != 'minimal') for location in unreachable_locations: add_item_rule(location, forbid_important_item_rule) From acf7fda26aeb9e75020c64c91bbac76435033f9c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 16 Oct 2022 14:32:13 +0200 Subject: [PATCH 066/105] Main: Fill: more opportunities for swapping --- Fill.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index 41a096769f..15430d1da4 100644 --- a/Fill.py +++ b/Fill.py @@ -82,13 +82,14 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: location.item = None placed_item.location = None - swap_state = sweep_from_pool(base_state) + swap_state = sweep_from_pool(base_state, [placed_item]) + # swap_state assumes we can collect placed item before item_to_place if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(swap_state, item_to_place, perform_access_check): - # Verify that placing this item won't reduce available locations + # Verify that placing this item won't reduce available locations, which could happen with rules + # that want to not have both items. Left in until removal is proven useful. prev_state = swap_state.copy() - prev_state.collect(placed_item) prev_loc_count = len( world.get_reachable_locations(prev_state)) From bb46ee7fc1242d3a942b018fc7d604c1be62bd29 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 17 Oct 2022 01:08:31 +0200 Subject: [PATCH 067/105] WebHost: optimize imports --- WebHost.py | 1 - WebHostLib/__init__.py | 6 +++--- WebHostLib/api/__init__.py | 4 ++-- WebHostLib/api/generate.py | 5 ++--- WebHostLib/api/user.py | 3 ++- WebHostLib/autolauncher.py | 16 ++++++++-------- WebHostLib/check.py | 2 +- WebHostLib/customserver.py | 5 +++-- WebHostLib/downloads.py | 2 +- WebHostLib/generate.py | 18 +++++++++--------- WebHostLib/landing.py | 8 ++++++-- WebHostLib/misc.py | 3 ++- WebHostLib/models.py | 2 +- WebHostLib/options.py | 11 ++++++----- WebHostLib/stats.py | 4 ++-- WebHostLib/tracker.py | 12 ++++++------ WebHostLib/upload.py | 14 +++++++------- 17 files changed, 61 insertions(+), 55 deletions(-) diff --git a/WebHost.py b/WebHost.py index 4c07e8b185..ce8443dbd9 100644 --- a/WebHost.py +++ b/WebHost.py @@ -1,5 +1,4 @@ import os -import sys import multiprocessing import logging import typing diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index f9c49c5a20..c979b40089 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -1,12 +1,12 @@ -import os -import uuid import base64 +import os import socket +import uuid -from pony.flask import Pony from flask import Flask from flask_caching import Cache from flask_compress import Compress +from pony.flask import Pony from werkzeug.routing import BaseConverter from Utils import title_sorted diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 80c60a093a..bac25255c2 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -1,11 +1,11 @@ """API endpoints package.""" -from uuid import UUID from typing import List, Tuple +from uuid import UUID from flask import Blueprint, abort -from ..models import Room, Seed from .. import cache +from ..models import Room, Seed api_endpoints = Blueprint('api', __name__, url_prefix="/api") diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index 45cca66ef7..0f090039fb 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -1,16 +1,15 @@ import json import pickle - from uuid import UUID -from . import api_endpoints from flask import request, session, url_for from pony.orm import commit from WebHostLib import app -from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR from WebHostLib.check import get_yaml_data, roll_options from WebHostLib.generate import get_meta +from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR +from . import api_endpoints @api_endpoints.route('/generate', methods=['POST']) diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 5c563dafac..116d3afa22 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -1,6 +1,7 @@ from flask import session, jsonify +from pony.orm import select -from WebHostLib.models import * +from WebHostLib.models import Room, Seed from . import api_endpoints, get_players diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 77445eadea..8de73ba11b 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -1,14 +1,14 @@ from __future__ import annotations -import logging -import json -import multiprocessing -import threading -from datetime import timedelta, datetime -import sys -import typing -import time +import json +import logging +import multiprocessing import os +import sys +import threading +import time +import typing +from datetime import timedelta, datetime from pony.orm import db_session, select, commit diff --git a/WebHostLib/check.py b/WebHostLib/check.py index a4e2f518db..cd45dff440 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -1,7 +1,7 @@ import zipfile from typing import * -from flask import request, flash, redirect, url_for, session, render_template +from flask import request, flash, redirect, url_for, render_template from WebHostLib import app diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 6272633f4e..59eb1d3de2 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -10,13 +10,14 @@ import random import socket import threading import time + import websockets +from pony.orm import db_session, commit, select import Utils -from .models import db_session, Room, select, commit, Command, db - from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless +from .models import Room, Command, db class CustomClientMessageProcessor(ClientMessageProcessor): diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 0386d1b0ae..eba0c265b3 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -1,5 +1,5 @@ -import zipfile import json +import zipfile from io import BytesIO from flask import send_file, Response, render_template diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 15067e131b..a9b1cb7685 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -1,23 +1,23 @@ -import os -import tempfile -import random import json +import os +import pickle +import random +import tempfile import zipfile from collections import Counter from typing import Dict, Optional, Any -from Utils import __version__ from flask import request, flash, redirect, url_for, session, render_template +from pony.orm import commit, db_session -from worlds.alttp.EntranceRandomizer import parse_arguments -from Main import main as ERmain from BaseClasses import seeddigits, get_seed from Generate import handle_name, PlandoSettings -import pickle - -from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID +from Main import main as ERmain +from Utils import __version__ from WebHostLib import app +from worlds.alttp.EntranceRandomizer import parse_arguments from .check import get_yaml_data, roll_options +from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID from .upload import upload_zip_to_db diff --git a/WebHostLib/landing.py b/WebHostLib/landing.py index 377758ed70..14e90cc28d 100644 --- a/WebHostLib/landing.py +++ b/WebHostLib/landing.py @@ -1,7 +1,11 @@ +from datetime import timedelta, datetime + from flask import render_template +from pony.orm import count + from WebHostLib import app, cache -from .models import * -from datetime import timedelta +from .models import Room, Seed + @app.route('/', methods=['GET', 'POST']) @cache.cached(timeout=300) # cache has to appear under app route for caching to work diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 6978e27c28..f78ec3926d 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -3,10 +3,11 @@ import os import jinja2.exceptions from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory +from pony.orm import count, commit, db_session -from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4 from worlds.AutoWorld import AutoWorldRegister from . import app, cache +from .models import Seed, Room, Command, UUID, uuid4 def get_world_theme(game_name: str): diff --git a/WebHostLib/models.py b/WebHostLib/models.py index 70f0318f85..0dc67a5196 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -1,6 +1,6 @@ from datetime import datetime from uuid import UUID, uuid4 -from pony.orm import * +from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr db = Database() diff --git a/WebHostLib/options.py b/WebHostLib/options.py index db1e57fdec..fc13e2c29e 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -1,13 +1,14 @@ +import json import logging import os -from Utils import __version__, local_path -from jinja2 import Template -import yaml -import json import typing -from worlds.AutoWorld import AutoWorldRegister +import yaml +from jinja2 import Template + import Options +from Utils import __version__, local_path +from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", "exclude_locations"} diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 54f5e598d1..e30ac82007 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -1,14 +1,14 @@ +import typing from collections import Counter, defaultdict from colorsys import hsv_to_rgb from datetime import datetime, timedelta, date from math import tau -import typing +from bokeh.colors import RGB from bokeh.embed import components from bokeh.models import HoverTool from bokeh.plotting import figure, ColumnDataSource from bokeh.resources import INLINE -from bokeh.colors import RGB from flask import render_template from pony.orm import select diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 8bbf7465d3..c28fb84ee8 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1,19 +1,19 @@ import collections +import datetime import typing from typing import Counter, Optional, Dict, Any, Tuple +from uuid import UUID from flask import render_template from werkzeug.exceptions import abort -import datetime -from uuid import UUID +from MultiServer import Context +from NetUtils import SlotType +from Utils import restricted_loads +from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name from worlds.alttp import Items from . import app, cache from .models import Room -from Utils import restricted_loads -from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name -from MultiServer import Context -from NetUtils import SlotType alttp_icons = { "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 173411bb64..22e1353fbe 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -1,19 +1,19 @@ -import typing -import zipfile -import json import base64 -import MultiServer +import json +import typing import uuid +import zipfile from io import BytesIO from flask import request, flash, redirect, url_for, session, render_template from pony.orm import flush, select -from . import app -from .models import Seed, Room, Slot +import MultiServer +from NetUtils import NetworkSlot, SlotType from Utils import VersionException, __version__ from worlds.Files import AutoPatchRegister -from NetUtils import NetworkSlot, SlotType +from . import app +from .models import Seed, Room, Slot banned_zip_contents = (".sfc",) From b533ffb9e8e8d93795a2962ec610fbf474a59626 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 17 Oct 2022 03:22:02 +0200 Subject: [PATCH 068/105] Locality: rewrite for linear memory consumption, from quadratic (#1091) --- Main.py | 8 ++--- test/general/TestFill.py | 3 +- worlds/generic/Rules.py | 74 ++++++++++++++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/Main.py b/Main.py index b26dcf7986..63f5b8a818 100644 --- a/Main.py +++ b/Main.py @@ -16,7 +16,7 @@ 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 -from worlds.generic.Rules import locality_rules, exclusion_rules, group_locality_rules +from worlds.generic.Rules import locality_rules, exclusion_rules from worlds import AutoWorld ordered_areas = ( @@ -107,7 +107,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: world.local_items[player].value.add('Triforce Piece') - # Not possible to place pendants/crystals out side of boss prizes yet. + # Not possible to place pendants/crystals outside boss prizes yet. world.non_local_items[player].value -= item_name_groups['Pendants'] world.non_local_items[player].value -= item_name_groups['Crystals'] @@ -122,9 +122,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info('Calculating Access Rules.') if world.players > 1: - for player in world.player_ids: - locality_rules(world, player) - group_locality_rules(world) + locality_rules(world) else: world.non_local_items[1].value = set() world.local_items[1].value = set() diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 8ce5b3b281..1893f6bd8a 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -575,8 +575,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase): multi_world.local_items[player1.id].value = set(names(player1.basic_items)) multi_world.local_items[player2.id].value = set(names(player2.basic_items)) - locality_rules(multi_world, player1.id) - locality_rules(multi_world, player2.id) + locality_rules(multi_world) distribute_items_restrictive(multi_world) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 6f70e1b584..e69a34c504 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,3 +1,4 @@ +import collections import typing from BaseClasses import LocationProgressType, MultiWorld @@ -12,29 +13,72 @@ else: ItemRule = typing.Callable[[object], bool] -def group_locality_rules(world): +def locality_needed(world: MultiWorld) -> bool: + for player in world.player_ids: + if world.local_items[player].value: + return True + if world.non_local_items[player].value: + return True + + # Group for group_id, group in world.groups.items(): if set(world.player_ids) == set(group["players"]): continue if group["local_items"]: - for location in world.get_locations(): - if location.player not in group["players"]: - forbid_items_for_player(location, group["local_items"], group_id) + return True if group["non_local_items"]: - for location in world.get_locations(): - if location.player in group["players"]: - forbid_items_for_player(location, group["non_local_items"], group_id) + return True -def locality_rules(world, player: int): - if world.local_items[player].value: +def locality_rules(world: MultiWorld): + if locality_needed(world): + + forbid_data: typing.Dict[int, typing.Dict[int, typing.Set[str]]] = \ + collections.defaultdict(lambda: collections.defaultdict(set)) + + def forbid(sender: int, receiver: int, items: typing.Set[str]): + forbid_data[sender][receiver].update(items) + + for receiving_player in world.player_ids: + local_items: typing.Set[str] = world.local_items[receiving_player].value + if local_items: + for sending_player in world.player_ids: + if receiving_player != sending_player: + forbid(sending_player, receiving_player, local_items) + non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value + if non_local_items: + forbid(receiving_player, receiving_player, non_local_items) + + # Group + for receiving_group_id, receiving_group in world.groups.items(): + if set(world.player_ids) == set(receiving_group["players"]): + continue + if receiving_group["local_items"]: + for sending_player in world.player_ids: + if sending_player not in receiving_group["players"]: + forbid(sending_player, receiving_group_id, receiving_group["local_items"]) + if receiving_group["non_local_items"]: + for sending_player in world.player_ids: + if sending_player in receiving_group["players"]: + forbid(sending_player, receiving_group_id, receiving_group["non_local_items"]) + + # create fewer lambda's to save memory and cache misses + func_cache = {} for location in world.get_locations(): - if location.player != player: - forbid_items_for_player(location, world.local_items[player].value, player) - if world.non_local_items[player].value: - for location in world.get_locations(): - if location.player == player: - forbid_items_for_player(location, world.non_local_items[player].value, player) + if (location.player, location.item_rule) in func_cache: + location.item_rule = func_cache[location.player, location.item_rule] + # empty rule that just returns True, overwrite + elif location.item_rule is location.__class__.item_rule: + func_cache[location.player, location.item_rule] = location.item_rule = \ + lambda i, sending_blockers = forbid_data[location.player], \ + old_rule = location.item_rule: \ + i.name not in sending_blockers[i.player] + # special rule, needs to also be fulfilled. + else: + func_cache[location.player, location.item_rule] = location.item_rule = \ + lambda i, sending_blockers = forbid_data[location.player], \ + old_rule = location.item_rule: \ + i.name not in sending_blockers[i.player] and old_rule(i) def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: From 1aa3e431c81591bc4aeb8f3863c8f04afa1f4241 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 17 Oct 2022 09:52:34 +0200 Subject: [PATCH 069/105] LttP: fix ganons tower trash fill deleting items that did not fit (#1113) --- worlds/alttp/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 169d21ab8c..561b489bfa 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -490,11 +490,15 @@ class ALTTPWorld(World): while gtower_locations and filleritempool and trash_count > 0: spot_to_fill = gtower_locations.pop() - item_to_place = filleritempool.pop() - if spot_to_fill.item_rule(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 + for index, item in enumerate(filleritempool): + if spot_to_fill.item_rule(item): + filleritempool.pop(index) # remove from outer fill + world.push_item(spot_to_fill, item, False) + fill_locations.remove(spot_to_fill) # very slow, unfortunately + trash_count -= 1 + break + else: + logging.warning(f"Could not trash fill Ganon's Tower for player {player}.") def get_filler_item_name(self) -> str: if self.world.goal[self.player] == "icerodhunt": From af0cfc5a38599239f5e862eb55f0139fe1b8f40f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 18 Oct 2022 01:07:06 +0200 Subject: [PATCH 070/105] Fill: Priority locks when placing and does not swap. (#1099) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Fill.py | 121 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/Fill.py b/Fill.py index 15430d1da4..2c03599503 100644 --- a/Fill.py +++ b/Fill.py @@ -23,7 +23,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], - itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False) -> None: + itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, + swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] @@ -70,61 +71,66 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: 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 - swap_count = swapped_items[placed_item.player, - placed_item.name] - if swap_count > 1: + if swap: + # 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 + swap_count = swapped_items[placed_item.player, + placed_item.name] + if swap_count > 1: + continue + + location.item = None + placed_item.location = None + swap_state = sweep_from_pool(base_state, [placed_item]) + # swap_state assumes we can collect placed item before item_to_place + if (not single_player_placement or location.player == item_to_place.player) \ + and location.can_fill(swap_state, item_to_place, perform_access_check): + + # Verify that placing this item won't reduce available locations, which could happen with rules + # that want to not have both items. Left in until removal is proven useful. + prev_state = swap_state.copy() + prev_loc_count = len( + world.get_reachable_locations(prev_state)) + + swap_state.collect(item_to_place, True) + new_loc_count = len( + world.get_reachable_locations(swap_state)) + + if new_loc_count >= prev_loc_count: + # Add this item to the existing placement, and + # add the old item to the back of the queue + spot_to_fill = placements.pop(i) + + swap_count += 1 + swapped_items[placed_item.player, + placed_item.name] = swap_count + + reachable_items[placed_item.player].appendleft( + placed_item) + 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 - - location.item = None - placed_item.location = None - swap_state = sweep_from_pool(base_state, [placed_item]) - # swap_state assumes we can collect placed item before item_to_place - if (not single_player_placement or location.player == item_to_place.player) \ - and location.can_fill(swap_state, item_to_place, perform_access_check): - - # Verify that placing this item won't reduce available locations, which could happen with rules - # that want to not have both items. Left in until removal is proven useful. - prev_state = swap_state.copy() - prev_loc_count = len( - world.get_reachable_locations(prev_state)) - - swap_state.collect(item_to_place, True) - new_loc_count = len( - world.get_reachable_locations(swap_state)) - - if new_loc_count >= prev_loc_count: - # Add this item to the existing placement, and - # add the old item to the back of the queue - spot_to_fill = placements.pop(i) - - swap_count += 1 - swapped_items[placed_item.player, - placed_item.name] = swap_count - - reachable_items[placed_item.player].appendleft( - placed_item) - 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 + else: unplaced_items.append(item_to_place) continue - world.push_item(spot_to_fill, item_to_place, False) spot_to_fill.locked = lock placements.append(spot_to_fill) spot_to_fill.event = item_to_place.advancement + if on_place: + on_place(spot_to_fill) if len(unplaced_items) > 0 and len(locations) > 0: # There are leftover unplaceable items and locations that won't accept them @@ -272,19 +278,26 @@ def distribute_items_restrictive(world: MultiWorld) -> None: defaultlocations = locations[LocationProgressType.DEFAULT] excludedlocations = locations[LocationProgressType.EXCLUDED] - prioritylocations_lock = prioritylocations.copy() + # can't lock due to accessibility corrections touching things, so we remember which ones got placed and lock later + lock_later = [] - fill_restrictive(world, world.state, prioritylocations, progitempool) + def mark_for_locking(location: Location): + nonlocal lock_later + lock_later.append(location) + + # "priority fill" + fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) accessibility_corrections(world, world.state, prioritylocations, progitempool) - for location in prioritylocations_lock: - if location.item: - location.locked = True + for location in lock_later: + location.locked = True + del mark_for_locking, lock_later if prioritylocations: defaultlocations = prioritylocations + defaultlocations if progitempool: + # "progression fill" fill_restrictive(world, world.state, defaultlocations, progitempool) if progitempool: raise FillError( From 4da6a0bb9804839a981cd5d231e55ce88a350d72 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 18 Oct 2022 02:22:05 +0200 Subject: [PATCH 071/105] Fill: fix duplicate event pickups --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index d91be28c77..143b1f511e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -691,7 +691,7 @@ class CollectionState(): locations = self.world.get_filled_locations() reachable_events = True # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.event and + locations = {location for location in locations if location.event and location not in self.events and not key_only or getattr(location.item, "locked_dungeon_item", False)} while reachable_events: reachable_events = {location for location in locations if location.can_reach(self)} From 49ae79e5ce059183b5b9f947aabbaaf6c709f7ea Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 18 Oct 2022 10:20:57 +0200 Subject: [PATCH 072/105] Tests: add fill tests for #1109 and #1114 (#1115) * Test: for fill issues fixed in PR #1109 * Test: for double sweep collect fixed in PR #1114 --- test/general/TestFill.py | 42 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 1893f6bd8a..86d86a223a 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -4,7 +4,7 @@ from worlds.AutoWorld import World from Fill import FillError, balance_multiworld_progression, fill_restrictive, distribute_items_restrictive from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location, \ ItemClassification -from worlds.generic.Rules import CollectionRule, locality_rules, set_rule +from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule def generate_multi_world(players: int = 1) -> MultiWorld: @@ -359,6 +359,46 @@ class TestFillRestrictive(unittest.TestCase): fill_restrictive(multi_world, multi_world.state, locations, player1.prog_items) + def test_swap_to_earlier_location_with_item_rule(self): + # test for PR#1109 + multi_world = generate_multi_world(1) + player1 = generate_player_data(multi_world, 1, 4, 4) + locations = player1.locations[:] # copy required + items = player1.prog_items[:] # copy required + # for the test to work, item and location order is relevant: Sphere 1 last, allowed_item not last + for location in locations[:-1]: # Sphere 2 + # any one provides access to Sphere 2 + set_rule(location, lambda state: any(state.has(item.name, player1.id) for item in items)) + # forbid all but 1 item in Sphere 1 + sphere1_loc = locations[-1] + allowed_item = items[1] + add_item_rule(sphere1_loc, lambda item_to_place: item_to_place == allowed_item) + # test our rules + self.assertTrue(location.can_fill(None, allowed_item, False), "Test is flawed") + self.assertTrue(location.can_fill(None, items[2], False), "Test is flawed") + self.assertTrue(sphere1_loc.can_fill(None, allowed_item, False), "Test is flawed") + self.assertFalse(sphere1_loc.can_fill(None, items[2], False), "Test is flawed") + # fill has to place items[1] in locations[0] which will result in a swap because of placement order + fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items) + # assert swap happened + self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1") + self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1") + + def test_double_sweep(self): + # test for PR1114 + multi_world = generate_multi_world(1) + player1 = generate_player_data(multi_world, 1, 1, 1) + location = player1.locations[0] + location.address = None + location.event = True + item = player1.prog_items[0] + item.code = None + location.place_locked_item(item) + multi_world.state.sweep_for_events() + multi_world.state.sweep_for_events() + self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed") + self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times") + class TestDistributeItemsRestrictive(unittest.TestCase): def test_basic_distribute(self): From f12b73f487c3188a2791eec8ae657fe8ce8889fe Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Tue, 18 Oct 2022 09:54:41 -0700 Subject: [PATCH 073/105] Tests: world test base class (#1116) * world test base class * game not instance property * I would have guessed that this only collected 1. * game property * move SoE tests into worlds * don't force auto world setup --- .gitignore | 2 +- BaseClasses.py | 4 +- Options.py | 3 ++ test/worlds/__init__.py | 0 test/{ => worlds}/soe/TestAccess.py | 0 test/{ => worlds}/soe/TestGoal.py | 0 test/worlds/soe/__init__.py | 5 +++ test/{soe/__init__.py => worlds/test_base.py} | 40 +++++++++++++------ 8 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 test/worlds/__init__.py rename test/{ => worlds}/soe/TestAccess.py (100%) rename test/{ => worlds}/soe/TestGoal.py (100%) create mode 100644 test/worlds/soe/__init__.py rename test/{soe/__init__.py => worlds/test_base.py} (70%) diff --git a/.gitignore b/.gitignore index 925a4bd0c7..3f62316892 100644 --- a/.gitignore +++ b/.gitignore @@ -128,7 +128,7 @@ ipython_config.py # Environments .env -.venv +.venv* env/ venv/ ENV/ diff --git a/BaseClasses.py b/BaseClasses.py index 143b1f511e..ce2fc9e3c5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,4 +1,5 @@ from __future__ import annotations +from argparse import Namespace import copy from enum import unique, IntEnum, IntFlag @@ -54,6 +55,7 @@ class MultiWorld(): indirect_connections: Dict[Region, Set[Entrance]] exclude_locations: Dict[int, Options.ExcludeLocations] + game: Dict[int, str] class AttributeProxy(): def __init__(self, rule): @@ -200,7 +202,7 @@ class MultiWorld(): self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in range(1, self.players + 1)} - def set_options(self, args): + def set_options(self, args: Namespace) -> None: for option_key in Options.common_options: setattr(self, option_key, getattr(args, option_key, {})) for option_key in Options.per_game_common_options: diff --git a/Options.py b/Options.py index c243c8feb4..c2007c1c41 100644 --- a/Options.py +++ b/Options.py @@ -78,6 +78,9 @@ class AssembleOptions(abc.ABCMeta): return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) + @abc.abstractclassmethod + def from_any(cls, value: typing.Any) -> "Option[typing.Any]": ... + T = typing.TypeVar('T') diff --git a/test/worlds/__init__.py b/test/worlds/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/soe/TestAccess.py b/test/worlds/soe/TestAccess.py similarity index 100% rename from test/soe/TestAccess.py rename to test/worlds/soe/TestAccess.py diff --git a/test/soe/TestGoal.py b/test/worlds/soe/TestGoal.py similarity index 100% rename from test/soe/TestGoal.py rename to test/worlds/soe/TestGoal.py diff --git a/test/worlds/soe/__init__.py b/test/worlds/soe/__init__.py new file mode 100644 index 0000000000..c79544e0de --- /dev/null +++ b/test/worlds/soe/__init__.py @@ -0,0 +1,5 @@ +from test.worlds.test_base import WorldTestBase + + +class SoETestBase(WorldTestBase): + game = "Secret of Evermore" diff --git a/test/soe/__init__.py b/test/worlds/test_base.py similarity index 70% rename from test/soe/__init__.py rename to test/worlds/test_base.py index 0161a6c32f..1aa6ff2317 100644 --- a/test/soe/__init__.py +++ b/test/worlds/test_base.py @@ -7,54 +7,66 @@ from worlds import AutoWorld from worlds.AutoWorld import call_all -class SoETestBase(unittest.TestCase): +class WorldTestBase(unittest.TestCase): options: typing.Dict[str, typing.Any] = {} world: MultiWorld - game = "Secret of Evermore" - def setUp(self): + game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore" + auto_construct: typing.ClassVar[bool] = True + """ automatically set up a world for each test in this class """ + + def setUp(self) -> None: + if self.auto_construct: + self.world_setup() + + def world_setup(self) -> None: + if not hasattr(self, "game"): + raise NotImplementedError("didn't define game name") self.world = MultiWorld(1) self.world.game[1] = self.game self.world.player_name = {1: "Tester"} self.world.set_seed() args = Namespace() for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items(): - setattr(args, name, {1: option.from_any(self.options.get(name, option.default))}) + setattr(args, name, { + 1: option.from_any(self.options.get(name, getattr(option, "default"))) + }) self.world.set_options(args) self.world.set_default_common_options() for step in gen_steps: call_all(self.world, step) - def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]): + def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None: if isinstance(item_names, str): item_names = (item_names,) for item in self.world.get_items(): if item.name not in item_names: self.world.state.collect(item) - def get_item_by_name(self, item_name: str): + def get_item_by_name(self, item_name: str) -> Item: for item in self.world.get_items(): if item.name == item_name: return item raise ValueError("No such item") - def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]): + def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: if isinstance(item_names, str): item_names = (item_names,) return [item for item in self.world.itempool if item.name in item_names] - def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]): + def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """ collect all of the items in the item pool that have the given names """ items = self.get_items_by_name(item_names) self.collect(items) return items - def collect(self, items: typing.Union[Item, typing.Iterable[Item]]): + def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: if isinstance(items, Item): items = (items,) for item in items: self.world.state.collect(item) - def remove(self, items: typing.Union[Item, typing.Iterable[Item]]): + def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: if isinstance(items, Item): items = (items,) for item in items: @@ -62,13 +74,15 @@ class SoETestBase(unittest.TestCase): self.world.state.events.remove(item.location) self.world.state.remove(item) - def can_reach_location(self, location): + def can_reach_location(self, location: str) -> bool: return self.world.state.can_reach(location, "Location", 1) - def count(self, item_name): + def count(self, item_name: str) -> int: return self.world.state.count(item_name, 1) - def assertAccessDependency(self, locations, possible_items): + def assertAccessDependency(self, + locations: typing.List[str], + possible_items: typing.Iterable[typing.Iterable[str]]) -> None: all_items = [item_name for item_names in possible_items for item_name in item_names] self.collect_all_but(all_items) From 1900d9382ad4f22c19fd7acbb2663e89b71dd69d Mon Sep 17 00:00:00 2001 From: Sunny Bat Date: Tue, 18 Oct 2022 23:47:33 -0700 Subject: [PATCH 074/105] Raft: Update rules to account for navigation (#1118) --- worlds/raft/Rules.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index 0432c2806f..a55c0ef3e8 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -98,37 +98,37 @@ class RaftLogic(LogicMixin): return self.raft_can_access_vasagatan(player) def raft_can_access_balboa_island(self, player): - return self.raft_can_drive(player) and self.has("Balboa Island Frequency", player) + return self.raft_can_navigate(player) and 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_drive(player) and self.has("Caravan Island Frequency", player) + return self.raft_can_navigate(player) and 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_drive(player) and self.has("Tangaroa Frequency", player) + return self.raft_can_navigate(player) and 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) and self.raft_can_craft_ziplineTool(player) def raft_can_access_varuna_point(self, player): - return self.raft_can_drive(player) and self.has("Varuna Point Frequency", player) + return self.raft_can_navigate(player) and 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) and self.raft_can_craft_ziplineTool(player) def raft_can_access_temperance(self, player): - return self.raft_can_drive(player) and self.has("Temperance Frequency", player) + return self.raft_can_navigate(player) and 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) # No zipline required on Temperance def raft_can_access_utopia(self, player): - return (self.raft_can_drive(player) + return (self.raft_can_navigate(player) and 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) From 40b7e78178ef3d221d2b52d23004526a1a96b417 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 20 Oct 2022 05:05:36 +0200 Subject: [PATCH 075/105] The Witness: More Puzzle Skips, Softlock Fix (#1117) * Softlock fix: 'Swamp Maze Controls' item also locks maze control other side now * Increase default & max amount of Puzzle Skips (in response to client side changes to how they work) * Logically require shortbox lasers to start the elevator --- worlds/witness/Options.py | 4 ++-- worlds/witness/WitnessItems.txt | 2 +- worlds/witness/WitnessLogic.txt | 2 +- worlds/witness/WitnessLogicExpert.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index b858eb1954..ef3c9fdf76 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -112,8 +112,8 @@ class PuzzleSkipAmount(Range): Works on most panels in the game - The only big exception is The Challenge.""" display_name = "Puzzle Skips" range_start = 0 - range_end = 20 - default = 5 + range_end = 30 + default = 10 class HintAmount(Range): diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index d58edab0cb..98e0038b25 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -58,7 +58,7 @@ Doors: 1190 - Swamp Entry (Panel) - 0x0056E 1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 1195 - Swamp Rotating Bridge (Panel) - 0x181F5 -1197 - Swamp Maze Control (Panel) - 0x17C0A +1197 - Swamp Maze Control (Panel) - 0x17C0A,0x17E07 1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 1400 - Caves Mountain Shortcut (Door) - 0x2D73F diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 236d220701..d9fb9503b0 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -927,6 +927,6 @@ Elevator (Mountain Final Room): 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 +158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index 3c44006054..96c7fdc07a 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -927,6 +927,6 @@ Elevator (Mountain Final Room): 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 +158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: From ed76c1396119bcdde2bfa4574f1e1cd0cbe86a9d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 20 Oct 2022 10:42:33 +0200 Subject: [PATCH 076/105] Core: assert that items have a single reference (#1075) * Core: assert that items have a single reference * Fix duplicate item reference in The Witness * Ori: fix duplicate item references * DKC3: fix duplicate item references * RL: fix duplicate item references * SA2B: fix duplicate item references * SMW: fix duplicate item references Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/AutoWorld.py | 8 ++++++++ worlds/dkc3/__init__.py | 16 ++++++---------- worlds/oribf/__init__.py | 2 +- worlds/rogue_legacy/__init__.py | 24 ++++++++++++------------ worlds/sa2b/__init__.py | 10 +++++----- worlds/smw/__init__.py | 13 +++++-------- worlds/witness/__init__.py | 2 +- 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5dea03481f..8d3fab64cf 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -79,8 +79,16 @@ def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None: world_types: Set[AutoWorldRegister] = set() for player in world.player_ids: + prev_item_count = len(world.itempool) world_types.add(world.worlds[player].__class__) call_single(world, method_name, player, *args) + if __debug__: + new_items = world.itempool[prev_item_count:] + for i, item in enumerate(new_items): + for other in new_items[i+1:]: + assert item is not other, ( + f"Duplicate item reference of \"{item.name}\" in \"{world.worlds[player].game}\" " + f"of player \"{world.player_name[player]}\". Please make a copy instead.") for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index 1389f83efd..d45de8f85a 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -65,10 +65,6 @@ class DKC3World(World): "active_levels": self.active_level_list, } - def _create_items(self, name: str): - data = item_table[name] - return [self.create_item(name)] * data.quantity - def fill_slot_data(self) -> dict: slot_data = self._get_slot_data() for option_name in dkc3_options: @@ -113,17 +109,17 @@ class DKC3World(World): number_of_bonus_coins = (self.world.krematoa_bonus_coin_cost[self.player] * 5) number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.world.percentage_of_extra_bonus_coins[self.player] / 100) - itempool += [self.create_item(ItemName.bonus_coin)] * number_of_bonus_coins - itempool += [self.create_item(ItemName.dk_coin)] * 41 - itempool += [self.create_item(ItemName.banana_bird)] * number_of_banana_birds - itempool += [self.create_item(ItemName.krematoa_cog)] * number_of_cogs - itempool += [self.create_item(ItemName.progressive_boat)] * 3 + itempool += [self.create_item(ItemName.bonus_coin) for _ in range(number_of_bonus_coins)] + itempool += [self.create_item(ItemName.dk_coin) for _ in range(41)] + itempool += [self.create_item(ItemName.banana_bird) for _ in range(number_of_banana_birds)] + itempool += [self.create_item(ItemName.krematoa_cog) for _ in range(number_of_cogs)] + itempool += [self.create_item(ItemName.progressive_boat) for _ in range(3)] total_junk_count = total_required_locations - len(itempool) 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)] + junk_pool.append(self.create_item(item_name)) itempool += junk_pool diff --git a/worlds/oribf/__init__.py b/worlds/oribf/__init__.py index 05d237659c..45e666efeb 100644 --- a/worlds/oribf/__init__.py +++ b/worlds/oribf/__init__.py @@ -62,7 +62,7 @@ class OriBlindForest(World): def generate_basic(self): for item_name, count in default_pool.items(): - self.world.itempool.extend([self.create_item(item_name)] * count) + self.world.itempool.extend([self.create_item(item_name) for _ in range(count)]) def create_item(self, name: str) -> Item: return Item(name, diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index ba58e133c1..81e6202787 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -66,7 +66,7 @@ class LegacyWorld(World): def _create_items(self, name: str): data = item_table[name] - return [self.create_item(name)] * data.quantity + return [self.create_item(name) for _ in range(data.quantity)] def fill_slot_data(self) -> dict: slot_data = self._get_slot_data() @@ -89,20 +89,20 @@ class LegacyWorld(World): # Blueprints if self.world.progressive_blueprints[self.player]: - itempool += [self.create_item(ItemName.progressive_blueprints)] * 15 + itempool += [self.create_item(ItemName.progressive_blueprints) for _ in range(15)] else: for item in blueprints_table: itempool += self._create_items(item) # Check Pool settings to add a certain amount of these items. - itempool += [self.create_item(ItemName.health)] * int(self.world.health_pool[self.player]) - itempool += [self.create_item(ItemName.mana)] * int(self.world.mana_pool[self.player]) - itempool += [self.create_item(ItemName.attack)] * int(self.world.attack_pool[self.player]) - itempool += [self.create_item(ItemName.magic_damage)] * int(self.world.magic_damage_pool[self.player]) - itempool += [self.create_item(ItemName.armor)] * int(self.world.armor_pool[self.player]) - itempool += [self.create_item(ItemName.equip)] * int(self.world.equip_pool[self.player]) - itempool += [self.create_item(ItemName.crit_chance)] * int(self.world.crit_chance_pool[self.player]) - itempool += [self.create_item(ItemName.crit_damage)] * int(self.world.crit_damage_pool[self.player]) + itempool += [self.create_item(ItemName.health) for _ in range(self.world.health_pool[self.player])] + itempool += [self.create_item(ItemName.mana) for _ in range(self.world.mana_pool[self.player])] + itempool += [self.create_item(ItemName.attack) for _ in range(self.world.attack_pool[self.player])] + itempool += [self.create_item(ItemName.magic_damage) for _ in range(self.world.magic_damage_pool[self.player])] + itempool += [self.create_item(ItemName.armor) for _ in range(self.world.armor_pool[self.player])] + itempool += [self.create_item(ItemName.equip) for _ in range(self.world.equip_pool[self.player])] + itempool += [self.create_item(ItemName.crit_chance) for _ in range(self.world.crit_chance_pool[self.player])] + itempool += [self.create_item(ItemName.crit_damage) for _ in range(self.world.crit_damage_pool[self.player])] classes = self.world.available_classes[self.player] if "Dragon" in classes: @@ -153,12 +153,12 @@ class LegacyWorld(World): if self.world.architect[self.player] == "start_unlocked": self.world.push_precollected(self.world.create_item(ItemName.architect, self.player)) elif self.world.architect[self.player] != "disabled": - itempool += [self.create_item(ItemName.architect)] + itempool.append(self.create_item(ItemName.architect)) # Fill item pool with the remaining for _ in range(len(itempool), total_required_locations): item = self.world.random.choice(list(misc_items_table.keys())) - itempool += [self.create_item(item)] + itempool.append(self.create_item(item)) self.world.itempool += itempool diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index ea24809521..7269a66c68 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -86,7 +86,7 @@ class SA2BWorld(World): def _create_items(self, name: str): data = item_table[name] - return [self.create_item(name)] * data.quantity + return [self.create_item(name) for _ in range(data.quantity)] def fill_slot_data(self) -> dict: slot_data = self._get_slot_data() @@ -198,11 +198,11 @@ class SA2BWorld(World): connect_regions(self.world, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses) max_required_emblems = max(max(emblem_requirement_list), self.emblems_for_cannons_core) - itempool += [self.create_item(ItemName.emblem)] * max_required_emblems + itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)] non_required_emblems = (total_emblem_count - max_required_emblems) junk_count = math.floor(non_required_emblems * (self.world.junk_fill_percentage[self.player].value / 100.0)) - itempool += [self.create_item(ItemName.emblem, True)] * (non_required_emblems - junk_count) + itempool += [self.create_item(ItemName.emblem, True) for _ in range(non_required_emblems - junk_count)] # Carve Traps out of junk_count trap_weights = [] @@ -219,14 +219,14 @@ class SA2BWorld(World): junk_keys = list(junk_table.keys()) for i in range(junk_count): junk_item = self.world.random.choice(junk_keys) - junk_pool += [self.create_item(junk_item)] + junk_pool.append(self.create_item(junk_item)) itempool += junk_pool trap_pool = [] for i in range(trap_count): trap_item = self.world.random.choice(trap_weights) - trap_pool += [self.create_item(trap_item)] + trap_pool.append(self.create_item(trap_item)) itempool += trap_pool diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py index 77931b7c72..1dd64f535f 100644 --- a/worlds/smw/__init__.py +++ b/worlds/smw/__init__.py @@ -65,10 +65,6 @@ class SMWWorld(World): "active_levels": self.active_level_dict, } - def _create_items(self, name: str): - data = item_table[name] - return [self.create_item(name)] * data.quantity - def fill_slot_data(self) -> dict: slot_data = self._get_slot_data() for option_name in smw_options: @@ -105,14 +101,15 @@ class SMWWorld(World): itempool += [self.create_item(ItemName.p_switch)] itempool += [self.create_item(ItemName.p_balloon)] itempool += [self.create_item(ItemName.super_star_active)] - itempool += [self.create_item(ItemName.progressive_powerup)] * 3 + itempool += [self.create_item(ItemName.progressive_powerup) for _ in range(3)] itempool += [self.create_item(ItemName.yellow_switch_palace)] itempool += [self.create_item(ItemName.green_switch_palace)] itempool += [self.create_item(ItemName.red_switch_palace)] itempool += [self.create_item(ItemName.blue_switch_palace)] if self.world.goal[self.player] == "yoshi_egg_hunt": - itempool += [self.create_item(ItemName.yoshi_egg)] * self.world.number_of_yoshi_eggs[self.player] + itempool += [self.create_item(ItemName.yoshi_egg) + for _ in range(self.world.number_of_yoshi_eggs[self.player])] self.world.get_location(LocationName.yoshis_house, self.player).place_locked_item(self.create_item(ItemName.victory)) else: self.world.get_location(LocationName.bowser, self.player).place_locked_item(self.create_item(ItemName.victory)) @@ -128,11 +125,11 @@ class SMWWorld(World): trap_pool = [] for i in range(trap_count): trap_item = self.world.random.choice(trap_weights) - trap_pool += [self.create_item(trap_item)] + trap_pool.append(self.create_item(trap_item)) itempool += trap_pool - itempool += [self.create_item(ItemName.one_up_mushroom)] * junk_count + itempool += [self.create_item(ItemName.one_up_mushroom) for _ in range(junk_count)] boss_location_names = [LocationName.yoshis_island_koopaling, LocationName.donut_plains_koopaling, LocationName.vanilla_dome_koopaling, LocationName.twin_bridges_koopaling, LocationName.forest_koopaling, LocationName.chocolate_koopaling, diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 7f5b9d2bfb..d4e9a59771 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -117,9 +117,9 @@ class WitnessWorld(World): pool.remove(items_by_name[item]) for item in self.items.EXTRA_AMOUNTS: - witness_item = self.create_item(item) for i in range(0, self.items.EXTRA_AMOUNTS[item]): if len(pool) < len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - less_junk: + witness_item = self.create_item(item) pool.append(witness_item) # Put in junk items to fill the rest From 265ee7098afdb9186755eb8673cdc39ff37ada7f Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Thu, 20 Oct 2022 10:41:11 -0700 Subject: [PATCH 077/105] New Game: Zillion (#1081) * Option RangeWithSpecialMax * amendment to typing in web options * compare string with number * lots of work on zillion * fix zillion fill logic * fix a few more issues in zillion fill logic * can make zillion patch and use it * put multi items in zillion rom * work on ZillionClient * logging and auth in client * work on sending and receiving items * implement item_handling flag * fix locations ids to NuktiServer package * use rewrite of zri * cache logic rule data for performance * use new id maps * fix some problems with the big recent merge * ZillionClient: use new context manager for Memory class * fix ItemClassification for Zillion items and some debug statements for asserts, documentation on running scripts for manual testing type correction in CommonContext * fix some issues in client, start on docs, put rescue and item ram addresses in slot data * use new location name system fix item locations getting out of sync in progression balancing * zillion client can read slot name from game * zillion: new item names * remove extra unneeded import * newer options (room gen and starting cards) * update comment in zillion patch * zillion non static regions * change some logging, update some comments * allow ZillionClient to exit in certain situations * todo note to fix options doc strings * don't force auto forfeit * rework validation of floppy requirement and item counts and fix race condition in generate_output * reorganize Zillion component structure with System class * documentation updates for Zillion * attempt inno_setup.iss * remove todo comment for something done * update comment * rework item count zillion options and some small cleanups * fix location check count * data package version 1 * Zillion can pass unit tests without rom * fix freeze if closing ZillionClient while it's waiting for server login * specify commit hash for zilliandomizer package * some changes to options validation * Zillion doors saved on multiworld server * add missing function in inno_setup and name of vanilla continues in options * rework zillion sync task and context * Apply documentation suggestions from SoldierofOrder Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * update zillion package * workaround for asyncio udp bug There is a bug in Python in Windows https://github.com/python/cpython/issues/91227 that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system. As a workaround, we don't look for RetroArch until the user asks for it with /sms * a few of the smaller suggestions from review * logic only looks at my locations instead of all the multiworld locations * some adjustments from pull request discussion and some unit tests * patch webhost changes from pull request discussion * zillion logic tests * better vblr test * test interaction of character rescue items with logic * move unit tests to new worlds folder * comment improvements * fix minor logic issue and add memory read timeout * capitalization in option display names Opa-Opa is a proper noun * redirect zz stdout to debug * fix option validation bug making unbeatable seeds * remove line that does nothing * attach logic cache to world Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Co-authored-by: Doug Hoskisson --- .gitignore | 1 + CommonClient.py | 7 +- Launcher.py | 3 + ModuleUpdate.py | 7 + README.md | 1 + Utils.py | 6 + WebHostLib/downloads.py | 2 + ZillionClient.py | 358 +++++++++++++++++++++++ host.yaml | 9 + inno_setup.iss | 82 ++++++ test/worlds/zillion/TestGoal.py | 144 ++++++++++ test/worlds/zillion/TestOptions.py | 26 ++ test/worlds/zillion/__init__.py | 5 + worlds/dkc3/Names/__init__.py | 0 worlds/rogue_legacy/Names/__init__.py | 0 worlds/zillion/__init__.py | 395 ++++++++++++++++++++++++++ worlds/zillion/config.py | 1 + worlds/zillion/docs/en_Zillion.md | 74 +++++ worlds/zillion/docs/setup_en.md | 104 +++++++ worlds/zillion/id_maps.py | 93 ++++++ worlds/zillion/item.py | 12 + worlds/zillion/logic.py | 77 +++++ worlds/zillion/options.py | 380 +++++++++++++++++++++++++ worlds/zillion/patch.py | 34 +++ worlds/zillion/py.typed | 0 worlds/zillion/region.py | 50 ++++ worlds/zillion/requirements.txt | 1 + 27 files changed, 1871 insertions(+), 1 deletion(-) create mode 100644 ZillionClient.py create mode 100644 test/worlds/zillion/TestGoal.py create mode 100644 test/worlds/zillion/TestOptions.py create mode 100644 test/worlds/zillion/__init__.py create mode 100644 worlds/dkc3/Names/__init__.py create mode 100644 worlds/rogue_legacy/Names/__init__.py create mode 100644 worlds/zillion/__init__.py create mode 100644 worlds/zillion/config.py create mode 100644 worlds/zillion/docs/en_Zillion.md create mode 100644 worlds/zillion/docs/setup_en.md create mode 100644 worlds/zillion/id_maps.py create mode 100644 worlds/zillion/item.py create mode 100644 worlds/zillion/logic.py create mode 100644 worlds/zillion/options.py create mode 100644 worlds/zillion/patch.py create mode 100644 worlds/zillion/py.typed create mode 100644 worlds/zillion/region.py create mode 100644 worlds/zillion/requirements.txt diff --git a/.gitignore b/.gitignore index 3f62316892..0caf00a978 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ *.z64 *.n64 *.nes +*.sms *.gb *.gbc *.gba diff --git a/CommonClient.py b/CommonClient.py index 2940ceed31..b17709eecf 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -151,6 +151,11 @@ class CommonContext: hint_cost: typing.Optional[int] player_names: typing.Dict[int, str] + finished_game: bool + ready: bool + auth: typing.Optional[str] + seed_name: typing.Optional[str] + # locations locations_checked: typing.Set[int] # local state locations_scouted: typing.Set[int] @@ -288,7 +293,7 @@ class CommonContext: self.input_requests += 1 return await self.input_queue.get() - async def connect(self, address=None): + async def connect(self, address: typing.Optional[str] = None) -> None: await self.disconnect() self.server_task = asyncio.create_task(server_loop(self, address), name="server loop") diff --git a/Launcher.py b/Launcher.py index c24e0c819d..7d5b2f7316 100644 --- a/Launcher.py +++ b/Launcher.py @@ -151,6 +151,9 @@ components: Iterable[Component] = ( Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 Component('Starcraft 2 Client', 'Starcraft2Client'), + # Zillion + Component('Zillion Client', 'ZillionClient', + file_identifier=SuffixIdentifier('.apzl')), # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 1fe7030e4e..0768b37619 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -44,6 +44,13 @@ def update(yes=False, force=False): wheel = line.split('/')[-1] name, version, _ = wheel.split('-', 2) line = f'{name}=={version}' + elif line.startswith('git+https://'): + # extract name and version + end = line.split('/')[-1] + name_hash, egg = end.split("#", 1) + name, _ = name_hash.split("@", 1) + version = egg.split('==')[-1] + line = f'{name}=={version}' requirements = pkg_resources.parse_requirements(line) for requirement in requirements: requirement = str(requirement) diff --git a/README.md b/README.md index 51e46d1c6e..a8f269f557 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Currently, the following games are supported: * Pokémon Red and Blue * Hylics 2 * Overcooked! 2 +* Zillion For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/Utils.py b/Utils.py index 7df5ce5978..d28834b698 100644 --- a/Utils.py +++ b/Utils.py @@ -295,6 +295,12 @@ def get_default_options() -> OptionsType: "sni": "SNI", "rom_start": True, }, + "zillion_options": { + "rom_file": "Zillion (UE) [!].sms", + # RetroArch doesn't make it easy to launch a game from the command line. + # You have to know the path to the emulator core library on the user's computer. + "rom_start": "retroarch", + }, "pokemon_rb_options": { "red_rom_file": "Pokemon Red (UE) [S][!].gb", "blue_rom_file": "Pokemon Blue (UE) [S][!].gb", diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index eba0c265b3..053fa35ce4 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -75,6 +75,8 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5" elif slot_data.game == "VVVVVV": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6" + elif slot_data.game == "Zillion": + fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apzl" elif slot_data.game == "Super Mario 64": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex" elif slot_data.game == "Dark Souls III": diff --git a/ZillionClient.py b/ZillionClient.py new file mode 100644 index 0000000000..dee5c2b756 --- /dev/null +++ b/ZillionClient.py @@ -0,0 +1,358 @@ +import asyncio +import base64 +import platform +from typing import Any, Coroutine, Dict, Optional, Type, cast + +# CommonClient import first to trigger ModuleUpdater +from CommonClient import CommonContext, server_loop, gui_enabled, \ + ClientCommandProcessor, logger, get_base_parser +from NetUtils import ClientStatus +import Utils + +import colorama # type: ignore + +from zilliandomizer.zri.memory import Memory +from zilliandomizer.zri import events +from zilliandomizer.utils.loc_name_maps import id_to_loc +from zilliandomizer.options import Chars +from zilliandomizer.patch import RescueInfo + +from worlds.zillion.id_maps import make_id_to_others +from worlds.zillion.config import base_id + + +class ZillionCommandProcessor(ClientCommandProcessor): + ctx: "ZillionContext" + + def _cmd_sms(self) -> None: + """ Tell the client that Zillion is running in RetroArch. """ + logger.info("ready to look for game") + self.ctx.look_for_retroarch.set() + + +class ZillionContext(CommonContext): + game = "Zillion" + command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor + items_handling = 1 # receive items from other players + + from_game: "asyncio.Queue[events.EventFromGame]" + to_game: "asyncio.Queue[events.EventToGame]" + ap_local_count: int + """ local checks watched by server """ + next_item: int + """ index in `items_received` """ + ap_id_to_name: Dict[int, str] + ap_id_to_zz_id: Dict[int, int] + start_char: Chars = "JJ" + rescues: Dict[int, RescueInfo] = {} + loc_mem_to_id: Dict[int, int] = {} + got_slot_data: asyncio.Event + """ serves as a flag for whether I am logged in to the server """ + + look_for_retroarch: asyncio.Event + """ + There is a bug in Python in Windows + https://github.com/python/cpython/issues/91227 + that makes it so if I look for RetroArch before it's ready, + it breaks the asyncio udp transport system. + + As a workaround, we don't look for RetroArch until this event is set. + """ + + def __init__(self, + server_address: str, + password: str) -> None: + super().__init__(server_address, password) + self.from_game = asyncio.Queue() + self.to_game = asyncio.Queue() + self.got_slot_data = asyncio.Event() + + self.look_for_retroarch = asyncio.Event() + if platform.system() != "Windows": + # asyncio udp bug is only on Windows + self.look_for_retroarch.set() + + self.reset_game_state() + + def reset_game_state(self) -> None: + for _ in range(self.from_game.qsize()): + self.from_game.get_nowait() + for _ in range(self.to_game.qsize()): + self.to_game.get_nowait() + self.got_slot_data.clear() + + self.ap_local_count = 0 + self.next_item = 0 + self.ap_id_to_name = {} + self.ap_id_to_zz_id = {} + self.rescues = {} + self.loc_mem_to_id = {} + + self.locations_checked.clear() + self.missing_locations.clear() + self.checked_locations.clear() + self.finished_game = False + self.items_received.clear() + + # override + def on_deathlink(self, data: Dict[str, Any]) -> None: + self.to_game.put_nowait(events.DeathEventToGame()) + return super().on_deathlink(data) + + # override + async def server_auth(self, password_requested: bool = False) -> None: + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + logger.info('waiting for connection to game...') + return + logger.info("logging in to server...") + await self.send_connect() + + # override + def run_gui(self) -> None: + from kvui import GameManager + + class ZillionManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zillion Client" + + self.ui = ZillionManager(self) + run_co: Coroutine[Any, Any, None] = self.ui.async_run() # type: ignore + # kivy types missing + self.ui_task = asyncio.create_task(run_co, name="UI") + + def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + if cmd == "Connected": + logger.info("logged in to Archipelago server") + if "slot_data" not in args: + logger.warn("`Connected` packet missing `slot_data`") + return + slot_data = args["slot_data"] + + if "start_char" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") + return + self.start_char = slot_data['start_char'] + if self.start_char not in {"Apple", "Champ", "JJ"}: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` `start_char` has invalid value: {self.start_char}") + + if "rescues" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") + return + rescues = slot_data["rescues"] + self.rescues = {} + for rescue_id, json_info in rescues.items(): + assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" + # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? + assert json_info["start_char"] == self.start_char, \ + f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' + ri = RescueInfo(json_info["start_char"], + json_info["room_code"], + json_info["mask"]) + self.rescues[0 if rescue_id == "0" else 1] = ri + + if "loc_mem_to_id" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + return + loc_mem_to_id = slot_data["loc_mem_to_id"] + self.loc_mem_to_id = {} + for mem_str, id_str in loc_mem_to_id.items(): + mem = int(mem_str) + id_ = int(id_str) + room_i = mem // 256 + assert 0 <= room_i < 74 + assert id_ in id_to_loc + self.loc_mem_to_id[mem] = id_ + + self.got_slot_data.set() + + payload = { + "cmd": "Get", + "keys": [f"zillion-{self.auth}-doors"] + } + asyncio.create_task(self.send_msgs([payload])) + elif cmd == "Retrieved": + if "keys" not in args: + logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") + return + keys = cast(Dict[str, Optional[str]], args["keys"]) + doors_b64 = keys[f"zillion-{self.auth}-doors"] + if doors_b64: + logger.info("received door data from server") + doors = base64.b64decode(doors_b64) + self.to_game.put_nowait(events.DoorEventToGame(doors)) + + def process_from_game_queue(self) -> None: + if self.from_game.qsize(): + event_from_game = self.from_game.get_nowait() + if isinstance(event_from_game, events.AcquireLocationEventFromGame): + server_id = event_from_game.id + base_id + loc_name = id_to_loc[event_from_game.id] + self.locations_checked.add(server_id) + if server_id in self.missing_locations: + self.ap_local_count += 1 + n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win + logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + asyncio.create_task(self.send_msgs([ + {"cmd": 'LocationChecks', "locations": [server_id]} + ])) + else: + # This will happen a lot in Zillion, + # because all the key words are local and unwatched by the server. + logger.debug(f"DEBUG: {loc_name} not in missing") + elif isinstance(event_from_game, events.DeathEventFromGame): + asyncio.create_task(self.send_death()) + elif isinstance(event_from_game, events.WinEventFromGame): + if not self.finished_game: + asyncio.create_task(self.send_msgs([ + {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} + ])) + self.finished_game = True + elif isinstance(event_from_game, events.DoorEventFromGame): + if self.auth: + doors_b64 = base64.b64encode(event_from_game.doors).decode() + payload = { + "cmd": "Set", + "key": f"zillion-{self.auth}-doors", + "operations": [{"operation": "replace", "value": doors_b64}] + } + asyncio.create_task(self.send_msgs([payload])) + else: + logger.warning(f"WARNING: unhandled event from game {event_from_game}") + + def process_items_received(self) -> None: + if len(self.items_received) > self.next_item: + zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] + for index in range(self.next_item, len(self.items_received)): + ap_id = self.items_received[index].item + from_name = self.player_names[self.items_received[index].player] + # TODO: colors in this text, like sni client? + logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + self.to_game.put_nowait( + events.ItemEventToGame(zz_item_ids) + ) + self.next_item = len(self.items_received) + + +async def zillion_sync_task(ctx: ZillionContext) -> None: + logger.info("started zillion sync task") + + # to work around the Python bug where we can't check for RetroArch + if not ctx.look_for_retroarch.is_set(): + logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") + await asyncio.wait(( + asyncio.create_task(ctx.look_for_retroarch.wait()), + asyncio.create_task(ctx.exit_event.wait()) + ), return_when=asyncio.FIRST_COMPLETED) + + last_log = "" + + def log_no_spam(msg: str) -> None: + nonlocal last_log + if msg != last_log: + last_log = msg + logger.info(msg) + + # to only show this message once per client run + help_message_shown = False + + with Memory(ctx.from_game, ctx.to_game) as memory: + while not ctx.exit_event.is_set(): + ram = await memory.read() + name = memory.get_player_name(ram).decode() + if len(name): + if name == ctx.auth: + # this is the name we know + if ctx.server and ctx.server.socket: # type: ignore + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + asyncio.create_task(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + ctx.got_slot_data.wait(), + ctx.exit_event.wait(), + asyncio.sleep(6) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # server not connected + log_no_spam("waiting for server connection...") + else: # new game + log_no_spam("connected to new game") + await ctx.disconnect() + ctx.reset_server_state() + ctx.reset_game_state() + memory.reset_game_state() + + ctx.auth = name + asyncio.create_task(ctx.connect()) + await asyncio.wait(( + ctx.got_slot_data.wait(), + ctx.exit_event.wait(), + asyncio.sleep(6) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # no name found in game + if not help_message_shown: + logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') + help_message_shown = True + log_no_spam("looking for connection to game...") + await asyncio.sleep(0.3) + + await asyncio.sleep(0.09375) + logger.info("zillion sync task ending") + + +async def main() -> None: + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apzl Archipelago Binary Patch file') + # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) + args = parser.parse_args() + print(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating sms rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + ctx = ZillionContext(args.connect, args.password) + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + sync_task = asyncio.create_task(zillion_sync_task(ctx)) + + await ctx.exit_event.wait() + + ctx.server_address = None + logger.debug("waiting for sync task to end") + await sync_task + logger.debug("sync task ended") + await ctx.shutdown() + + +if __name__ == "__main__": + Utils.init_logging("ZillionClient", exception_logger="Client") + + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/host.yaml b/host.yaml index f36f014af4..2bb0e5ef5d 100644 --- a/host.yaml +++ b/host.yaml @@ -155,3 +155,12 @@ smw_options: # True for operating system default program # Alternatively, a path to a program to open the .sfc file with rom_start: true +zillion_options: + # File name of the Zillion US rom + rom_file: "Zillion (UE) [!].sms" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program + # Alternatively, a path to a program to open the .sfc file with + # RetroArch doesn't make it easy to launch a game from the command line. + # You have to know the path to the emulator core library on the user's computer. + rom_start: "retroarch" diff --git a/inno_setup.iss b/inno_setup.iss index 578add59f6..5e7414d181 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -59,6 +59,7 @@ Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: ful Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning +Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting Name: "server"; Description: "Server"; Types: full hosting @@ -77,6 +78,7 @@ Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Typ Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576 Name: "client/cf"; Description: "ChecksFinder"; Types: full playing Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing +Name: "client/zl"; Description: "Zillion"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] @@ -89,6 +91,7 @@ Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Countr Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot +Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs @@ -104,6 +107,7 @@ Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: i Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot +Source: "{#source_path}\ArchipelagoZillionClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/zl Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1 Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf @@ -118,6 +122,7 @@ Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.e Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot +Name: "{group}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Components: client/zl Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1 Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf @@ -129,6 +134,7 @@ Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNI Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot +Name: "{commondesktop}\{#MyAppName} Zillion Client"; Filename: "{app}\ArchipelagoZillionClient.exe"; Tasks: desktopicon; Components: client/zl Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/ff1 Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf @@ -169,6 +175,11 @@ Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Arch Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smwpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: ".apzl"; ValueData: "{#MyAppName}zlpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/zl +Root: HKCR; Subkey: "{#MyAppName}zlpatch"; ValueData: "Archipelago Zillion Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/zl +Root: HKCR; Subkey: "{#MyAppName}zlpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZillionClient.exe,0"; ValueType: string; ValueName: ""; Components: client/zl +Root: HKCR; Subkey: "{#MyAppName}zlpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZillionClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/zl + Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni @@ -254,6 +265,9 @@ var SoERomFilePage: TInputFileWizardPage; var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; +var zlrom: string; +var ZlROMFilePage: TInputFileWizardPage; + var redrom: string; var RedROMFilePage: TInputFileWizardPage; @@ -273,6 +287,15 @@ begin end; end; +function GetSMSMD5OfFile(const rom: string): string; +var data: AnsiString; +begin + if LoadStringFromFile(rom, data) then + begin + Result := GetMD5OfString(data); + end; +end; + function CheckRom(name: string; hash: string): string; var rom: string; begin @@ -292,6 +315,25 @@ begin end; end; +function CheckSMSRom(name: string; hash: string): string; +var rom: string; +begin + log('Handling ' + name) + rom := FileSearch(name, WizardDirValue()); + if Length(rom) > 0 then + begin + log('existing ROM found'); + log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash))); + if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then + begin + log('existing ROM verified'); + Result := rom; + exit; + end; + log('existing ROM failed verification'); + end; +end; + function AddRomPage(name: string): TInputFileWizardPage; begin Result := @@ -307,6 +349,7 @@ begin '.sfc'); end; + function AddGBRomPage(name: string): TInputFileWizardPage; begin Result := @@ -322,6 +365,21 @@ begin '.gb'); end; +function AddSMSRomPage(name: string): TInputFileWizardPage; +begin + Result := + CreateInputFilePage( + wpSelectComponents, + 'Select ROM File', + 'Where is your ' + name + ' located?', + 'Select the file, then click Next.'); + + Result.Add( + 'Location of ROM file:', + 'SMS ROM files|*.sms|All files|*.*', + '.sms'); +end; + procedure AddOoTRomPage(); begin ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue()); @@ -366,6 +424,8 @@ begin Result := not (SoEROMFilePage.Values[0] = '') else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then Result := not (OoTROMFilePage.Values[0] = '') + else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then + Result := not (ZlROMFilePage.Values[0] = '') else Result := True; end; @@ -466,6 +526,22 @@ begin Result := ''; end; +function GetZlROMPath(Param: string): string; +begin + if Length(zlrom) > 0 then + Result := zlrom + else if Assigned(ZlROMFilePage) then + begin + R := CompareStr(GetMD5OfFile(ZlROMFilePage.Values[0]), 'd4bf9e7bcf9a48da53785d2ae7bc4270'); + if R <> 0 then + MsgBox('Zillion ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := ZlROMFilePage.Values[0] + end + else + Result := ''; +end; + function GetRedROMPath(Param: string): string; begin if Length(redrom) > 0 then @@ -522,6 +598,10 @@ begin if Length(soerom) = 0 then SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); + zlrom := CheckSMSRom('Zillion (UE) [!].sms', 'd4bf9e7bcf9a48da53785d2ae7bc4270'); + if Length(zlrom) = 0 then + ZlROMFilePage:= AddSMSRomPage('Zillion (UE) [!].sms'); + redrom := CheckRom('Pokemon Red (UE) [S][!].gb','3d45c1ee9abd5738df46d2bdda8b57dc'); if Length(redrom) = 0 then RedROMFilePage:= AddGBRomPage('Pokemon Red (UE) [S][!].gb'); @@ -547,6 +627,8 @@ begin Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/oot') or WizardIsComponentSelected('client/oot')); + if (assigned(ZlROMFilePage)) and (PageID = ZlROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/zl') or WizardIsComponentSelected('client/zl')); if (assigned(RedROMFilePage)) and (PageID = RedROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red')); if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then diff --git a/test/worlds/zillion/TestGoal.py b/test/worlds/zillion/TestGoal.py new file mode 100644 index 0000000000..96701e8352 --- /dev/null +++ b/test/worlds/zillion/TestGoal.py @@ -0,0 +1,144 @@ +from . import ZillionTestBase + + +class TestGoalVanilla(ZillionTestBase): + options = { + "start_char": "JJ", + "jump_levels": "vanilla", + "gun_levels": "vanilla", + "floppy_disk_count": 7, + "floppy_req": 6, + } + + def test_floppies(self): + self.collect_by_name(["Apple", "Champ", "Red ID Card"]) + self.assertBeatable(False) # 0 floppies + floppies = self.get_items_by_name("Floppy Disk") + win = self.get_item_by_name("Win") + self.collect(floppies[:-2]) # 1 too few + self.assertEqual(self.count("Floppy Disk"), 5) + self.assertBeatable(False) + self.collect(floppies[-2:-1]) # exact + self.assertEqual(self.count("Floppy Disk"), 6) + self.assertBeatable(True) + self.remove([win]) # reset + self.collect(floppies[-1:]) # 1 extra + self.assertEqual(self.count("Floppy Disk"), 7) + self.assertBeatable(True) + + def test_with_everything(self): + self.collect_by_name(["Apple", "Champ", "Red ID Card", "Floppy Disk"]) + self.assertBeatable(True) + + def test_no_jump(self): + self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk"]) + self.assertBeatable(False) + + def test_no_gun(self): + self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk"]) + self.assertBeatable(False) + + def test_no_red(self): + self.collect_by_name(["Apple", "Champ", "Floppy Disk"]) + self.assertBeatable(False) + + +class TestGoalBalanced(ZillionTestBase): + options = { + "start_char": "JJ", + "jump_levels": "balanced", + "gun_levels": "balanced", + } + + def test_jump(self): + self.collect_by_name(["Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + opas = self.get_items_by_name("Opa-Opa") + self.collect(opas[:1]) # too few + self.assertEqual(self.count("Opa-Opa"), 1) + self.assertBeatable(False) + self.collect(opas[1:]) + self.assertBeatable(True) + + def test_guns(self): + self.collect_by_name(["Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + guns = self.get_items_by_name("Zillion") + self.collect(guns[:1]) # too few + self.assertEqual(self.count("Zillion"), 1) + self.assertBeatable(False) + self.collect(guns[1:]) + self.assertBeatable(True) + + +class TestGoalRestrictive(ZillionTestBase): + options = { + "start_char": "JJ", + "jump_levels": "restrictive", + "gun_levels": "restrictive", + } + + def test_jump(self): + self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + self.collect_by_name("Opa-Opa") + self.assertBeatable(False) # with all opas, jj champ can't jump + self.collect_by_name("Apple") + self.assertBeatable(True) + + def test_guns(self): + self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + self.collect_by_name("Zillion") + self.assertBeatable(False) # with all guns, jj apple can't gun + self.collect_by_name("Champ") + self.assertBeatable(True) + + +class TestGoalAppleStart(ZillionTestBase): + """ creation of character rescue items has some special interactions with logic """ + options = { + "start_char": "Apple", + "jump_levels": "balanced", + "gun_levels": "low", + "zillion_count": 5 + } + + def test_guns_jj_first(self): + """ with low gun levels, 5 Zillion is enough to get JJ to gun 3 """ + self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + self.collect_by_name("Zillion") + self.assertBeatable(True) + + def test_guns_zillions_first(self): + """ with low gun levels, 5 Zillion is enough to get JJ to gun 3 """ + self.collect_by_name(["Zillion", "Red ID Card", "Floppy Disk", "Opa-Opa"]) + self.assertBeatable(False) # not enough gun + self.collect_by_name("JJ") + self.assertBeatable(True) + + +class TestGoalChampStart(ZillionTestBase): + """ creation of character rescue items has some special interactions with logic """ + options = { + "start_char": "Champ", + "jump_levels": "low", + "gun_levels": "balanced", + "opa_opa_count": 5, + "opas_per_level": 1 + } + + def test_jump_jj_first(self): + """ with low jump levels, 5 level-ups is enough to get JJ to jump 3 """ + self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + self.collect_by_name("Opa-Opa") + self.assertBeatable(True) + + def test_jump_opa_first(self): + """ with low jump levels, 5 level-ups is enough to get JJ to jump 3 """ + self.collect_by_name(["Opa-Opa", "Red ID Card", "Floppy Disk", "Zillion"]) + self.assertBeatable(False) # not enough jump + self.collect_by_name("JJ") + self.assertBeatable(True) diff --git a/test/worlds/zillion/TestOptions.py b/test/worlds/zillion/TestOptions.py new file mode 100644 index 0000000000..20397a4ec8 --- /dev/null +++ b/test/worlds/zillion/TestOptions.py @@ -0,0 +1,26 @@ +from test.worlds.zillion import ZillionTestBase + +from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, validate +from zilliandomizer.options import VBLR_CHOICES + + +class OptionsTest(ZillionTestBase): + auto_construct = False + + def test_validate_default(self) -> None: + self.world_setup() + validate(self.world, 1) + + def test_vblr_ap_to_zz(self) -> None: + """ all of the valid values for the AP options map to valid values for ZZ options """ + for option_name, vblr_class in ( + ("jump_levels", ZillionJumpLevels), + ("gun_levels", ZillionGunLevels) + ): + for value in vblr_class.name_lookup.values(): + self.options = {option_name: value} + self.world_setup() + zz_options, _item_counts = validate(self.world, 1) + assert getattr(zz_options, option_name) in VBLR_CHOICES + + # TODO: test validate with invalid combinations of options diff --git a/test/worlds/zillion/__init__.py b/test/worlds/zillion/__init__.py new file mode 100644 index 0000000000..7b2ec66927 --- /dev/null +++ b/test/worlds/zillion/__init__.py @@ -0,0 +1,5 @@ +from test.worlds.test_base import WorldTestBase + + +class ZillionTestBase(WorldTestBase): + game = "Zillion" diff --git a/worlds/dkc3/Names/__init__.py b/worlds/dkc3/Names/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/rogue_legacy/Names/__init__.py b/worlds/rogue_legacy/Names/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py new file mode 100644 index 0000000000..32b84015f1 --- /dev/null +++ b/worlds/zillion/__init__.py @@ -0,0 +1,395 @@ +from collections import deque, Counter +from contextlib import redirect_stdout +import functools +from typing import Any, Dict, List, Set, Tuple, Optional, cast +import os +import logging + +from BaseClasses import ItemClassification, LocationProgressType, \ + MultiWorld, Item, CollectionState, RegionType, \ + Entrance, Tutorial +from Options import AssembleOptions +from .logic import cs_to_zz_locs +from .region import ZillionLocation, ZillionRegion +from .options import zillion_options, validate +from .id_maps import item_name_to_id as _item_name_to_id, \ + loc_name_to_id as _loc_name_to_id, make_id_to_others, \ + zz_reg_name_to_reg_name, base_id +from .item import ZillionItem +from .patch import ZillionDeltaPatch, get_base_rom_path + +from zilliandomizer.randomizer import Randomizer as ZzRandomizer +from zilliandomizer.system import System +from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem +from zilliandomizer.logic_components.locations import Location as ZzLocation, Req +from zilliandomizer.options import Chars + +from ..AutoWorld import World, WebWorld + + +class ZillionWebWorld(WebWorld): + theme = "stone" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Zillion randomizer.", + "English", + "setup_en.md", + "setup/en", + ["beauxq"] + )] + + +class ZillionWorld(World): + """ + Zillion is a metroidvania style game released in 1987 for the 8-bit Sega Master System. + + It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion). + """ + game = "Zillion" + web = ZillionWebWorld() + + option_definitions: Dict[str, AssembleOptions] = zillion_options + topology_present: bool = True # indicate if world type has any meaningful layout/pathing + + # map names to their IDs + item_name_to_id: Dict[str, int] = _item_name_to_id + location_name_to_id: Dict[str, int] = _loc_name_to_id + + # increment this every time something in your world's names/id mappings changes. + # While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be + # retrieved by clients on every connection. + data_version: int = 1 + + # NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set. + # These values will be removed. + # if a world is set to remote_items, then it just needs to send location checks to the server and the server + # sends back the items + # if a world is set to remote_items = False, then the server never sends an item where receiver == finder, + # the client finds its own items in its own world. + remote_items: bool = False + + logger: logging.Logger + + class LogStreamInterface: + logger: logging.Logger + buffer: List[str] + + def __init__(self, logger: logging.Logger) -> None: + self.logger = logger + self.buffer = [] + + def write(self, msg: str) -> None: + if msg.endswith('\n'): + self.buffer.append(msg[:-1]) + self.logger.debug("".join(self.buffer)) + self.buffer = [] + else: + self.buffer.append(msg) + + def flush(self) -> None: + pass + + lsi: LogStreamInterface + + id_to_zz_item: Optional[Dict[int, ZzItem]] = None + zz_system: System + _item_counts: "Counter[str]" = Counter() + """ + These are the items counts that will be in the game, + which might be different from the item counts the player asked for in options + (if the player asked for something invalid). + """ + my_locations: List[ZillionLocation] = [] + """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.logger = logging.getLogger("Zillion") + self.lsi = ZillionWorld.LogStreamInterface(self.logger) + self.zz_system = System() + + def _make_item_maps(self, start_char: Chars) -> None: + _id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char) + self.id_to_zz_item = id_to_zz_item + + @classmethod + def stage_assert_generate(cls, world: MultiWorld) -> None: + """Checks that a game is capable of generating, usually checks for some base file like a ROM. + Not run for unittests since they don't produce output""" + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + + def generate_early(self) -> None: + if not hasattr(self.world, "zillion_logic_cache"): + setattr(self.world, "zillion_logic_cache", {}) + + zz_op, item_counts = validate(self.world, self.player) + + self._item_counts = item_counts + + rom_dir_name = os.path.dirname(get_base_rom_path()) + with redirect_stdout(self.lsi): # type: ignore + self.zz_system.make_patcher(rom_dir_name) + self.zz_system.make_randomizer(zz_op) + + self.zz_system.make_map() + + # just in case the options changed anything (I don't think they do) + assert self.zz_system.randomizer, "init failed" + for zz_name in self.zz_system.randomizer.locations: + if zz_name != 'main': + assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \ + f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map" + + self._make_item_maps(zz_op.start_char) + + def create_regions(self) -> None: + assert self.zz_system.randomizer, "generate_early hasn't been called" + assert self.id_to_zz_item, "generate_early hasn't been called" + p = self.player + w = self.world + self.my_locations = [] + + self.zz_system.randomizer.place_canister_gun_reqs() + + start = self.zz_system.randomizer.regions['start'] + + all: Dict[str, ZillionRegion] = {} + for here_zz_name, zz_r in self.zz_system.randomizer.regions.items(): + here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name) + all[here_name] = ZillionRegion(zz_r, here_name, RegionType.Generic, here_name, p, w) + self.world.regions.append(all[here_name]) + + limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126) + queue = deque([start]) + done: Set[str] = set() + while len(queue): + zz_here = queue.popleft() + here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name) + if here_name in done: + continue + here = all[here_name] + + for zz_loc in zz_here.locations: + # if local gun reqs didn't place "keyword" item + if not zz_loc.item: + + def access_rule_wrapped(zz_loc_local: ZzLocation, + p: int, + zz_r: ZzRandomizer, + id_to_zz_item: Dict[int, ZzItem], + cs: CollectionState) -> bool: + accessible = cs_to_zz_locs(cs, p, zz_r, id_to_zz_item) + return zz_loc_local in accessible + + access_rule = functools.partial(access_rule_wrapped, + zz_loc, self.player, self.zz_system.randomizer, self.id_to_zz_item) + + loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name] + loc = ZillionLocation(zz_loc, self.player, loc_name, here) + loc.access_rule = access_rule + if not (limited_skill >= zz_loc.req): + loc.progress_type = LocationProgressType.EXCLUDED + self.world.exclude_locations[p].value.add(loc.name) + here.locations.append(loc) + self.my_locations.append(loc) + + for zz_dest in zz_here.connections.keys(): + dest_name = "Menu" if zz_dest.name == 'start' else zz_reg_name_to_reg_name(zz_dest.name) + dest = all[dest_name] + exit = Entrance(p, f"{here_name} to {dest_name}", here) + here.exits.append(exit) + exit.connect(dest) + + queue.append(zz_dest) + done.add(here.name) + + def create_items(self) -> None: + if not self.id_to_zz_item: + self._make_item_maps("JJ") + self.logger.warning("warning: called `create_items` without calling `generate_early` first") + assert self.id_to_zz_item, "failed to get item maps" + + # in zilliandomizer, the Randomizer class puts empties in the item pool to fill space, + # but here in AP, empties are in the options from options.validate + item_counts = self._item_counts + self.logger.debug(item_counts) + + for item_name, item_id in self.item_name_to_id.items(): + zz_item = self.id_to_zz_item[item_id] + if item_id >= (4 + base_id): # normal item + if item_name in item_counts: + count = item_counts[item_name] + self.logger.debug(f"Zillion Items: {item_name} {count}") + self.world.itempool += [self.create_item(item_name) for _ in range(count)] + elif item_id < (3 + base_id) and zz_item.code == RESCUE: + # One of the 3 rescues will not be in the pool and its zz_item will be 'empty'. + self.logger.debug(f"Zillion Items: {item_name} 1") + self.world.itempool.append(self.create_item(item_name)) + + def set_rules(self) -> None: + # logic for this game is in create_regions + pass + + def generate_basic(self) -> None: + assert self.zz_system.randomizer, "generate_early hasn't been called" + # main location name is an alias + main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations['main'].name] + + self.world.get_location(main_loc_name, self.player)\ + .place_locked_item(self.create_item("Win")) + self.world.completion_condition[self.player] = \ + lambda state: state.has("Win", self.player) + + def post_fill(self) -> None: + """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. + This happens before progression balancing, so the items may not be in their final locations yet.""" + + self.zz_system.post_fill() + + def finalize_item_locations(self) -> None: + """ + sync zilliandomizer item locations with AP item locations + """ + assert self.zz_system.randomizer and self.zz_system.patcher, "generate_early hasn't been called" + zz_options = self.zz_system.randomizer.options + + # debug_zz_loc_ids: Dict[str, int] = {} + empty = zz_items[4] + multi_item = empty # a different patcher method differentiates empty from ap multi item + multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) + for loc in self.world.get_locations(): + if loc.player == self.player: + z_loc = cast(ZillionLocation, loc) + # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) + if z_loc.item is None: + self.logger.warn("generate_output location has no item - is that ok?") + z_loc.zz_loc.item = empty + elif z_loc.item.player == self.player: + z_item = cast(ZillionItem, z_loc.item) + z_loc.zz_loc.item = z_item.zz_item + else: # another player's item + # print(f"put multi item in {z_loc.zz_loc.name}") + z_loc.zz_loc.item = multi_item + multi_items[z_loc.zz_loc.name] = ( + z_loc.item.name, + self.world.get_player_name(z_loc.item.player) + ) + # debug_zz_loc_ids.sort() + # for name, id_ in debug_zz_loc_ids.items(): + # print(id_) + # print("size:", len(debug_zz_loc_ids)) + + # debug_loc_to_id: Dict[str, int] = {} + # regions = self.zz_randomizer.regions + # for region in regions.values(): + # for loc in region.locations: + # if loc.name not in self.zz_randomizer.locations: + # print(f"region {region.name} had location {loc.name} not in locations") + # debug_loc_to_id[loc.name] = id(loc) + + # verify that every location got an item + for zz_loc in self.zz_system.randomizer.locations.values(): + assert zz_loc.item, ( + f"location {self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name]} " + f"in world {self.player} didn't get an item" + ) + + zz_patcher = self.zz_system.patcher + + zz_patcher.write_locations(self.zz_system.randomizer.regions, + zz_options.start_char, + self.zz_system.randomizer.loc_name_2_pretty) + zz_patcher.all_fixes_and_options(zz_options) + zz_patcher.set_external_item_interface(zz_options.start_char, zz_options.max_level) + zz_patcher.set_multiworld_items(multi_items) + zz_patcher.set_rom_to_ram_data(self.world.player_name[self.player].replace(' ', '_').encode()) + + def generate_output(self, output_directory: str) -> None: + """This method gets called from a threadpool, do not use world.random here. + If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" + self.finalize_item_locations() + + assert self.zz_system.patcher, "didn't get patcher from generate_early" + # original_rom_bytes = self.zz_patcher.rom + patched_rom_bytes = self.zz_system.patcher.get_patched_bytes() + + out_file_base = self.world.get_out_file_name_base(self.player) + + filename = os.path.join( + output_directory, + f'{out_file_base}{ZillionDeltaPatch.result_file_ending}' + ) + with open(filename, "wb") as binary_file: + binary_file.write(patched_rom_bytes) + patch = ZillionDeltaPatch( + os.path.splitext(filename)[0] + ZillionDeltaPatch.patch_file_ending, + player=self.player, + player_name=self.world.player_name[self.player], + patched_path=filename + ) + patch.write() + os.remove(filename) + + def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot + """Fill in the `slot_data` field in the `Connected` network package. + This is a way the generator can give custom data to the client. + The client will receive this as JSON in the `Connected` response.""" + + # TODO: share a TypedDict data structure with client + + # TODO: tell client which canisters are keywords + # so it can open and get those when restoring doors + + zz_patcher = self.zz_system.patcher + assert zz_patcher, "didn't get patcher from generate_early" + assert self.zz_system.randomizer, "didn't get randomizer from generate_early" + + rescues: Dict[str, Any] = {} + for i in (0, 1): + if i in zz_patcher.rescue_locations: + ri = zz_patcher.rescue_locations[i] + rescues[str(i)] = { + "start_char": ri.start_char, + "room_code": ri.room_code, + "mask": ri.mask + } + return { + "start_char": self.zz_system.randomizer.options.start_char, + "rescues": rescues, + "loc_mem_to_id": zz_patcher.loc_memory_to_loc_id + } + + # def modify_multidata(self, multidata: Dict[str, Any]) -> None: + # """For deeper modification of server multidata.""" + # # not modifying multidata, just want to call this at the end of the generation process + # cache = getattr(self.world, "zillion_logic_cache") + # import sys + # print(sys.getsizeof(cache)) + + # end of ordered Main.py calls + + def create_item(self, name: str) -> Item: + """Create an item for this world type and player. + Warning: this may be called with self.world = None, for example by MultiServer""" + item_id = _item_name_to_id[name] + + if not self.id_to_zz_item: + self._make_item_maps("JJ") + self.logger.warning("warning: called `create_item` without calling `generate_early` first") + assert self.id_to_zz_item, "failed to get item maps" + + classification = ItemClassification.filler + zz_item = self.id_to_zz_item[item_id] + if zz_item.required: + classification = ItemClassification.progression + if not zz_item.is_progression: + classification = ItemClassification.progression_skip_balancing + + z_item = ZillionItem(name, classification, item_id, self.player, zz_item) + return z_item + + def get_filler_item_name(self) -> str: + """Called when the item pool needs to be filled with additional items to match location count.""" + return "Empty" diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py new file mode 100644 index 0000000000..e08c4f4278 --- /dev/null +++ b/worlds/zillion/config.py @@ -0,0 +1 @@ +base_id = 8675309 diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md new file mode 100644 index 0000000000..b5d37cc202 --- /dev/null +++ b/worlds/zillion/docs/en_Zillion.md @@ -0,0 +1,74 @@ +# Zillion + +Zillion is a metroidvania-style game released in 1987 for the 8-bit Sega Master System. + +It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion). + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What changes are made to this game? + +The way the original game lets the player choose who to level up has a few drawbacks in a multiworld randomizer: + - Possible softlock from making bad choices (example: nobody has jump 3 when it's required) + - In multiworld, you won't be able to choose because you won't know it's coming beforehand. + +So this randomizer uses a new level-up system: + - Everyone levels up together (even if they're not rescued yet). + - You can choose how many opa-opas are required for a level up. + - You can set a max level from 1 to 8. + - The currently active character is still the only one that gets the health refill. + +--- + +You can set these options to choose when characters will be able to attain certain jump levels: + +``` +jump levels + +vanilla balanced low restrictive + +jj ap ch jj ap ch jj ap ch jj ap ch +2 3 1 1 2 1 1 1 1 1 1 1 +2 3 1 2 2 1 1 2 1 1 1 1 +2 3 1 2 3 1 2 2 1 1 2 1 +2 3 1 2 3 2 2 3 1 1 2 1 +3 3 2 3 3 2 2 3 2 2 2 1 +3 3 2 3 3 2 3 3 2 2 2 1 +3 3 3 3 3 3 3 3 2 2 3 1 +3 3 3 3 3 3 3 3 3 2 3 2 +``` + +Note that in "restrictive" mode, Apple is the only one that can get jump level 3. + +--- + +You can set these options to choose when characters will be able to attain certain Zillion power (gun) levels: + +``` +zillion power + +vanilla balanced low restrictive + +jj ap ch jj ap ch jj ap ch jj ap ch +1 1 3 1 1 2 1 1 1 1 1 1 +2 2 3 2 1 2 1 1 2 1 1 2 +3 3 3 2 2 3 2 1 2 2 1 2 + 3 2 3 2 1 3 2 1 3 + 3 3 3 2 2 3 2 2 3 + 3 2 3 + 3 3 3 +``` + +Note that in "restrictive" mode, Champ is the only one that can get Zillion power level 3. + +## What does another world's item look like in Zillion? + +Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it. + +When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected. + +## When the player receives an item, what happens? + +The item collect sound is played. You can see in the client log what item was received. diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md new file mode 100644 index 0000000000..43a1296748 --- /dev/null +++ b/worlds/zillion/docs/setup_en.md @@ -0,0 +1,104 @@ +# Zillion Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Zillion Client - Zillion Patch Setup` + +- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). + +- Your legally obtained Zillion ROM file, named `Zillion (UE) [!].sms` + +## Installation Procedures + +### RetroArch + +RetroArch 1.9.x will not work, as it is older than 1.10.3. + +1. Enter the RetroArch main menu screen. +2. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and install "Sega - MS/GG (SMS Plus GX)". +3. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +4. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) + +### Linux Setup + +Put your Zillion ROM file in the Archipelago directory in your home directory. + +### Windows Setup + +1. During the installation of Archipelago, install the Zillion Client. If you did not do this, + or you are on an older version, you may run the installer again to install the Zillion Client. +2. During setup, you will be asked to locate your base ROM file. This is the Zillion ROM file mentioned above in Required Software. + +--- +# Play + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The [player settings page](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from +them. + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck). + +## Generating a Single-Player Game + +1. Navigate to the [player settings page](/games/Zillion/player-settings), configure your options, and click the "Generate Game" button. +2. A "Seed Info" page will appear. +3. Click the "Create New Room" link. +4. A server page will appear. Download your patch file from this page. +5. Patch your ROM file. + - Linux + - In the launcher, choose "Open Patch" and select your patch file. + - Windows + - Double-click on your patch file. + The Zillion Client will launch automatically, and create your ROM in the location of the patch file. +6. Open the ROM in RetroArch using the core "SMS Plus GX". + - For a single player game, any emulator (or a Sega Master System) can be used, but there are additional features with RetroArch and the Zillion Client. + - If you press reset or restore a save state and return to the surface in the game, the Zillion Client will keep open all the doors that you have opened. + +## Joining a MultiWorld Game + +1. Provide your config (yaml) file to the host and obtain your patch file. + - When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch files. Your patch file should have a `.apzl` extension. + - If you activate the "room generation" option in your config (yaml), you might want to tell your host that the generation will take longer than normal. It takes approximately 20 seconds longer for each Zillion player that enables this option. +2. Create your ROM. + - Linux + - In the Archipelago Launcher, choose "Open Patch" and select your `.apzl` patch file. + - Windows + - Put your patch file on your desktop or somewhere convenient, and double click it. + - This should automatically launch the client, and will also create your ROM in the same place as your patch file. +3. Connect to the client. + - Use RetroArch to open the ROM that was generated. + - Be sure to select the **SMS Plus GX** core. This core will allow external tools to read RAM data. +4. Connect to the Archipelago Server. + - The patch file which launched your client should have automatically connected you to the AP Server. There are a few reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press enter. + - The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". +5. Play the game. + - When the client shows both Game and Server as connected, you're ready to begin playing. Congratulations on successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the [Generation page](/generate). + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, a "Seed Info" page will appear. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. diff --git a/worlds/zillion/id_maps.py b/worlds/zillion/id_maps.py new file mode 100644 index 0000000000..bc9caeeece --- /dev/null +++ b/worlds/zillion/id_maps.py @@ -0,0 +1,93 @@ +from typing import Dict, Tuple +from zilliandomizer.logic_components.items import Item as ZzItem, \ + item_name_to_id as zz_item_name_to_zz_id, items as zz_items, \ + item_name_to_item as zz_item_name_to_zz_item +from zilliandomizer.options import Chars +from zilliandomizer.utils.loc_name_maps import loc_to_id as pretty_loc_name_to_id +from zilliandomizer.utils import parse_reg_name +from .config import base_id as base_id + +item_name_to_id = { + "Apple": 0 + base_id, + "Champ": 1 + base_id, + "JJ": 2 + base_id, + "Win": 3 + base_id, + "Empty": 4 + base_id, + "ID Card": 5 + base_id, + "Red ID Card": 6 + base_id, + "Floppy Disk": 7 + base_id, + "Bread": 8 + base_id, + "Opa-Opa": 9 + base_id, + "Zillion": 10 + base_id, + "Scope": 11 + base_id, +} + + +_zz_rescue_0 = zz_item_name_to_zz_item["rescue_0"] +_zz_rescue_1 = zz_item_name_to_zz_item["rescue_1"] +_zz_empty = zz_item_name_to_zz_item["empty"] + + +def make_id_to_others(start_char: Chars) -> Tuple[ + Dict[int, str], Dict[int, int], Dict[int, ZzItem] +]: + """ returns id_to_name, id_to_zz_id, id_to_zz_item """ + id_to_name: Dict[int, str] = {} + id_to_zz_id: Dict[int, int] = {} + id_to_zz_item: Dict[int, ZzItem] = {} + + if start_char == "JJ": + name_to_zz_item = { + "Apple": _zz_rescue_0, + "Champ": _zz_rescue_1, + "JJ": _zz_empty + } + elif start_char == "Apple": + name_to_zz_item = { + "Apple": _zz_empty, + "Champ": _zz_rescue_1, + "JJ": _zz_rescue_0 + } + else: # Champ + name_to_zz_item = { + "Apple": _zz_rescue_0, + "Champ": _zz_empty, + "JJ": _zz_rescue_1 + } + + for name, ap_id in item_name_to_id.items(): + id_to_name[ap_id] = name + + if ap_id >= 4 + base_id: + index = ap_id - base_id + zz_item = zz_items[index] + assert zz_item.id == index and zz_item.name == name + elif ap_id < 3 + base_id: + # rescue + assert name in {"Apple", "Champ", "JJ"} + zz_item = name_to_zz_item[name] + else: # main + zz_item = zz_item_name_to_zz_item["main"] + + id_to_zz_id[ap_id] = zz_item_name_to_zz_id[zz_item.debug_name] + id_to_zz_item[ap_id] = zz_item + + return id_to_name, id_to_zz_id, id_to_zz_item + + +def make_room_name(row: int, col: int) -> str: + return f"{chr(ord('A') + row - 1)}-{col + 1}" + + +loc_name_to_id: Dict[str, int] = { + name: id_ + base_id + for name, id_ in pretty_loc_name_to_id.items() +} + + +def zz_reg_name_to_reg_name(zz_reg_name: str) -> str: + if zz_reg_name[0] == 'r' and zz_reg_name[3] == 'c': + row, col = parse_reg_name(zz_reg_name) + end = zz_reg_name[5:] + return f"{make_room_name(row, col)} {end.upper()}" + return zz_reg_name diff --git a/worlds/zillion/item.py b/worlds/zillion/item.py new file mode 100644 index 0000000000..fdf0fa8ba2 --- /dev/null +++ b/worlds/zillion/item.py @@ -0,0 +1,12 @@ +from BaseClasses import Item, ItemClassification as IC +from zilliandomizer.logic_components.items import Item as ZzItem + + +class ZillionItem(Item): + game = "Zillion" + __slots__ = ("zz_item",) + zz_item: ZzItem + + def __init__(self, name: str, classification: IC, code: int, player: int, zz_item: ZzItem) -> None: + super().__init__(name, classification, code, player) + self.zz_item = zz_item diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py new file mode 100644 index 0000000000..01ed14346a --- /dev/null +++ b/worlds/zillion/logic.py @@ -0,0 +1,77 @@ +from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter +from BaseClasses import CollectionState +from zilliandomizer.logic_components.locations import Location +from zilliandomizer.randomizer import Randomizer +from zilliandomizer.logic_components.items import Item, items +from .region import ZillionLocation +from .item import ZillionItem +from .id_maps import item_name_to_id + +zz_empty = items[4] + +# TODO: unit tests for these + + +def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int: + """ + sync up zilliandomizer locations with archipelago locations + + returns a hash of the player and of the set locations with their items + """ + z_world = cs.world.worlds[p] + my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations")) + + _hash = p + for z_loc in my_locations: + zz_name = z_loc.zz_loc.name + zz_item = z_loc.item.zz_item \ + if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \ + else zz_empty + zz_r.locations[zz_name].item = zz_item + _hash += hash(zz_name) ^ hash(zz_item) + return _hash + + +def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: + """ + the zilliandomizer items that player p has collected + + ((item_name, count), (item_name, count), ...) + """ + return tuple((item_name, cs.item_count(item_name, p)) for item_name in item_name_to_id) + + +LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]] + + +def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]: + """ + given an Archipelago `CollectionState`, + returns frozenset of accessible zilliandomizer locations + """ + # caching this function because it would be slow + logic_cache: LogicCacheType = getattr(cs.world, "zillion_logic_cache", {}) + _hash = set_randomizer_locs(cs, p, zz_r) + counts = item_counts(cs, p) + _hash += hash(counts) + + if _hash in logic_cache and logic_cache[_hash][0] == cs.prog_items: + # print("cache hit") + return logic_cache[_hash][1] + + # print("cache miss") + have_items: List[Item] = [] + for name, count in counts: + have_items.extend([id_to_zz_item[item_name_to_id[name]]] * count) + # have_req is the result of converting AP CollectionState to zilliandomizer collection state + have_req = zz_r.make_ability(have_items) + + # This `get_locations` is where the core of the logic comes in. + # It takes a zilliandomizer collection state (a set of the abilities that I have) + # and returns list of all the zilliandomizer locations I can access with those abilities. + tr = frozenset(zz_r.get_locations(have_req)) + + # save result in cache + logic_cache[_hash] = (cs.prog_items.copy(), tr) + + return tr diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py new file mode 100644 index 0000000000..4e7d3b6a70 --- /dev/null +++ b/worlds/zillion/options.py @@ -0,0 +1,380 @@ +from collections import Counter +# import logging +from typing import TYPE_CHECKING, Any, Dict, Tuple, cast +from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice +from zilliandomizer.options import \ + Options as ZzOptions, char_to_gun, char_to_jump, ID, \ + VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts +from zilliandomizer.options.parsing import validate as zz_validate +if TYPE_CHECKING: + from BaseClasses import MultiWorld + + +class ZillionContinues(SpecialRange): + """ + number of continues before game over + + game over teleports you to your ship, keeping items and open doors + """ + default = 3 + range_start = 0 + range_end = 21 + display_name = "continues" + special_range_names = { + "vanilla": 3, + "infinity": 21 + } + + +class ZillionEarlyScope(Toggle): + """ whether to make sure there is a scope available early """ + display_name = "early scope" + + +class ZillionFloppyReq(Range): + """ how many floppy disks are required """ + range_start = 0 + range_end = 8 + default = 5 + display_name = "floppies required" + + +class VBLR(Choice): + option_vanilla = 0 + option_balanced = 1 + option_low = 2 + option_restrictive = 3 + default = 1 + + +class ZillionGunLevels(VBLR): + """ + Zillion gun power for the number of Zillion power ups you pick up + + For "restrictive", Champ is the only one that can get Zillion gun power level 3. + """ + display_name = "gun levels" + + +class ZillionJumpLevels(VBLR): + """ + jump levels for each character level + + For "restrictive", Apple is the only one that can get jump level 3. + """ + display_name = "jump levels" + + +class ZillionRandomizeAlarms(DefaultOnToggle): + """ whether to randomize the locations of alarm sensors """ + display_name = "randomize alarms" + + +class ZillionMaxLevel(Range): + """ the highest level you can get """ + range_start = 3 + range_end = 8 + default = 8 + display_name = "max level" + + +class ZillionOpasPerLevel(Range): + """ + how many Opa-Opas are required to level up + + Lower makes you level up faster. + """ + range_start = 1 + range_end = 5 + default = 2 + display_name = "Opa-Opas per level" + + +class ZillionStartChar(Choice): + """ which character you start with """ + option_jj = 0 + option_apple = 1 + option_champ = 2 + display_name = "start character" + default = "random" + + +class ZillionIDCardCount(Range): + """ + how many ID Cards are in the game + + Vanilla is 63 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 42 + display_name = "ID Card count" + + +class ZillionBreadCount(Range): + """ + how many Breads are in the game + + Vanilla is 33 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 50 + display_name = "Bread count" + + +class ZillionOpaOpaCount(Range): + """ + how many Opa-Opas are in the game + + Vanilla is 26 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 26 + display_name = "Opa-Opa count" + + +class ZillionZillionCount(Range): + """ + how many Zillion gun power ups are in the game + + Vanilla is 6 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 8 + display_name = "Zillion power up count" + + +class ZillionFloppyDiskCount(Range): + """ + how many Floppy Disks are in the game + + Vanilla is 5 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 7 + display_name = "Floppy Disk count" + + +class ZillionScopeCount(Range): + """ + how many Scopes are in the game + + Vanilla is 4 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 4 + display_name = "Scope count" + + +class ZillionRedIDCardCount(Range): + """ + how many Red ID Cards are in the game + + Vanilla is 1 + + maximum total for all items is 144 + """ + range_start = 0 + range_end = 126 + default = 2 + display_name = "Red ID Card count" + + +class ZillionSkill(Range): + """ the difficulty level of the game """ + range_start = 0 + range_end = 5 + default = 2 + + +class ZillionStartingCards(SpecialRange): + """ + how many ID Cards to start the game with + + Refilling at the ship also ensures you have at least this many cards. + 0 gives vanilla behavior. + """ + default = 2 + range_start = 0 + range_end = 10 + display_name = "starting cards" + special_range_names = { + "vanilla": 0 + } + + +class ZillionRoomGen(Toggle): + """ whether to generate rooms with random terrain """ + display_name = "room generation" + + +zillion_options: Dict[str, AssembleOptions] = { + "continues": ZillionContinues, + # "early_scope": ZillionEarlyScope, # TODO: implement + "floppy_req": ZillionFloppyReq, + "gun_levels": ZillionGunLevels, + "jump_levels": ZillionJumpLevels, + "randomize_alarms": ZillionRandomizeAlarms, + "max_level": ZillionMaxLevel, + "start_char": ZillionStartChar, + "opas_per_level": ZillionOpasPerLevel, + "id_card_count": ZillionIDCardCount, + "bread_count": ZillionBreadCount, + "opa_opa_count": ZillionOpaOpaCount, + "zillion_count": ZillionZillionCount, + "floppy_disk_count": ZillionFloppyDiskCount, + "scope_count": ZillionScopeCount, + "red_id_card_count": ZillionRedIDCardCount, + "skill": ZillionSkill, + "starting_cards": ZillionStartingCards, + "room_gen": ZillionRoomGen, +} + + +def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: + tr: ZzItemCounts = { + ID.card: ic["ID Card"], + ID.red: ic["Red ID Card"], + ID.floppy: ic["Floppy Disk"], + ID.bread: ic["Bread"], + ID.gun: ic["Zillion"], + ID.opa: ic["Opa-Opa"], + ID.scope: ic["Scope"], + ID.empty: ic["Empty"], + } + return tr + + +def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": + """ + adjusts options to make game completion possible + + `world` parameter is MultiWorld object that has my options on it + `p` is my player id + """ + for option_name in zillion_options: + assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object" + wo = cast(Any, world) # so I don't need getattr on all the options + + skill = wo.skill[p].value + + jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p]) + jump_option = jump_levels.get_current_option_name().lower() + required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1 + if skill == 0: + # because of hp logic on final boss + required_level = 8 + + gun_levels = cast(ZillionGunLevels, wo.gun_levels[p]) + gun_option = gun_levels.get_current_option_name().lower() + guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3) + + floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p]) + + card = cast(ZillionIDCardCount, wo.id_card_count[p]) + bread = cast(ZillionBreadCount, wo.bread_count[p]) + opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p]) + gun = cast(ZillionZillionCount, wo.zillion_count[p]) + floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p]) + scope = cast(ZillionScopeCount, wo.scope_count[p]) + red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p]) + item_counts = Counter({ + "ID Card": card, + "Bread": bread, + "Opa-Opa": opa, + "Zillion": gun, + "Floppy Disk": floppy, + "Scope": scope, + "Red ID Card": red + }) + minimums = Counter({ + "ID Card": 0, + "Bread": 0, + "Opa-Opa": required_level - 1, + "Zillion": guns_required, + "Floppy Disk": floppy_req.value, + "Scope": 0, + "Red ID Card": 1 + }) + for key in minimums: + item_counts[key] = max(minimums[key], item_counts[key]) + max_movables = 144 - sum(minimums.values()) + movables = item_counts - minimums + while sum(movables.values()) > max_movables: + # logging.warning("zillion options validate: player options item counts too high") + total = sum(movables.values()) + scaler = max_movables / total + for key in movables: + movables[key] = int(movables[key] * scaler) + item_counts = movables + minimums + + # now have required items, and <= 144 + + # now fill remaining with empty + total = sum(item_counts.values()) + diff = 144 - total + if "Empty" not in item_counts: + item_counts["Empty"] = 0 + item_counts["Empty"] += diff + assert sum(item_counts.values()) == 144 + + max_level = cast(ZillionMaxLevel, wo.max_level[p]) + max_level.value = max(required_level, max_level.value) + + opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p]) + while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value): + # logging.warning( + # "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count" + # ) + opas_per_level.value -= 1 + + # that should be all of the level requirements met + + start_char = cast(ZillionStartChar, wo.start_char[p]) + start_char_name = start_char.get_current_option_name() + if start_char_name == "Jj": + start_char_name = "JJ" + assert start_char_name in chars + start_char_name = cast(Chars, start_char_name) + + starting_cards = cast(ZillionStartingCards, wo.starting_cards[p]) + + room_gen = cast(ZillionRoomGen, wo.room_gen[p]) + + zz_item_counts = convert_item_counts(item_counts) + zz_op = ZzOptions( + zz_item_counts, + cast(ZzVBLR, jump_option), + cast(ZzVBLR, gun_option), + opas_per_level.value, + max_level.value, + False, # tutorial + skill, + start_char_name, + floppy_req.value, + wo.continues[p].value, + wo.randomize_alarms[p].value, + False, # wo.early_scope[p].value, + True, # balance defense + starting_cards.value, + bool(room_gen.value) + ) + zz_validate(zz_op) + return zz_op, item_counts diff --git a/worlds/zillion/patch.py b/worlds/zillion/patch.py new file mode 100644 index 0000000000..148caac9fb --- /dev/null +++ b/worlds/zillion/patch.py @@ -0,0 +1,34 @@ +from typing import BinaryIO, Optional, cast +import Utils +from worlds.Files import APDeltaPatch +import os + +USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270' + + +class ZillionDeltaPatch(APDeltaPatch): + hash = USHASH + game = "Zillion" + patch_file_ending = ".apzl" + result_file_ending = ".sms" + + @classmethod + def get_source_data(cls) -> bytes: + with open(get_base_rom_path(), "rb") as stream: + return read_rom(stream) + + +def get_base_rom_path(file_name: Optional[str] = None) -> str: + options = Utils.get_options() + if not file_name: + file_name = cast(str, options["zillion_options"]["rom_file"]) + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name + + +def read_rom(stream: BinaryIO) -> bytes: + """ reads rom into bytearray """ + data = stream.read() + # I'm not aware of any sms header. + return data diff --git a/worlds/zillion/py.typed b/worlds/zillion/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/zillion/region.py b/worlds/zillion/region.py new file mode 100644 index 0000000000..53949f7336 --- /dev/null +++ b/worlds/zillion/region.py @@ -0,0 +1,50 @@ +from typing import Optional +from BaseClasses import MultiWorld, Region, RegionType, Location, Item, CollectionState +from zilliandomizer.logic_components.regions import Region as ZzRegion +from zilliandomizer.logic_components.locations import Location as ZzLocation +from zilliandomizer.logic_components.items import RESCUE + +from .id_maps import loc_name_to_id +from .item import ZillionItem + + +class ZillionRegion(Region): + zz_r: ZzRegion + + def __init__(self, + zz_r: ZzRegion, + name: str, + type_: RegionType, + hint: str, + player: int, + world: Optional[MultiWorld] = None) -> None: + super().__init__(name, type_, hint, player, world) + self.zz_r = zz_r + + +class ZillionLocation(Location): + zz_loc: ZzLocation + game: str = "Zillion" + + def __init__(self, + zz_loc: ZzLocation, + player: int, + name: str, + parent: Optional[Region] = None) -> None: + loc_id = loc_name_to_id[name] + super().__init__(player, name, loc_id, parent) + self.zz_loc = zz_loc + + # override + def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: + saved_gun_req = -1 + if isinstance(item, ZillionItem) \ + and item.zz_item.code == RESCUE \ + and self.player == item.player: + # RESCUE removes the gun requirement from a location. + saved_gun_req = self.zz_loc.req.gun + self.zz_loc.req.gun = 0 + super_result = super().can_fill(state, item, check_access) + if saved_gun_req != -1: + self.zz_loc.req.gun = saved_gun_req + return super_result diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt new file mode 100644 index 0000000000..0ed98771bd --- /dev/null +++ b/worlds/zillion/requirements.txt @@ -0,0 +1 @@ +git+https://github.com/beauxq/zilliandomizer@45a45eaca4119a4d06d2c31546ad19f3abd77f63#egg=zilliandomizer==0.4.4 From 47b4e2782bcb83eb541229e0de500735f0a13102 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Thu, 20 Oct 2022 21:10:38 -0400 Subject: [PATCH 078/105] WebHost: Fix weighted-settings to not save full set of range options to localStorage (#1100) --- WebHostLib/static/assets/weighted-settings.js | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index a438d0c64a..da4d60fcad 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -78,13 +78,16 @@ const createDefaultSettings = (settingData) => { break; case 'range': case 'special_range': - for (let i = setting.min; i <= setting.max; ++i){ - newSettings[game][gameSetting][i] = - (setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0; - } + newSettings[game][gameSetting][setting.min] = 0; + newSettings[game][gameSetting][setting.max] = 0; newSettings[game][gameSetting]['random'] = 0; newSettings[game][gameSetting]['random-low'] = 0; newSettings[game][gameSetting]['random-high'] = 0; + if (setting.hasOwnProperty('defaultValue')) { + newSettings[game][gameSetting][setting.defaultValue] = 25; + } else { + newSettings[game][gameSetting][setting.min] = 25; + } break; case 'items-list': @@ -401,11 +404,17 @@ const buildWeightedSettingsDiv = (game, settings) => { tr.appendChild(tdDelete); rangeTbody.appendChild(tr); + + // Save new option to settings + range.dispatchEvent(new Event('change')); }); Object.keys(currentSettings[game][settingName]).forEach((option) => { - if (currentSettings[game][settingName][option] > 0) { - const tr = document.createElement('tr'); + // These options are statically generated below, and should always appear even if they are deleted + // from localStorage + if (['random-low', 'random', 'random-high'].includes(option)) { return; } + + const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); tdLeft.innerText = option; @@ -439,14 +448,15 @@ const buildWeightedSettingsDiv = (game, settings) => { deleteButton.innerText = '❌'; deleteButton.addEventListener('click', () => { range.value = 0; - range.dispatchEvent(new Event('change')); + const changeEvent = new Event('change'); + changeEvent.action = 'rangeDelete'; + range.dispatchEvent(changeEvent); rangeTbody.removeChild(tr); }); tdDelete.appendChild(deleteButton); tr.appendChild(tdDelete); rangeTbody.appendChild(tr); - } }); } @@ -904,8 +914,12 @@ const updateGameSetting = (evt) => { const setting = evt.target.getAttribute('data-setting'); const option = evt.target.getAttribute('data-option'); document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value; - options[game][setting][option] = isNaN(evt.target.value) ? - evt.target.value : parseInt(evt.target.value, 10); + console.log(event); + if (evt.action && evt.action === 'rangeDelete') { + delete options[game][setting][option]; + } else { + options[game][setting][option] = parseInt(evt.target.value, 10); + } localStorage.setItem('weighted-settings', JSON.stringify(options)); }; From fa077defe0117dd9612c4aca51af67f5a9ab2774 Mon Sep 17 00:00:00 2001 From: ScootyPuffJr1 <77215594+ScootyPuffJr1@users.noreply.github.com> Date: Thu, 20 Oct 2022 20:28:38 -0400 Subject: [PATCH 079/105] [Factorio] Minor fix for typo on setup doc --- worlds/factorio/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md index 7ee91fb4f1..560a37d1e3 100644 --- a/worlds/factorio/docs/setup_en.md +++ b/worlds/factorio/docs/setup_en.md @@ -118,7 +118,7 @@ This allows you to host your own Factorio game. 3. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located at `C:\Users\YourName\AppData\Roaming\Factorio\mods`. 4. Obtain the Archipelago Server address from the website's host room, or from the server host. -5. Run your Archipelago Client, which is named `ArchilepagoFactorioClient.exe`. This was installed along with +5. Run your Archipelago Client, which is named `ArchipelagoFactorioClient.exe`. This was installed along with Archipelago if you chose to include it during the installation process. 6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter" From 28483a6c1461ef8005c1560d1a5312b14bcf9934 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 20 Oct 2022 23:57:02 +0200 Subject: [PATCH 080/105] Generate: don't try to include meta or filler weights file as player --- Generate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Generate.py b/Generate.py index 57d060d4d4..f19e23700c 100644 --- a/Generate.py +++ b/Generate.py @@ -154,11 +154,12 @@ def main(args=None, callback=ERmain): # sort dict for consistent results across platforms: weights_cache = {key: value for key, value in sorted(weights_cache.items())} for filename, yaml_data in weights_cache.items(): - for yaml in yaml_data: - print(f"P{player_id} Weights: {filename} >> " - f"{get_choice('description', yaml, 'No description specified')}") - player_files[player_id] = filename - player_id += 1 + if filename not in {args.meta_file_path, args.weights_file_path}: + for yaml in yaml_data: + print(f"P{player_id} Weights: {filename} >> " + f"{get_choice('description', yaml, 'No description specified')}") + player_files[player_id] = filename + player_id += 1 args.multi = max(player_id - 1, args.multi) print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: " From 40c3ef35c760b821eb2fd202b071124ad279abb3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 20 Oct 2022 03:35:28 +0200 Subject: [PATCH 081/105] LttP: fix Inverted Big Bomb Shop indirect connection rule --- worlds/alttp/EntranceShuffle.py | 23 +++++++++++++++-------- worlds/alttp/__init__.py | 10 +++++++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index c2b56d6fd4..e10f4d5445 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -3834,14 +3834,21 @@ inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Dese # Regions that can be required to access entrances through rules, not paths indirect_connections = { - 'Turtle Rock (Top)': 'Turtle Rock', - 'East Dark World': 'Pyramid Fairy', - 'Big Bomb Shop': 'Pyramid Fairy', - 'Dark Desert': 'Pyramid Fairy', - 'West Dark World': 'Pyramid Fairy', - 'South Dark World': 'Pyramid Fairy', - 'Light World': 'Pyramid Fairy', - 'Old Man Cave': 'Old Man S&Q' + "Turtle Rock (Top)": "Turtle Rock", + "East Dark World": "Pyramid Fairy", + "Dark Desert": "Pyramid Fairy", + "West Dark World": "Pyramid Fairy", + "South Dark World": "Pyramid Fairy", + "Light World": "Pyramid Fairy", + "Old Man Cave": "Old Man S&Q" +} + +indirect_connections_inverted = { + "Inverted Big Bomb Shop": "Pyramid Fairy", +} + +indirect_connections_not_inverted = { + "Big Bomb Shop": "Pyramid Fairy", } # format: diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 561b489bfa..ce53154e92 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -7,7 +7,8 @@ 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, indirect_connections +from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ + indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted 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 @@ -216,9 +217,15 @@ class ALTTPWorld(World): if world.mode[player] != 'inverted': link_entrances(world, player) mark_light_world_regions(world, player) + for region_name, entrance_name in indirect_connections_not_inverted.items(): + world.register_indirect_condition(self.world.get_region(region_name, player), + self.world.get_entrance(entrance_name, player)) else: link_inverted_entrances(world, player) mark_dark_world_regions(world, player) + for region_name, entrance_name in indirect_connections_inverted.items(): + world.register_indirect_condition(self.world.get_region(region_name, player), + self.world.get_entrance(entrance_name, player)) world.random = old_random plando_connect(world, player) @@ -227,6 +234,7 @@ class ALTTPWorld(World): world.register_indirect_condition(self.world.get_region(region_name, player), self.world.get_entrance(entrance_name, player)) + def collect_item(self, state: CollectionState, item: Item, remove=False): item_name = item.name if item_name.startswith('Progressive '): From 04b6c310768a58d75e33c90e5eb1e5fe3d167ec6 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 21 Oct 2022 23:26:40 +0200 Subject: [PATCH 082/105] SoE: update to v042 and balancing changes (#1125) * SoE: rebalancing and cleanup * ModuleUpdate: make url detection more generic * SoE: change item rules to depend on target player difficulty * SoE: Update to pyevermizer 0.41.0 * adds footknight * adds location difficulty * SoE: minor optimization in item rule if .. in is faster with sets * SoE: drop support of patch format v3 * SoE: fix some typing and warnings * SoE: cleanup imports --- ModuleUpdate.py | 31 ++++++----- worlds/soe/Logic.py | 22 +++++--- worlds/soe/Options.py | 29 ++++++++-- worlds/soe/Patch.py | 31 ++--------- worlds/soe/__init__.py | 102 ++++++++++++++++++++++++++++-------- worlds/soe/requirements.txt | 28 +++++----- 6 files changed, 157 insertions(+), 86 deletions(-) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 0768b37619..2b6aed1f39 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -39,18 +39,25 @@ def update(yes=False, force=False): path = os.path.join(os.path.dirname(__file__), req_file) with open(path) as requirementsfile: for line in requirementsfile: - if line.startswith('https://'): - # extract name and version from url - wheel = line.split('/')[-1] - name, version, _ = wheel.split('-', 2) - line = f'{name}=={version}' - elif line.startswith('git+https://'): - # extract name and version - end = line.split('/')[-1] - name_hash, egg = end.split("#", 1) - name, _ = name_hash.split("@", 1) - version = egg.split('==')[-1] - line = f'{name}=={version}' + if line.startswith(("https://", "git+https://")): + # extract name and version for url + rest = line.split('/')[-1] + line = "" + if "#egg=" in rest: + # from egg info + rest, egg = rest.split("#egg=", 1) + egg = egg.split(";", 1)[0] + if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")): + line = egg + else: + egg = "" + if "@" in rest and not line: + raise ValueError("Can't deduce version from requirement") + elif not line: + # from filename + rest = rest.replace(".zip", "-").replace(".tar.gz", "-") + name, version, _ = rest.split("-", 2) + line = f'{egg or name}=={version}' requirements = pkg_resources.parse_requirements(line) for requirement in requirements: requirement = str(requirement) diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index 97c73a1bd1..3c173dec2f 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -1,10 +1,11 @@ -from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin -from .Options import EnergyCore -from typing import Set -# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? +from typing import Protocol, Set +from BaseClasses import MultiWorld +from worlds.AutoWorld import LogicMixin from . import pyevermizer +from .Options import EnergyCore + +# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? # TODO: resolve/flatten/expand rules to get rid of recursion below where possible # Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) @@ -15,9 +16,16 @@ items = [item for item in filter(lambda item: item.progression, pyevermizer.get_ if item.name not in item_names and not item_names.add(item.name)] +class LogicProtocol(Protocol): + def has(self, name: str, player: int) -> bool: ... + def item_count(self, name: str, player: int) -> int: ... + def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ... + def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ... + + # when this module is loaded, this mixin will extend BaseClasses.CollectionState class SecretOfEvermoreLogic(LogicMixin): - def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: + def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: """ Returns reached count of one of evermizer's progress steps based on collected items. i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP @@ -44,7 +52,7 @@ class SecretOfEvermoreLogic(LogicMixin): return n return n - def soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: + def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: """ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE """ diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index c718cb4abd..f1a30745f8 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -1,18 +1,33 @@ import typing -from Options import Option, Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing + +from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing +# typing boilerplate +class FlagsProtocol(typing.Protocol): + value: int + default: int + flags: typing.List[str] + + +class FlagProtocol(typing.Protocol): + value: int + default: int + flag: str + + +# meta options class EvermizerFlags: flags: typing.List[str] - def to_flag(self) -> str: + def to_flag(self: FlagsProtocol) -> str: return self.flags[self.value] class EvermizerFlag: flag: str - def to_flag(self) -> str: + def to_flag(self: FlagProtocol) -> str: return self.flag if self.value != self.default else '' @@ -23,6 +38,7 @@ class OffOnFullChoice(Choice): alias_chaos = 2 +# actual options class Difficulty(EvermizerFlags, Choice): """Changes relative spell cost and stuff""" display_name = "Difficulty" @@ -168,6 +184,7 @@ class TrapCount(Range): default = 0 +# more meta options class ItemChanceMeta(AssembleOptions): def __new__(mcs, name, bases, attrs): if 'item_name' in attrs: @@ -183,6 +200,7 @@ class TrapChance(Range, metaclass=ItemChanceMeta): default = 20 +# more actual options class TrapChanceQuake(TrapChance): """Sets the chance/ratio of quake traps""" item_name = "Quake Trap" @@ -210,11 +228,12 @@ class TrapChanceOHKO(TrapChance): class SoEProgressionBalancing(ProgressionBalancing): default = 30 - __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") + __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") \ + if ProgressionBalancing.__doc__ else None special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} -soe_options: typing.Dict[str, type(Option)] = { +soe_options: typing.Dict[str, AssembleOptions] = { "difficulty": Difficulty, "energy_core": EnergyCore, "required_fragments": RequiredFragments, diff --git a/worlds/soe/Patch.py b/worlds/soe/Patch.py index f6a0a69f55..f4de5d06ea 100644 --- a/worlds/soe/Patch.py +++ b/worlds/soe/Patch.py @@ -1,9 +1,8 @@ -import bsdiff4 -import yaml +import os from typing import Optional + import Utils from worlds.Files import APDeltaPatch -import os USHASH = '6e9c94511d04fac6e0a1e582c170be3a' @@ -24,6 +23,8 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str: options = Utils.get_options() if not file_name: file_name = options["soe_options"]["rom_file"] + if not file_name: + raise ValueError("Missing soe_options -> rom_file from host.yaml") if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name @@ -37,30 +38,6 @@ def read_rom(stream, strip_header=True) -> bytes: return data -def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: - """Generate old (<4) apbp format yaml""" - patch = yaml.dump({"meta": metadata, - "patch": patch, - "game": "Secret of Evermore", - # minimum version of patch system expected for patching to be successful - "compatible_version": 1, - "version": 2, - "base_checksum": USHASH}) - return patch.encode(encoding="utf-8-sig") - - -def generate_patch(vanilla_file, randomized_file, metadata: Optional[dict] = None) -> bytes: - """Generate old (<4) apbp format patch data. Run through lzma to get a complete apbp file.""" - with open(vanilla_file, "rb") as f: - vanilla = read_rom(f) - with open(randomized_file, "rb") as f: - randomized = read_rom(f) - if metadata is None: - metadata = {} - patch = bsdiff4.diff(vanilla, randomized) - return generate_yaml(patch, metadata) - - if __name__ == '__main__': import sys print('Please use ../../Patch.py', file=sys.stderr) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 4885fd3179..007bc6dc84 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -1,12 +1,12 @@ -from ..AutoWorld import World, WebWorld -from ..generic.Rules import set_rule -from BaseClasses import Region, Location, Entrance, Item, RegionType, Tutorial, ItemClassification -from Utils import output_path -import typing +import itertools import os import os.path import threading -import itertools +import typing +from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import add_item_rule, set_rule +from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, RegionType, Tutorial +from Utils import output_path try: import pyevermizer # from package @@ -16,7 +16,7 @@ except ImportError: from . import pyevermizer # as part of the source tree from . import Logic # load logic mixin -from .Options import soe_options, EnergyCore, RequiredFragments, AvailableFragments +from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments from .Patch import SoEDeltaPatch, get_base_rom_path """ @@ -154,9 +154,9 @@ class SoEWorld(World): option_definitions = soe_options topology_present = False remote_items = False - data_version = 3 + data_version = 4 web = SoEWebWorld() - required_client_version = (0, 3, 3) + required_client_version = (0, 3, 5) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() @@ -188,7 +188,7 @@ class SoEWorld(World): return SoEItem(event, ItemClassification.progression, None, self.player) def create_item(self, item: typing.Union[pyevermizer.Item, str]) -> Item: - if type(item) is str: + if isinstance(item, str): item = self.item_id_to_raw[self.item_name_to_id[item]] if item.type == pyevermizer.CHECK_TRAP: classification = ItemClassification.trap @@ -208,14 +208,68 @@ class SoEWorld(World): raise FileNotFoundError(rom_file) def create_regions(self): + # exclude 'hidden' on easy + max_difficulty = 1 if self.world.difficulty[self.player] == Difficulty.option_easy else 256 + # TODO: generate *some* regions from locations' requirements? r = Region('Menu', RegionType.Generic, 'Menu', self.player, self.world) r.exits = [Entrance(self.player, 'New Game', r)] self.world.regions += [r] + # group locations into spheres (1, 2, 3+ at index 0, 1, 2) + spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {} + for loc in _locations: + spheres.setdefault(min(2, len(loc.requires)), {}).setdefault(loc.type, []).append( + SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r, + loc.difficulty > max_difficulty)) + + # location balancing data + trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = { + 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40)}, # remove up to 40 gourds from sphere 1 + 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90)}, # remove up to 90 gourds from sphere 2 + } + + # mark some as excluded based on numbers above + for trash_sphere, fills in trash_fills.items(): + for typ, counts in fills.items(): + count = counts[self.world.difficulty[self.player].value] + for location in self.world.random.sample(spheres[trash_sphere][typ], count): + location.progress_type = LocationProgressType.EXCLUDED + # TODO: do we need to set an item rule? + + def sphere1_blocked_items_rule(item): + if isinstance(item, SoEItem): + # disable certain items in sphere 1 + if item.name in {"Gauge", "Wheel"}: + return False + # and some more for non-easy, non-mystery + if self.world.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery): + if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}: + return False + return True + + for locations in spheres[0].values(): + for location in locations: + add_item_rule(location, sphere1_blocked_items_rule) + + # make some logically late(r) bosses priority locations to increase complexity + if self.world.difficulty[self.player] == Difficulty.option_mystery: + late_count = self.world.random.randint(0, 2) + else: + late_count = self.world.difficulty[self.player].value + late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala", + "Mungola", "Lightning Storm", "Magmar", "Volcano Viper") + late_locations = self.world.random.sample(late_bosses, late_count) + + # add locations to the world r = Region('Ingame', RegionType.Generic, 'Ingame', self.player, self.world) - r.locations = [SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r) - for loc in _locations] + for sphere in spheres.values(): + for locations in sphere.values(): + for location in locations: + r.locations.append(location) + if location.name in late_locations: + location.progress_type = LocationProgressType.PRIORITY + r.locations.append(SoELocation(self.player, 'Done', None, r)) self.world.regions += [r] @@ -269,6 +323,7 @@ class SoEWorld(World): if v < c: return self.create_item(trap_names[t]) v -= c + assert False, "Bug in create_trap" for _ in range(trap_count): if len(ingredients) < 1: @@ -289,7 +344,7 @@ class SoEWorld(World): location = self.world.get_location(loc.name, self.player) set_rule(location, self.make_rule(loc.requires)) - def make_rule(self, requires: typing.List[typing.Tuple[int]]) -> typing.Callable[[typing.Any], bool]: + def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]: def rule(state) -> bool: for count, progress in requires: if not state.soe_has(progress, self.world, self.player, count): @@ -321,8 +376,8 @@ class SoEWorld(World): while len(self.connect_name.encode('utf-8')) > 32: self.connect_name = self.connect_name[:-1] self.connect_name_available_event.set() - placement_file = None - out_file = None + placement_file = "" + out_file = "" try: money = self.world.money_modifier[self.player].value exp = self.world.exp_modifier[self.player].value @@ -346,14 +401,15 @@ class SoEWorld(World): with open(placement_file, "wb") as f: # generate placement file for location in filter(lambda l: l.player == self.player, self.world.get_locations()): item = location.item - if item.code is None: + assert item is not None, "Can't handle unfilled location" + if item.code is None or location.address is None: continue # skip events loc = self.location_id_to_raw[location.address] if item.player != self.player: line = f'{loc.type},{loc.index}:{pyevermizer.CHECK_NONE},{item.code},{item.player}\n' else: - item = self.item_id_to_raw[item.code] - line = f'{loc.type},{loc.index}:{item.type},{item.index}\n' + soe_item = self.item_id_to_raw[item.code] + line = f'{loc.type},{loc.index}:{soe_item.type},{soe_item.index}\n' f.write(line.encode('utf-8')) if not os.path.exists(rom_file): @@ -364,14 +420,14 @@ class SoEWorld(World): patch = SoEDeltaPatch(patch_file, player=self.player, player_name=player_name, patched_path=out_file) patch.write() - except: + except Exception: raise finally: try: os.unlink(placement_file) os.unlink(out_file) os.unlink(out_file[:-4] + '_SPOILER.log') - except: + except FileNotFoundError: pass def modify_multidata(self, multidata: dict): @@ -388,11 +444,15 @@ class SoEWorld(World): class SoEItem(Item): game: str = "Secret of Evermore" + __slots__ = () # disable __dict__ class SoELocation(Location): game: str = "Secret of Evermore" + __slots__ = () # disables __dict__ once Location has __slots__ - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): + def __init__(self, player: int, name: str, address: typing.Optional[int], parent: Region, exclude: bool = False): super().__init__(player, name, address, parent) + # unconditional assignments favor a split dict, saving memory + self.progress_type = LocationProgressType.EXCLUDED if exclude else LocationProgressType.DEFAULT self.event = not address diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 7f6a11e490..54eae8f1de 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,14 +1,14 @@ -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3.tar.gz#egg=pyevermizer; python_version == '3.11' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0.tar.gz#egg=pyevermizer==0.42.0; python_version < '3.8' or python_version > '3.10' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64') From f18df4c1df35bee66173dd219db5dbc0042f619a Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 21 Oct 2022 21:29:20 -0400 Subject: [PATCH 083/105] [Core] Fix priority location handling in accessibility corrections (#1121) * Fix priority location handling in accessibility corrections * Don't lock empty locations * black sliver's suggested change Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Fill.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Fill.py b/Fill.py index 2c03599503..cb9844b442 100644 --- a/Fill.py +++ b/Fill.py @@ -232,13 +232,13 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio if location in state.events: state.events.remove(location) locations.append(location) - - if pool: + if pool and locations: + locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) fill_restrictive(world, state, locations, pool) def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): - maximum_exploration_state = sweep_from_pool(state, []) + maximum_exploration_state = sweep_from_pool(state) unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] if unreachable_locations: def forbid_important_item_rule(item: Item): @@ -285,15 +285,10 @@ def distribute_items_restrictive(world: MultiWorld) -> None: nonlocal lock_later lock_later.append(location) - # "priority fill" - fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) - accessibility_corrections(world, world.state, prioritylocations, progitempool) - - for location in lock_later: - location.locked = True - del mark_for_locking, lock_later - if prioritylocations: + # "priority fill" + fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) + accessibility_corrections(world, world.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: @@ -304,6 +299,11 @@ def distribute_items_restrictive(world: MultiWorld) -> None: f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') accessibility_corrections(world, world.state, defaultlocations) + for location in lock_later: + if location.item: + location.locked = True + del mark_for_locking, lock_later + inaccessible_location_rules(world, world.state, defaultlocations) remaining_fill(world, excludedlocations, filleritempool) From 24105ac249ddfb6eb708cba016934c757e2287cf Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 21 Oct 2022 18:42:16 -0700 Subject: [PATCH 084/105] Tests: fix random failures on Zillion tests (#1128) * tests: fix random failures on Zillion tests Normally there's a low probably that the game doesn't require a power-up that it usually requires. This makes sure it always has that requirement for tests. * better type narrowing --- test/worlds/zillion/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/worlds/zillion/__init__.py b/test/worlds/zillion/__init__.py index 7b2ec66927..43100d3a8c 100644 --- a/test/worlds/zillion/__init__.py +++ b/test/worlds/zillion/__init__.py @@ -1,5 +1,13 @@ from test.worlds.test_base import WorldTestBase +from worlds.zillion.region import ZillionLocation class ZillionTestBase(WorldTestBase): game = "Zillion" + + def world_setup(self) -> None: + super().world_setup() + # make sure game requires gun 3 for tests + for location in self.world.get_locations(): + if isinstance(location, ZillionLocation) and location.name.startswith("O-7"): + location.zz_loc.req.gun = 3 From f2eb6060380cc66ad091cce71be10fbdc4b65437 Mon Sep 17 00:00:00 2001 From: Marechal-l Date: Sat, 22 Oct 2022 23:51:48 +0200 Subject: [PATCH 085/105] DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility --- worlds/dark_souls_3/Items.py | 19 +++++++++++++++ worlds/dark_souls_3/Locations.py | 19 +++++++++++++++ worlds/dark_souls_3/__init__.py | 27 +++++++++------------- worlds/dark_souls_3/data/items_data.py | 4 +++- worlds/dark_souls_3/data/locations_data.py | 13 +++++++---- 5 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 worlds/dark_souls_3/Items.py create mode 100644 worlds/dark_souls_3/Locations.py diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py new file mode 100644 index 0000000000..2ad3199451 --- /dev/null +++ b/worlds/dark_souls_3/Items.py @@ -0,0 +1,19 @@ +from BaseClasses import Item +from worlds.dark_souls_3.data.items_data import item_tables + + +class DarkSouls3Item(Item): + game: str = "Dark Souls III" + + @staticmethod + def get_name_to_id() -> dict: + base_id = 100000 + table_offset = 100 + + output = {} + i = 0 + for table in item_tables: + output |= {name: id for id, name in enumerate(table, base_id + (table_offset * i))} + i += 1 + + return output diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py new file mode 100644 index 0000000000..7eb842c4d2 --- /dev/null +++ b/worlds/dark_souls_3/Locations.py @@ -0,0 +1,19 @@ +from BaseClasses import Location +from worlds.dark_souls_3.data.locations_data import location_tables + + +class DarkSouls3Location(Location): + game: str = "Dark Souls III" + + @staticmethod + def get_name_to_id() -> dict: + base_id = 100000 + table_offset = 100 + + output = {} + i = 0 + for table in location_tables: + output |= {name: id for id, name in enumerate(table, base_id + (table_offset * i))} + i += 1 + + return output diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 8d58de1221..c56e6752dc 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -2,9 +2,11 @@ import json import os +from .Items import DarkSouls3Item +from .Locations import DarkSouls3Location from .Options import dark_souls_options -from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary_table, key_items_list -from .data.locations_data import location_dictionary_table, cemetery_of_ash_table, fire_link_shrine_table, \ +from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list +from .data.locations_data import location_dictionary, cemetery_of_ash_table, fire_link_shrine_table, \ high_wall_of_lothric, \ undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \ farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \ @@ -53,8 +55,9 @@ class DarkSouls3World(World): web = DarkSouls3Web() data_version = 3 base_id = 100000 - item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)} - location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)} + required_client_version = (0, 3, 6) + item_name_to_id = DarkSouls3Item.get_name_to_id() + location_name_to_id = DarkSouls3Location.get_name_to_id() def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) @@ -239,18 +242,18 @@ class DarkSouls3World(World): def generate_output(self, output_directory: str): # Depending on the specified option, modify items hexadecimal value to add an upgrade level - item_dictionary = item_dictionary_table.copy() + item_dictionary_copy = item_dictionary.copy() if self.world.randomize_weapons_level[self.player]: # Randomize some weapons upgrades for name in weapons_upgrade_5_table.keys(): if self.world.random.randint(0, 100) < 33: value = self.world.random.randint(1, 5) - item_dictionary[name] += value + item_dictionary_copy[name] += value for name in weapons_upgrade_10_table.keys(): if self.world.random.randint(0, 100) < 33: value = self.world.random.randint(1, 10) - item_dictionary[name] += value + item_dictionary_copy[name] += value # Create the mandatory lists to generate the player's output file items_id = [] @@ -264,7 +267,7 @@ class DarkSouls3World(World): items_address.append(item_dictionary[location.item.name]) if location.player == self.player: - locations_address.append(location_dictionary_table[location.name]) + locations_address.append(location_dictionary[location.name]) locations_id.append(location.address) if location.item.player == self.player: locations_target.append(item_dictionary[location.item.name]) @@ -291,11 +294,3 @@ class DarkSouls3World(World): filename = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}.json" with open(os.path.join(output_directory, filename), 'w') as outfile: json.dump(data, outfile) - - -class DarkSouls3Location(Location): - game: str = "Dark Souls III" - - -class DarkSouls3Item(Item): - game: str = "Dark Souls III" diff --git a/worlds/dark_souls_3/data/items_data.py b/worlds/dark_souls_3/data/items_data.py index f155558c1f..e951b674fe 100644 --- a/worlds/dark_souls_3/data/items_data.py +++ b/worlds/dark_souls_3/data/items_data.py @@ -391,4 +391,6 @@ key_items_list = { "Jailer's Key Ring", } -item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} +item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table, armor_table, rings_table, spells_table, misc_items_table, goods_table] + +item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py index 94a3c4ea81..7230bb795d 100644 --- a/worlds/dark_souls_3/data/locations_data.py +++ b/worlds/dark_souls_3/data/locations_data.py @@ -440,7 +440,12 @@ archdragon_peak_table = { "AP: Havel's Greatshield": 0x013376F0, } -location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, - **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, - **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, - **grand_archives_table, **untended_graves_table, **archdragon_peak_table} +location_tables = [cemetery_of_ash_table, fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table, + cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, + irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table, + grand_archives_table, untended_graves_table, archdragon_peak_table] + +location_dictionary = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, + **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, + **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, + **grand_archives_table, **untended_graves_table, **archdragon_peak_table} From 766f3290a19fee77eac8c3dcb9b22c980e0c62df Mon Sep 17 00:00:00 2001 From: Marechal-l Date: Sun, 23 Oct 2022 10:27:52 +0200 Subject: [PATCH 086/105] DS3: Resolve Python 3.8 compatibility --- worlds/dark_souls_3/Items.py | 2 +- worlds/dark_souls_3/Locations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index 2ad3199451..bb13dc7514 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -13,7 +13,7 @@ class DarkSouls3Item(Item): output = {} i = 0 for table in item_tables: - output |= {name: id for id, name in enumerate(table, base_id + (table_offset * i))} + output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) i += 1 return output diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index 7eb842c4d2..d7196f8295 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -13,7 +13,7 @@ class DarkSouls3Location(Location): output = {} i = 0 for table in location_tables: - output |= {name: id for id, name in enumerate(table, base_id + (table_offset * i))} + output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) i += 1 return output From cca898587730ab78c0d77ac8557fe958cfa2cea3 Mon Sep 17 00:00:00 2001 From: Marechal-l Date: Sun, 23 Oct 2022 10:31:07 +0200 Subject: [PATCH 087/105] DS3: Removed useless region for locations IDs consistency --- worlds/dark_souls_3/__init__.py | 7 ++----- worlds/dark_souls_3/data/locations_data.py | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index c56e6752dc..df56306cc5 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -6,7 +6,7 @@ from .Items import DarkSouls3Item from .Locations import DarkSouls3Location from .Options import dark_souls_options from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list -from .data.locations_data import location_dictionary, cemetery_of_ash_table, fire_link_shrine_table, \ +from .data.locations_data import location_dictionary, fire_link_shrine_table, \ high_wall_of_lothric, \ undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \ farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \ @@ -82,7 +82,6 @@ class DarkSouls3World(World): self.world.regions.append(menu_region) # Create all Vanilla regions of Dark Souls III - cemetery_of_ash_region = self.create_region("Cemetery Of Ash", cemetery_of_ash_table) firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table) firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower", firelink_shrine_bell_tower_table) @@ -107,9 +106,7 @@ class DarkSouls3World(World): # Create the entrance to connect those regions menu_region.exits.append(Entrance(self.player, "New Game", menu_region)) - self.world.get_entrance("New Game", self.player).connect(cemetery_of_ash_region) - cemetery_of_ash_region.exits.append(Entrance(self.player, "Goto Firelink Shrine", cemetery_of_ash_region)) - self.world.get_entrance("Goto Firelink Shrine", self.player).connect(firelink_shrine_region) + self.world.get_entrance("New Game", self.player).connect(firelink_shrine_region) firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric", firelink_shrine_region)) firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame", diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py index 7230bb795d..855b4a9ab3 100644 --- a/worlds/dark_souls_3/data/locations_data.py +++ b/worlds/dark_souls_3/data/locations_data.py @@ -5,9 +5,6 @@ Regular expression parser https://regex101.com/r/XdtiLR/2 List of locations https://darksouls3.wiki.fextralife.com/Locations """ -cemetery_of_ash_table = { -} - fire_link_shrine_table = { # "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless "FS: Broken Straight Sword": 0x001EF9B0, @@ -440,12 +437,12 @@ archdragon_peak_table = { "AP: Havel's Greatshield": 0x013376F0, } -location_tables = [cemetery_of_ash_table, fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table, +location_tables = [fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table, cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table, grand_archives_table, untended_graves_table, archdragon_peak_table] -location_dictionary = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, +location_dictionary = {**fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, **grand_archives_table, **untended_graves_table, **archdragon_peak_table} From 52726139b4632a0ce9a4a0aa717b80d6ce460eed Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 23 Oct 2022 09:18:05 -0700 Subject: [PATCH 088/105] Zillion: support unicode player names (#1131) * work on unicode and seed verification * update zilliandomizer * fix log message --- CommonClient.py | 2 + ZillionClient.py | 83 +++++++++++++++++++++++---------- worlds/zillion/__init__.py | 3 +- worlds/zillion/requirements.txt | 2 +- 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index b17709eecf..7960be0e92 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -279,6 +279,7 @@ class CommonContext: self.auth = await self.console_input() async def send_connect(self, **kwargs: typing.Any) -> None: + """ send `Connect` packet to log in to server """ payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -294,6 +295,7 @@ class CommonContext: return await self.input_queue.get() async def connect(self, address: typing.Optional[str] = None) -> None: + """ disconnect any previous connection, and open new connection to the server """ await self.disconnect() self.server_task = asyncio.create_task(server_loop(self, address), name="server loop") diff --git a/ZillionClient.py b/ZillionClient.py index dee5c2b756..8ad1065057 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,7 +1,7 @@ import asyncio import base64 import platform -from typing import Any, Coroutine, Dict, Optional, Type, cast +from typing import Any, Coroutine, Dict, Optional, Tuple, Type, cast # CommonClient import first to trigger ModuleUpdater from CommonClient import CommonContext, server_loop, gui_enabled, \ @@ -46,6 +46,8 @@ class ZillionContext(CommonContext): start_char: Chars = "JJ" rescues: Dict[int, RescueInfo] = {} loc_mem_to_id: Dict[int, int] = {} + got_room_info: asyncio.Event + """ flag for connected to server """ got_slot_data: asyncio.Event """ serves as a flag for whether I am logged in to the server """ @@ -65,6 +67,7 @@ class ZillionContext(CommonContext): super().__init__(server_address, password) self.from_game = asyncio.Queue() self.to_game = asyncio.Queue() + self.got_room_info = asyncio.Event() self.got_slot_data = asyncio.Event() self.look_for_retroarch = asyncio.Event() @@ -185,6 +188,9 @@ class ZillionContext(CommonContext): logger.info("received door data from server") doors = base64.b64decode(doors_b64) self.to_game.put_nowait(events.DoorEventToGame(doors)) + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.got_room_info.set() def process_from_game_queue(self) -> None: if self.from_game.qsize(): @@ -238,6 +244,24 @@ class ZillionContext(CommonContext): self.next_item = len(self.items_received) +def name_seed_from_ram(data: bytes) -> Tuple[str, str]: + """ returns player name, and end of seed string """ + if len(data) == 0: + # no connection to game + return "", "xxx" + null_index = data.find(b'\x00') + if null_index == -1: + logger.warning(f"invalid game id in rom {data}") + null_index = len(data) + name = data[:null_index].decode() + null_index_2 = data.find(b'\x00', null_index + 1) + if null_index_2 == -1: + null_index_2 = len(data) + seed_name = data[null_index + 1:null_index_2].decode() + + return name, seed_name + + async def zillion_sync_task(ctx: ZillionContext) -> None: logger.info("started zillion sync task") @@ -263,47 +287,58 @@ async def zillion_sync_task(ctx: ZillionContext) -> None: with Memory(ctx.from_game, ctx.to_game) as memory: while not ctx.exit_event.is_set(): ram = await memory.read() - name = memory.get_player_name(ram).decode() + game_id = memory.get_rom_to_ram_data(ram) + name, seed_end = name_seed_from_ram(game_id) if len(name): if name == ctx.auth: # this is the name we know if ctx.server and ctx.server.socket: # type: ignore - if memory.have_generation_info(): - log_no_spam("everything connected") - await memory.process_ram(ram) - ctx.process_from_game_queue() - ctx.process_items_received() - else: # no generation info - if ctx.got_slot_data.is_set(): - memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) - ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ - make_id_to_others(ctx.start_char) - ctx.next_item = 0 - ctx.ap_local_count = len(ctx.checked_locations) - else: # no slot data yet - asyncio.create_task(ctx.send_connect()) - log_no_spam("logging in to server...") - await asyncio.wait(( - ctx.got_slot_data.wait(), - ctx.exit_event.wait(), - asyncio.sleep(6) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + if ctx.got_room_info.is_set(): + if ctx.seed_name and ctx.seed_name.endswith(seed_end): + # correct seed + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + asyncio.create_task(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + ctx.got_slot_data.wait(), + ctx.exit_event.wait(), + asyncio.sleep(6) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # not correct seed name + log_no_spam("incorrect seed - did you mix up roms?") + else: # no room info + # If we get here, it looks like `RoomInfo` packet got lost + log_no_spam("waiting for room info from server...") else: # server not connected log_no_spam("waiting for server connection...") else: # new game log_no_spam("connected to new game") await ctx.disconnect() ctx.reset_server_state() + ctx.seed_name = None + ctx.got_room_info.clear() ctx.reset_game_state() memory.reset_game_state() ctx.auth = name asyncio.create_task(ctx.connect()) await asyncio.wait(( - ctx.got_slot_data.wait(), + ctx.got_room_info.wait(), ctx.exit_event.wait(), asyncio.sleep(6) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + ), return_when=asyncio.FIRST_COMPLETED) else: # no name found in game if not help_message_shown: logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 32b84015f1..d982782840 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -304,7 +304,8 @@ class ZillionWorld(World): zz_patcher.all_fixes_and_options(zz_options) zz_patcher.set_external_item_interface(zz_options.start_char, zz_options.max_level) zz_patcher.set_multiworld_items(multi_items) - zz_patcher.set_rom_to_ram_data(self.world.player_name[self.player].replace(' ', '_').encode()) + game_id = self.world.player_name[self.player].encode() + b'\x00' + self.world.seed_name[-6:].encode() + zz_patcher.set_rom_to_ram_data(game_id) def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use world.random here. diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index 0ed98771bd..62f66899f3 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1 +1 @@ -git+https://github.com/beauxq/zilliandomizer@45a45eaca4119a4d06d2c31546ad19f3abd77f63#egg=zilliandomizer==0.4.4 +git+https://github.com/beauxq/zilliandomizer@c97298ecb1bca58c3dd3376a1e1609fad53788cf#egg=zilliandomizer==0.4.5 From 37c5865c0e34cf087df2f8efea7a3e0b637440b5 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 23 Oct 2022 09:28:09 -0700 Subject: [PATCH 089/105] Core: Options: fix shared default instances (#1130) --- Options.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Options.py b/Options.py index c2007c1c41..536f388efb 100644 --- a/Options.py +++ b/Options.py @@ -1,5 +1,6 @@ from __future__ import annotations import abc +from copy import deepcopy import math import numbers import typing @@ -753,7 +754,7 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys): supports_weighting = False def __init__(self, value: typing.Dict[str, typing.Any]): - self.value = value + self.value = deepcopy(value) @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: @@ -784,7 +785,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys): supports_weighting = False def __init__(self, value: typing.List[typing.Any]): - self.value = value or [] + self.value = deepcopy(value) super(OptionList, self).__init__() @classmethod @@ -806,11 +807,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys): class OptionSet(Option[typing.Set[str]], VerifyKeys): - default = frozenset() + default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset() supports_weighting = False - def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]): - self.value = set(value) + def __init__(self, value: typing.Iterable[str]): + self.value = set(deepcopy(value)) super(OptionSet, self).__init__() @classmethod From ad445629bde627983ab3dd33e0765547836f1ae0 Mon Sep 17 00:00:00 2001 From: beauxq Date: Sun, 23 Oct 2022 13:23:30 -0700 Subject: [PATCH 090/105] Zillion: fix unit tests previous fix was incorrect --- test/worlds/test_base.py | 4 ++-- test/worlds/zillion/TestGoal.py | 31 ++++++++++++++++++------------- test/worlds/zillion/__init__.py | 21 ++++++++++++++------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/test/worlds/test_base.py b/test/worlds/test_base.py index 1aa6ff2317..0d7272be19 100644 --- a/test/worlds/test_base.py +++ b/test/worlds/test_base.py @@ -19,13 +19,13 @@ class WorldTestBase(unittest.TestCase): if self.auto_construct: self.world_setup() - def world_setup(self) -> None: + def world_setup(self, seed: typing.Optional[int] = None) -> None: if not hasattr(self, "game"): raise NotImplementedError("didn't define game name") self.world = MultiWorld(1) self.world.game[1] = self.game self.world.player_name = {1: "Tester"} - self.world.set_seed() + self.world.set_seed(seed) args = Namespace() for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items(): setattr(args, name, { diff --git a/test/worlds/zillion/TestGoal.py b/test/worlds/zillion/TestGoal.py index 96701e8352..1c79305699 100644 --- a/test/worlds/zillion/TestGoal.py +++ b/test/worlds/zillion/TestGoal.py @@ -10,7 +10,7 @@ class TestGoalVanilla(ZillionTestBase): "floppy_req": 6, } - def test_floppies(self): + def test_floppies(self) -> None: self.collect_by_name(["Apple", "Champ", "Red ID Card"]) self.assertBeatable(False) # 0 floppies floppies = self.get_items_by_name("Floppy Disk") @@ -26,19 +26,20 @@ class TestGoalVanilla(ZillionTestBase): self.assertEqual(self.count("Floppy Disk"), 7) self.assertBeatable(True) - def test_with_everything(self): + def test_with_everything(self) -> None: self.collect_by_name(["Apple", "Champ", "Red ID Card", "Floppy Disk"]) self.assertBeatable(True) - def test_no_jump(self): + def test_no_jump(self) -> None: self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk"]) self.assertBeatable(False) - def test_no_gun(self): + def test_no_gun(self) -> None: + self.ensure_gun_3_requirement() self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk"]) self.assertBeatable(False) - def test_no_red(self): + def test_no_red(self) -> None: self.collect_by_name(["Apple", "Champ", "Floppy Disk"]) self.assertBeatable(False) @@ -50,7 +51,7 @@ class TestGoalBalanced(ZillionTestBase): "gun_levels": "balanced", } - def test_jump(self): + def test_jump(self) -> None: self.collect_by_name(["Red ID Card", "Floppy Disk", "Zillion"]) self.assertBeatable(False) # not enough jump opas = self.get_items_by_name("Opa-Opa") @@ -60,7 +61,8 @@ class TestGoalBalanced(ZillionTestBase): self.collect(opas[1:]) self.assertBeatable(True) - def test_guns(self): + def test_guns(self) -> None: + self.ensure_gun_3_requirement() self.collect_by_name(["Red ID Card", "Floppy Disk", "Opa-Opa"]) self.assertBeatable(False) # not enough gun guns = self.get_items_by_name("Zillion") @@ -78,7 +80,7 @@ class TestGoalRestrictive(ZillionTestBase): "gun_levels": "restrictive", } - def test_jump(self): + def test_jump(self) -> None: self.collect_by_name(["Champ", "Red ID Card", "Floppy Disk", "Zillion"]) self.assertBeatable(False) # not enough jump self.collect_by_name("Opa-Opa") @@ -86,7 +88,8 @@ class TestGoalRestrictive(ZillionTestBase): self.collect_by_name("Apple") self.assertBeatable(True) - def test_guns(self): + def test_guns(self) -> None: + self.ensure_gun_3_requirement() self.collect_by_name(["Apple", "Red ID Card", "Floppy Disk", "Opa-Opa"]) self.assertBeatable(False) # not enough gun self.collect_by_name("Zillion") @@ -104,15 +107,17 @@ class TestGoalAppleStart(ZillionTestBase): "zillion_count": 5 } - def test_guns_jj_first(self): + def test_guns_jj_first(self) -> None: """ with low gun levels, 5 Zillion is enough to get JJ to gun 3 """ + self.ensure_gun_3_requirement() self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Opa-Opa"]) self.assertBeatable(False) # not enough gun self.collect_by_name("Zillion") self.assertBeatable(True) - def test_guns_zillions_first(self): + def test_guns_zillions_first(self) -> None: """ with low gun levels, 5 Zillion is enough to get JJ to gun 3 """ + self.ensure_gun_3_requirement() self.collect_by_name(["Zillion", "Red ID Card", "Floppy Disk", "Opa-Opa"]) self.assertBeatable(False) # not enough gun self.collect_by_name("JJ") @@ -129,14 +134,14 @@ class TestGoalChampStart(ZillionTestBase): "opas_per_level": 1 } - def test_jump_jj_first(self): + def test_jump_jj_first(self) -> None: """ with low jump levels, 5 level-ups is enough to get JJ to jump 3 """ self.collect_by_name(["JJ", "Red ID Card", "Floppy Disk", "Zillion"]) self.assertBeatable(False) # not enough jump self.collect_by_name("Opa-Opa") self.assertBeatable(True) - def test_jump_opa_first(self): + def test_jump_opa_first(self) -> None: """ with low jump levels, 5 level-ups is enough to get JJ to jump 3 """ self.collect_by_name(["Opa-Opa", "Red ID Card", "Floppy Disk", "Zillion"]) self.assertBeatable(False) # not enough jump diff --git a/test/worlds/zillion/__init__.py b/test/worlds/zillion/__init__.py index 43100d3a8c..fb81bda522 100644 --- a/test/worlds/zillion/__init__.py +++ b/test/worlds/zillion/__init__.py @@ -1,13 +1,20 @@ +from typing import cast from test.worlds.test_base import WorldTestBase -from worlds.zillion.region import ZillionLocation +from worlds.zillion import ZillionWorld class ZillionTestBase(WorldTestBase): game = "Zillion" - def world_setup(self) -> None: - super().world_setup() - # make sure game requires gun 3 for tests - for location in self.world.get_locations(): - if isinstance(location, ZillionLocation) and location.name.startswith("O-7"): - location.zz_loc.req.gun = 3 + def ensure_gun_3_requirement(self) -> None: + """ + There's a low probability that gun 3 is not required. + + This makes sure that gun 3 is required by making all the canisters + in O-7 (including key word canisters) require gun 3. + """ + zz_world = cast(ZillionWorld, self.world.worlds[1]) + assert zz_world.zz_system.randomizer + for zz_loc_name, zz_loc in zz_world.zz_system.randomizer.locations.items(): + if zz_loc_name.startswith("r15c6"): + zz_loc.req.gun = 3 From 89d1a80e016194bb8c7b2bf8f69cc6081f409ace Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Mon, 24 Oct 2022 04:28:08 -0400 Subject: [PATCH 091/105] SM: morph first in pool remove (#1134) * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * removed now unecessary sorting of Morph balls at end of item pool Its messing with priority locations feature. --- worlds/sm/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index d901303215..500233bb71 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -657,12 +657,6 @@ class SMWorld(World): loc.place_locked_item(item) loc.address = loc.item.code = None - @classmethod - 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) - @classmethod def stage_post_fill(cls, world): new_state = CollectionState(world) From 6535836e5cec984354ba2680093a2c773fb90da8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 21 Oct 2022 22:50:36 +0200 Subject: [PATCH 092/105] Subnautica: don't override plando during pre_fill --- worlds/subnautica/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 7dc23bf405..830bc831ef 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -153,7 +153,8 @@ class SubnauticaWorld(World): return self.prefill_items def pre_fill(self) -> None: - reachable = self.world.get_reachable_locations(player=self.player) + reachable = [location for location in self.world.get_reachable_locations(player=self.player) + if not location.item] self.world.random.shuffle(reachable) items = self.prefill_items.copy() for item in items: From d5efc713444dec62a5d398561303ff3edbf0db6f Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Tue, 25 Oct 2022 13:54:43 -0400 Subject: [PATCH 093/105] Core: SNI Client Refactor (#1083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First Pass removal of game-specific code * SMW, DKC3, and SM hooked into AutoClient * All SNES autoclients functional * Fix ALttP Deathlink * Don't default to being ALttP, and properly error check ctx.game * Adjust variable naming * In response to: > we should probably document usage somewhere. I'm open to suggestions of where this should be documented. I think the most valuable documentation for APIs is docstrings and full typing. about websockets change in imports - from websockets documentation: > For convenience, many public APIs can be imported from the websockets package. However, this feature is incompatible with static code analysis. It breaks autocompletion in an IDE or type checking with mypy. If you’re using such tools, use the real import paths. * todo note for python 3.11 typing.NotRequired * missed staging in previous commit * added missing death Game States for DeathLink Co-authored-by: beauxq Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com> --- CommonClient.py | 6 + LttPAdjuster.py | 4 +- MultiServer.py | 6 +- Patch.py | 10 - SNIClient.py | 1129 ++++++-------------------------------- Utils.py | 15 +- host.yaml | 44 +- worlds/AutoSNIClient.py | 42 ++ worlds/alttp/Client.py | 693 +++++++++++++++++++++++ worlds/alttp/__init__.py | 1 + worlds/dkc3/Client.py | 64 +-- worlds/dkc3/__init__.py | 1 + worlds/sm/Client.py | 158 ++++++ worlds/sm/__init__.py | 1 + worlds/smw/Client.py | 145 +++-- worlds/smw/__init__.py | 1 + worlds/smz3/Client.py | 118 ++++ worlds/smz3/__init__.py | 1 + 18 files changed, 1304 insertions(+), 1135 deletions(-) create mode 100644 worlds/AutoSNIClient.py create mode 100644 worlds/alttp/Client.py create mode 100644 worlds/sm/Client.py create mode 100644 worlds/smz3/Client.py diff --git a/CommonClient.py b/CommonClient.py index 7960be0e92..c713373592 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -91,12 +91,18 @@ class ClientCommandProcessor(CommandProcessor): def _cmd_items(self): """List all item names for the currently running game.""" + if not self.ctx.game: + self.output("No game set, cannot determine existing items.") + return False self.output(f"Item Names for {self.ctx.game}") for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id: self.output(item_name) def _cmd_locations(self): """List all location names for the currently running game.""" + if not self.ctx.game: + self.output("No game set, cannot determine existing locations.") + return False self.output(f"Location Names for {self.ctx.game}") for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id: self.output(location_name) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 9fab226c67..a2cc2eeba5 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -26,7 +26,9 @@ ModuleUpdate.update() from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \ get_adjuster_settings, tkinter_center_window, init_logging -from Patch import GAME_ALTTP + + +GAME_ALTTP = "A Link to the Past" class AdjusterWorld(object): diff --git a/MultiServer.py b/MultiServer.py index 9f0865d425..bab762c84b 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -998,7 +998,11 @@ class CommandMeta(type): return super(CommandMeta, cls).__new__(cls, name, bases, attrs) -def mark_raw(function): +_Return = typing.TypeVar("_Return") +# TODO: when python 3.10 is lowest supported, typing.ParamSpec + + +def mark_raw(function: typing.Callable[[typing.Any], _Return]) -> typing.Callable[[typing.Any], _Return]: function.raw_text = True return function diff --git a/Patch.py b/Patch.py index 4ff0e9602a..113d0658c6 100644 --- a/Patch.py +++ b/Patch.py @@ -11,16 +11,6 @@ if __name__ == "__main__": from worlds.Files import AutoPatchRegister, APDeltaPatch -GAME_ALTTP = "A Link to the Past" -GAME_SM = "Super Metroid" -GAME_SOE = "Secret of Evermore" -GAME_SMZ3 = "SMZ3" -GAME_DKC3 = "Donkey Kong Country 3" - -GAME_SMW = "Super Mario World" - - - class RomMeta(TypedDict): server: str player: Optional[int] diff --git a/SNIClient.py b/SNIClient.py index 188822bce7..03e1ff5783 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -7,7 +7,6 @@ import multiprocessing import os import subprocess import base64 -import shutil import logging import asyncio import enum @@ -20,24 +19,19 @@ from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui import Utils +from MultiServer import mark_raw +if typing.TYPE_CHECKING: + from worlds.AutoSNIClient import SNIClient + if __name__ == "__main__": Utils.init_logging("SNIClient", exception_logger="Client") import colorama -import websockets - -from NetUtils import ClientStatus, color -from worlds.alttp import Regions, Shops -from worlds.alttp.Rom import ROM_PLAYER_LIMIT -from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT -from worlds.smz3.Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT -from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3, GAME_SMW - +from websockets.client import connect as websockets_connect, WebSocketClientProtocol +from websockets.exceptions import WebSocketException, ConnectionClosed snes_logger = logging.getLogger("SNES") -from MultiServer import mark_raw - class DeathState(enum.IntEnum): killing_player = 1 @@ -46,9 +40,9 @@ class DeathState(enum.IntEnum): class SNIClientCommandProcessor(ClientCommandProcessor): - ctx: Context + ctx: SNIContext - def _cmd_slow_mode(self, toggle: str = ""): + def _cmd_slow_mode(self, toggle: str = "") -> None: """Toggle slow mode, which limits how fast you send / receive items.""" if toggle: self.ctx.slow_mode = toggle.lower() in {"1", "true", "on"} @@ -63,6 +57,9 @@ class SNIClientCommandProcessor(ClientCommandProcessor): otherwise show available devices; and a SNES device number if more than one SNES is detected. Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """ + return self.connect_to_snes(snes_options) + + def connect_to_snes(self, snes_options: str = "") -> bool: snes_address = self.ctx.snes_address snes_device_number = -1 @@ -79,8 +76,8 @@ class SNIClientCommandProcessor(ClientCommandProcessor): self.ctx.snes_reconnect_address = None if self.ctx.snes_connect_task: self.ctx.snes_connect_task.cancel() - self.ctx.snes_connect_task = asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number), - name="SNES Connect") + self.ctx.snes_connect_task = asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number), + name="SNES Connect") return True def _cmd_snes_close(self) -> bool: @@ -113,14 +110,36 @@ class SNIClientCommandProcessor(ClientCommandProcessor): # return True -class Context(CommonContext): - command_processor = SNIClientCommandProcessor - game = "A Link to the Past" +class SNIContext(CommonContext): + command_processor: typing.Type[SNIClientCommandProcessor] = SNIClientCommandProcessor + game = None # set in validate_rom items_handling = None # set in game_watcher - snes_connect_task: typing.Optional[asyncio.Task] = None + snes_connect_task: "typing.Optional[asyncio.Task[None]]" = None - def __init__(self, snes_address, server_address, password): - super(Context, self).__init__(server_address, password) + snes_address: str + snes_socket: typing.Optional[WebSocketClientProtocol] + snes_state: SNESState + snes_attached_device: typing.Optional[typing.Tuple[int, str]] + snes_reconnect_address: typing.Optional[str] + snes_recv_queue: "asyncio.Queue[bytes]" + snes_request_lock: asyncio.Lock + snes_write_buffer: typing.List[typing.Tuple[int, bytes]] + snes_connector_lock: threading.Lock + death_state: DeathState + killing_player_task: "typing.Optional[asyncio.Task[None]]" + allow_collect: bool + slow_mode: bool + + client_handler: typing.Optional[SNIClient] + awaiting_rom: bool + rom: typing.Optional[bytes] + prev_rom: typing.Optional[bytes] + + hud_message_queue: typing.List[str] # TODO: str is a guess, is this right? + death_link_allow_survive: bool + + def __init__(self, snes_address: str, server_address: str, password: str) -> None: + super(SNIContext, self).__init__(server_address, password) # snes stuff self.snes_address = snes_address @@ -137,39 +156,48 @@ class Context(CommonContext): self.allow_collect = False self.slow_mode = False + self.client_handler = None self.awaiting_rom = False self.rom = None self.prev_rom = None - async def connection_closed(self): - await super(Context, self).connection_closed() + async def connection_closed(self) -> None: + await super(SNIContext, self).connection_closed() self.awaiting_rom = False - def event_invalid_slot(self): + def event_invalid_slot(self) -> typing.NoReturn: 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)") - async def server_auth(self, password_requested: bool = False): + async def server_auth(self, password_requested: bool = False) -> None: if password_requested and not self.password: - await super(Context, self).server_auth(password_requested) + await super(SNIContext, self).server_auth(password_requested) 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)") return self.awaiting_rom = False + # TODO: This looks kind of hacky... + # Context.auth is meant to be the "name" parameter in send_connect, + # which has to be a str (bytes is not json serializable). + # But here, Context.auth is being used for something else + # (where it has to be bytes because it is compared with rom elsewhere). + # If we need to save something to compare with rom elsewhere, + # it should probably be in a different variable, + # and let auth be used for what it's meant for. self.auth = self.rom auth = base64.b64encode(self.rom).decode() await self.send_connect(name=auth) - def on_deathlink(self, data: dict): + def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: if not self.killing_player_task or self.killing_player_task.done(): self.killing_player_task = asyncio.create_task(deathlink_kill_player(self)) - super(Context, self).on_deathlink(data) + super(SNIContext, self).on_deathlink(data) - async def handle_deathlink_state(self, currently_dead: bool): + async def handle_deathlink_state(self, currently_dead: bool) -> None: # in this state we only care about triggering a death send if self.death_state == DeathState.alive: if currently_dead: @@ -184,25 +212,27 @@ class Context(CommonContext): if not currently_dead: self.death_state = DeathState.alive - async def shutdown(self): - await super(Context, self).shutdown() + async def shutdown(self) -> None: + await super(SNIContext, self).shutdown() if self.snes_connect_task: try: await asyncio.wait_for(self.snes_connect_task, 1) except asyncio.TimeoutError: self.snes_connect_task.cancel() - def on_package(self, cmd: str, args: dict): + def on_package(self, cmd: str, args: typing.Dict[str, typing.Any]) -> None: if cmd in {"Connected", "RoomUpdate"}: if "checked_locations" in args and args["checked_locations"]: new_locations = set(args["checked_locations"]) self.checked_locations |= new_locations self.locations_scouted |= new_locations - # Items belonging to the player should not be marked as checked in game, since the player will likely need that item. - # Once the games handled by SNIClient gets made to be remote items, this will no longer be needed. + # Items belonging to the player should not be marked as checked in game, + # since the player will likely need that item. + # Once the games handled by SNIClient gets made to be remote items, + # this will no longer be needed. asyncio.create_task(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}])) - def run_gui(self): + def run_gui(self) -> None: from kvui import GameManager class SNIManager(GameManager): @@ -213,391 +243,23 @@ class Context(CommonContext): base_title = "Archipelago SNI Client" self.ui = SNIManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") # type: ignore -async def deathlink_kill_player(ctx: Context): +async def deathlink_kill_player(ctx: SNIContext) -> None: ctx.death_state = DeathState.killing_player while ctx.death_state == DeathState.killing_player and \ ctx.snes_state == SNESState.SNES_ATTACHED: - if ctx.game == GAME_ALTTP: - invincible = await snes_read(ctx, WRAM_START + 0x037B, 1) - last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1) - await asyncio.sleep(0.25) - health = await snes_read(ctx, WRAM_START + 0xF36D, 1) - if not invincible or not last_health or not health: - ctx.death_state = DeathState.dead - ctx.last_death_link = time.time() - continue - if not invincible[0] and last_health[0] == health[0]: - snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0 - snes_buffered_write(ctx, WRAM_START + 0x0373, - bytes([8])) # deal 1 full heart of damage at next opportunity - elif ctx.game == GAME_SM: - snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([1, 0])) # set current health to 1 (to prevent saving with 0 energy) - snes_buffered_write(ctx, WRAM_START + 0x0A50, bytes([255])) # deal 255 of damage at next opportunity - if not ctx.death_link_allow_survive: - snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0 - elif ctx.game == GAME_SMW: - from worlds.smw.Client import deathlink_kill_player as smw_deathlink_kill_player - await smw_deathlink_kill_player(ctx) - await snes_flush_writes(ctx) - await asyncio.sleep(1) + if ctx.client_handler is None: + continue + + await ctx.client_handler.deathlink_kill_player(ctx) - if ctx.game == GAME_ALTTP: - gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) - if not gamemode or gamemode[0] in DEATH_MODES: - ctx.death_state = DeathState.dead - elif ctx.game == GAME_SM: - gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) - health = await snes_read(ctx, WRAM_START + 0x09C2, 2) - if health is not None: - health = health[0] | (health[1] << 8) - if not gamemode or gamemode[0] in SM_DEATH_MODES or ( - ctx.death_link_allow_survive and health is not None and health > 0): - ctx.death_state = DeathState.dead - elif ctx.game == GAME_DKC3: - from worlds.dkc3.Client import deathlink_kill_player as dkc3_deathlink_kill_player - await dkc3_deathlink_kill_player(ctx) ctx.last_death_link = time.time() -SNES_RECONNECT_DELAY = 5 - -# FXPAK Pro protocol memory mapping used by SNI -ROM_START = 0x000000 -WRAM_START = 0xF50000 -WRAM_SIZE = 0x20000 -SRAM_START = 0xE00000 - -ROMNAME_START = SRAM_START + 0x2000 -ROMNAME_SIZE = 0x15 - -INGAME_MODES = {0x07, 0x09, 0x0b} -ENDGAME_MODES = {0x19, 0x1a} -DEATH_MODES = {0x12} - -SAVEDATA_START = WRAM_START + 0xF000 -SAVEDATA_SIZE = 0x500 - -RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes -RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte -RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte -ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes -ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte -SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte -SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte -SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte -SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte -SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes -SHOP_LEN = (len(Shops.shop_table) * 3) + 5 - -DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte - -# SM -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} - -# 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 = ROM_START + 0x00FFC0 - -SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b} -SMZ3_ENDGAME_MODES = {0x26, 0x27} -SMZ3_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A} - -SMZ3_RECV_PROGRESS_ADDR = SRAM_START + 0x4000 # 2 bytes -SMZ3_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte -SMZ3_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte - - -location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()]) - -location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), - "Blind's Hideout - Left": (0x11d, 0x20), - "Blind's Hideout - Right": (0x11d, 0x40), - "Blind's Hideout - Far Left": (0x11d, 0x80), - "Blind's Hideout - Far Right": (0x11d, 0x100), - 'Secret Passage': (0x55, 0x10), - 'Waterfall Fairy - Left': (0x114, 0x10), - 'Waterfall Fairy - Right': (0x114, 0x20), - "King's Tomb": (0x113, 0x10), - 'Floodgate Chest': (0x10b, 0x10), - "Link's House": (0x104, 0x10), - 'Kakariko Tavern': (0x103, 0x10), - 'Chicken House': (0x108, 0x10), - "Aginah's Cave": (0x10a, 0x10), - "Sahasrahla's Hut - Left": (0x105, 0x10), - "Sahasrahla's Hut - Middle": (0x105, 0x20), - "Sahasrahla's Hut - Right": (0x105, 0x40), - 'Kakariko Well - Top': (0x2f, 0x10), - 'Kakariko Well - Left': (0x2f, 0x20), - 'Kakariko Well - Middle': (0x2f, 0x40), - 'Kakariko Well - Right': (0x2f, 0x80), - 'Kakariko Well - Bottom': (0x2f, 0x100), - 'Lost Woods Hideout': (0xe1, 0x200), - 'Lumberjack Tree': (0xe2, 0x200), - 'Cave 45': (0x11b, 0x400), - 'Graveyard Cave': (0x11b, 0x200), - 'Checkerboard Cave': (0x126, 0x200), - 'Mini Moldorm Cave - Far Left': (0x123, 0x10), - 'Mini Moldorm Cave - Left': (0x123, 0x20), - 'Mini Moldorm Cave - Right': (0x123, 0x40), - 'Mini Moldorm Cave - Far Right': (0x123, 0x80), - 'Mini Moldorm Cave - Generous Guy': (0x123, 0x400), - 'Ice Rod Cave': (0x120, 0x10), - 'Bonk Rock Cave': (0x124, 0x10), - 'Desert Palace - Big Chest': (0x73, 0x10), - 'Desert Palace - Torch': (0x73, 0x400), - 'Desert Palace - Map Chest': (0x74, 0x10), - 'Desert Palace - Compass Chest': (0x85, 0x10), - 'Desert Palace - Big Key Chest': (0x75, 0x10), - 'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400), - 'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400), - 'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400), - 'Desert Palace - Boss': (0x33, 0x800), - 'Eastern Palace - Compass Chest': (0xa8, 0x10), - 'Eastern Palace - Big Chest': (0xa9, 0x10), - 'Eastern Palace - Dark Square Pot Key': (0xba, 0x400), - 'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400), - 'Eastern Palace - Cannonball Chest': (0xb9, 0x10), - 'Eastern Palace - Big Key Chest': (0xb8, 0x10), - 'Eastern Palace - Map Chest': (0xaa, 0x10), - 'Eastern Palace - Boss': (0xc8, 0x800), - 'Hyrule Castle - Boomerang Chest': (0x71, 0x10), - 'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400), - 'Hyrule Castle - Map Chest': (0x72, 0x10), - 'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400), - "Hyrule Castle - Zelda's Chest": (0x80, 0x10), - 'Hyrule Castle - Big Key Drop': (0x80, 0x400), - 'Sewers - Dark Cross': (0x32, 0x10), - 'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400), - 'Sewers - Secret Room - Left': (0x11, 0x10), - 'Sewers - Secret Room - Middle': (0x11, 0x20), - 'Sewers - Secret Room - Right': (0x11, 0x40), - 'Sanctuary': (0x12, 0x10), - 'Castle Tower - Room 03': (0xe0, 0x10), - 'Castle Tower - Dark Maze': (0xd0, 0x10), - 'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400), - 'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400), - 'Spectacle Rock Cave': (0xea, 0x400), - 'Paradox Cave Lower - Far Left': (0xef, 0x10), - 'Paradox Cave Lower - Left': (0xef, 0x20), - 'Paradox Cave Lower - Right': (0xef, 0x40), - 'Paradox Cave Lower - Far Right': (0xef, 0x80), - 'Paradox Cave Lower - Middle': (0xef, 0x100), - 'Paradox Cave Upper - Left': (0xff, 0x10), - 'Paradox Cave Upper - Right': (0xff, 0x20), - 'Spiral Cave': (0xfe, 0x10), - 'Tower of Hera - Basement Cage': (0x87, 0x400), - 'Tower of Hera - Map Chest': (0x77, 0x10), - 'Tower of Hera - Big Key Chest': (0x87, 0x10), - 'Tower of Hera - Compass Chest': (0x27, 0x20), - 'Tower of Hera - Big Chest': (0x27, 0x10), - 'Tower of Hera - Boss': (0x7, 0x800), - 'Hype Cave - Top': (0x11e, 0x10), - 'Hype Cave - Middle Right': (0x11e, 0x20), - 'Hype Cave - Middle Left': (0x11e, 0x40), - 'Hype Cave - Bottom': (0x11e, 0x80), - 'Hype Cave - Generous Guy': (0x11e, 0x400), - 'Peg Cave': (0x127, 0x400), - 'Pyramid Fairy - Left': (0x116, 0x10), - 'Pyramid Fairy - Right': (0x116, 0x20), - 'Brewery': (0x106, 0x10), - 'C-Shaped House': (0x11c, 0x10), - 'Chest Game': (0x106, 0x400), - 'Mire Shed - Left': (0x10d, 0x10), - 'Mire Shed - Right': (0x10d, 0x20), - 'Superbunny Cave - Top': (0xf8, 0x10), - 'Superbunny Cave - Bottom': (0xf8, 0x20), - 'Spike Cave': (0x117, 0x10), - 'Hookshot Cave - Top Right': (0x3c, 0x10), - 'Hookshot Cave - Top Left': (0x3c, 0x20), - 'Hookshot Cave - Bottom Right': (0x3c, 0x80), - 'Hookshot Cave - Bottom Left': (0x3c, 0x40), - 'Mimic Cave': (0x10c, 0x10), - 'Swamp Palace - Entrance': (0x28, 0x10), - 'Swamp Palace - Map Chest': (0x37, 0x10), - 'Swamp Palace - Pot Row Pot Key': (0x38, 0x400), - 'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400), - 'Swamp Palace - Hookshot Pot Key': (0x36, 0x400), - 'Swamp Palace - Big Chest': (0x36, 0x10), - 'Swamp Palace - Compass Chest': (0x46, 0x10), - 'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400), - 'Swamp Palace - Big Key Chest': (0x35, 0x10), - 'Swamp Palace - West Chest': (0x34, 0x10), - 'Swamp Palace - Flooded Room - Left': (0x76, 0x10), - 'Swamp Palace - Flooded Room - Right': (0x76, 0x20), - 'Swamp Palace - Waterfall Room': (0x66, 0x10), - 'Swamp Palace - Waterway Pot Key': (0x16, 0x400), - 'Swamp Palace - Boss': (0x6, 0x800), - "Thieves' Town - Big Key Chest": (0xdb, 0x20), - "Thieves' Town - Map Chest": (0xdb, 0x10), - "Thieves' Town - Compass Chest": (0xdc, 0x10), - "Thieves' Town - Ambush Chest": (0xcb, 0x10), - "Thieves' Town - Hallway Pot Key": (0xbc, 0x400), - "Thieves' Town - Spike Switch Pot Key": (0xab, 0x400), - "Thieves' Town - Attic": (0x65, 0x10), - "Thieves' Town - Big Chest": (0x44, 0x10), - "Thieves' Town - Blind's Cell": (0x45, 0x10), - "Thieves' Town - Boss": (0xac, 0x800), - 'Skull Woods - Compass Chest': (0x67, 0x10), - 'Skull Woods - Map Chest': (0x58, 0x20), - 'Skull Woods - Big Chest': (0x58, 0x10), - 'Skull Woods - Pot Prison': (0x57, 0x20), - 'Skull Woods - Pinball Room': (0x68, 0x10), - 'Skull Woods - Big Key Chest': (0x57, 0x10), - 'Skull Woods - West Lobby Pot Key': (0x56, 0x400), - 'Skull Woods - Bridge Room': (0x59, 0x10), - 'Skull Woods - Spike Corner Key Drop': (0x39, 0x400), - 'Skull Woods - Boss': (0x29, 0x800), - 'Ice Palace - Jelly Key Drop': (0x0e, 0x400), - 'Ice Palace - Compass Chest': (0x2e, 0x10), - 'Ice Palace - Conveyor Key Drop': (0x3e, 0x400), - 'Ice Palace - Freezor Chest': (0x7e, 0x10), - 'Ice Palace - Big Chest': (0x9e, 0x10), - 'Ice Palace - Iced T Room': (0xae, 0x10), - 'Ice Palace - Many Pots Pot Key': (0x9f, 0x400), - 'Ice Palace - Spike Room': (0x5f, 0x10), - 'Ice Palace - Big Key Chest': (0x1f, 0x10), - 'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400), - 'Ice Palace - Map Chest': (0x3f, 0x10), - 'Ice Palace - Boss': (0xde, 0x800), - 'Misery Mire - Big Chest': (0xc3, 0x10), - 'Misery Mire - Map Chest': (0xc3, 0x20), - 'Misery Mire - Main Lobby': (0xc2, 0x10), - 'Misery Mire - Bridge Chest': (0xa2, 0x10), - 'Misery Mire - Spikes Pot Key': (0xb3, 0x400), - 'Misery Mire - Spike Chest': (0xb3, 0x10), - 'Misery Mire - Fishbone Pot Key': (0xa1, 0x400), - 'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400), - 'Misery Mire - Compass Chest': (0xc1, 0x10), - 'Misery Mire - Big Key Chest': (0xd1, 0x10), - 'Misery Mire - Boss': (0x90, 0x800), - 'Turtle Rock - Compass Chest': (0xd6, 0x10), - 'Turtle Rock - Roller Room - Left': (0xb7, 0x10), - 'Turtle Rock - Roller Room - Right': (0xb7, 0x20), - 'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400), - 'Turtle Rock - Chain Chomps': (0xb6, 0x10), - 'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400), - 'Turtle Rock - Big Key Chest': (0x14, 0x10), - 'Turtle Rock - Big Chest': (0x24, 0x10), - 'Turtle Rock - Crystaroller Room': (0x4, 0x10), - 'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80), - 'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40), - 'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20), - 'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10), - 'Turtle Rock - Boss': (0xa4, 0x800), - 'Palace of Darkness - Shooter Room': (0x9, 0x10), - 'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20), - 'Palace of Darkness - Stalfos Basement': (0xa, 0x10), - 'Palace of Darkness - Big Key Chest': (0x3a, 0x10), - 'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10), - 'Palace of Darkness - Map Chest': (0x2b, 0x10), - 'Palace of Darkness - Compass Chest': (0x1a, 0x20), - 'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10), - 'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20), - 'Palace of Darkness - Dark Maze - Top': (0x19, 0x10), - 'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20), - 'Palace of Darkness - Big Chest': (0x1a, 0x10), - 'Palace of Darkness - Harmless Hellway': (0x1a, 0x40), - 'Palace of Darkness - Boss': (0x5a, 0x800), - 'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400), - "Ganons Tower - Bob's Torch": (0x8c, 0x400), - 'Ganons Tower - Hope Room - Left': (0x8c, 0x20), - 'Ganons Tower - Hope Room - Right': (0x8c, 0x40), - 'Ganons Tower - Tile Room': (0x8d, 0x10), - 'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10), - 'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20), - 'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40), - 'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80), - 'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400), - 'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10), - 'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20), - 'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40), - 'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80), - 'Ganons Tower - Map Chest': (0x8b, 0x10), - 'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400), - 'Ganons Tower - Firesnake Room': (0x7d, 0x10), - 'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10), - 'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20), - 'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40), - 'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80), - "Ganons Tower - Bob's Chest": (0x8c, 0x80), - 'Ganons Tower - Big Chest': (0x8c, 0x10), - 'Ganons Tower - Big Key Room - Left': (0x1c, 0x20), - 'Ganons Tower - Big Key Room - Right': (0x1c, 0x40), - 'Ganons Tower - Big Key Chest': (0x1c, 0x10), - 'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10), - 'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20), - 'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400), - 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), - 'Ganons Tower - Validation Chest': (0x4d, 0x10)} - -boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss', - 'Desert Palace - Boss', - 'Tower of Hera - Boss', - 'Palace of Darkness - Boss', - 'Swamp Palace - Boss', - 'Skull Woods - Boss', - "Thieves' Town - Boss", - 'Ice Palace - Boss', - 'Misery Mire - Boss', - 'Turtle Rock - Boss', - 'Sahasrahla'}} - -location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()} - -location_table_npc = {'Mushroom': 0x1000, - 'King Zora': 0x2, - 'Sahasrahla': 0x10, - 'Blacksmith': 0x400, - 'Magic Bat': 0x8000, - 'Sick Kid': 0x4, - 'Library': 0x80, - 'Potion Shop': 0x2000, - 'Old Man': 0x1, - 'Ether Tablet': 0x100, - 'Catfish': 0x20, - 'Stumpy': 0x8, - 'Bombos Tablet': 0x200} - -location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()} - -location_table_ow = {'Flute Spot': 0x2a, - 'Sunken Treasure': 0x3b, - "Zora's Ledge": 0x81, - 'Lake Hylia Island': 0x35, - 'Maze Race': 0x28, - 'Desert Ledge': 0x30, - 'Master Sword Pedestal': 0x80, - 'Spectacle Rock': 0x3, - 'Pyramid': 0x5b, - 'Digging Game': 0x68, - 'Bumper Cave Ledge': 0x4a, - 'Floating Island': 0x5} - -location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()} - -location_table_misc = {'Bottle Merchant': (0x3c9, 0x2), - 'Purple Chest': (0x3c9, 0x10), - "Link's Uncle": (0x3c6, 0x1), - 'Hobo': (0x3c9, 0x1)} - -location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()} +_global_snes_reconnect_delay = 5 class SNESState(enum.IntEnum): @@ -607,13 +269,13 @@ class SNESState(enum.IntEnum): SNES_ATTACHED = 3 -def launch_sni(): - sni_path = Utils.get_options()["lttp_options"]["sni"] +def launch_sni() -> None: + sni_path = Utils.get_options()["sni_options"]["sni_path"] if not os.path.isdir(sni_path): sni_path = Utils.local_path(sni_path) if os.path.isdir(sni_path): - dir_entry: os.DirEntry + dir_entry: "os.DirEntry[str]" for dir_entry in os.scandir(sni_path): if dir_entry.is_file(): lower_file = dir_entry.name.lower() @@ -641,13 +303,13 @@ def launch_sni(): f"please start it yourself if it is not running") -async def _snes_connect(ctx: Context, address: str): +async def _snes_connect(ctx: SNIContext, address: str) -> WebSocketClientProtocol: address = f"ws://{address}" if "://" not in address else address snes_logger.info("Connecting to SNI at %s ..." % address) - seen_problems = set() - while 1: + seen_problems: typing.Set[str] = set() + while True: try: - snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None) + snes_socket = await websockets_connect(address, ping_timeout=None, ping_interval=None) except Exception as e: problem = "%s" % e # only tell the user about new problems, otherwise silently lay in wait for a working connection @@ -664,15 +326,24 @@ async def _snes_connect(ctx: Context, address: str): return snes_socket -async def get_snes_devices(ctx: Context) -> typing.List[str]: +class SNESRequest(typing.TypedDict): + Opcode: str + Space: str + Operands: typing.List[str] + # TODO: When Python 3.11 is the lowest version supported, `Operands` can use `typing.NotRequired` (pep-0655) + # Then the `Operands` key doesn't need to be given for opcodes that don't use it. + + +async def get_snes_devices(ctx: SNIContext) -> typing.List[str]: socket = await _snes_connect(ctx, ctx.snes_address) # establish new connection to poll - DeviceList_Request = { + DeviceList_Request: SNESRequest = { "Opcode": "DeviceList", - "Space": "SNES" + "Space": "SNES", + "Operands": [] } await socket.send(dumps(DeviceList_Request)) - reply: dict = loads(await socket.recv()) + reply: typing.Dict[str, typing.Any] = loads(await socket.recv()) devices: typing.List[str] = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else [] if not devices: @@ -688,7 +359,7 @@ async def get_snes_devices(ctx: Context) -> typing.List[str]: return sorted(devices) -async def verify_snes_app(socket): +async def verify_snes_app(socket: WebSocketClientProtocol) -> None: AppVersion_Request = { "Opcode": "AppVersion", } @@ -699,8 +370,8 @@ async def verify_snes_app(socket): snes_logger.warning(f"Warning: Did not find SNI as the endpoint, instead {app} was found.") -async def snes_connect(ctx: Context, address, deviceIndex=-1): - global SNES_RECONNECT_DELAY +async def snes_connect(ctx: SNIContext, address: str, deviceIndex: int = -1) -> None: + global _global_snes_reconnect_delay if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED: if ctx.rom: snes_logger.error('Already connected to SNES, with rom loaded.') @@ -722,6 +393,7 @@ async def snes_connect(ctx: Context, address, deviceIndex=-1): if device_count == 1: device = devices[0] elif ctx.snes_reconnect_address: + assert ctx.snes_attached_device if ctx.snes_attached_device[1] in devices: device = ctx.snes_attached_device[1] else: @@ -746,7 +418,7 @@ async def snes_connect(ctx: Context, address, deviceIndex=-1): snes_logger.info("Attaching to " + device) - Attach_Request = { + Attach_Request: SNESRequest = { "Opcode": "Attach", "Space": "SNES", "Operands": [device] @@ -770,35 +442,37 @@ async def snes_connect(ctx: Context, address, deviceIndex=-1): if not ctx.snes_reconnect_address: snes_logger.error("Error connecting to snes (%s)" % e) else: - snes_logger.error(f"Error connecting to snes, attempt again in {SNES_RECONNECT_DELAY}s") + snes_logger.error(f"Error connecting to snes, attempt again in {_global_snes_reconnect_delay}s") asyncio.create_task(snes_autoreconnect(ctx)) - SNES_RECONNECT_DELAY *= 2 + _global_snes_reconnect_delay *= 2 else: - SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay + _global_snes_reconnect_delay = ctx.starting_reconnect_delay snes_logger.info(f"Attached to {device}") -async def snes_disconnect(ctx: Context): +async def snes_disconnect(ctx: SNIContext) -> None: if ctx.snes_socket: if not ctx.snes_socket.closed: await ctx.snes_socket.close() ctx.snes_socket = None -async def snes_autoreconnect(ctx: Context): - await asyncio.sleep(SNES_RECONNECT_DELAY) +async def snes_autoreconnect(ctx: SNIContext) -> None: + await asyncio.sleep(_global_snes_reconnect_delay) if ctx.snes_reconnect_address and ctx.snes_socket is None: await snes_connect(ctx, ctx.snes_reconnect_address) -async def snes_recv_loop(ctx: Context): +async def snes_recv_loop(ctx: SNIContext) -> None: try: + if ctx.snes_socket is None: + raise Exception("invalid context state - snes_socket not connected") async for msg in ctx.snes_socket: - ctx.snes_recv_queue.put_nowait(msg) + ctx.snes_recv_queue.put_nowait(typing.cast(bytes, msg)) snes_logger.warning("Snes disconnected") except Exception as e: - if not isinstance(e, websockets.WebSocketException): + if not isinstance(e, WebSocketException): snes_logger.exception(e) snes_logger.error("Lost connection to the snes, type /snes to reconnect") finally: @@ -813,28 +487,33 @@ async def snes_recv_loop(ctx: Context): ctx.rom = None if ctx.snes_reconnect_address: - snes_logger.info(f"...reconnecting in {SNES_RECONNECT_DELAY}s") + snes_logger.info(f"...reconnecting in {_global_snes_reconnect_delay}s") asyncio.create_task(snes_autoreconnect(ctx)) -async def snes_read(ctx: Context, address, size): +async def snes_read(ctx: SNIContext, address: int, size: int) -> typing.Optional[bytes]: try: await ctx.snes_request_lock.acquire() - if ctx.snes_state != SNESState.SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed: + if ( + ctx.snes_state != SNESState.SNES_ATTACHED or + ctx.snes_socket is None or + not ctx.snes_socket.open or + ctx.snes_socket.closed + ): return None - GetAddress_Request = { + GetAddress_Request: SNESRequest = { "Opcode": "GetAddress", "Space": "SNES", "Operands": [hex(address)[2:], hex(size)[2:]] } try: await ctx.snes_socket.send(dumps(GetAddress_Request)) - except websockets.ConnectionClosed: + except ConnectionClosed: return None - data = bytes() + data: bytes = bytes() while len(data) < size: try: data += await asyncio.wait_for(ctx.snes_recv_queue.get(), 5) @@ -855,7 +534,7 @@ async def snes_read(ctx: Context, address, size): ctx.snes_request_lock.release() -async def snes_write(ctx: Context, write_list): +async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int, bytes]]) -> bool: try: await ctx.snes_request_lock.acquire() @@ -863,16 +542,18 @@ async def snes_write(ctx: Context, write_list): not ctx.snes_socket.open or ctx.snes_socket.closed: return False - PutAddress_Request = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'} + PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'} try: for address, data in write_list: PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]] + # REVIEW: above: `if snes_socket is None: return False` + # Does it need to be checked again? if ctx.snes_socket is not None: await ctx.snes_socket.send(dumps(PutAddress_Request)) await ctx.snes_socket.send(data) else: snes_logger.warning(f"Could not send data to SNES: {data}") - except websockets.ConnectionClosed: + except ConnectionClosed: return False return True @@ -880,7 +561,7 @@ async def snes_write(ctx: Context, write_list): ctx.snes_request_lock.release() -def snes_buffered_write(ctx: Context, address, data): +def snes_buffered_write(ctx: SNIContext, address: int, data: bytes) -> None: if ctx.snes_write_buffer and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address: # append to existing write command, bundling them ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data) @@ -888,7 +569,7 @@ def snes_buffered_write(ctx: Context, address, data): ctx.snes_write_buffer.append((address, data)) -async def snes_flush_writes(ctx: Context): +async def snes_flush_writes(ctx: SNIContext) -> None: if not ctx.snes_write_buffer: return @@ -897,142 +578,7 @@ async def snes_flush_writes(ctx: Context): await snes_write(ctx, writes) -async def track_locations(ctx: Context, roomid, roomdata): - new_locations = [] - - def new_check(location_id): - new_locations.append(location_id) - ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] - snes_logger.info( - f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') - - try: - shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN) - shop_data_changed = False - shop_data = list(shop_data) - for cnt, b in enumerate(shop_data): - location = Shops.SHOP_ID_START + cnt - if int(b) and location not in ctx.locations_checked: - new_check(location) - if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \ - and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot: - if not int(b): - shop_data[cnt] += 1 - shop_data_changed = True - if shop_data_changed: - snes_buffered_write(ctx, SHOP_ADDR, bytes(shop_data)) - except Exception as e: - snes_logger.info(f"Exception: {e}") - - for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items(): - try: - if location_id not in ctx.locations_checked and loc_roomid == roomid and \ - (roomdata << 4) & loc_mask != 0: - new_check(location_id) - except Exception as e: - snes_logger.exception(f"Exception: {e}") - - uw_begin = 0x129 - ow_end = uw_end = 0 - uw_unchecked = {} - uw_checked = {} - for location, (roomid, mask) in location_table_uw.items(): - location_id = Regions.lookup_name_to_id[location] - if location_id not in ctx.locations_checked: - uw_unchecked[location_id] = (roomid, mask) - uw_begin = min(uw_begin, roomid) - uw_end = max(uw_end, roomid + 1) - if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ - and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: - uw_begin = min(uw_begin, roomid) - uw_end = max(uw_end, roomid + 1) - uw_checked[location_id] = (roomid, mask) - - if uw_begin < uw_end: - uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2) - if uw_data is not None: - for location_id, (roomid, mask) in uw_unchecked.items(): - offset = (roomid - uw_begin) * 2 - roomdata = uw_data[offset] | (uw_data[offset + 1] << 8) - if roomdata & mask != 0: - new_check(location_id) - if uw_checked: - uw_data = list(uw_data) - for location_id, (roomid, mask) in uw_checked.items(): - offset = (roomid - uw_begin) * 2 - roomdata = uw_data[offset] | (uw_data[offset + 1] << 8) - roomdata |= mask - uw_data[offset] = roomdata & 0xFF - uw_data[offset + 1] = roomdata >> 8 - snes_buffered_write(ctx, SAVEDATA_START + (uw_begin * 2), bytes(uw_data)) - - ow_begin = 0x82 - ow_unchecked = {} - ow_checked = {} - for location_id, screenid in location_table_ow_id.items(): - if location_id not in ctx.locations_checked: - ow_unchecked[location_id] = screenid - ow_begin = min(ow_begin, screenid) - ow_end = max(ow_end, screenid + 1) - if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: - ow_checked[location_id] = screenid - - if ow_begin < ow_end: - ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin) - if ow_data is not None: - for location_id, screenid in ow_unchecked.items(): - if ow_data[screenid - ow_begin] & 0x40 != 0: - new_check(location_id) - if ow_checked: - ow_data = list(ow_data) - for location_id, screenid in ow_checked.items(): - ow_data[screenid - ow_begin] |= 0x40 - snes_buffered_write(ctx, SAVEDATA_START + 0x280 + ow_begin, bytes(ow_data)) - - if not ctx.locations_checked.issuperset(location_table_npc_id): - npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2) - if npc_data is not None: - npc_value_changed = False - npc_value = npc_data[0] | (npc_data[1] << 8) - for location_id, mask in location_table_npc_id.items(): - if npc_value & mask != 0 and location_id not in ctx.locations_checked: - new_check(location_id) - if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ - and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: - npc_value |= mask - npc_value_changed = True - if npc_value_changed: - npc_data = bytes([npc_value & 0xFF, npc_value >> 8]) - snes_buffered_write(ctx, SAVEDATA_START + 0x410, npc_data) - - if not ctx.locations_checked.issuperset(location_table_misc_id): - misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4) - if misc_data is not None: - misc_data = list(misc_data) - misc_data_changed = False - for location_id, (offset, mask) in location_table_misc_id.items(): - assert (0x3c6 <= offset <= 0x3c9) - if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked: - new_check(location_id) - if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ - and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot: - misc_data_changed = True - misc_data[offset - 0x3c6] |= mask - if misc_data_changed: - snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data)) - - - if new_locations: - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}]) - await snes_flush_writes(ctx) - - -async def game_watcher(ctx: Context): - prev_game_timer = 0 +async def game_watcher(ctx: SNIContext) -> None: perf_counter = time.perf_counter() while not ctx.exit_event.is_set(): try: @@ -1041,54 +587,24 @@ async def game_watcher(ctx: Context): pass ctx.watcher_event.clear() - if not ctx.rom: + if not ctx.rom or not ctx.client_handler: ctx.finished_game = False ctx.death_link_allow_survive = False - from worlds.dkc3.Client import dkc3_rom_init - init_handled = await dkc3_rom_init(ctx) - if not init_handled: - from worlds.smw.Client import smw_rom_init - init_handled = await smw_rom_init(ctx) - if not init_handled: - game_name = await snes_read(ctx, SM_ROMNAME_START, 5) - if game_name is None: - continue - elif game_name[:2] == b"SM": - ctx.game = GAME_SM - # versions lower than 0.3.0 dont have item handling flag nor remote item support - romVersion = int(game_name[2:5].decode('UTF-8')) - if romVersion < 30: - ctx.items_handling = 0b001 # full local - else: - item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1) - ctx.items_handling = 0b001 if item_handling is None else item_handling[0] - else: - game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3) - if game_name == b"ZSM": - ctx.game = GAME_SMZ3 - ctx.items_handling = 0b101 # local items and remote start inventory - else: - ctx.game = GAME_ALTTP - ctx.items_handling = 0b001 # full local + from worlds.AutoSNIClient import AutoSNIClientRegister + ctx.client_handler = await AutoSNIClientRegister.get_handler(ctx) - rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE) - if rom is None or rom == bytes([0] * ROMNAME_SIZE): - continue + if not ctx.client_handler: + continue - ctx.rom = rom - if ctx.game != GAME_SMZ3: - death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else - SM_DEATH_LINK_ACTIVE_ADDR, 1) - if death_link: - ctx.allow_collect = bool(death_link[0] & 0b100) - ctx.death_link_allow_survive = bool(death_link[0] & 0b10) - await ctx.update_death_link(bool(death_link[0] & 0b1)) - if not ctx.prev_rom or ctx.prev_rom != ctx.rom: - ctx.locations_checked = set() - ctx.locations_scouted = set() - ctx.locations_info = {} - ctx.prev_rom = ctx.rom + if not ctx.rom: + continue + + if not ctx.prev_rom or ctx.prev_rom != ctx.rom: + ctx.locations_checked = set() + ctx.locations_scouted = set() + ctx.locations_info = {} + ctx.prev_rom = ctx.rom if ctx.awaiting_rom: await ctx.server_auth(False) @@ -1096,234 +612,40 @@ async def game_watcher(ctx: Context): 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: + if not ctx.client_handler: + continue + + rom_validated = await ctx.client_handler.validate_rom(ctx) + + if not rom_validated or (ctx.auth and ctx.auth != ctx.rom): snes_logger.warning("ROM change detected, please reconnect to the multiworld server") await ctx.disconnect() + ctx.client_handler = None + ctx.rom = None + ctx.command_processor(ctx).connect_to_snes() + continue - if ctx.game == GAME_ALTTP: - gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) - if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): - currently_dead = gamemode[0] in DEATH_MODES - await ctx.handle_deathlink_state(currently_dead) + delay = 7 if ctx.slow_mode else 0 + if time.perf_counter() - perf_counter < delay: + continue - gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1) - game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4) - if gamemode is None or gameend is None or game_timer is None or \ - (gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES): - continue + perf_counter = time.perf_counter() - delay = 7 if ctx.slow_mode else 2 - if gameend[0]: - if not ctx.finished_game: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - - if time.perf_counter() - perf_counter < delay: - continue - else: - perf_counter = time.perf_counter() - else: - game_timer = game_timer[0] | (game_timer[1] << 8) | (game_timer[2] << 16) | (game_timer[3] << 24) - if abs(game_timer - prev_game_timer) < (delay * 60): - continue - else: - prev_game_timer = game_timer - - if gamemode in ENDGAME_MODES: # triforce room and credits - continue - - data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8) - if data is None: - continue - - recv_index = data[0] | (data[1] << 8) - recv_item = data[2] - roomid = data[4] | (data[5] << 8) - roomdata = data[6] - scout_location = data[7] - - if recv_index < len(ctx.items_received) and recv_item == 0: - item = ctx.items_received[recv_index] - recv_index += 1 - logging.info('Received %s from %s (%s) (%d/%d in list)' % ( - color(ctx.item_names[item.item], 'red', 'bold'), - color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], recv_index, len(ctx.items_received))) - - snes_buffered_write(ctx, RECV_PROGRESS_ADDR, - bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - snes_buffered_write(ctx, RECV_ITEM_ADDR, - bytes([item.item])) - snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, - bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0])) - if scout_location > 0 and scout_location in ctx.locations_info: - snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, - bytes([scout_location])) - snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, - bytes([ctx.locations_info[scout_location].item])) - snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, - bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)])) - - await snes_flush_writes(ctx) - - if scout_location > 0 and scout_location not in ctx.locations_scouted: - ctx.locations_scouted.add(scout_location) - 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 - await ctx.handle_deathlink_state(currently_dead) - if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES: - if not ctx.finished_game: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - continue - - 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) # this is actually SM_SEND_QUEUE_WCOUNT - - while (recv_index < recv_item): - itemAdress = recv_index * 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_SEND_QUEUE_RCOUNT, - bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - - from worlds.sm import locations_start_id - location_id = locations_start_id + itemIndex - - ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] - snes_logger.info( - f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - - data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2) - if data is None: - continue - - itemOutPtr = data[0] | (data[1] << 8) - - 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 - if bool(ctx.items_handling & 0b010): - locationId = (item.location - locations_start_id) if (item.location >= 0 and item.player == ctx.slot) else 0xFF - else: - locationId = 0x00 #backward compat - - playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0 - 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_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'), - color(ctx.player_names[item.player], 'yellow'), - 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): - gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) - endGameModes = SM_ENDGAME_MODES - else: - gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) - endGameModes = ENDGAME_MODES - - if gamemode is not None and (gamemode[0] in endGameModes): - if not ctx.finished_game: - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - continue - - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4) - if data is None: - continue - - recv_index = data[0] | (data[1] << 8) - recv_item = data[2] | (data[3] << 8) - - while (recv_index < recv_item): - itemAdress = recv_index * 8 - message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8) - # worldId = message[0] | (message[1] << 8) # unused - # itemId = message[2] | (message[3] << 8) # unused - isZ3Item = ((message[5] & 0x80) != 0) - maskedPart = (message[5] & 0x7F) if isZ3Item else message[5] - itemIndex = ((message[4] | (maskedPart << 8)) >> 3) + (256 if isZ3Item else 0) - - recv_index += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - - from worlds.smz3.TotalSMZ3.Location import locations_start_id - from worlds.smz3 import convertLocSMZ3IDToAPID - location_id = locations_start_id + convertLocSMZ3IDToAPID(itemIndex) - - ctx.locations_checked.add(location_id) - location = ctx.location_names[location_id] - snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') - await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4) - if data is None: - continue - - # recv_itemOutPtr = data[0] | (data[1] << 8) # unused - itemOutPtr = data[2] | (data[3] << 8) - - from worlds.smz3.TotalSMZ3.Item import items_start_id - if itemOutPtr < len(ctx.items_received): - item = ctx.items_received[itemOutPtr] - itemId = item.item - items_start_id - - playerID = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes([playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, (itemId >> 8) & 0xFF])) - itemOutPtr += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, 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'), color(ctx.player_names[item.player], 'yellow'), - ctx.location_names[item.location], itemOutPtr, len(ctx.items_received))) - await snes_flush_writes(ctx) - elif ctx.game == GAME_DKC3: - from worlds.dkc3.Client import dkc3_game_watcher - await dkc3_game_watcher(ctx) - elif ctx.game == GAME_SMW: - from worlds.smw.Client import smw_game_watcher - await smw_game_watcher(ctx) + await ctx.client_handler.game_watcher(ctx) -async def run_game(romfile): - auto_start = Utils.get_options()["lttp_options"].get("rom_start", True) +async def run_game(romfile: str) -> None: + auto_start = typing.cast(typing.Union[bool, str], + Utils.get_options()["sni_options"].get("snes_rom_start", True)) if auto_start is True: import webbrowser webbrowser.open(romfile) - elif os.path.isfile(auto_start): + elif isinstance(auto_start, str) and os.path.isfile(auto_start): subprocess.Popen([auto_start, romfile], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) -async def main(): +async def main() -> None: multiprocessing.freeze_support() parser = get_base_parser() parser.add_argument('diff_file', default="", type=str, nargs="?", @@ -1350,12 +672,13 @@ async def main(): time.sleep(3) sys.exit() elif args.diff_file.endswith(".aplttp"): + from worlds.alttp.Client import get_alttp_settings adjustedromfile, adjusted = get_alttp_settings(romfile) asyncio.create_task(run_game(adjustedromfile if adjusted else romfile)) else: asyncio.create_task(run_game(romfile)) - ctx = Context(args.snes, args.connect, args.password) + ctx = SNIContext(args.snes, args.connect, args.password) if ctx.server_task is None: ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") @@ -1376,132 +699,6 @@ async def main(): await ctx.shutdown() -def get_alttp_settings(romfile: str): - lastSettings = Utils.get_adjuster_settings(GAME_ALTTP) - adjustedromfile = '' - if lastSettings: - choice = 'no' - if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply: - - whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", - "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", - "reduceflashing", "deathlink", "allowcollect"} - printed_options = {name: value for name, value in vars(lastSettings).items() if name in whitelist} - if hasattr(lastSettings, "sprite_pool"): - sprite_pool = {} - for sprite in lastSettings.sprite_pool: - if sprite in sprite_pool: - sprite_pool[sprite] += 1 - else: - sprite_pool[sprite] = 1 - if sprite_pool: - printed_options["sprite_pool"] = sprite_pool - import pprint - - if gui_enabled: - - try: - from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button - applyPromptWindow = Tk() - except Exception as e: - logging.error('Could not load tkinter, which is likely not installed.') - return '', False - - applyPromptWindow.resizable(False, False) - applyPromptWindow.protocol('WM_DELETE_WINDOW', lambda: onButtonClick()) - logo = PhotoImage(file=Utils.local_path('data', 'icon.png')) - applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo) - applyPromptWindow.wm_title("Last adjuster settings LttP") - - label = LabelFrame(applyPromptWindow, - text='Last used adjuster settings were found. Would you like to apply these?') - label.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5) - label.grid_columnconfigure(0, weight=1) - label.grid_columnconfigure(1, weight=1) - label.grid_columnconfigure(2, weight=1) - label.grid_columnconfigure(3, weight=1) - - def onButtonClick(answer: str = 'no'): - setattr(onButtonClick, 'choice', answer) - applyPromptWindow.destroy() - - framedOptions = Frame(label) - framedOptions.grid(column=0, columnspan=4, row=0) - framedOptions.grid_columnconfigure(0, weight=1) - framedOptions.grid_columnconfigure(1, weight=1) - framedOptions.grid_columnconfigure(2, weight=1) - curRow = 0 - curCol = 0 - for name, value in printed_options.items(): - Label(framedOptions, text=name + ": " + str(value)).grid(column=curCol, row=curRow, padx=5) - if (curCol == 2): - curRow += 1 - curCol = 0 - else: - curCol += 1 - - yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10) - yesButton.grid(column=0, row=1) - noButton = Button(label, text='No', command=lambda: onButtonClick('no'), width=10) - noButton.grid(column=1, row=1) - alwaysButton = Button(label, text='Always', command=lambda: onButtonClick('always'), width=10) - alwaysButton.grid(column=2, row=1) - neverButton = Button(label, text='Never', command=lambda: onButtonClick('never'), width=10) - neverButton.grid(column=3, row=1) - - Utils.tkinter_center_window(applyPromptWindow) - applyPromptWindow.mainloop() - choice = getattr(onButtonClick, 'choice') - else: - choice = input(f"Last used adjuster settings were found. Would you like to apply these? \n" - f"{pprint.pformat(printed_options)}\n" - f"Enter yes, no, always or never: ") - if choice and choice.startswith("y"): - choice = 'yes' - elif choice and "never" in choice: - choice = 'no' - lastSettings.auto_apply = 'never' - Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings) - elif choice and "always" in choice: - choice = 'yes' - lastSettings.auto_apply = 'always' - Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings) - else: - choice = 'no' - elif 'never' in lastSettings.auto_apply: - choice = 'no' - elif 'always' in lastSettings.auto_apply: - choice = 'yes' - - if 'yes' in choice: - from worlds.alttp.Rom import get_base_rom_path - lastSettings.rom = romfile - lastSettings.baserom = get_base_rom_path() - lastSettings.world = None - - if hasattr(lastSettings, "sprite_pool"): - from LttPAdjuster import AdjusterWorld - lastSettings.world = AdjusterWorld(getattr(lastSettings, "sprite_pool")) - - adjusted = True - import LttPAdjuster - _, adjustedromfile = LttPAdjuster.adjust(lastSettings) - - if hasattr(lastSettings, "world"): - delattr(lastSettings, "world") - else: - adjusted = False - if adjusted: - try: - shutil.move(adjustedromfile, romfile) - adjustedromfile = romfile - except Exception as e: - logging.exception(e) - else: - adjusted = False - return adjustedromfile, adjusted - - if __name__ == '__main__': colorama.init() asyncio.run(main()) diff --git a/Utils.py b/Utils.py index d28834b698..64a028fc33 100644 --- a/Utils.py +++ b/Utils.py @@ -141,7 +141,7 @@ def user_path(*path: str) -> str: return os.path.join(user_path.cached_path, *path) -def output_path(*path: str): +def output_path(*path: str) -> str: if hasattr(output_path, 'cached_path'): return os.path.join(output_path.cached_path, *path) output_path.cached_path = user_path(get_options()["general_options"]["output_path"]) @@ -232,19 +232,18 @@ def get_default_options() -> OptionsType: "factorio_options": { "executable": os.path.join("factorio", "bin", "x64", "factorio"), }, + "sni_options": { + "sni": "SNI", + "snes_rom_start": True, + }, "sm_options": { "rom_file": "Super Metroid (JU).sfc", - "sni": "SNI", - "rom_start": True, }, "soe_options": { "rom_file": "Secret of Evermore (USA).sfc", }, "lttp_options": { "rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc", - "sni": "SNI", - "rom_start": True, - }, "server_options": { "host": None, @@ -287,13 +286,9 @@ def get_default_options() -> OptionsType: }, "dkc3_options": { "rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc", - "sni": "SNI", - "rom_start": True, }, "smw_options": { "rom_file": "Super Mario World (USA).sfc", - "sni": "SNI", - "rom_start": True, }, "zillion_options": { "rom_file": "Zillion (UE) [!].sms", diff --git a/host.yaml b/host.yaml index 2bb0e5ef5d..2c5a8e3e1d 100644 --- a/host.yaml +++ b/host.yaml @@ -82,24 +82,19 @@ generator: # List of options that can be plando'd. Can be combined, for example "bosses, items" # Available options: bosses, items, texts, connections plando_options: "bosses" +sni_options: + # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found + sni_path: "SNI" + # Set this to false to never autostart a rom (such as after patching) + # True for operating system default program + # Alternatively, a path to a program to open the .sfc file with + snes_rom_start: true lttp_options: # File name of the v1.0 J rom rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" - # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found - sni: "SNI" - # Set this to false to never autostart a rom (such as after patching) - # True for operating system default program - # Alternatively, a path to a program to open the .sfc file with - rom_start: true sm_options: # File name of the v1.0 J rom rom_file: "Super Metroid (JU).sfc" - # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found - sni: "SNI" - # Set this to false to never autostart a rom (such as after patching) - # True for operating system default program - # Alternatively, a path to a program to open the .sfc file with - rom_start: true factorio_options: executable: "factorio/bin/x64/factorio" # by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used. @@ -122,22 +117,12 @@ soe_options: rom_file: "Secret of Evermore (USA).sfc" ffr_options: display_msgs: true -smz3_options: - # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found - sni: "SNI" - # Set this to false to never autostart a rom (such as after patching) - # True for operating system default program - # Alternatively, a path to a program to open the .sfc file with - rom_start: true dkc3_options: # File name of the DKC3 US rom rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc" - # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found - sni: "SNI" - # Set this to false to never autostart a rom (such as after patching) - # True for operating system default program - # Alternatively, a path to a program to open the .sfc file with - rom_start: true +smw_options: + # File name of the SMW US rom + rom_file: "Super Mario World (USA).sfc" pokemon_rb_options: # File names of the Pokemon Red and Blue roms red_rom_file: "Pokemon Red (UE) [S][!].gb" @@ -146,15 +131,6 @@ pokemon_rb_options: # True for operating system default program # Alternatively, a path to a program to open the .gb file with rom_start: true -smw_options: - # File name of the SMW US rom - rom_file: "Super Mario World (USA).sfc" - # Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found - sni: "SNI" - # Set this to false to never autostart a rom (such as after patching) - # True for operating system default program - # Alternatively, a path to a program to open the .sfc file with - rom_start: true zillion_options: # File name of the Zillion US rom rom_file: "Zillion (UE) [!].sms" diff --git a/worlds/AutoSNIClient.py b/worlds/AutoSNIClient.py new file mode 100644 index 0000000000..a30dbbb46d --- /dev/null +++ b/worlds/AutoSNIClient.py @@ -0,0 +1,42 @@ + +from __future__ import annotations +import abc +from typing import TYPE_CHECKING, ClassVar, Dict, Tuple, Any, Optional + +if TYPE_CHECKING: + from SNIClient import SNIContext + + +class AutoSNIClientRegister(abc.ABCMeta): + game_handlers: ClassVar[Dict[str, SNIClient]] = {} + + def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoSNIClientRegister: + # construct class + new_class = super().__new__(cls, name, bases, dct) + if "game" in dct: + AutoSNIClientRegister.game_handlers[dct["game"]] = new_class() + return new_class + + @staticmethod + async def get_handler(ctx: SNIContext) -> Optional[SNIClient]: + for _game, handler in AutoSNIClientRegister.game_handlers.items(): + if await handler.validate_rom(ctx): + return handler + return None + + +class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister): + + @abc.abstractmethod + async def validate_rom(self, ctx: SNIContext) -> bool: + """ TODO: interface documentation here """ + ... + + @abc.abstractmethod + async def game_watcher(self, ctx: SNIContext) -> None: + """ TODO: interface documentation here """ + ... + + async def deathlink_kill_player(self, ctx: SNIContext) -> None: + """ override this with implementation to kill player """ + pass diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py new file mode 100644 index 0000000000..b3a12a7ff8 --- /dev/null +++ b/worlds/alttp/Client.py @@ -0,0 +1,693 @@ +from __future__ import annotations + +import logging +import asyncio +import shutil +import time + +import Utils + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient + +from worlds.alttp import Shops, Regions +from .Rom import ROM_PLAYER_LIMIT + +snes_logger = logging.getLogger("SNES") + +GAME_ALTTP = "A Link to the Past" + +# FXPAK Pro protocol memory mapping used by SNI +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +ROMNAME_START = SRAM_START + 0x2000 +ROMNAME_SIZE = 0x15 + +INGAME_MODES = {0x07, 0x09, 0x0b} +ENDGAME_MODES = {0x19, 0x1a} +DEATH_MODES = {0x12} + +SAVEDATA_START = WRAM_START + 0xF000 +SAVEDATA_SIZE = 0x500 + +RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes +RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte +RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte +ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes +ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte +SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte +SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte +SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte +SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte +SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes +SHOP_LEN = (len(Shops.shop_table) * 3) + 5 + +DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte + +location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()]) + +location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), + "Blind's Hideout - Left": (0x11d, 0x20), + "Blind's Hideout - Right": (0x11d, 0x40), + "Blind's Hideout - Far Left": (0x11d, 0x80), + "Blind's Hideout - Far Right": (0x11d, 0x100), + 'Secret Passage': (0x55, 0x10), + 'Waterfall Fairy - Left': (0x114, 0x10), + 'Waterfall Fairy - Right': (0x114, 0x20), + "King's Tomb": (0x113, 0x10), + 'Floodgate Chest': (0x10b, 0x10), + "Link's House": (0x104, 0x10), + 'Kakariko Tavern': (0x103, 0x10), + 'Chicken House': (0x108, 0x10), + "Aginah's Cave": (0x10a, 0x10), + "Sahasrahla's Hut - Left": (0x105, 0x10), + "Sahasrahla's Hut - Middle": (0x105, 0x20), + "Sahasrahla's Hut - Right": (0x105, 0x40), + 'Kakariko Well - Top': (0x2f, 0x10), + 'Kakariko Well - Left': (0x2f, 0x20), + 'Kakariko Well - Middle': (0x2f, 0x40), + 'Kakariko Well - Right': (0x2f, 0x80), + 'Kakariko Well - Bottom': (0x2f, 0x100), + 'Lost Woods Hideout': (0xe1, 0x200), + 'Lumberjack Tree': (0xe2, 0x200), + 'Cave 45': (0x11b, 0x400), + 'Graveyard Cave': (0x11b, 0x200), + 'Checkerboard Cave': (0x126, 0x200), + 'Mini Moldorm Cave - Far Left': (0x123, 0x10), + 'Mini Moldorm Cave - Left': (0x123, 0x20), + 'Mini Moldorm Cave - Right': (0x123, 0x40), + 'Mini Moldorm Cave - Far Right': (0x123, 0x80), + 'Mini Moldorm Cave - Generous Guy': (0x123, 0x400), + 'Ice Rod Cave': (0x120, 0x10), + 'Bonk Rock Cave': (0x124, 0x10), + 'Desert Palace - Big Chest': (0x73, 0x10), + 'Desert Palace - Torch': (0x73, 0x400), + 'Desert Palace - Map Chest': (0x74, 0x10), + 'Desert Palace - Compass Chest': (0x85, 0x10), + 'Desert Palace - Big Key Chest': (0x75, 0x10), + 'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400), + 'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400), + 'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400), + 'Desert Palace - Boss': (0x33, 0x800), + 'Eastern Palace - Compass Chest': (0xa8, 0x10), + 'Eastern Palace - Big Chest': (0xa9, 0x10), + 'Eastern Palace - Dark Square Pot Key': (0xba, 0x400), + 'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400), + 'Eastern Palace - Cannonball Chest': (0xb9, 0x10), + 'Eastern Palace - Big Key Chest': (0xb8, 0x10), + 'Eastern Palace - Map Chest': (0xaa, 0x10), + 'Eastern Palace - Boss': (0xc8, 0x800), + 'Hyrule Castle - Boomerang Chest': (0x71, 0x10), + 'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400), + 'Hyrule Castle - Map Chest': (0x72, 0x10), + 'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400), + "Hyrule Castle - Zelda's Chest": (0x80, 0x10), + 'Hyrule Castle - Big Key Drop': (0x80, 0x400), + 'Sewers - Dark Cross': (0x32, 0x10), + 'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400), + 'Sewers - Secret Room - Left': (0x11, 0x10), + 'Sewers - Secret Room - Middle': (0x11, 0x20), + 'Sewers - Secret Room - Right': (0x11, 0x40), + 'Sanctuary': (0x12, 0x10), + 'Castle Tower - Room 03': (0xe0, 0x10), + 'Castle Tower - Dark Maze': (0xd0, 0x10), + 'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400), + 'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400), + 'Spectacle Rock Cave': (0xea, 0x400), + 'Paradox Cave Lower - Far Left': (0xef, 0x10), + 'Paradox Cave Lower - Left': (0xef, 0x20), + 'Paradox Cave Lower - Right': (0xef, 0x40), + 'Paradox Cave Lower - Far Right': (0xef, 0x80), + 'Paradox Cave Lower - Middle': (0xef, 0x100), + 'Paradox Cave Upper - Left': (0xff, 0x10), + 'Paradox Cave Upper - Right': (0xff, 0x20), + 'Spiral Cave': (0xfe, 0x10), + 'Tower of Hera - Basement Cage': (0x87, 0x400), + 'Tower of Hera - Map Chest': (0x77, 0x10), + 'Tower of Hera - Big Key Chest': (0x87, 0x10), + 'Tower of Hera - Compass Chest': (0x27, 0x20), + 'Tower of Hera - Big Chest': (0x27, 0x10), + 'Tower of Hera - Boss': (0x7, 0x800), + 'Hype Cave - Top': (0x11e, 0x10), + 'Hype Cave - Middle Right': (0x11e, 0x20), + 'Hype Cave - Middle Left': (0x11e, 0x40), + 'Hype Cave - Bottom': (0x11e, 0x80), + 'Hype Cave - Generous Guy': (0x11e, 0x400), + 'Peg Cave': (0x127, 0x400), + 'Pyramid Fairy - Left': (0x116, 0x10), + 'Pyramid Fairy - Right': (0x116, 0x20), + 'Brewery': (0x106, 0x10), + 'C-Shaped House': (0x11c, 0x10), + 'Chest Game': (0x106, 0x400), + 'Mire Shed - Left': (0x10d, 0x10), + 'Mire Shed - Right': (0x10d, 0x20), + 'Superbunny Cave - Top': (0xf8, 0x10), + 'Superbunny Cave - Bottom': (0xf8, 0x20), + 'Spike Cave': (0x117, 0x10), + 'Hookshot Cave - Top Right': (0x3c, 0x10), + 'Hookshot Cave - Top Left': (0x3c, 0x20), + 'Hookshot Cave - Bottom Right': (0x3c, 0x80), + 'Hookshot Cave - Bottom Left': (0x3c, 0x40), + 'Mimic Cave': (0x10c, 0x10), + 'Swamp Palace - Entrance': (0x28, 0x10), + 'Swamp Palace - Map Chest': (0x37, 0x10), + 'Swamp Palace - Pot Row Pot Key': (0x38, 0x400), + 'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400), + 'Swamp Palace - Hookshot Pot Key': (0x36, 0x400), + 'Swamp Palace - Big Chest': (0x36, 0x10), + 'Swamp Palace - Compass Chest': (0x46, 0x10), + 'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400), + 'Swamp Palace - Big Key Chest': (0x35, 0x10), + 'Swamp Palace - West Chest': (0x34, 0x10), + 'Swamp Palace - Flooded Room - Left': (0x76, 0x10), + 'Swamp Palace - Flooded Room - Right': (0x76, 0x20), + 'Swamp Palace - Waterfall Room': (0x66, 0x10), + 'Swamp Palace - Waterway Pot Key': (0x16, 0x400), + 'Swamp Palace - Boss': (0x6, 0x800), + "Thieves' Town - Big Key Chest": (0xdb, 0x20), + "Thieves' Town - Map Chest": (0xdb, 0x10), + "Thieves' Town - Compass Chest": (0xdc, 0x10), + "Thieves' Town - Ambush Chest": (0xcb, 0x10), + "Thieves' Town - Hallway Pot Key": (0xbc, 0x400), + "Thieves' Town - Spike Switch Pot Key": (0xab, 0x400), + "Thieves' Town - Attic": (0x65, 0x10), + "Thieves' Town - Big Chest": (0x44, 0x10), + "Thieves' Town - Blind's Cell": (0x45, 0x10), + "Thieves' Town - Boss": (0xac, 0x800), + 'Skull Woods - Compass Chest': (0x67, 0x10), + 'Skull Woods - Map Chest': (0x58, 0x20), + 'Skull Woods - Big Chest': (0x58, 0x10), + 'Skull Woods - Pot Prison': (0x57, 0x20), + 'Skull Woods - Pinball Room': (0x68, 0x10), + 'Skull Woods - Big Key Chest': (0x57, 0x10), + 'Skull Woods - West Lobby Pot Key': (0x56, 0x400), + 'Skull Woods - Bridge Room': (0x59, 0x10), + 'Skull Woods - Spike Corner Key Drop': (0x39, 0x400), + 'Skull Woods - Boss': (0x29, 0x800), + 'Ice Palace - Jelly Key Drop': (0x0e, 0x400), + 'Ice Palace - Compass Chest': (0x2e, 0x10), + 'Ice Palace - Conveyor Key Drop': (0x3e, 0x400), + 'Ice Palace - Freezor Chest': (0x7e, 0x10), + 'Ice Palace - Big Chest': (0x9e, 0x10), + 'Ice Palace - Iced T Room': (0xae, 0x10), + 'Ice Palace - Many Pots Pot Key': (0x9f, 0x400), + 'Ice Palace - Spike Room': (0x5f, 0x10), + 'Ice Palace - Big Key Chest': (0x1f, 0x10), + 'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400), + 'Ice Palace - Map Chest': (0x3f, 0x10), + 'Ice Palace - Boss': (0xde, 0x800), + 'Misery Mire - Big Chest': (0xc3, 0x10), + 'Misery Mire - Map Chest': (0xc3, 0x20), + 'Misery Mire - Main Lobby': (0xc2, 0x10), + 'Misery Mire - Bridge Chest': (0xa2, 0x10), + 'Misery Mire - Spikes Pot Key': (0xb3, 0x400), + 'Misery Mire - Spike Chest': (0xb3, 0x10), + 'Misery Mire - Fishbone Pot Key': (0xa1, 0x400), + 'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400), + 'Misery Mire - Compass Chest': (0xc1, 0x10), + 'Misery Mire - Big Key Chest': (0xd1, 0x10), + 'Misery Mire - Boss': (0x90, 0x800), + 'Turtle Rock - Compass Chest': (0xd6, 0x10), + 'Turtle Rock - Roller Room - Left': (0xb7, 0x10), + 'Turtle Rock - Roller Room - Right': (0xb7, 0x20), + 'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400), + 'Turtle Rock - Chain Chomps': (0xb6, 0x10), + 'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400), + 'Turtle Rock - Big Key Chest': (0x14, 0x10), + 'Turtle Rock - Big Chest': (0x24, 0x10), + 'Turtle Rock - Crystaroller Room': (0x4, 0x10), + 'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80), + 'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40), + 'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20), + 'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10), + 'Turtle Rock - Boss': (0xa4, 0x800), + 'Palace of Darkness - Shooter Room': (0x9, 0x10), + 'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20), + 'Palace of Darkness - Stalfos Basement': (0xa, 0x10), + 'Palace of Darkness - Big Key Chest': (0x3a, 0x10), + 'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10), + 'Palace of Darkness - Map Chest': (0x2b, 0x10), + 'Palace of Darkness - Compass Chest': (0x1a, 0x20), + 'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10), + 'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20), + 'Palace of Darkness - Dark Maze - Top': (0x19, 0x10), + 'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20), + 'Palace of Darkness - Big Chest': (0x1a, 0x10), + 'Palace of Darkness - Harmless Hellway': (0x1a, 0x40), + 'Palace of Darkness - Boss': (0x5a, 0x800), + 'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400), + "Ganons Tower - Bob's Torch": (0x8c, 0x400), + 'Ganons Tower - Hope Room - Left': (0x8c, 0x20), + 'Ganons Tower - Hope Room - Right': (0x8c, 0x40), + 'Ganons Tower - Tile Room': (0x8d, 0x10), + 'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10), + 'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20), + 'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40), + 'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80), + 'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400), + 'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10), + 'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20), + 'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40), + 'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80), + 'Ganons Tower - Map Chest': (0x8b, 0x10), + 'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400), + 'Ganons Tower - Firesnake Room': (0x7d, 0x10), + 'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10), + 'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20), + 'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40), + 'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80), + "Ganons Tower - Bob's Chest": (0x8c, 0x80), + 'Ganons Tower - Big Chest': (0x8c, 0x10), + 'Ganons Tower - Big Key Room - Left': (0x1c, 0x20), + 'Ganons Tower - Big Key Room - Right': (0x1c, 0x40), + 'Ganons Tower - Big Key Chest': (0x1c, 0x10), + 'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10), + 'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20), + 'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400), + 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), + 'Ganons Tower - Validation Chest': (0x4d, 0x10)} + +boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss', + 'Desert Palace - Boss', + 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', + 'Swamp Palace - Boss', + 'Skull Woods - Boss', + "Thieves' Town - Boss", + 'Ice Palace - Boss', + 'Misery Mire - Boss', + 'Turtle Rock - Boss', + 'Sahasrahla'}} + +location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()} + +location_table_npc = {'Mushroom': 0x1000, + 'King Zora': 0x2, + 'Sahasrahla': 0x10, + 'Blacksmith': 0x400, + 'Magic Bat': 0x8000, + 'Sick Kid': 0x4, + 'Library': 0x80, + 'Potion Shop': 0x2000, + 'Old Man': 0x1, + 'Ether Tablet': 0x100, + 'Catfish': 0x20, + 'Stumpy': 0x8, + 'Bombos Tablet': 0x200} + +location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()} + +location_table_ow = {'Flute Spot': 0x2a, + 'Sunken Treasure': 0x3b, + "Zora's Ledge": 0x81, + 'Lake Hylia Island': 0x35, + 'Maze Race': 0x28, + 'Desert Ledge': 0x30, + 'Master Sword Pedestal': 0x80, + 'Spectacle Rock': 0x3, + 'Pyramid': 0x5b, + 'Digging Game': 0x68, + 'Bumper Cave Ledge': 0x4a, + 'Floating Island': 0x5} + +location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()} + +location_table_misc = {'Bottle Merchant': (0x3c9, 0x2), + 'Purple Chest': (0x3c9, 0x10), + "Link's Uncle": (0x3c6, 0x1), + 'Hobo': (0x3c9, 0x1)} +location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()} + + +async def track_locations(ctx, roomid, roomdata): + from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + new_locations = [] + + def new_check(location_id): + new_locations.append(location_id) + ctx.locations_checked.add(location_id) + location = ctx.location_names[location_id] + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + + try: + shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN) + shop_data_changed = False + shop_data = list(shop_data) + for cnt, b in enumerate(shop_data): + location = Shops.SHOP_ID_START + cnt + if int(b) and location not in ctx.locations_checked: + new_check(location) + if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \ + and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot: + if not int(b): + shop_data[cnt] += 1 + shop_data_changed = True + if shop_data_changed: + snes_buffered_write(ctx, SHOP_ADDR, bytes(shop_data)) + except Exception as e: + snes_logger.info(f"Exception: {e}") + + for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items(): + try: + if location_id not in ctx.locations_checked and loc_roomid == roomid and \ + (roomdata << 4) & loc_mask != 0: + new_check(location_id) + except Exception as e: + snes_logger.exception(f"Exception: {e}") + + uw_begin = 0x129 + ow_end = uw_end = 0 + uw_unchecked = {} + uw_checked = {} + for location, (roomid, mask) in location_table_uw.items(): + location_id = Regions.lookup_name_to_id[location] + if location_id not in ctx.locations_checked: + uw_unchecked[location_id] = (roomid, mask) + uw_begin = min(uw_begin, roomid) + uw_end = max(uw_end, roomid + 1) + if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ + and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ + and ctx.locations_info[location_id].player != ctx.slot: + uw_begin = min(uw_begin, roomid) + uw_end = max(uw_end, roomid + 1) + uw_checked[location_id] = (roomid, mask) + + if uw_begin < uw_end: + uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2) + if uw_data is not None: + for location_id, (roomid, mask) in uw_unchecked.items(): + offset = (roomid - uw_begin) * 2 + roomdata = uw_data[offset] | (uw_data[offset + 1] << 8) + if roomdata & mask != 0: + new_check(location_id) + if uw_checked: + uw_data = list(uw_data) + for location_id, (roomid, mask) in uw_checked.items(): + offset = (roomid - uw_begin) * 2 + roomdata = uw_data[offset] | (uw_data[offset + 1] << 8) + roomdata |= mask + uw_data[offset] = roomdata & 0xFF + uw_data[offset + 1] = roomdata >> 8 + snes_buffered_write(ctx, SAVEDATA_START + (uw_begin * 2), bytes(uw_data)) + + ow_begin = 0x82 + ow_unchecked = {} + ow_checked = {} + for location_id, screenid in location_table_ow_id.items(): + if location_id not in ctx.locations_checked: + ow_unchecked[location_id] = screenid + ow_begin = min(ow_begin, screenid) + ow_end = max(ow_end, screenid + 1) + if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \ + and ctx.locations_info[location_id].player != ctx.slot: + ow_checked[location_id] = screenid + + if ow_begin < ow_end: + ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin) + if ow_data is not None: + for location_id, screenid in ow_unchecked.items(): + if ow_data[screenid - ow_begin] & 0x40 != 0: + new_check(location_id) + if ow_checked: + ow_data = list(ow_data) + for location_id, screenid in ow_checked.items(): + ow_data[screenid - ow_begin] |= 0x40 + snes_buffered_write(ctx, SAVEDATA_START + 0x280 + ow_begin, bytes(ow_data)) + + if not ctx.locations_checked.issuperset(location_table_npc_id): + npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2) + if npc_data is not None: + npc_value_changed = False + npc_value = npc_data[0] | (npc_data[1] << 8) + for location_id, mask in location_table_npc_id.items(): + if npc_value & mask != 0 and location_id not in ctx.locations_checked: + new_check(location_id) + if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ + and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ + and ctx.locations_info[location_id].player != ctx.slot: + npc_value |= mask + npc_value_changed = True + if npc_value_changed: + npc_data = bytes([npc_value & 0xFF, npc_value >> 8]) + snes_buffered_write(ctx, SAVEDATA_START + 0x410, npc_data) + + if not ctx.locations_checked.issuperset(location_table_misc_id): + misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4) + if misc_data is not None: + misc_data = list(misc_data) + misc_data_changed = False + for location_id, (offset, mask) in location_table_misc_id.items(): + assert (0x3c6 <= offset <= 0x3c9) + if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked: + new_check(location_id) + if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ + and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot: + misc_data_changed = True + misc_data[offset - 0x3c6] |= mask + if misc_data_changed: + snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data)) + + + if new_locations: + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}]) + await snes_flush_writes(ctx) + + +def get_alttp_settings(romfile: str): + lastSettings = Utils.get_adjuster_settings(GAME_ALTTP) + adjustedromfile = '' + if lastSettings: + choice = 'no' + if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply: + + whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", + "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", + "reduceflashing", "deathlink", "allowcollect"} + printed_options = {name: value for name, value in vars(lastSettings).items() if name in whitelist} + if hasattr(lastSettings, "sprite_pool"): + sprite_pool = {} + for sprite in lastSettings.sprite_pool: + if sprite in sprite_pool: + sprite_pool[sprite] += 1 + else: + sprite_pool[sprite] = 1 + if sprite_pool: + printed_options["sprite_pool"] = sprite_pool + import pprint + + from CommonClient import gui_enabled + if gui_enabled: + + try: + from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button + applyPromptWindow = Tk() + except Exception as e: + logging.error('Could not load tkinter, which is likely not installed.') + return '', False + + applyPromptWindow.resizable(False, False) + applyPromptWindow.protocol('WM_DELETE_WINDOW', lambda: onButtonClick()) + logo = PhotoImage(file=Utils.local_path('data', 'icon.png')) + applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo) + applyPromptWindow.wm_title("Last adjuster settings LttP") + + label = LabelFrame(applyPromptWindow, + text='Last used adjuster settings were found. Would you like to apply these?') + label.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5) + label.grid_columnconfigure(0, weight=1) + label.grid_columnconfigure(1, weight=1) + label.grid_columnconfigure(2, weight=1) + label.grid_columnconfigure(3, weight=1) + + def onButtonClick(answer: str = 'no'): + setattr(onButtonClick, 'choice', answer) + applyPromptWindow.destroy() + + framedOptions = Frame(label) + framedOptions.grid(column=0, columnspan=4, row=0) + framedOptions.grid_columnconfigure(0, weight=1) + framedOptions.grid_columnconfigure(1, weight=1) + framedOptions.grid_columnconfigure(2, weight=1) + curRow = 0 + curCol = 0 + for name, value in printed_options.items(): + Label(framedOptions, text=name + ": " + str(value)).grid(column=curCol, row=curRow, padx=5) + if (curCol == 2): + curRow += 1 + curCol = 0 + else: + curCol += 1 + + yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10) + yesButton.grid(column=0, row=1) + noButton = Button(label, text='No', command=lambda: onButtonClick('no'), width=10) + noButton.grid(column=1, row=1) + alwaysButton = Button(label, text='Always', command=lambda: onButtonClick('always'), width=10) + alwaysButton.grid(column=2, row=1) + neverButton = Button(label, text='Never', command=lambda: onButtonClick('never'), width=10) + neverButton.grid(column=3, row=1) + + Utils.tkinter_center_window(applyPromptWindow) + applyPromptWindow.mainloop() + choice = getattr(onButtonClick, 'choice') + else: + choice = input(f"Last used adjuster settings were found. Would you like to apply these? \n" + f"{pprint.pformat(printed_options)}\n" + f"Enter yes, no, always or never: ") + if choice and choice.startswith("y"): + choice = 'yes' + elif choice and "never" in choice: + choice = 'no' + lastSettings.auto_apply = 'never' + Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings) + elif choice and "always" in choice: + choice = 'yes' + lastSettings.auto_apply = 'always' + Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings) + else: + choice = 'no' + elif 'never' in lastSettings.auto_apply: + choice = 'no' + elif 'always' in lastSettings.auto_apply: + choice = 'yes' + + if 'yes' in choice: + from worlds.alttp.Rom import get_base_rom_path + lastSettings.rom = romfile + lastSettings.baserom = get_base_rom_path() + lastSettings.world = None + + if hasattr(lastSettings, "sprite_pool"): + from LttPAdjuster import AdjusterWorld + lastSettings.world = AdjusterWorld(getattr(lastSettings, "sprite_pool")) + + adjusted = True + import LttPAdjuster + _, adjustedromfile = LttPAdjuster.adjust(lastSettings) + + if hasattr(lastSettings, "world"): + delattr(lastSettings, "world") + else: + adjusted = False + if adjusted: + try: + shutil.move(adjustedromfile, romfile) + adjustedromfile = romfile + except Exception as e: + logging.exception(e) + else: + adjusted = False + return adjustedromfile, adjusted + + +class ALTTPSNIClient(SNIClient): + game = "A Link to the Past" + + async def deathlink_kill_player(self, ctx): + from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes + invincible = await snes_read(ctx, WRAM_START + 0x037B, 1) + last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1) + await asyncio.sleep(0.25) + health = await snes_read(ctx, WRAM_START + 0xF36D, 1) + if not invincible or not last_health or not health: + ctx.death_state = DeathState.dead + ctx.last_death_link = time.time() + return + if not invincible[0] and last_health[0] == health[0]: + snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0 + snes_buffered_write(ctx, WRAM_START + 0x0373, + bytes([8])) # deal 1 full heart of damage at next opportunity + + await snes_flush_writes(ctx) + await asyncio.sleep(1) + + gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) + if not gamemode or gamemode[0] in DEATH_MODES: + ctx.death_state = DeathState.dead + + + async def validate_rom(self, ctx): + from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + + rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE) + if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"AP": + return False + + ctx.game = self.game + ctx.items_handling = 0b001 # full local + + ctx.rom = rom_name + + death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1) + + if death_link: + ctx.allow_collect = bool(death_link[0] & 0b100) + ctx.death_link_allow_survive = bool(death_link[0] & 0b10) + await ctx.update_death_link(bool(death_link[0] & 0b1)) + + return True + + + async def game_watcher(self, ctx): + from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) + if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): + currently_dead = gamemode[0] in DEATH_MODES + await ctx.handle_deathlink_state(currently_dead) + + gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1) + game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4) + if gamemode is None or gameend is None or game_timer is None or \ + (gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES): + return + + if gameend[0]: + if not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + + if gamemode in ENDGAME_MODES: # triforce room and credits + return + + data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8) + if data is None: + return + + recv_index = data[0] | (data[1] << 8) + recv_item = data[2] + roomid = data[4] | (data[5] << 8) + roomdata = data[6] + scout_location = data[7] + + if recv_index < len(ctx.items_received) and recv_item == 0: + item = ctx.items_received[recv_index] + recv_index += 1 + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names[item.location], recv_index, len(ctx.items_received))) + + snes_buffered_write(ctx, RECV_PROGRESS_ADDR, + bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) + snes_buffered_write(ctx, RECV_ITEM_ADDR, + bytes([item.item])) + snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, + bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0])) + if scout_location > 0 and scout_location in ctx.locations_info: + snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, + bytes([scout_location])) + snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, + bytes([ctx.locations_info[scout_location].item])) + snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, + bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)])) + + await snes_flush_writes(ctx) + + if scout_location > 0 and scout_location not in ctx.locations_scouted: + ctx.locations_scouted.add(scout_location) + await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}]) + await track_locations(ctx, roomid, roomdata) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index ce53154e92..8431af9a26 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -15,6 +15,7 @@ from .Items import item_init_table, item_name_groups, item_table, GetBeemizerIte from .Options import alttp_options, smallkey_shuffle from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ is_main_entrance +from .Client import ALTTPSNIClient 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 diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 7ab82187b0..77ed51fecb 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -2,75 +2,69 @@ import logging import asyncio from NetUtils import ClientStatus, color -from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read -from Patch import GAME_DKC3 +from worlds.AutoSNIClient import SNIClient snes_logger = logging.getLogger("SNES") -# DKC3 - DKC3_TODO: Check these values +# FXPAK Pro protocol memory mapping used by SNI ROM_START = 0x000000 WRAM_START = 0xF50000 WRAM_SIZE = 0x20000 SRAM_START = 0xE00000 -SAVEDATA_START = WRAM_START + 0xF000 -SAVEDATA_SIZE = 0x500 - DKC3_ROMNAME_START = 0x00FFC0 DKC3_ROMHASH_START = 0x7FC0 ROMNAME_SIZE = 0x15 ROMHASH_SIZE = 0x15 -DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 # DKC3_TODO: Find a permanent home for this +DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9 DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this -async def deathlink_kill_player(ctx: Context): - pass - #if ctx.game == GAME_DKC3: +class DKC3SNIClient(SNIClient): + game = "Donkey Kong Country 3" + + async def deathlink_kill_player(self, ctx): + pass # DKC3_TODO: Handle Receiving Deathlink -async def dkc3_rom_init(ctx: Context): - if not ctx.rom: - ctx.finished_game = False - ctx.death_link_allow_survive = False - game_name = await snes_read(ctx, DKC3_ROMNAME_START, 0x15) - if game_name is None or game_name != b"DONKEY KONG COUNTRY 3": - return False - else: - ctx.game = GAME_DKC3 - ctx.items_handling = 0b111 # remote items + async def validate_rom(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) - if rom is None or rom == bytes([0] * ROMHASH_SIZE): + rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) + if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3": return False - ctx.rom = rom + ctx.game = self.game + ctx.items_handling = 0b111 # remote items + + ctx.rom = rom_name #death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1) ## DKC3_TODO: Handle Deathlink #if death_link: # ctx.allow_collect = bool(death_link[0] & 0b100) # await ctx.update_death_link(bool(death_link[0] & 0b1)) - return True + return True -async def dkc3_game_watcher(ctx: Context): - if ctx.game == GAME_DKC3: + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read # DKC3_TODO: Handle Deathlink save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) - if save_file_name is None or save_file_name[0] == 0x00: + if save_file_name is None or save_file_name[0] == 0x00 or save_file_name == bytes([0x55] * 0x05): # We haven't loaded a save file return new_checks = [] from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map + location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81) 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) - masked_data = data[0] & (1 << loc_data[1]) + data = location_ram_data[loc_data[0] - 0x5FE] + masked_data = data & (1 << loc_data[1]) bit_set = (masked_data != 0) invert_bit = ((len(loc_data) >= 3) and loc_data[2]) if bit_set != invert_bit: @@ -78,8 +72,9 @@ async def dkc3_game_watcher(ctx: Context): new_checks.append(loc_id) verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) - if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name: + if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name == bytes([0x55] * 0x05) or verify_save_file_name != save_file_name: # We have somehow exited the save file (or worse) + ctx.rom = None return rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) @@ -184,8 +179,9 @@ async def dkc3_game_watcher(ctx: Context): await snes_flush_writes(ctx) - # DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged # Handle Collected Locations + levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60) + tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60) 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] @@ -193,30 +189,24 @@ async def dkc3_game_watcher(ctx: Context): 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) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index d45de8f85a..332f23e491 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -11,6 +11,7 @@ from .Regions import create_regions, connect_regions from .Levels import level_list from .Rules import set_rules from .Names import ItemName, LocationName +from .Client import DKC3SNIClient from ..AutoWorld import WebWorld, World from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch import Patch diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py new file mode 100644 index 0000000000..190ce29ecc --- /dev/null +++ b/worlds/sm/Client.py @@ -0,0 +1,158 @@ +import logging +import asyncio +import time + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient +from .Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT + +snes_logger = logging.getLogger("SNES") + +GAME_SM = "Super Metroid" + +# FXPAK Pro protocol memory mapping used by SNI +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +# SM +SM_ROMNAME_START = ROM_START + 0x007FC0 +ROMNAME_SIZE = 0x15 + +SM_INGAME_MODES = {0x07, 0x09, 0x0b} +SM_ENDGAME_MODES = {0x26, 0x27} +SM_DEATH_MODES = {0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A} + +# 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 + + +class SMSNIClient(SNIClient): + game = "Super Metroid" + + async def deathlink_kill_player(self, ctx): + from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read + snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([1, 0])) # set current health to 1 (to prevent saving with 0 energy) + snes_buffered_write(ctx, WRAM_START + 0x0A50, bytes([255])) # deal 255 of damage at next opportunity + if not ctx.death_link_allow_survive: + snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0 + + await snes_flush_writes(ctx) + await asyncio.sleep(1) + + gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) + health = await snes_read(ctx, WRAM_START + 0x09C2, 2) + if health is not None: + health = health[0] | (health[1] << 8) + if not gamemode or gamemode[0] in SM_DEATH_MODES or ( + ctx.death_link_allow_survive and health is not None and health > 0): + ctx.death_state = DeathState.dead + + + async def validate_rom(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE) + if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[:3] == b"SMW": + return False + + ctx.game = self.game + + # versions lower than 0.3.0 dont have item handling flag nor remote item support + romVersion = int(rom_name[2:5].decode('UTF-8')) + if romVersion < 30: + ctx.items_handling = 0b001 # full local + else: + item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1) + ctx.items_handling = 0b001 if item_handling is None else item_handling[0] + + ctx.rom = rom_name + + death_link = await snes_read(ctx, SM_DEATH_LINK_ACTIVE_ADDR, 1) + + if death_link: + ctx.allow_collect = bool(death_link[0] & 0b100) + ctx.death_link_allow_survive = bool(death_link[0] & 0b10) + await ctx.update_death_link(bool(death_link[0] & 0b1)) + + return True + + + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + return + + 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 + await ctx.handle_deathlink_state(currently_dead) + if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES: + if not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + return + + data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4) + if data is None: + return + + recv_index = data[0] | (data[1] << 8) + recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT + + while (recv_index < recv_item): + item_address = recv_index * 8 + message = await snes_read(ctx, SM_SEND_QUEUE_START + item_address, 8) + item_index = (message[4] | (message[5] << 8)) >> 3 + + recv_index += 1 + snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, + bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) + + from worlds.sm import locations_start_id + location_id = locations_start_id + item_index + + ctx.locations_checked.add(location_id) + location = ctx.location_names[location_id] + snes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) + + data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2) + if data is None: + return + + item_out_ptr = data[0] | (data[1] << 8) + + from worlds.sm import items_start_id + from worlds.sm import locations_start_id + if item_out_ptr < len(ctx.items_received): + item = ctx.items_received[item_out_ptr] + item_id = item.item - items_start_id + if bool(ctx.items_handling & 0b010): + location_id = (item.location - locations_start_id) if (item.location >= 0 and item.player == ctx.slot) else 0xFF + else: + location_id = 0x00 #backward compat + + player_id = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0 + snes_buffered_write(ctx, SM_RECV_QUEUE_START + item_out_ptr * 4, bytes( + [player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, location_id & 0xFF])) + item_out_ptr += 1 + snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, + bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names[item.item], 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + + await snes_flush_writes(ctx) + diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 500233bb71..fc19b4e133 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -14,6 +14,7 @@ logger = logging.getLogger("Super Metroid") from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options +from .Client import SMSNIClient from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch, get_sm_symbols import Utils diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 6ddd4e1073..9cf5a5fcfb 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -3,21 +3,17 @@ import asyncio import time from NetUtils import ClientStatus, color -from worlds import AutoWorldRegister -from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read +from worlds.AutoSNIClient import SNIClient from .Names.TextBox import generate_received_text -from Patch import GAME_SMW snes_logger = logging.getLogger("SNES") +# FXPAK Pro protocol memory mapping used by SNI ROM_START = 0x000000 WRAM_START = 0xF50000 WRAM_SIZE = 0x20000 SRAM_START = 0xE00000 -SAVEDATA_START = WRAM_START + 0xF000 -SAVEDATA_SIZE = 0x500 - SMW_ROMHASH_START = 0x7FC0 ROMHASH_SIZE = 0x15 @@ -58,8 +54,12 @@ SMW_BAD_TEXT_BOX_LEVELS = [0x26, 0x02, 0x4B] SMW_BOSS_STATES = [0x80, 0xC0, 0xC1] SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32] -async def deathlink_kill_player(ctx: Context): - if ctx.game == GAME_SMW: + +class SMWSNIClient(SNIClient): + game = "Super Mario World" + + async def deathlink_kill_player(self, ctx): + from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) if game_state[0] != 0x14: return @@ -88,25 +88,19 @@ async def deathlink_kill_player(ctx: Context): await snes_flush_writes(ctx) - from SNIClient import DeathState ctx.death_state = DeathState.dead ctx.last_death_link = time.time() - return + async def validate_rom(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read -async def smw_rom_init(ctx: Context): - if not ctx.rom: - ctx.finished_game = False - ctx.death_link_allow_survive = False - game_hash = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE) - if game_hash is None or game_hash == bytes([0] * ROMHASH_SIZE) or game_hash[:3] != b"SMW": + rom_name = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE) + if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:3] != b"SMW": return False - else: - ctx.game = GAME_SMW - ctx.items_handling = 0b111 # remote items - ctx.rom = game_hash + ctx.game = self.game + ctx.items_handling = 0b111 # remote items receive_option = await snes_read(ctx, SMW_RECEIVE_MSG_DATA, 0x1) send_option = await snes_read(ctx, SMW_SEND_MSG_DATA, 0x1) @@ -114,73 +108,73 @@ async def smw_rom_init(ctx: Context): ctx.receive_option = receive_option[0] ctx.send_option = send_option[0] - ctx.message_queue = [] - ctx.allow_collect = True death_link = await snes_read(ctx, SMW_DEATH_LINK_ACTIVE_ADDR, 1) if death_link: await ctx.update_death_link(bool(death_link[0] & 0b1)) - return True + + ctx.rom = rom_name + + return True -def add_message_to_queue(ctx: Context, new_message): + def add_message_to_queue(self, new_message): - if not hasattr(ctx, "message_queue"): - ctx.message_queue = [] + if not hasattr(self, "message_queue"): + self.message_queue = [] - ctx.message_queue.append(new_message) - - return + self.message_queue.append(new_message) -async def handle_message_queue(ctx: Context): + async def handle_message_queue(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + if not hasattr(self, "message_queue") or len(self.message_queue) == 0: + return + + game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) + if game_state[0] != 0x14: + return + + mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) + if mario_state[0] != 0x00: + return + + message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) + if message_box[0] != 0x00: + return + + pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1) + if pause_state[0] != 0x00: + return + + current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) + if current_level[0] in SMW_BAD_TEXT_BOX_LEVELS: + return + + boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1) + if boss_state[0] in SMW_BOSS_STATES: + return + + active_boss = await snes_read(ctx, SMW_ACTIVE_BOSS_ADDR, 0x1) + if active_boss[0] != 0x00: + return + + next_message = self.message_queue.pop(0) + + snes_buffered_write(ctx, SMW_MESSAGE_QUEUE_ADDR, bytes(next_message)) + snes_buffered_write(ctx, SMW_MESSAGE_BOX_ADDR, bytes([0x03])) + snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x22])) + + await snes_flush_writes(ctx) - game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) - if game_state[0] != 0x14: return - mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) - if mario_state[0] != 0x00: - return - message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1) - if message_box[0] != 0x00: - return + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1) - if pause_state[0] != 0x00: - return - - current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1) - if current_level[0] in SMW_BAD_TEXT_BOX_LEVELS: - return - - boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1) - if boss_state[0] in SMW_BOSS_STATES: - return - - active_boss = await snes_read(ctx, SMW_ACTIVE_BOSS_ADDR, 0x1) - if active_boss[0] != 0x00: - return - - if not hasattr(ctx, "message_queue") or len(ctx.message_queue) == 0: - return - - next_message = ctx.message_queue.pop(0) - - snes_buffered_write(ctx, SMW_MESSAGE_QUEUE_ADDR, bytes(next_message)) - snes_buffered_write(ctx, SMW_MESSAGE_BOX_ADDR, bytes([0x03])) - snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x22])) - - await snes_flush_writes(ctx) - - return - - -async def smw_game_watcher(ctx: Context): - if ctx.game == GAME_SMW: - # SMW_TODO: Handle Deathlink game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1) if game_state is None: @@ -234,7 +228,7 @@ async def smw_game_watcher(ctx: Context): snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([egg_count[0]])) await snes_flush_writes(ctx) - await handle_message_queue(ctx) + await self.handle_message_queue(ctx) new_checks = [] event_data = await snes_read(ctx, SMW_EVENT_ROM_DATA, 0x60) @@ -243,6 +237,7 @@ async def smw_game_watcher(ctx: Context): dragon_coins_active = await snes_read(ctx, SMW_DRAGON_COINS_ACTIVE_ADDR, 0x1) from worlds.smw.Rom import item_rom_data, ability_rom_data from worlds.smw.Levels import location_id_to_level_id, level_info_dict + from worlds import AutoWorldRegister for loc_name, level_data in location_id_to_level_id.items(): loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name] if loc_id not in ctx.locations_checked: @@ -262,7 +257,6 @@ async def smw_game_watcher(ctx: Context): bit_set = (masked_data != 0) if bit_set: - # SMW_TODO: Handle non-included checks new_checks.append(loc_id) else: event_id_value = event_id + level_data[1] @@ -275,7 +269,6 @@ async def smw_game_watcher(ctx: Context): bit_set = (masked_data != 0) if bit_set: - # SMW_TODO: Handle non-included checks new_checks.append(loc_id) verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1) @@ -320,7 +313,7 @@ async def smw_game_watcher(ctx: Context): player_name = ctx.player_names[item.player] receive_message = generate_received_text(item_name, player_name) - add_message_to_queue(ctx, receive_message) + self.add_message_to_queue(receive_message) snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index])) if item.item in item_rom_data: @@ -372,7 +365,7 @@ async def smw_game_watcher(ctx: Context): rand_trap = random.choice(lit_trap_text_list) for message in rand_trap: - add_message_to_queue(ctx, message) + self.add_message_to_queue(message) await snes_flush_writes(ctx) diff --git a/worlds/smw/__init__.py b/worlds/smw/__init__.py index 1dd64f535f..2e9be535e9 100644 --- a/worlds/smw/__init__.py +++ b/worlds/smw/__init__.py @@ -12,6 +12,7 @@ from .Levels import full_level_list, generate_level_list, location_id_to_level_i from .Rules import set_rules from ..generic.Rules import add_rule from .Names import ItemName, LocationName +from .Client import SMWSNIClient from ..AutoWorld import WebWorld, World from .Rom import LocalRom, patch_rom, get_base_rom_path, SMWDeltaPatch diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py new file mode 100644 index 0000000000..c942c66c71 --- /dev/null +++ b/worlds/smz3/Client.py @@ -0,0 +1,118 @@ +import logging +import asyncio +import time + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient +from .Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT + +snes_logger = logging.getLogger("SNES") + +# FXPAK Pro protocol memory mapping used by SNI +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +# SMZ3 +SMZ3_ROMNAME_START = ROM_START + 0x00FFC0 +ROMNAME_SIZE = 0x15 + +SAVEDATA_START = WRAM_START + 0xF000 + +SMZ3_INGAME_MODES = {0x07, 0x09, 0x0B} +ENDGAME_MODES = {0x19, 0x1A} +SM_ENDGAME_MODES = {0x26, 0x27} +SMZ3_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A} + +SMZ3_RECV_PROGRESS_ADDR = SRAM_START + 0x4000 # 2 bytes +SMZ3_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte +SMZ3_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte + + +class SMZ3SNIClient(SNIClient): + game = "SMZ3" + + async def validate_rom(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + rom_name = await snes_read(ctx, SMZ3_ROMNAME_START, ROMNAME_SIZE) + if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:3] != b"ZSM": + return False + + ctx.game = self.game + ctx.items_handling = 0b101 # local items and remote start inventory + + ctx.rom = rom_name + + return True + + + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + return + + currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2) + if (currentGame is not None): + if (currentGame[0] != 0): + gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) + endGameModes = SM_ENDGAME_MODES + else: + gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) + endGameModes = ENDGAME_MODES + + if gamemode is not None and (gamemode[0] in endGameModes): + if not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + return + + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4) + if data is None: + return + + recv_index = data[0] | (data[1] << 8) + recv_item = data[2] | (data[3] << 8) + + while (recv_index < recv_item): + item_address = recv_index * 8 + message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8) + is_z3_item = ((message[5] & 0x80) != 0) + masked_part = (message[5] & 0x7F) if is_z3_item else message[5] + item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) + + recv_index += 1 + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) + + from worlds.smz3.TotalSMZ3.Location import locations_start_id + from worlds.smz3 import convertLocSMZ3IDToAPID + location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index) + + ctx.locations_checked.add(location_id) + location = ctx.location_names[location_id] + snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) + + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4) + if data is None: + return + + item_out_ptr = data[2] | (data[3] << 8) + + from worlds.smz3.TotalSMZ3.Item import items_start_id + if item_out_ptr < len(ctx.items_received): + item = ctx.items_received[item_out_ptr] + item_id = item.item - items_start_id + + player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0 + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 4, bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF])) + item_out_ptr += 1 + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), + ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) + + await snes_flush_writes(ctx) + diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 753fb556ae..320d506fd2 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -17,6 +17,7 @@ from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Loc from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray from worlds.smz3.TotalSMZ3.WorldState import WorldState from ..AutoWorld import World, AutoLogicRegister, WebWorld +from .Client import SMZ3SNIClient from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch from .Options import smz3_options From 700fe8b75e10a2e19e0f9b9986a440c30fff0819 Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Wed, 26 Oct 2022 06:24:54 -0400 Subject: [PATCH 094/105] SC2: New Settings, Logic improvements (#1110) * Switched mission item group to a list comprehension to fix missile shuffle errors * Logic for reducing mission and item counts * SC2: Piercing the Shroud/Maw of the Void requirements now DRY * SC2: Logic for All-In, may need further refinement * SC2: Additional mission orders and starting locations * SC2: New Mission Order options for shorter campaigns and smaller item pools * Using location table for hardcoded starter unit * SC2: Options to curate random item pool and control early unit placement * SC2: Proper All-In logic * SC2: Grid, Mini Grid and Blitz mission orders * SC2: Required Tactics and Unit Upgrade options, better connected item handling * SC2: Client compatibility with Grid settings * SC2: Mission rando now uses world random * SC2: Alternate final missions, new logic, fixes * SC2: Handling alternate final missions, identifying final mission on client * SC2: Minor changes to handle edge-case generation failures * SC2: Removed invalid type hints for Python 3.8 * Revert "SC2: Removed invalid type hints for Python 3.8" This reverts commit 7851b9f7a39396c8ee1d85d4e4e46e61e8dc80f6. * SC2: Removed invalid type hints for Python 3.8 * SC2: Removed invalid type hints for Python 3.8 * SC2: Removed invalid type hints for Python 3.8 * SC2: Removed invalid type hints for Python 3.8 * SC2: Changed location loop to enumerate * SC2: Passing category names through slot data * SC2: Cleaned up unnecessary _create_items method * SC2: Removed vestigial extra_locations field from MissionInfo * SC2: Client backwards compatibility * SC2: Fixed item generation issue where item is present in both locked and unlocked inventories * SC2: Removed Missile Turret from defense rating on maps without air * SC2: No logic locations point to same access rule Co-authored-by: michaelasantiago Co-authored-by: Fabian Dill --- Starcraft2Client.py | 51 ++++++- worlds/sc2wol/Items.py | 140 +++++++++++------- worlds/sc2wol/Locations.py | 135 +++++++++-------- worlds/sc2wol/LogicMixin.py | 77 +++++++--- worlds/sc2wol/MissionTables.py | 182 +++++++++++++++++++---- worlds/sc2wol/Options.py | 91 ++++++++++-- worlds/sc2wol/PoolFilter.py | 257 +++++++++++++++++++++++++++++++++ worlds/sc2wol/Regions.py | 127 ++++++++-------- worlds/sc2wol/__init__.py | 91 ++++++++---- 9 files changed, 872 insertions(+), 279 deletions(-) create mode 100644 worlds/sc2wol/PoolFilter.py diff --git a/Starcraft2Client.py b/Starcraft2Client.py index de0a90411e..7431b6ea61 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -155,7 +155,9 @@ class SC2Context(CommonContext): items_handling = 0b111 difficulty = -1 all_in_choice = 0 + mission_order = 0 mission_req_table: typing.Dict[str, MissionInfo] = {} + final_mission: int = 29 announcements = queue.Queue() sc2_run_task: typing.Optional[asyncio.Task] = None missions_unlocked: bool = False # allow launching missions ignoring requirements @@ -180,9 +182,15 @@ 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"] + # Maintaining backwards compatibility with older slot data self.mission_req_table = { - mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table + mission: MissionInfo( + **{field: value for field, value in mission_info.items() if field in MissionInfo._fields} + ) + for mission, mission_info in slot_req_table.items() } + self.mission_order = args["slot_data"].get("mission_order", 0) + self.final_mission = args["slot_data"].get("final_mission", 29) self.build_location_to_mission_mapping() @@ -304,7 +312,6 @@ class SC2Context(CommonContext): self.refresh_from_launching = True self.mission_panel.clear_widgets() - if self.ctx.mission_req_table: self.last_checked_locations = self.ctx.checked_locations.copy() self.first_check = False @@ -322,17 +329,20 @@ class SC2Context(CommonContext): for category in categories: category_panel = MissionCategory() + if category.startswith('_'): + category_display_name = '' + else: + category_display_name = category category_panel.add_widget( - Label(text=category, size_hint_y=None, height=50, outline_width=1)) + Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1)) for mission in categories[category]: text: str = mission tooltip: str = "" - + mission_id: int = self.ctx.mission_req_table[mission].id # Map has uncollected locations if mission in unfinished_missions: text = f"[color=6495ED]{text}[/color]" - elif mission in available_missions: text = f"[color=FFFFFF]{text}[/color]" # Map requirements not met @@ -351,6 +361,16 @@ class SC2Context(CommonContext): 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 mission_id == self.ctx.final_mission: + if mission in available_missions: + text = f"[color=FFBC95]{mission}[/color]" + else: + text = f"[color=D0C0BE]{mission}[/color]" + if tooltip: + tooltip += "\n" + tooltip += "Final Mission" + if remaining_location_names: if tooltip: tooltip += "\n" @@ -360,7 +380,7 @@ class SC2Context(CommonContext): mission_button = MissionButton(text=text, size_hint_y=None, height=50) mission_button.tooltip_text = tooltip mission_button.bind(on_press=self.mission_callback) - self.mission_id_to_button[self.ctx.mission_req_table[mission].id] = mission_button + self.mission_id_to_button[mission_id] = mission_button category_panel.add_widget(mission_button) category_panel.add_widget(Label(text="")) @@ -469,6 +489,9 @@ wol_default_categories = [ "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy", "Char", "Char", "Char", "Char" ] +wol_default_category_names = [ + "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char" +] def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]: @@ -586,7 +609,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): if self.can_read_game: if game_state & (1 << 1) and not self.mission_completed: - if self.mission_id != 29: + if self.mission_id != self.ctx.final_mission: print("Mission Completed") await self.ctx.send_msgs( [{"cmd": 'LocationChecks', @@ -742,13 +765,14 @@ def calc_available_missions(ctx: SC2Context, unlocks=None): return available_missions -def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete): +def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int): """Returns a bool signifying if the mission has all requirements complete and can be done 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 + mission_path -- a list of missions that have already been checked """ if len(ctx.mission_req_table[mission_name].required_world) >= 1: # A check for when the requirements are being or'd @@ -766,7 +790,18 @@ def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete else: req_success = False + # Grid-specific logic (to avoid long path checks and infinite recursion) + if ctx.mission_order in (3, 4): + if req_success: + return True + else: + if req_mission is ctx.mission_req_table[mission_name].required_world[-1]: + return False + else: + continue + # Recursively check required mission to see if it's requirements are met, in case !collect has been done + # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion 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 diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 6bb74076fb..6cb768de58 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -1,5 +1,7 @@ -from BaseClasses import Item, ItemClassification +from BaseClasses import Item, ItemClassification, MultiWorld import typing + +from .Options import get_option_value from .MissionTables import vanilla_mission_req_table @@ -9,6 +11,7 @@ class ItemData(typing.NamedTuple): number: typing.Optional[int] classification: ItemClassification = ItemClassification.useful quantity: int = 1 + parent_item: str = None class StarcraftWoLItem(Item): @@ -48,51 +51,51 @@ item_table = { "Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3), "Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3), - "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, 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), + "Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"), + "Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"), + "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"), + "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"), + "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"), + "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"), "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler), "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, 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.progression), + "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"), + "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"), + "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression, parent_item="Medic"), + "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"), + "Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"), + "Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"), + "Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, parent_item="Marauder"), + "Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"), + "U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"), + "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"), - "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, 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), - "Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler), - "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, 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, 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), - "Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17), - "Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler), - "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, 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), + "Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"), + "Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"), + "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler, parent_item="Vulture"), + "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"), + "Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"), + "Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"), + "Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler, parent_item="Diamondback"), + "Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler, parent_item="Diamondback"), + "Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, classification=ItemClassification.progression, parent_item="Siege Tank"), + "Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, parent_item="Siege Tank"), + "Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler, parent_item="Medivac"), + "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler, parent_item="Medivac"), + "Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler, parent_item="Wraith"), + "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"), + "Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"), + "Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"), + "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"), + "Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"), + "Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"), + "Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"), + "Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, parent_item="Ghost"), + "Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, parent_item="Ghost"), + "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression, parent_item="Spectre"), + "Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"), + "330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"), + "Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"), "Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression), "Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression), @@ -117,16 +120,16 @@ item_table = { "Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression), "Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8), "Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9), - "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10), - "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11), - "Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12), - "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13), + "Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"), + "Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"), + "Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression), + "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression), "Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler), "Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression), "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler), "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.filler), - "Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18), - "Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.filler), + "Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression), + "Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression), "Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression), "Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, classification=ItemClassification.progression), @@ -141,15 +144,33 @@ item_table = { "+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler), "+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler), "+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler), + + # "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing) } -basic_unit: typing.Tuple[str, ...] = ( + +basic_units = { 'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture' -) +} + +advanced_basic_units = { + 'Reaper', + 'Goliath', + 'Diamondback', + 'Viking' +} + + +def get_basic_units(world: MultiWorld, player: int) -> typing.Set[str]: + if get_option_value(world, player, 'required_tactics') > 0: + return basic_units.union(advanced_basic_units) + else: + return basic_units + item_name_groups = {} for item, data in item_table.items(): @@ -161,6 +182,22 @@ filler_items: typing.Tuple[str, ...] = ( '+15 Starting Vespene' ) +defense_ratings = { + "Siege Tank": 5, + "Maelstrom Rounds": 2, + "Planetary Fortress": 3, + # Bunker w/ Marine/Marauder: 3, + "Perdition Turret": 2, + "Missile Turret": 2, + "Vulture": 2 +} +zerg_defense_ratings = { + "Perdition Turret": 2, + # Bunker w/ Firebat + "Hive Mind Emulator": 3, + "Psi Disruptor": 3 +} + 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 @@ -176,4 +213,5 @@ type_flaggroups: typing.Dict[str, int] = { "Minerals": 8, "Vespene": 9, "Supply": 10, + "Goal": 11 } diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index 14dd25fd52..f778c91be8 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -1,5 +1,6 @@ from typing import List, Tuple, Optional, Callable, NamedTuple from BaseClasses import MultiWorld +from .Options import get_option_value from BaseClasses import Location @@ -19,6 +20,7 @@ class LocationData(NamedTuple): def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: # Note: rules which are ended with or True are rules identified as needed later when restricted units is an option + logic_level = get_option_value(world, player, 'required_tactics') location_table: List[LocationData] = [ LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100), LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101), @@ -32,26 +34,33 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 2 and + (logic_level > 0 or state._sc2wol_has_anti_air(world, player))), LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301), LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 2), LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_competent_anti_air(world, player)), + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or state._sc2wol_has_competent_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)), LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403, lambda state: state._sc2wol_has_common_unit(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)), + lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 4 and + (state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))), LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, - lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 2 and + (state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))), LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502, - lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 2 and + (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_competent_anti_air(world, player)), @@ -66,38 +75,48 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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_competent_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), 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)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), 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)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), 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)), + state._sc2wol_has_competent_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, True) >= 3), 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_competent_anti_air(world, player)), + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or 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, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or state._sc2wol_has_competent_anti_air(world, player))), LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_anti_air(world, player)), + (logic_level > 0 and state._sc2wol_has_anti_air(world, player) + or state._sc2wol_has_competent_anti_air(world, player))), LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900, - lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_anti_air(world, player) and - state._sc2wol_has_heavy_defense(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player) and + state._sc2wol_defense_rating(world, player, False) >= 7), LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_defense_rating(world, player, False) >= 5), LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_defense_rating(world, player, False) >= 5), LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_defense_rating(world, player, False) >= 5), LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, - lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player) and + (state._sc2wol_has_air(world, player) + or state.has_any({'Medivac', 'Hercules'}, player) + and state._sc2wol_has_common_unit(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, @@ -109,7 +128,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, - lambda state: state._sc2wol_has_air(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player) and + (state._sc2wol_has_air(world, player) + or state.has_any({'Medivac', 'Hercules'}, player) + and state._sc2wol_has_common_unit(world, player))), LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), @@ -119,37 +141,23 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, 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._sc2wol_has_air(world, player) and - state._sc2wol_has_competent_anti_air(world, player) and - state.has('Science Vessel', player)), + lambda state: state._sc2wol_survives_rip_field(world, 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, - 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)), + lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, 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._sc2wol_has_air(world, player) and - state._sc2wol_has_competent_anti_air(world, player) and - state.has('Science Vessel', player)), + lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, 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._sc2wol_has_air(world, player) and - state._sc2wol_has_competent_anti_air(world, player) and - state.has('Science Vessel', player)), + lambda state: state._sc2wol_survives_rip_field(world, 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._sc2wol_has_air(world, player) and - state._sc2wol_has_competent_anti_air(world, player) and - state.has('Science Vessel', player)), + lambda state: state._sc2wol_survives_rip_field(world, player)), LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, - lambda state: state._sc2wol_has_anti_air(world, player) and ( - state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))), + lambda state: logic_level > 0 or + 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)), + lambda state: logic_level > 0 or 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_competent_anti_air(world, player)), @@ -176,7 +184,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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), LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_has_common_unit(world, player) and + (logic_level > 0 or state._sc2wol_has_anti_air)), LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802, @@ -208,40 +217,44 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), 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)), + lambda state: state._sc2wol_has_mm_upgrade(world, 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, - lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + lambda state: state._sc2wol_has_mm_upgrade(world, 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)), + lambda state: state._sc2wol_has_mm_upgrade(world, 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)), + lambda state: state._sc2wol_has_mm_upgrade(world, 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)), + lambda state: state._sc2wol_has_mm_upgrade(world, 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), LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203), LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), - LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301), - LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302), + LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)), + LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302, + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)), LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303, lambda state: state._sc2wol_has_protoss_common_units(world, player)), LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(world, player)), LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401), LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402, - lambda state: state._sc2wol_has_protoss_common_units(world, player)), + lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_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, lambda state: state._sc2wol_has_protoss_common_units(world, player)), LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, - lambda state: state._sc2wol_has_competent_comp(world, player)), + lambda state: state._sc2wol_has_competent_comp(world, player) and + state._sc2wol_defense_rating(world, player, True) > 6), LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, - lambda state: state._sc2wol_has_competent_comp(world, player)), + lambda state: state._sc2wol_has_competent_comp(world, player) and + state._sc2wol_defense_rating(world, player, True) > 6), LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700), LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701), LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702), @@ -258,15 +271,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("All-In", "All-In: Victory", None) + LocationData("All-In", "All-In: Victory", None, + lambda state: state._sc2wol_final_mission_requirements(world, player)) ] beat_events = [] - for location_data in location_table: + for i, location_data in enumerate(location_table): + # Removing all item-based logic on No Logic + if logic_level == 2: + location_table[i] = location_data._replace(rule=Location.access_rule) + # Generating Beat event locations if location_data.name.endswith((": Victory", ": Defeat")): beat_events.append( location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None) ) - return tuple(location_table + beat_events) diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 52bb6b09a8..1de8295970 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -1,31 +1,43 @@ from BaseClasses import MultiWorld from worlds.AutoWorld import LogicMixin +from .Options import get_option_value +from .Items import get_basic_units, defense_ratings, zerg_defense_ratings class SC2WoLLogic(LogicMixin): def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture'}, player) - - def _sc2wol_has_bunker_unit(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Marine', 'Marauder'}, player) + return self.has_any(get_basic_units(world, player), player) def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \ - self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player) + return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(world, player, 'required_tactics') > 0 \ + and 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) + return self.has('Viking', player) \ + or get_option_value(world, player, 'required_tactics') > 0 and self.has('Wraith', player) 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_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) or self._sc2wol_has_competent_anti_air(world, player) + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Wraith'}, player) \ + or self._sc2wol_has_competent_anti_air(world, player) \ + or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) - def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool: - return (self.has_any({'Siege Tank', 'Vulture'}, player) or - self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \ - self._sc2wol_has_anti_air(world, player) + def _sc2wol_defense_rating(self, world: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool: + defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player))) + if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player): + defense_score += 3 + if zerg_enemy: + defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player))) + if self.has('Firebat', player) and self.has('Bunker', player): + defense_score += 2 + if not air_enemy and self.has('Missile Turret', player): + defense_score -= defense_ratings['Missile Turret'] + # Advanced Tactics bumps defense rating requirements down by 2 + if get_option_value(world, player, 'required_tactics') > 0: + defense_score += 2 + return defense_score def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool: return (self.has('Marine', player) or self.has('Marauder', player) and @@ -35,25 +47,50 @@ class SC2WoLLogic(LogicMixin): 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) - or self.has('Marauders', player)) + return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(world, player, 'required_tactics') > 0 + and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) + return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(world, player, 'required_tactics') > 0 def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) + return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \ + or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player) def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool: return self._sc2wol_has_protoss_common_units(world, player) and \ - self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) + self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \ + or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, 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_has_mm_upgrade(self, world: MultiWorld, player: int) -> bool: + return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player) + + def _sc2wol_survives_rip_field(self, world: MultiWorld, player: int) -> bool: + return self.has("Battlecruiser", player) or \ + self._sc2wol_has_air(world, player) and \ + self._sc2wol_has_competent_anti_air(world, player) and \ + self.has("Science Vessel", player) + + def _sc2wol_has_nukes(self, world: MultiWorld, player: int) -> bool: + return get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player) + + def _sc2wol_final_mission_requirements(self, world: MultiWorld, player: int): + defense_rating = self._sc2wol_defense_rating(world, player, True) + beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(world, player, 'required_tactics') > 0 + if get_option_value(world, player, 'all_in_map') == 0: + # Ground + if self.has_any({'Battlecruiser', 'Banshee'}, player): + defense_rating += 3 + return defense_rating >= 12 and beats_kerrigan + else: + # Air + return defense_rating >= 8 and beats_kerrigan \ + and self.has_any({'Viking', 'Battlecruiser'}, player) \ + and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, 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/MissionTables.py b/worlds/sc2wol/MissionTables.py index 4f1b1157ec..8d06944662 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -1,4 +1,7 @@ -from typing import NamedTuple, Dict, List +from typing import NamedTuple, Dict, List, Set + +from BaseClasses import MultiWorld +from .Options import get_option_value no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom", "Belly of the Beast"] @@ -12,7 +15,6 @@ hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkn class MissionInfo(NamedTuple): id: int - extra_locations: int required_world: List[int] category: str number: int = 0 # number of worlds need beaten @@ -62,38 +64,156 @@ vanilla_shuffle_order = [ FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True) ] +mini_campaign_order = [ + FillMission("no_build", [-1], "Mar Sara", completion_critical=True), + FillMission("easy", [0], "Colonist"), + FillMission("medium", [1], "Colonist"), + FillMission("medium", [0], "Artifact", completion_critical=True), + FillMission("medium", [3], "Artifact", number=4, completion_critical=True), + FillMission("hard", [4], "Artifact", number=8, completion_critical=True), + FillMission("medium", [0], "Covert", number=2), + FillMission("hard", [6], "Covert"), + FillMission("medium", [0], "Rebellion", number=3), + FillMission("hard", [8], "Rebellion"), + FillMission("medium", [4], "Prophecy"), + FillMission("hard", [10], "Prophecy"), + FillMission("hard", [5], "Char", completion_critical=True), + FillMission("hard", [5], "Char", completion_critical=True), + FillMission("all_in", [12, 13], "Char", completion_critical=True, or_requirements=True) +] + +gauntlet_order = [ + FillMission("no_build", [-1], "I", completion_critical=True), + FillMission("easy", [0], "II", completion_critical=True), + FillMission("medium", [1], "III", completion_critical=True), + FillMission("medium", [2], "IV", completion_critical=True), + FillMission("hard", [3], "V", completion_critical=True), + FillMission("hard", [4], "VI", completion_critical=True), + FillMission("all_in", [5], "Final", completion_critical=True) +] + +grid_order = [ + FillMission("no_build", [-1], "_1"), + FillMission("medium", [0], "_1"), + FillMission("medium", [1, 6, 3], "_1", or_requirements=True), + FillMission("hard", [2, 7], "_1", or_requirements=True), + FillMission("easy", [0], "_2"), + FillMission("medium", [1, 4], "_2", or_requirements=True), + FillMission("hard", [2, 5, 10, 7], "_2", or_requirements=True), + FillMission("hard", [3, 6, 11], "_2", or_requirements=True), + FillMission("medium", [4, 9, 12], "_3", or_requirements=True), + FillMission("hard", [5, 8, 10, 13], "_3", or_requirements=True), + FillMission("hard", [6, 9, 11, 14], "_3", or_requirements=True), + FillMission("hard", [7, 10], "_3", or_requirements=True), + FillMission("hard", [8, 13], "_4", or_requirements=True), + FillMission("hard", [9, 12, 14], "_4", or_requirements=True), + FillMission("hard", [10, 13], "_4", or_requirements=True), + FillMission("all_in", [11, 14], "_4", or_requirements=True) +] + +mini_grid_order = [ + FillMission("no_build", [-1], "_1"), + FillMission("medium", [0], "_1"), + FillMission("medium", [1, 5], "_1", or_requirements=True), + FillMission("easy", [0], "_2"), + FillMission("medium", [1, 3], "_2", or_requirements=True), + FillMission("hard", [2, 4], "_2", or_requirements=True), + FillMission("medium", [3, 7], "_3", or_requirements=True), + FillMission("hard", [4, 6], "_3", or_requirements=True), + FillMission("all_in", [5, 7], "_3", or_requirements=True) +] + +blitz_order = [ + FillMission("no_build", [-1], "I"), + FillMission("easy", [-1], "I"), + FillMission("medium", [0, 1], "II", number=1, or_requirements=True), + FillMission("medium", [0, 1], "II", number=1, or_requirements=True), + FillMission("medium", [0, 1], "III", number=2, or_requirements=True), + FillMission("medium", [0, 1], "III", number=2, or_requirements=True), + FillMission("hard", [0, 1], "IV", number=3, or_requirements=True), + FillMission("hard", [0, 1], "IV", number=3, or_requirements=True), + FillMission("hard", [0, 1], "V", number=4, or_requirements=True), + FillMission("hard", [0, 1], "V", number=4, or_requirements=True), + FillMission("hard", [0, 1], "Final", number=5, or_requirements=True), + FillMission("all_in", [0, 1], "Final", number=5, or_requirements=True) +] + +mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order] + vanilla_mission_req_table = { - "Liberation Day": MissionInfo(1, 7, [], "Mar Sara", completion_critical=True), - "The Outlaws": MissionInfo(2, 2, [1], "Mar Sara", completion_critical=True), - "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, 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), - "Supernova": MissionInfo(11, 5, [10], "Artifact", number=14, completion_critical=True), - "Maw of the Void": MissionInfo(12, 6, [11], "Artifact", completion_critical=True), - "Devil's Playground": MissionInfo(13, 3, [3], "Covert", number=4), - "Welcome to the Jungle": MissionInfo(14, 4, [13], "Covert"), - "Breakout": MissionInfo(15, 3, [14], "Covert", number=8), - "Ghost of a Chance": MissionInfo(16, 6, [14], "Covert", number=8), - "The Great Train Robbery": MissionInfo(17, 4, [3], "Rebellion", number=6), - "Cutthroat": MissionInfo(18, 5, [17], "Rebellion"), - "Engine of Destruction": MissionInfo(19, 6, [18], "Rebellion"), - "Media Blitz": MissionInfo(20, 5, [19], "Rebellion"), - "Piercing the Shroud": MissionInfo(21, 6, [20], "Rebellion"), - "Whispers of Doom": MissionInfo(22, 4, [9], "Prophecy"), - "A Sinister Turn": MissionInfo(23, 4, [22], "Prophecy"), - "Echoes of the Future": MissionInfo(24, 3, [23], "Prophecy"), - "In Utter Darkness": MissionInfo(25, 3, [24], "Prophecy"), - "Gates of Hell": MissionInfo(26, 2, [12], "Char", completion_critical=True), - "Belly of the Beast": MissionInfo(27, 4, [26], "Char", completion_critical=True), - "Shatter the Sky": MissionInfo(28, 5, [26], "Char", completion_critical=True), - "All-In": MissionInfo(29, -1, [27, 28], "Char", completion_critical=True, or_requirements=True) + "Liberation Day": MissionInfo(1, [], "Mar Sara", completion_critical=True), + "The Outlaws": MissionInfo(2, [1], "Mar Sara", completion_critical=True), + "Zero Hour": MissionInfo(3, [2], "Mar Sara", completion_critical=True), + "Evacuation": MissionInfo(4, [3], "Colonist"), + "Outbreak": MissionInfo(5, [4], "Colonist"), + "Safe Haven": MissionInfo(6, [5], "Colonist", number=7), + "Haven's Fall": MissionInfo(7, [5], "Colonist", number=7), + "Smash and Grab": MissionInfo(8, [3], "Artifact", completion_critical=True), + "The Dig": MissionInfo(9, [8], "Artifact", number=8, completion_critical=True), + "The Moebius Factor": MissionInfo(10, [9], "Artifact", number=11, completion_critical=True), + "Supernova": MissionInfo(11, [10], "Artifact", number=14, completion_critical=True), + "Maw of the Void": MissionInfo(12, [11], "Artifact", completion_critical=True), + "Devil's Playground": MissionInfo(13, [3], "Covert", number=4), + "Welcome to the Jungle": MissionInfo(14, [13], "Covert"), + "Breakout": MissionInfo(15, [14], "Covert", number=8), + "Ghost of a Chance": MissionInfo(16, [14], "Covert", number=8), + "The Great Train Robbery": MissionInfo(17, [3], "Rebellion", number=6), + "Cutthroat": MissionInfo(18, [17], "Rebellion"), + "Engine of Destruction": MissionInfo(19, [18], "Rebellion"), + "Media Blitz": MissionInfo(20, [19], "Rebellion"), + "Piercing the Shroud": MissionInfo(21, [20], "Rebellion"), + "Whispers of Doom": MissionInfo(22, [9], "Prophecy"), + "A Sinister Turn": MissionInfo(23, [22], "Prophecy"), + "Echoes of the Future": MissionInfo(24, [23], "Prophecy"), + "In Utter Darkness": MissionInfo(25, [24], "Prophecy"), + "Gates of Hell": MissionInfo(26, [12], "Char", completion_critical=True), + "Belly of the Beast": MissionInfo(27, [26], "Char", completion_critical=True), + "Shatter the Sky": MissionInfo(28, [26], "Char", completion_critical=True), + "All-In": MissionInfo(29, [27, 28], "Char", completion_critical=True, or_requirements=True) } lookup_id_to_mission: Dict[int, str] = { data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id} + +no_build_starting_mission_locations = { + "Liberation Day": "Liberation Day: Victory", + "Breakout": "Breakout: Victory", + "Ghost of a Chance": "Ghost of a Chance: Victory", + "Piercing the Shroud": "Piercing the Shroud: Victory", + "Whispers of Doom": "Whispers of Doom: Victory", + "Belly of the Beast": "Belly of the Beast: Victory", +} + +build_starting_mission_locations = { + "Zero Hour": "Zero Hour: First Group Rescued", + "Evacuation": "Evacuation: First Chysalis", + "Devil's Playground": "Devil's Playground: Tosh's Miners" +} + +advanced_starting_mission_locations = { + "Smash and Grab": "Smash and Grab: First Relic", + "The Great Train Robbery": "The Great Train Robbery: North Defiler" +} + + +def get_starting_mission_locations(world: MultiWorld, player: int) -> Set[str]: + if get_option_value(world, player, 'shuffle_no_build') or get_option_value(world, player, 'mission_order') < 2: + # Always start with a no-build mission unless explicitly relegating them + # Vanilla and Vanilla Shuffled always start with a no-build even when relegated + return no_build_starting_mission_locations + elif get_option_value(world, player, 'required_tactics') > 0: + # Advanced Tactics/No Logic add more starting missions to the pool + return {**build_starting_mission_locations, **advanced_starting_mission_locations} + else: + # Standard starting missions when relegate is on + return build_starting_mission_locations + + +alt_final_mission_locations = { + "Maw of the Void": "Maw of the Void: Victory", + "Engine of Destruction": "Engine of Destruction: Victory", + "Supernova": "Supernova: Victory", + "Gates of Hell": "Gates of Hell: Victory", + "Shatter the Sky": "Shatter the Sky: Victory" +} \ No newline at end of file diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index efd0872527..9cd86f2c0b 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -1,6 +1,6 @@ from typing import Dict from BaseClasses import MultiWorld -from Options import Choice, Option, DefaultOnToggle +from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range class GameDifficulty(Choice): @@ -36,25 +36,75 @@ class AllInMap(Choice): class MissionOrder(Choice): - """Determines the order the missions are played in. - Vanilla: Keeps the standard mission order and branching from the WoL Campaign. - Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.""" + """Determines the order the missions are played in. The last three mission orders end in a random mission. + Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign. + Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within. + Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches. + Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In. + Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win. + Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win. + Gauntlet (7): Linear series of 7 random missions to complete the campaign.""" display_name = "Mission Order" option_vanilla = 0 option_vanilla_shuffled = 1 + option_mini_campaign = 2 + option_grid = 3 + option_mini_grid = 4 + option_blitz = 5 + option_gauntlet = 6 class ShuffleProtoss(DefaultOnToggle): - """Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is - not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete - the game.""" + """Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled. + If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled. + If turned off with reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed from the pool.""" display_name = "Shuffle Protoss Missions" -class RelegateNoBuildMissions(DefaultOnToggle): - """If enabled, all no build missions besides the needed first one will be placed at the end of optional routes so - that none of them become required to complete the game. Only takes effect if mission order is not set to vanilla.""" - display_name = "Relegate No-Build Missions" +class ShuffleNoBuild(DefaultOnToggle): + """Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled. + If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes. + If turned off with reduced mission settings, the 5 no-build missions will not appear.""" + display_name = "Shuffle No-Build Missions" + + +class EarlyUnit(DefaultOnToggle): + """Guarantees that the first mission will contain a unit.""" + display_name = "Early Unit" + + +class RequiredTactics(Choice): + """Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings increase randomness. + Standard: All missions can be completed with good micro and macro. + Advanced: Completing missions may require relying on starting units and micro-heavy units. + No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!""" + display_name = "Required Tactics" + option_standard = 0 + option_advanced = 1 + option_no_logic = 2 + + +class UnitsAlwaysHaveUpgrades(DefaultOnToggle): + """If turned on, both upgrades will be present for each unit and structure in the seed. + This usually results in fewer units.""" + display_name = "Units Always Have Upgrades" + + +class LockedItems(ItemSet): + """Guarantees that these items will be unlockable""" + display_name = "Locked Items" + + +class ExcludedItems(ItemSet): + """Guarantees that these items will not be unlockable""" + display_name = "Excluded Items" + + +class ExcludedMissions(OptionSet): + """Guarantees that these missions will not appear in the campaign + Only applies on shortened mission orders. + It may be impossible to build a valid campaign if too many missions are excluded.""" + display_name = "Excluded Missions" # noinspection PyTypeChecker @@ -65,14 +115,29 @@ sc2wol_options: Dict[str, Option] = { "all_in_map": AllInMap, "mission_order": MissionOrder, "shuffle_protoss": ShuffleProtoss, - "relegate_no_build": RelegateNoBuildMissions + "shuffle_no_build": ShuffleNoBuild, + "early_unit": EarlyUnit, + "required_tactics": RequiredTactics, + "units_always_have_upgrades": UnitsAlwaysHaveUpgrades, + "locked_items": LockedItems, + "excluded_items": ExcludedItems, + "excluded_missions": ExcludedMissions } def get_option_value(world: MultiWorld, player: int, name: str) -> int: option = getattr(world, name, None) - if option == None: + if option is None: return 0 return int(option[player].value) + + +def get_option_set_value(world: MultiWorld, player: int, name: str) -> set: + option = getattr(world, name, None) + + if option is None: + return set() + + return option[player].value diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py new file mode 100644 index 0000000000..5b6970e721 --- /dev/null +++ b/worlds/sc2wol/PoolFilter.py @@ -0,0 +1,257 @@ +from typing import Callable, Dict, List, Set +from BaseClasses import MultiWorld, ItemClassification, Item, Location +from .Items import item_table +from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\ + mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table, alt_final_mission_locations +from .Options import get_option_value, get_option_set_value +from .LogicMixin import SC2WoLLogic + +# Items with associated upgrades +UPGRADABLE_ITEMS = [ + "Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre", + "Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", + "Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", + "Bunker", "Missile Turret" +] + +BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"} +FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"} +STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"} + +PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"} + + +def filter_missions(world: MultiWorld, player: int) -> Dict[str, List[str]]: + """ + Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets + """ + + mission_order_type = get_option_value(world, player, "mission_order") + shuffle_protoss = get_option_value(world, player, "shuffle_protoss") + excluded_missions = set(get_option_set_value(world, player, "excluded_missions")) + invalid_mission_names = excluded_missions.difference(vanilla_mission_req_table.keys()) + if invalid_mission_names: + raise Exception("Error in locked_missions - the following are not valid mission names: " + ", ".join(invalid_mission_names)) + mission_count = len(mission_orders[mission_order_type]) - 1 + # Vanilla and Vanilla Shuffled use the entire mission pool + if mission_count == 28: + return { + "no_build": no_build_regions_list[:], + "easy": easy_regions_list[:], + "medium": medium_regions_list[:], + "hard": hard_regions_list[:], + "all_in": ["All-In"] + } + + mission_pools = [ + [], + easy_regions_list, + medium_regions_list, + hard_regions_list + ] + # Omitting Protoss missions if not shuffling protoss + if not shuffle_protoss: + excluded_missions = excluded_missions.union(PROTOSS_REGIONS) + # Replacing All-In on low mission counts + if mission_count < 14: + final_mission = world.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions]) + excluded_missions.add(final_mission) + else: + final_mission = 'All-In' + # Yaml settings determine which missions can be placed in the first slot + mission_pools[0] = [mission for mission in get_starting_mission_locations(world, player).keys() if mission not in excluded_missions] + # Removing the new no-build missions from their original sets + for i in range(1, len(mission_pools)): + mission_pools[i] = [mission for mission in mission_pools[i] if mission not in excluded_missions.union(mission_pools[0])] + # If the first mission is a build mission, there may not be enough locations to reach Outbreak as a second mission + if not get_option_value(world, player, 'shuffle_no_build'): + # Swapping Outbreak and The Great Train Robbery + if "Outbreak" in mission_pools[1]: + mission_pools[1].remove("Outbreak") + mission_pools[2].append("Outbreak") + if "The Great Train Robbery" in mission_pools[2]: + mission_pools[2].remove("The Great Train Robbery") + mission_pools[1].append("The Great Train Robbery") + # Removing random missions from each difficulty set in a cycle + set_cycle = 0 + current_count = sum(len(mission_pool) for mission_pool in mission_pools) + + if current_count < mission_count: + raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.") + while current_count > mission_count: + if set_cycle == 4: + set_cycle = 0 + # Must contain at least one mission per set + mission_pool = mission_pools[set_cycle] + if len(mission_pool) <= 1: + if all(len(mission_pool) <= 1 for mission_pool in mission_pools): + raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.") + else: + mission_pool.remove(world.random.choice(mission_pool)) + current_count -= 1 + set_cycle += 1 + + return { + "no_build": mission_pools[0], + "easy": mission_pools[1], + "medium": mission_pools[2], + "hard": mission_pools[3], + "all_in": [final_mission] + } + + +def get_item_upgrades(inventory: List[Item], parent_item: Item or str): + item_name = parent_item.name if isinstance(parent_item, Item) else parent_item + return [ + inv_item for inv_item in inventory + if item_table[inv_item.name].parent_item == item_name + ] + + +class ValidInventory: + + def has(self, item: str, player: int): + return item in self.logical_inventory + + def has_any(self, items: Set[str], player: int): + return any(item in self.logical_inventory for item in items) + + def has_all(self, items: Set[str], player: int): + return all(item in self.logical_inventory for item in items) + + def has_units_per_structure(self) -> bool: + return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \ + len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \ + len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure + + def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Callable]) -> List[Item]: + """Attempts to generate a reduced inventory that can fulfill the mission requirements.""" + inventory = list(self.item_pool) + locked_items = list(self.locked_items) + self.logical_inventory = { + item.name for item in inventory + locked_items + self.existing_items + if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing) + } + requirements = mission_requirements + cascade_keys = self.cascade_removal_map.keys() + units_always_have_upgrades = get_option_value(self.world, self.player, "units_always_have_upgrades") + if self.min_units_per_structure > 0: + requirements.append(lambda state: state.has_units_per_structure()) + + def attempt_removal(item: Item) -> bool: + # If item can be removed and has associated items, remove them as well + inventory.remove(item) + # Only run logic checks when removing logic items + if item.name in self.logical_inventory: + self.logical_inventory.remove(item.name) + if not all(requirement(self) for requirement in requirements): + # If item cannot be removed, lock or revert + self.logical_inventory.add(item.name) + locked_items.append(item) + return False + return True + + while len(inventory) + len(locked_items) > inventory_size: + if len(inventory) == 0: + raise Exception("Reduced item pool generation failed - not enough locations available to place items.") + # Select random item from removable items + item = self.world.random.choice(inventory) + # Cascade removals to associated items + if item in cascade_keys: + items_to_remove = self.cascade_removal_map[item] + transient_items = [] + while len(items_to_remove) > 0: + item_to_remove = items_to_remove.pop() + if item_to_remove not in inventory: + continue + success = attempt_removal(item_to_remove) + if success: + transient_items.append(item_to_remove) + elif units_always_have_upgrades: + # Lock all associated items if any of them cannot be removed + transient_items += items_to_remove + for transient_item in transient_items: + if transient_item not in inventory and transient_item not in locked_items: + locked_items += transient_item + if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing): + self.logical_inventory.add(transient_item.name) + break + else: + attempt_removal(item) + + return inventory + locked_items + + def _read_logic(self): + self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player) + self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player) + self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player) + self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player) + self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player) + self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy) + self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player) + self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player) + self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player) + self._sc2wol_beats_protoss_deathball = lambda world, player: SC2WoLLogic._sc2wol_beats_protoss_deathball(self, world, player) + self._sc2wol_survives_rip_field = lambda world, player: SC2WoLLogic._sc2wol_survives_rip_field(self, world, player) + self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player) + self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player) + self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player) + self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player) + + def __init__(self, world: MultiWorld, player: int, + item_pool: List[Item], existing_items: List[Item], locked_items: List[Item], + has_protoss: bool): + self.world = world + self.player = player + self.logical_inventory = set() + self.locked_items = locked_items[:] + self.existing_items = existing_items + self._read_logic() + # Initial filter of item pool + self.item_pool = [] + item_quantities: dict[str, int] = dict() + # Inventory restrictiveness based on number of missions with checks + mission_order_type = get_option_value(self.world, self.player, "mission_order") + mission_count = len(mission_orders[mission_order_type]) - 1 + self.min_units_per_structure = int(mission_count / 7) + min_upgrades = 1 if mission_count < 10 else 2 + for item in item_pool: + item_info = item_table[item.name] + if item_info.type == "Upgrade": + # Locking upgrades based on mission duration + if item.name not in item_quantities: + item_quantities[item.name] = 0 + item_quantities[item.name] += 1 + if item_quantities[item.name] < min_upgrades: + self.locked_items.append(item) + else: + self.item_pool.append(item) + elif item_info.type == "Goal": + locked_items.append(item) + elif item_info.type != "Protoss" or has_protoss: + self.item_pool.append(item) + self.cascade_removal_map: Dict[Item, List[Item]] = dict() + for item in self.item_pool + locked_items + existing_items: + if item.name in UPGRADABLE_ITEMS: + upgrades = get_item_upgrades(self.item_pool, item) + associated_items = [*upgrades, item] + self.cascade_removal_map[item] = associated_items + if get_option_value(world, player, "units_always_have_upgrades"): + for upgrade in upgrades: + self.cascade_removal_map[upgrade] = associated_items + + +def filter_items(world: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo], location_cache: List[Location], + item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]: + """ + Returns a semi-randomly pruned set of items based on number of available locations. + The returned inventory must be capable of logically accessing every location in the world. + """ + open_locations = [location for location in location_cache if location.item is None] + inventory_size = len(open_locations) + has_protoss = bool(PROTOSS_REGIONS.intersection(mission_req_table.keys())) + mission_requirements = [location.access_rule for location in location_cache] + valid_inventory = ValidInventory(world, player, item_pool, existing_items, locked_items, has_protoss) + + valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements) + return valid_items diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 8219a982c9..b0a3a51e44 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -2,55 +2,47 @@ from typing import List, Set, Dict, Tuple, Optional, Callable from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType from .Locations import LocationData from .Options import get_option_value -from .MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \ - no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list +from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations +from .PoolFilter import filter_missions import random -def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]): +def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\ + -> Tuple[Dict[str, MissionInfo], int, str]: locations_per_region = get_locations_per_region(locations) - regions = [ - create_region(world, player, locations_per_region, location_cache, "Menu"), - create_region(world, player, locations_per_region, location_cache, "Liberation Day"), - create_region(world, player, locations_per_region, location_cache, "The Outlaws"), - create_region(world, player, locations_per_region, location_cache, "Zero Hour"), - create_region(world, player, locations_per_region, location_cache, "Evacuation"), - create_region(world, player, locations_per_region, location_cache, "Outbreak"), - create_region(world, player, locations_per_region, location_cache, "Safe Haven"), - create_region(world, player, locations_per_region, location_cache, "Haven's Fall"), - create_region(world, player, locations_per_region, location_cache, "Smash and Grab"), - create_region(world, player, locations_per_region, location_cache, "The Dig"), - create_region(world, player, locations_per_region, location_cache, "The Moebius Factor"), - create_region(world, player, locations_per_region, location_cache, "Supernova"), - create_region(world, player, locations_per_region, location_cache, "Maw of the Void"), - create_region(world, player, locations_per_region, location_cache, "Devil's Playground"), - create_region(world, player, locations_per_region, location_cache, "Welcome to the Jungle"), - create_region(world, player, locations_per_region, location_cache, "Breakout"), - create_region(world, player, locations_per_region, location_cache, "Ghost of a Chance"), - create_region(world, player, locations_per_region, location_cache, "The Great Train Robbery"), - create_region(world, player, locations_per_region, location_cache, "Cutthroat"), - create_region(world, player, locations_per_region, location_cache, "Engine of Destruction"), - create_region(world, player, locations_per_region, location_cache, "Media Blitz"), - create_region(world, player, locations_per_region, location_cache, "Piercing the Shroud"), - create_region(world, player, locations_per_region, location_cache, "Whispers of Doom"), - create_region(world, player, locations_per_region, location_cache, "A Sinister Turn"), - create_region(world, player, locations_per_region, location_cache, "Echoes of the Future"), - create_region(world, player, locations_per_region, location_cache, "In Utter Darkness"), - create_region(world, player, locations_per_region, location_cache, "Gates of Hell"), - create_region(world, player, locations_per_region, location_cache, "Belly of the Beast"), - create_region(world, player, locations_per_region, location_cache, "Shatter the Sky"), - create_region(world, player, locations_per_region, location_cache, "All-In") - ] + mission_order_type = get_option_value(world, player, "mission_order") + mission_order = mission_orders[mission_order_type] + + mission_pools = filter_missions(world, player) + final_mission = mission_pools['all_in'][0] + + used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool] + regions = [create_region(world, player, locations_per_region, location_cache, "Menu")] + for region_name in used_regions: + regions.append(create_region(world, player, locations_per_region, location_cache, region_name)) + # Changing the completion condition for alternate final missions into an event + if final_mission != 'All-In': + final_location = alt_final_mission_locations[final_mission] + # Final location should be near the end of the cache + for i in range(len(location_cache) - 1, -1, -1): + if location_cache[i].name == final_location: + location_cache[i].locked = True + location_cache[i].event = True + location_cache[i].address = None + break + else: + final_location = 'All-In: Victory' if __debug__: - throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) + if mission_order_type in (0, 1): + throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) world.regions += regions names: Dict[str, int] = {} - if get_option_value(world, player, "mission_order") == 0: + if mission_order_type == 0: connect(world, player, names, 'Menu', 'Liberation Day'), connect(world, player, names, 'Liberation Day', 'The Outlaws', lambda state: state.has("Beat Liberation Day", player)), @@ -119,32 +111,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData lambda state: state.has('Beat Gates of Hell', player) and ( state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player))) - return vanilla_mission_req_table + return vanilla_mission_req_table, 29, final_location - elif get_option_value(world, player, "mission_order") == 1: + else: missions = [] - no_build_pool = no_build_regions_list[:] - easy_pool = easy_regions_list[:] - medium_pool = medium_regions_list[:] - hard_pool = hard_regions_list[:] # Initial fill out of mission list and marking all-in mission - for mission in vanilla_shuffle_order: - if mission.type == "all_in": - missions.append("All-In") - elif get_option_value(world, player, "relegate_no_build") and mission.relegate: + for mission in mission_order: + if mission is None: + missions.append(None) + elif mission.type == "all_in": + missions.append(final_mission) + elif mission.relegate and not get_option_value(world, player, "shuffle_no_build"): missions.append("no_build") else: missions.append(mission.type) - # Place Protoss Missions if we are not using ShuffleProtoss - if get_option_value(world, player, "shuffle_protoss") == 0: + # Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled + if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1: missions[22] = "A Sinister Turn" - medium_pool.remove("A Sinister Turn") + mission_pools['medium'].remove("A Sinister Turn") missions[23] = "Echoes of the Future" - medium_pool.remove("Echoes of the Future") + mission_pools['medium'].remove("Echoes of the Future") missions[24] = "In Utter Darkness" - hard_pool.remove("In Utter Darkness") + mission_pools['hard'].remove("In Utter Darkness") no_build_slots = [] easy_slots = [] @@ -153,6 +143,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData # Search through missions to find slots needed to fill for i in range(len(missions)): + if missions[i] is None: + continue if missions[i] == "no_build": no_build_slots.append(i) elif missions[i] == "easy": @@ -163,30 +155,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData hard_slots.append(i) # Add no_build missions to the pool and fill in no_build slots - missions_to_add = no_build_pool + missions_to_add = mission_pools['no_build'] for slot in no_build_slots: - filler = random.randint(0, len(missions_to_add)-1) + filler = world.random.randint(0, len(missions_to_add)-1) missions[slot] = missions_to_add.pop(filler) # Add easy missions into pool and fill in easy slots - missions_to_add = missions_to_add + easy_pool + missions_to_add = missions_to_add + mission_pools['easy'] for slot in easy_slots: - filler = random.randint(0, len(missions_to_add) - 1) + filler = world.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add medium missions into pool and fill in medium slots - missions_to_add = missions_to_add + medium_pool + missions_to_add = missions_to_add + mission_pools['medium'] for slot in medium_slots: - filler = random.randint(0, len(missions_to_add) - 1) + filler = world.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) # Add hard missions into pool and fill in hard slots - missions_to_add = missions_to_add + hard_pool + missions_to_add = missions_to_add + mission_pools['hard'] for slot in hard_slots: - filler = random.randint(0, len(missions_to_add) - 1) + filler = world.random.randint(0, len(missions_to_add) - 1) missions[slot] = missions_to_add.pop(filler) @@ -195,7 +187,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData mission_req_table = {} for i in range(len(missions)): connections = [] - for connection in vanilla_shuffle_order[i].connect_to: + for connection in mission_order[i].connect_to: if connection == -1: connect(world, player, names, "Menu", missions[i]) else: @@ -203,16 +195,17 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData (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)) + (missions[connection], mission_order[i].number)) connections.append(connection + 1) mission_req_table.update({missions[i]: MissionInfo( - vanilla_mission_req_table[missions[i]].id, vanilla_mission_req_table[missions[i]].extra_locations, - connections, vanilla_shuffle_order[i].category, number=vanilla_shuffle_order[i].number, - completion_critical=vanilla_shuffle_order[i].completion_critical, - or_requirements=vanilla_shuffle_order[i].or_requirements)}) + vanilla_mission_req_table[missions[i]].id, connections, mission_order[i].category, + number=mission_order[i].number, + completion_critical=mission_order[i].completion_critical, + or_requirements=mission_order[i].or_requirements)}) - return mission_req_table + final_mission_id = vanilla_mission_req_table[final_mission].id + return mission_req_table, final_mission_id, final_mission + ': Victory' def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]): diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 6d056df808..70226e7afd 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -1,14 +1,16 @@ import typing -from typing import List, Set, Tuple +from typing import List, Set, Tuple, Dict from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification from worlds.AutoWorld import WebWorld, World from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \ - basic_unit + get_basic_units from .Locations import get_locations from .Regions import create_regions -from .Options import sc2wol_options, get_option_value +from .Options import sc2wol_options, get_option_value, get_option_set_value from .LogicMixin import SC2WoLLogic +from .PoolFilter import filter_missions, filter_items, get_item_upgrades +from .MissionTables import get_starting_mission_locations, MissionInfo class Starcraft2WoLWebWorld(WebWorld): @@ -42,6 +44,8 @@ class SC2WoLWorld(World): locked_locations: typing.List[str] location_cache: typing.List[Location] mission_req_table = {} + final_mission_id: int + victory_item: str required_client_version = 0, 3, 5 def __init__(self, world: MultiWorld, player: int): @@ -49,24 +53,21 @@ class SC2WoLWorld(World): self.location_cache = [] self.locked_locations = [] - def _create_items(self, name: str): - data = get_full_item_list()[name] - return [self.create_item(name) for _ in range(data.quantity)] - def create_item(self, name: str) -> Item: data = get_full_item_list()[name] return StarcraftWoLItem(name, data.classification, data.code, self.player) def create_regions(self): - self.mission_req_table = create_regions(self.world, self.player, get_locations(self.world, self.player), - self.location_cache) + self.mission_req_table, self.final_mission_id, self.victory_item = create_regions( + self.world, self.player, get_locations(self.world, self.player), self.location_cache + ) def generate_basic(self): excluded_items = get_excluded_items(self, self.world, self.player) - assign_starter_items(self.world, self.player, excluded_items, self.locked_locations) + starter_items = assign_starter_items(self.world, self.player, excluded_items, self.locked_locations) - pool = get_item_pool(self.world, self.player, excluded_items) + pool = get_item_pool(self.world, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache) fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool) @@ -74,8 +75,7 @@ class SC2WoLWorld(World): def set_rules(self): setup_events(self.world, self.player, self.locked_locations, self.location_cache) - - self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player) + self.world.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player) def get_filler_item_name(self) -> str: return self.world.random.choice(filler_items) @@ -91,6 +91,7 @@ class SC2WoLWorld(World): slot_req_table[mission] = self.mission_req_table[mission]._asdict() slot_data["mission_req"] = slot_req_table + slot_data["final_mission"] = self.final_mission_id return slot_data @@ -120,30 +121,37 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set for item in world.precollected_items[player]: excluded_items.add(item.name) + excluded_items_option = getattr(world, 'excluded_items', []) + + excluded_items.update(excluded_items_option[player].value) + return excluded_items -def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): +def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]: non_local_items = world.non_local_items[player].value + if get_option_value(world, player, "early_unit"): + local_basic_unit = tuple(item for item in get_basic_units(world, player) if item not in non_local_items) + if not local_basic_unit: + raise Exception("At least one basic unit must be local") - local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items) - if not local_basic_unit: - raise Exception("At least one basic unit must be local") + # The first world should also be the starting world + first_mission = list(world.worlds[player].mission_req_table)[0] + starting_mission_locations = get_starting_mission_locations(world, player) + if first_mission in starting_mission_locations: + first_location = starting_mission_locations[first_mission] + elif first_mission == "In Utter Darkness": + first_location = first_mission + ": Defeat" + else: + first_location = first_mission + ": Victory" - # The first world should also be the starting world - first_location = list(world.worlds[player].mission_req_table)[0] - - if first_location == "In Utter Darkness": - first_location = first_location + ": Defeat" + return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)] else: - first_location = first_location + ": Victory" - - assign_starter_item(world, player, excluded_items, locked_locations, first_location, - local_basic_unit) + return [] def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str], - location: str, item_list: Tuple[str, ...]): + location: str, item_list: Tuple[str, ...]) -> Item: item_name = world.random.choice(item_list) @@ -155,17 +163,40 @@ def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str] locked_locations.append(location) + return item -def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]: + +def get_item_pool(world: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo], + starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]: pool: List[Item] = [] + # For the future: goal items like Artifact Shards go here + locked_items = [] + + # YAML items + yaml_locked_items = get_option_set_value(world, player, 'locked_items') + for name, data in item_table.items(): if name not in excluded_items: for _ in range(data.quantity): item = create_item_with_correct_settings(world, player, name) - pool.append(item) + if name in yaml_locked_items: + locked_items.append(item) + else: + pool.append(item) - return pool + existing_items = starter_items + [item for item in world.precollected_items[player]] + existing_names = [item.name for item in existing_items] + # Removing upgrades for excluded items + for item_name in excluded_items: + if item_name in existing_names: + continue + invalid_upgrades = get_item_upgrades(pool, item_name) + for invalid_upgrade in invalid_upgrades: + pool.remove(invalid_upgrade) + + filtered_pool = filter_items(world, player, mission_req_table, location_cache, pool, existing_items, locked_items) + return filtered_pool def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str], From 4b18920819a71a44b2a6455a2632ea8fb843028d Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 27 Oct 2022 03:00:24 -0400 Subject: [PATCH 095/105] Early Items option (#1086) * Early Items option * Early Items description update * Change Early Items to dict * Rewrite early items as extra fill steps * Move if statement up * Document early_items * Move early_items handling before fill_hook * Apply suggestions from code review Co-authored-by: Doug Hoskisson * Subnautica pre-fill moved to early_items * Subnautica early items fix * Remove unit test bug workaround * beauxq's pr Co-authored-by: Doug Hoskisson --- BaseClasses.py | 1 + Fill.py | 39 +++++++++++++++++++++ Options.py | 6 ++++ worlds/generic/docs/advanced_settings_en.md | 9 ++++- worlds/subnautica/Items.py | 2 +- worlds/subnautica/__init__.py | 19 ++-------- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ce2fc9e3c5..016c80ec83 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -48,6 +48,7 @@ class MultiWorld(): state: CollectionState accessibility: Dict[int, Options.Accessibility] + early_items: Dict[int, Options.EarlyItems] local_items: Dict[int, Options.LocalItems] non_local_items: Dict[int, Options.NonLocalItems] progression_balancing: Dict[int, Options.ProgressionBalancing] diff --git a/Fill.py b/Fill.py index cb9844b442..105b359134 100644 --- a/Fill.py +++ b/Fill.py @@ -258,6 +258,45 @@ def distribute_items_restrictive(world: MultiWorld) -> None: usefulitempool: typing.List[Item] = [] filleritempool: typing.List[Item] = [] + early_items_count: typing.Dict[typing.Tuple[str, int], int] = {} + for player in world.player_ids: + for item, count in world.early_items[player].value.items(): + early_items_count[(item, player)] = count + if early_items_count: + early_locations: typing.List[Location] = [] + early_priority_locations: typing.List[Location] = [] + for loc in reversed(fill_locations): + if loc.can_reach(world.state): + if loc.progress_type == LocationProgressType.PRIORITY: + early_priority_locations.append(loc) + else: + early_locations.append(loc) + fill_locations.remove(loc) + + early_prog_items: typing.List[Item] = [] + early_rest_items: typing.List[Item] = [] + for item in reversed(itempool): + if (item.name, item.player) in early_items_count: + if item.advancement: + early_prog_items.append(item) + else: + early_rest_items.append(item) + itempool.remove(item) + early_items_count[(item.name, item.player)] -= 1 + if early_items_count[(item.name, item.player)] == 0: + del early_items_count[(item.name, item.player)] + fill_restrictive(world, world.state, early_locations, early_rest_items, lock=True) + early_locations += early_priority_locations + fill_restrictive(world, world.state, early_locations, early_prog_items, lock=True) + unplaced_early_items = early_rest_items + early_prog_items + if unplaced_early_items: + logging.warning(f"Ran out of early locations for early items. Failed to place \ + {len(unplaced_early_items)} items early.") + itempool += unplaced_early_items + + fill_locations += early_locations + early_priority_locations + world.random.shuffle(fill_locations) + for item in itempool: if item.advancement: progitempool.append(item) diff --git a/Options.py b/Options.py index 536f388efb..ad87f5ebf8 100644 --- a/Options.py +++ b/Options.py @@ -883,6 +883,11 @@ class NonLocalItems(ItemSet): display_name = "Not Local Items" +class EarlyItems(ItemDict): + """Force the specified items to be in locations that are reachable from the start.""" + display_name = "Early Items" + + class StartInventory(ItemDict): """Start with these items.""" verify_item_name = True @@ -981,6 +986,7 @@ per_game_common_options = { **common_options, # can be overwritten per-game "local_items": LocalItems, "non_local_items": NonLocalItems, + "early_items": EarlyItems, "start_inventory": StartInventory, "start_hints": StartHints, "start_location_hints": StartLocationHints, diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index d19c9d5ee6..45f653e8bb 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -106,7 +106,7 @@ settings. If a game can be rolled it **must** have a settings section even if it Some options in Archipelago can be used by every game but must still be placed within the relevant game's section. -Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints` +Currently, these options are `start_inventory`, `early_items`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints` , `exclude_locations`, and various plando options. See the plando guide for more info on plando options. Plando @@ -115,6 +115,8 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which will give you 30 rupees. +* `early_items` is formatted in the same way as `start_inventory` and will force the number of each item specified to be +forced into locations that are reachable from the start, before obtaining any items. * `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for the location without using any hint points. * `local_items` will force any items you want to be in your world instead of being in another world. @@ -172,6 +174,8 @@ A Link to the Past: - Quake non_local_items: - Moon Pearl + early_items: + Flute: 1 start_location_hints: - Spike Cave priority_locations: @@ -235,6 +239,9 @@ Timespinner: * `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we have to find it ourselves. * `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it. +* `early_items` forces the `Flute` to be placed in a location that is available from the beginning of the game ("Sphere +1"). Since it is not specified in `local_items` or `non_local_items`, it can be placed one of these locations in any +world. * `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld that can be used for no cost. * `priority_locations` forces a progression item to be placed on the `Link's House` location. diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index 4201cf3910..4a9eeabdfd 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -175,7 +175,7 @@ item_table: Dict[int, ItemDict] = { 'name': 'Thermal Plant Fragment', 'tech_type': 'ThermalPlantFragment'}, 35041: {'classification': ItemClassification.progression, - 'count': 2, + 'count': 4, 'name': 'Seaglide Fragment', 'tech_type': 'SeaglideFragment'}, 35042: {'classification': ItemClassification.progression, diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 830bc831ef..bd86dc5ce7 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,14 +44,12 @@ class SubnauticaWorld(World): data_version = 7 required_client_version = (0, 3, 5) - prefill_items: List[Item] creatures_to_scan: List[str] def generate_early(self) -> None: - self.prefill_items = [ - self.create_item("Seaglide Fragment"), - self.create_item("Seaglide Fragment") - ] + if "Seaglide Fragment" not in self.world.early_items[self.player]: + self.world.early_items[self.player].value["Seaglide Fragment"] = 2 + scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player] creature_pool = scan_option.get_pool() @@ -149,17 +147,6 @@ class SubnauticaWorld(World): ret.exits.append(Entrance(self.player, region_exit, ret)) return ret - def get_pre_fill_items(self) -> List[Item]: - return self.prefill_items - - def pre_fill(self) -> None: - reachable = [location for location in self.world.get_reachable_locations(player=self.player) - if not location.item] - self.world.random.shuffle(reachable) - items = self.prefill_items.copy() - for item in items: - reachable.pop().place_locked_item(item) - class SubnauticaLocation(Location): game: str = "Subnautica" From b57ca33c31a8af43f561a8625d8aae59d042bca3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 27 Oct 2022 09:18:25 +0200 Subject: [PATCH 096/105] Logging: more digits for IDs and counts (#1141) * Logging: we now need 9 digits for IDs * Logging: we now need {dynamic} digits for IDs * Logging: we now need {dynamic} digits for counts --- Main.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/Main.py b/Main.py index 63f5b8a818..38100bd050 100644 --- a/Main.py +++ b/Main.py @@ -80,15 +80,30 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info("Found World Types:") longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) - numlength = 8 + + max_item = 0 + max_location = 0 + for cls in AutoWorld.AutoWorldRegister.world_types.values(): + if cls.item_id_to_name: + max_item = max(max_item, max(cls.item_id_to_name)) + max_location = max(max_location, max(cls.location_id_to_name)) + + item_digits = len(str(max_item)) + location_digits = len(str(max_location)) + item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values()))) + location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values()))) + del max_item, max_location + for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): if not cls.hidden and len(cls.item_names) > 0: - logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} " - f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - " - f"{max(cls.item_id_to_name):{numlength}}) | " - f"{len(cls.location_names):3} " - f"Locations (IDs: {min(cls.location_id_to_name):{numlength}} - " - f"{max(cls.location_id_to_name):{numlength}})") + logger.info(f" {name:{longest_name}}: {len(cls.item_names):{item_count}} " + f"Items (IDs: {min(cls.item_id_to_name):{item_digits}} - " + f"{max(cls.item_id_to_name):{item_digits}}) | " + f"{len(cls.location_names):{location_count}} " + f"Locations (IDs: {min(cls.location_id_to_name):{location_digits}} - " + f"{max(cls.location_id_to_name):{location_digits}})") + + del item_digits, location_digits, item_count, location_count AutoWorld.call_stage(world, "assert_generate") From 6134578c60341f4603f371c6cbb1ea6dad2135ed Mon Sep 17 00:00:00 2001 From: toasterparty Date: Thu, 27 Oct 2022 02:19:48 -0700 Subject: [PATCH 097/105] Overcooked! 2: slightly relax 3-star logic (#1144) --- worlds/overcooked2/Logic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index 6fb1a50a41..a4f1f0fb86 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -291,6 +291,11 @@ level_logic = { "Progressive Throw/Catch", ], { # Additive + ("Sharp Knife", 1.0), + ("Dish Scrubber", 1.0), + ("Clean Dishes", 0.5), + ("Guest Patience", 0.25), + ("Burn Leniency", 0.25), }, ) ), From aeb78eaa1000b906f97da7c0e605f3f83b749fca Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Thu, 27 Oct 2022 02:30:22 -0700 Subject: [PATCH 098/105] Zillion: map tracker in client (#1136) * Option RangeWithSpecialMax * amendment to typing in web options * compare string with number * lots of work on zillion * fix zillion fill logic * fix a few more issues in zillion fill logic * can make zillion patch and use it * put multi items in zillion rom * work on ZillionClient * logging and auth in client * work on sending and receiving items * implement item_handling flag * fix locations ids to NuktiServer package * use rewrite of zri * cache logic rule data for performance * use new id maps * fix some problems with the big recent merge * ZillionClient: use new context manager for Memory class * fix ItemClassification for Zillion items and some debug statements for asserts, documentation on running scripts for manual testing type correction in CommonContext * fix some issues in client, start on docs, put rescue and item ram addresses in slot data * use new location name system fix item locations getting out of sync in progression balancing * zillion client can read slot name from game * zillion: new item names * remove extra unneeded import * newer options (room gen and starting cards) * update comment in zillion patch * zillion non static regions * change some logging, update some comments * allow ZillionClient to exit in certain situations * todo note to fix options doc strings * don't force auto forfeit * rework validation of floppy requirement and item counts and fix race condition in generate_output * reorganize Zillion component structure with System class * documentation updates for Zillion * attempt inno_setup.iss * remove todo comment for something done * update comment * rework item count zillion options and some small cleanups * fix location check count * data package version 1 * Zillion can pass unit tests without rom * fix freeze if closing ZillionClient while it's waiting for server login * specify commit hash for zilliandomizer package * some changes to options validation * Zillion doors saved on multiworld server * add missing function in inno_setup and name of vanilla continues in options * rework zillion sync task and context * Apply documentation suggestions from SoldierofOrder Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * update zillion package * workaround for asyncio udp bug There is a bug in Python in Windows https://github.com/python/cpython/issues/91227 that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system. As a workaround, we don't look for RetroArch until the user asks for it with /sms * a few of the smaller suggestions from review * logic only looks at my locations instead of all the multiworld locations * some adjustments from pull request discussion and some unit tests * patch webhost changes from pull request discussion * zillion logic tests * better vblr test * test interaction of character rescue items with logic * move unit tests to new worlds folder * comment improvements * fix minor logic issue and add memory read timeout * capitalization in option display names Opa-Opa is a proper noun * client toggle side panel with /map * displays map * fix map transparency * fix broken launcher * better way to specify grid container * start kivy typing * have a map that updates with item checks but it breaks other parts of the UI * fix layout bug * aspect ratio of image and some type checking details * Fix loading of map for compiled builds Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Co-authored-by: Doug Hoskisson Co-authored-by: CaitSith2 --- ZillionClient.py | 111 +++++++++++++++++- kvui.py | 11 +- typings/kivy/__init__.pyi | 0 typings/kivy/app.pyi | 2 + typings/kivy/core/__init__.pyi | 0 typings/kivy/core/text.pyi | 7 ++ typings/kivy/graphics.pyi | 40 +++++++ typings/kivy/uix/__init__.pyi | 0 typings/kivy/uix/layout.pyi | 8 ++ typings/kivy/uix/tabbedpanel.pyi | 12 ++ typings/kivy/uix/widget.pyi | 31 +++++ worlds/sa2b/Names/__init__.py | 0 worlds/zillion/config.py | 3 + .../empty-zillion-map-row-col-labels-281.png | Bin 0 -> 29903 bytes 14 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 typings/kivy/__init__.pyi create mode 100644 typings/kivy/app.pyi create mode 100644 typings/kivy/core/__init__.pyi create mode 100644 typings/kivy/core/text.pyi create mode 100644 typings/kivy/graphics.pyi create mode 100644 typings/kivy/uix/__init__.pyi create mode 100644 typings/kivy/uix/layout.pyi create mode 100644 typings/kivy/uix/tabbedpanel.pyi create mode 100644 typings/kivy/uix/widget.pyi create mode 100644 worlds/sa2b/Names/__init__.py create mode 100644 worlds/zillion/empty-zillion-map-row-col-labels-281.png diff --git a/ZillionClient.py b/ZillionClient.py index 8ad1065057..e2ce697c8a 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,7 +1,7 @@ import asyncio import base64 import platform -from typing import Any, Coroutine, Dict, Optional, Tuple, Type, cast +from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, cast # CommonClient import first to trigger ModuleUpdater from CommonClient import CommonContext, server_loop, gui_enabled, \ @@ -18,7 +18,7 @@ from zilliandomizer.options import Chars from zilliandomizer.patch import RescueInfo from worlds.zillion.id_maps import make_id_to_others -from worlds.zillion.config import base_id +from worlds.zillion.config import base_id, zillion_map class ZillionCommandProcessor(ClientCommandProcessor): @@ -29,6 +29,18 @@ class ZillionCommandProcessor(ClientCommandProcessor): logger.info("ready to look for game") self.ctx.look_for_retroarch.set() + def _cmd_map(self) -> None: + """ Toggle view of the map tracker. """ + self.ctx.ui_toggle_map() + + +class ToggleCallback(Protocol): + def __call__(self) -> None: ... + + +class SetRoomCallback(Protocol): + def __call__(self, rooms: List[List[int]]) -> None: ... + class ZillionContext(CommonContext): game = "Zillion" @@ -61,6 +73,10 @@ class ZillionContext(CommonContext): As a workaround, we don't look for RetroArch until this event is set. """ + ui_toggle_map: ToggleCallback + ui_set_rooms: SetRoomCallback + """ parameter is y 16 x 8 numbers to show in each room """ + def __init__(self, server_address: str, password: str) -> None: @@ -69,6 +85,8 @@ class ZillionContext(CommonContext): self.to_game = asyncio.Queue() self.got_room_info = asyncio.Event() self.got_slot_data = asyncio.Event() + self.ui_toggle_map = lambda: None + self.ui_set_rooms = lambda rooms: None self.look_for_retroarch = asyncio.Event() if platform.system() != "Windows": @@ -115,6 +133,10 @@ class ZillionContext(CommonContext): # override def run_gui(self) -> None: from kvui import GameManager + from kivy.core.text import Label as CoreLabel + from kivy.graphics import Ellipse, Color, Rectangle + from kivy.uix.layout import Layout + from kivy.uix.widget import Widget class ZillionManager(GameManager): logging_pairs = [ @@ -122,12 +144,76 @@ class ZillionContext(CommonContext): ] base_title = "Archipelago Zillion Client" + class MapPanel(Widget): + MAP_WIDTH: ClassVar[int] = 281 + + _number_textures: List[Any] = [] + rooms: List[List[int]] = [] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.rooms = [[0 for _ in range(8)] for _ in range(16)] + + self._make_numbers() + self.update_map() + + self.bind(pos=self.update_map) + # self.bind(size=self.update_bg) + + def _make_numbers(self) -> None: + self._number_textures = [] + for n in range(10): + label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) + label.refresh() + self._number_textures.append(label.texture) + + def update_map(self, *args: Any) -> None: + self.canvas.clear() + + with self.canvas: + Color(1, 1, 1, 1) + Rectangle(source=zillion_map, + pos=self.pos, + size=(ZillionManager.MapPanel.MAP_WIDTH, + int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image + for y in range(16): + for x in range(8): + num = self.rooms[15 - y][x] + if num > 0: + Color(0, 0, 0, 0.4) + pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] + Ellipse(size=[22, 22], pos=pos) + Color(1, 1, 1, 1) + pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] + num_texture = self._number_textures[num] + Rectangle(texture=num_texture, size=num_texture.size, pos=pos) + + def build(self) -> Layout: + container = super().build() + self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) + self.main_area_container.add_widget(self.map_widget) + return container + + def toggle_map_width(self) -> None: + if self.map_widget.width == 0: + self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH + else: + self.map_widget.width = 0 + self.container.do_layout() + + def set_rooms(self, rooms: List[List[int]]) -> None: + self.map_widget.rooms = rooms + self.map_widget.update_map() + self.ui = ZillionManager(self) - run_co: Coroutine[Any, Any, None] = self.ui.async_run() # type: ignore - # kivy types missing + self.ui_toggle_map = lambda: self.ui.toggle_map_width() + self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) + run_co: Coroutine[Any, Any, None] = self.ui.async_run() self.ui_task = asyncio.create_task(run_co, name="UI") def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + self.room_item_numbers_to_ui() if cmd == "Connected": logger.info("logged in to Archipelago server") if "slot_data" not in args: @@ -192,6 +278,21 @@ class ZillionContext(CommonContext): self.seed_name = args["seed_name"] self.got_room_info.set() + def room_item_numbers_to_ui(self) -> None: + rooms = [[0 for _ in range(8)] for _ in range(16)] + for loc_id in self.missing_locations: + loc_id_small = loc_id - base_id + loc_name = id_to_loc[loc_id_small] + y = ord(loc_name[0]) - 65 + x = ord(loc_name[2]) - 49 + if y == 9 and x == 5: + # don't show main computer in numbers + continue + assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" + rooms[y][x] += 1 + # TODO: also add locations with locals lost from loading save state or reset + self.ui_set_rooms(rooms) + def process_from_game_queue(self) -> None: if self.from_game.qsize(): event_from_game = self.from_game.get_nowait() @@ -251,7 +352,7 @@ def name_seed_from_ram(data: bytes) -> Tuple[str, str]: return "", "xxx" null_index = data.find(b'\x00') if null_index == -1: - logger.warning(f"invalid game id in rom {data}") + logger.warning(f"invalid game id in rom {repr(data)}") null_index = len(data) name = data[:null_index].decode() null_index_2 = data.find(b'\x00', null_index + 1) diff --git a/kvui.py b/kvui.py index 3c1161f99b..3820864538 100644 --- a/kvui.py +++ b/kvui.py @@ -28,6 +28,7 @@ from kivy.factory import Factory from kivy.properties import BooleanProperty, ObjectProperty from kivy.uix.button import Button from kivy.uix.gridlayout import GridLayout +from kivy.uix.layout import Layout from kivy.uix.textinput import TextInput from kivy.uix.recycleview import RecycleView from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem @@ -299,6 +300,9 @@ class GameManager(App): base_title: str = "Archipelago Client" last_autofillable_command: str + main_area_container: GridLayout + """ subclasses can add more columns beside the tabs """ + def __init__(self, ctx: context_type): self.title = self.base_title self.ctx = ctx @@ -325,7 +329,7 @@ class GameManager(App): super(GameManager, self).__init__() - def build(self): + def build(self) -> Layout: self.container = ContainerLayout() self.grid = MainLayout() @@ -358,7 +362,10 @@ class GameManager(App): self.log_panels[display_name] = panel.content = UILog(bridge_logger) self.tabs.add_widget(panel) - self.grid.add_widget(self.tabs) + self.main_area_container = GridLayout(size_hint_y=1, rows=1) + self.main_area_container.add_widget(self.tabs) + + self.grid.add_widget(self.main_area_container) if len(self.logging_pairs) == 1: # Hide Tab selection if only one tab diff --git a/typings/kivy/__init__.pyi b/typings/kivy/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/typings/kivy/app.pyi b/typings/kivy/app.pyi new file mode 100644 index 0000000000..bb41bf6b2b --- /dev/null +++ b/typings/kivy/app.pyi @@ -0,0 +1,2 @@ +class App: + async def async_run(self) -> None: ... diff --git a/typings/kivy/core/__init__.pyi b/typings/kivy/core/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/typings/kivy/core/text.pyi b/typings/kivy/core/text.pyi new file mode 100644 index 0000000000..7b13ad3424 --- /dev/null +++ b/typings/kivy/core/text.pyi @@ -0,0 +1,7 @@ +from typing import Tuple +from ..graphics import FillType_Shape +from ..uix.widget import Widget + + +class Label(FillType_Shape, Widget): + def __init__(self, *, text: str, font_size: int, color: Tuple[float, float, float, float]) -> None: ... diff --git a/typings/kivy/graphics.pyi b/typings/kivy/graphics.pyi new file mode 100644 index 0000000000..1950910661 --- /dev/null +++ b/typings/kivy/graphics.pyi @@ -0,0 +1,40 @@ +""" FillType_* is not a real kivy type - just something to fill unknown typing. """ + +from typing import Sequence + +FillType_Vec = Sequence[int] + + +class FillType_Drawable: + def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... + + +class FillType_Texture(FillType_Drawable): + pass + + +class FillType_Shape(FillType_Drawable): + texture: FillType_Texture + + def __init__(self, + *, + texture: FillType_Texture = ..., + pos: FillType_Vec = ..., + size: FillType_Vec = ...) -> None: ... + + +class Ellipse(FillType_Shape): + pass + + +class Color: + def __init__(self, r: float, g: float, b: float, a: float) -> None: ... + + +class Rectangle(FillType_Shape): + def __init__(self, + *, + source: str = ..., + texture: FillType_Texture = ..., + pos: FillType_Vec = ..., + size: FillType_Vec = ...) -> None: ... diff --git a/typings/kivy/uix/__init__.pyi b/typings/kivy/uix/__init__.pyi new file mode 100644 index 0000000000..e69de29bb2 diff --git a/typings/kivy/uix/layout.pyi b/typings/kivy/uix/layout.pyi new file mode 100644 index 0000000000..2a418a1d8b --- /dev/null +++ b/typings/kivy/uix/layout.pyi @@ -0,0 +1,8 @@ +from typing import Any +from .widget import Widget + + +class Layout(Widget): + def add_widget(self, widget: Widget) -> None: ... + + def do_layout(self, *largs: Any, **kwargs: Any) -> None: ... diff --git a/typings/kivy/uix/tabbedpanel.pyi b/typings/kivy/uix/tabbedpanel.pyi new file mode 100644 index 0000000000..9183b4c8b3 --- /dev/null +++ b/typings/kivy/uix/tabbedpanel.pyi @@ -0,0 +1,12 @@ +from .layout import Layout +from .widget import Widget + + +class TabbedPanel(Layout): + pass + + +class TabbedPanelItem(Widget): + content: Widget + + def __init__(self, *, text: str = ...) -> None: ... diff --git a/typings/kivy/uix/widget.pyi b/typings/kivy/uix/widget.pyi new file mode 100644 index 0000000000..54e3b781ea --- /dev/null +++ b/typings/kivy/uix/widget.pyi @@ -0,0 +1,31 @@ +""" FillType_* is not a real kivy type - just something to fill unknown typing. """ + +from typing import Any, Optional, Protocol +from ..graphics import FillType_Drawable, FillType_Vec + + +class FillType_BindCallback(Protocol): + def __call__(self, *args: Any) -> None: ... + + +class FillType_Canvas: + def add(self, drawable: FillType_Drawable) -> None: ... + + def clear(self) -> None: ... + + def __enter__(self) -> None: ... + + def __exit__(self, *args: Any) -> None: ... + + +class Widget: + canvas: FillType_Canvas + width: int + pos: FillType_Vec + + def bind(self, + *, + pos: Optional[FillType_BindCallback] = ..., + size: Optional[FillType_BindCallback] = ...) -> None: ... + + def refresh(self) -> None: ... diff --git a/worlds/sa2b/Names/__init__.py b/worlds/sa2b/Names/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py index e08c4f4278..ca02f9a99f 100644 --- a/worlds/zillion/config.py +++ b/worlds/zillion/config.py @@ -1 +1,4 @@ +import os + base_id = 8675309 +zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png") diff --git a/worlds/zillion/empty-zillion-map-row-col-labels-281.png b/worlds/zillion/empty-zillion-map-row-col-labels-281.png new file mode 100644 index 0000000000000000000000000000000000000000..3084301f7b0274e9351370168509d59924964e66 GIT binary patch literal 29903 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 wF8>9n Date: Fri, 28 Oct 2022 00:07:57 +0200 Subject: [PATCH 099/105] Factorio: Add optional filtering for item sends displayed in-game (#1142) * Factorio: Added feature to filter item sends displayed in-game. * Factorio: Document item send filter feature. * Factorio: Fix item send filter for item links. * (Removed superfluous type annotations.) * CommonClient: Added is_uninteresting_item_send helper. --- CommonClient.py | 6 +++++ FactorioClient.py | 27 +++++++++++++++++-- Utils.py | 1 + host.yaml | 2 ++ worlds/factorio/data/mod_template/control.lua | 4 +++ worlds/factorio/docs/setup_en.md | 12 +++++++++ 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index c713373592..4fc82b5e67 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -312,6 +312,12 @@ class CommonContext: return self.slot in self.slot_info[slot].group_members return False + def is_uninteresting_item_send(self, print_json_packet: dict) -> bool: + """Helper function for filtering out ItemSend prints that do not concern the local player.""" + return print_json_packet.get("type", "") == "ItemSend" \ + and not self.slot_concerns_self(print_json_packet["receiving"]) \ + and not self.slot_concerns_self(print_json_packet["item"].player) + def on_print(self, args: dict): logger.info(args["text"]) diff --git a/FactorioClient.py b/FactorioClient.py index 1efca05d3c..8ab9799b7c 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -4,6 +4,7 @@ import logging import json import string import copy +import re import subprocess import time import random @@ -46,6 +47,10 @@ class FactorioCommandProcessor(ClientCommandProcessor): """Manually trigger a resync.""" self.ctx.awaiting_bridge = True + def _cmd_toggle_send_filter(self): + """Toggle filtering of item sends that get displayed in-game to only those that involve you.""" + self.ctx.toggle_filter_item_sends() + class FactorioContext(CommonContext): command_processor = FactorioCommandProcessor @@ -65,6 +70,7 @@ class FactorioContext(CommonContext): self.factorio_json_text_parser = FactorioJSONtoTextParser(self) self.energy_link_increment = 0 self.last_deplete = 0 + self.filter_item_sends: bool = False async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -85,8 +91,9 @@ class FactorioContext(CommonContext): def on_print_json(self, args: dict): if self.rcon_client: - text = self.factorio_json_text_parser(copy.deepcopy(args["data"])) - self.print_to_game(text) + if not self.filter_item_sends or not self.is_uninteresting_item_send(args): + text = self.factorio_json_text_parser(copy.deepcopy(args["data"])) + self.print_to_game(text) super(FactorioContext, self).on_print_json(args) @property @@ -123,6 +130,15 @@ class FactorioContext(CommonContext): f"{Utils.format_SI_prefix(args['value'])}J remaining.") self.rcon_client.send_command(f"/ap-energylink {gained}") + def toggle_filter_item_sends(self) -> None: + self.filter_item_sends = not self.filter_item_sends + if self.filter_item_sends: + announcement = "Item sends are now filtered." + else: + announcement = "Item sends are no longer filtered." + logger.info(announcement) + self.print_to_game(announcement) + def run_gui(self): from kvui import GameManager @@ -262,6 +278,9 @@ async def factorio_server_watcher(ctx: FactorioContext): if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg: ctx.awaiting_bridge = True factorio_server_logger.debug(msg) + elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-send-filter", msg): + factorio_server_logger.debug(msg) + ctx.toggle_filter_item_sends() else: factorio_server_logger.info(msg) if ctx.rcon_client: @@ -363,6 +382,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool: async def main(args): ctx = FactorioContext(args.connect, args.password) + ctx.filter_item_sends = initial_filter_item_sends ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") if gui_enabled: @@ -415,6 +435,9 @@ if __name__ == '__main__': server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None) if server_settings: server_settings = os.path.abspath(server_settings) + if not isinstance(options["factorio_options"]["filter_item_sends"], bool): + logging.warning(f"Warning: Option filter_item_sends should be a bool.") + initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"]) if not os.path.exists(os.path.dirname(executable)): raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.") diff --git a/Utils.py b/Utils.py index 64a028fc33..b2e98358bc 100644 --- a/Utils.py +++ b/Utils.py @@ -231,6 +231,7 @@ def get_default_options() -> OptionsType: }, "factorio_options": { "executable": os.path.join("factorio", "bin", "x64", "factorio"), + "filter_item_sends": False, }, "sni_options": { "sni": "SNI", diff --git a/host.yaml b/host.yaml index 2c5a8e3e1d..0bdd95356e 100644 --- a/host.yaml +++ b/host.yaml @@ -99,6 +99,8 @@ factorio_options: executable: "factorio/bin/x64/factorio" # by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used. # server_settings: "factorio\\data\\server-settings.json" + # Whether to filter item send messages displayed in-game to only those that involve you. + filter_item_sends: false minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 51cd21e4da..86e83b9f4d 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -596,5 +596,9 @@ commands.add_command("ap-energylink", "Used by the Archipelago client to manage global.forcedata[force].energy = global.forcedata[force].energy + change end) +commands.add_command("toggle-ap-send-filter", "Toggle filtering of item sends that get displayed in-game to only those that involve you.", function(call) + log("Player command toggle-ap-send-filter") -- notifies client +end) + -- data progressive_technologies = {{ dict_to_lua(progressive_technology_table) }} diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md index 560a37d1e3..8b24da13d5 100644 --- a/worlds/factorio/docs/setup_en.md +++ b/worlds/factorio/docs/setup_en.md @@ -141,6 +141,18 @@ you can also issue the `!help` command to learn about additional commands like ` 4. Provide your IP address to anyone you want to join your game, and have them follow the steps for "Connecting to Someone Else's Factorio Game" above. +## Other Settings + +- By default, all item sends are displayed in-game. In larger async seeds this may become overly spammy. + To hide all item sends that are not to or from your factory, do one of the following: + - Type `/toggle-ap-send-filter` in-game + - Type `/toggle_send_filter` in the Archipelago Client + - In your `host.yaml` set + ``` + factorio_options: + filter_item_sends: true + ``` + ## Troubleshooting In case any problems should occur, the Archipelago Client will create a file `FactorioClient.txt` in the `/logs`. The From cfff12d8d7a7ae36cef355d0f93449dd2d8b1027 Mon Sep 17 00:00:00 2001 From: recklesscoder <57289227+recklesscoder@users.noreply.github.com> Date: Fri, 28 Oct 2022 00:45:26 +0200 Subject: [PATCH 100/105] Factorio: Added ability to chat from within the game. (#1068) * Factorio: Added ability to chat from within the game. This also allows using commands such as !hint from within the game. * Factorio: Only prepend player names to chat in multiplayer. * Factorio: Mirror chat sent from the FactorioClient UI to the Factorio server. * Factorio: Remove local coordinates from outgoing chat. * Factorio: Added setting to disable bridging chat out. Added client command to toggle this setting at run-time. * Factorio: Added in-game command to toggle chat bridging setting at run-time. * . * Factorio: Document toggle for chat bridging feature. * (Removed superfluous type annotations.) * (Removed hard to read regex.) * Docs/Factorio: Fix display of multiline code snippets. --- FactorioClient.py | 53 ++++++++++++++++++- Utils.py | 1 + host.yaml | 2 + worlds/factorio/data/mod_template/control.lua | 6 +++ worlds/factorio/docs/setup_en.md | 27 ++++++---- 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/FactorioClient.py b/FactorioClient.py index 8ab9799b7c..12ec22916b 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -8,6 +8,7 @@ import re import subprocess import time import random +import typing import ModuleUpdate ModuleUpdate.update() @@ -51,6 +52,9 @@ class FactorioCommandProcessor(ClientCommandProcessor): """Toggle filtering of item sends that get displayed in-game to only those that involve you.""" self.ctx.toggle_filter_item_sends() + def _cmd_toggle_chat(self): + """Toggle sending of chat messages from players on the Factorio server to Archipelago.""" + self.ctx.toggle_bridge_chat_out() class FactorioContext(CommonContext): command_processor = FactorioCommandProcessor @@ -71,6 +75,8 @@ class FactorioContext(CommonContext): self.energy_link_increment = 0 self.last_deplete = 0 self.filter_item_sends: bool = False + self.multiplayer: bool = False # whether multiple different players have connected + self.bridge_chat_out: bool = True async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -87,13 +93,15 @@ class FactorioContext(CommonContext): def on_print(self, args: dict): super(FactorioContext, self).on_print(args) if self.rcon_client: - self.print_to_game(args['text']) + if not args['text'].startswith(self.player_names[self.slot] + ":"): + self.print_to_game(args['text']) def on_print_json(self, args: dict): if self.rcon_client: if not self.filter_item_sends or not self.is_uninteresting_item_send(args): text = self.factorio_json_text_parser(copy.deepcopy(args["data"])) - self.print_to_game(text) + if not text.startswith(self.player_names[self.slot] + ":"): + self.print_to_game(text) super(FactorioContext, self).on_print_json(args) @property @@ -130,6 +138,27 @@ class FactorioContext(CommonContext): f"{Utils.format_SI_prefix(args['value'])}J remaining.") self.rcon_client.send_command(f"/ap-energylink {gained}") + def on_user_say(self, text: str) -> typing.Optional[str]: + # Mirror chat sent from the UI to the Factorio server. + self.print_to_game(f"{self.player_names[self.slot]}: {text}") + return text + + async def chat_from_factorio(self, user: str, message: str) -> None: + if not self.bridge_chat_out: + return + + # Pass through commands + if message.startswith("!"): + await self.send_msgs([{"cmd": "Say", "text": message}]) + return + + # Omit messages that contain local coordinates + if "[gps=" in message: + return + + prefix = f"({user}) " if self.multiplayer else "" + await self.send_msgs([{"cmd": "Say", "text": f"{prefix}{message}"}]) + def toggle_filter_item_sends(self) -> None: self.filter_item_sends = not self.filter_item_sends if self.filter_item_sends: @@ -139,6 +168,15 @@ class FactorioContext(CommonContext): logger.info(announcement) self.print_to_game(announcement) + def toggle_bridge_chat_out(self) -> None: + self.bridge_chat_out = not self.bridge_chat_out + if self.bridge_chat_out: + announcement = "Chat is now bridged to Archipelago." + else: + announcement = "Chat is no longer bridged to Archipelago." + logger.info(announcement) + self.print_to_game(announcement) + def run_gui(self): from kvui import GameManager @@ -178,6 +216,7 @@ async def game_watcher(ctx: FactorioContext): research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} victory = data["victory"] await ctx.update_death_link(data["death_link"]) + ctx.multiplayer = data.get("multiplayer", False) if not ctx.finished_game and victory: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) @@ -281,8 +320,14 @@ async def factorio_server_watcher(ctx: FactorioContext): elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-send-filter", msg): factorio_server_logger.debug(msg) ctx.toggle_filter_item_sends() + elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-chat$", msg): + factorio_server_logger.debug(msg) + ctx.toggle_bridge_chat_out() else: factorio_server_logger.info(msg) + match = re.match(r"^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[CHAT\] ([^:]+): (.*)$", msg) + if match: + await ctx.chat_from_factorio(match.group(1), match.group(2)) if ctx.rcon_client: commands = {} while ctx.send_index < len(ctx.items_received): @@ -383,6 +428,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool: async def main(args): ctx = FactorioContext(args.connect, args.password) ctx.filter_item_sends = initial_filter_item_sends + ctx.bridge_chat_out = initial_bridge_chat_out ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") if gui_enabled: @@ -438,6 +484,9 @@ if __name__ == '__main__': if not isinstance(options["factorio_options"]["filter_item_sends"], bool): logging.warning(f"Warning: Option filter_item_sends should be a bool.") initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"]) + if not isinstance(options["factorio_options"]["bridge_chat_out"], bool): + logging.warning(f"Warning: Option bridge_chat_out should be a bool.") + initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"]) if not os.path.exists(os.path.dirname(executable)): raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.") diff --git a/Utils.py b/Utils.py index b2e98358bc..e0c86ddb39 100644 --- a/Utils.py +++ b/Utils.py @@ -232,6 +232,7 @@ def get_default_options() -> OptionsType: "factorio_options": { "executable": os.path.join("factorio", "bin", "x64", "factorio"), "filter_item_sends": False, + "bridge_chat_out": True, }, "sni_options": { "sni": "SNI", diff --git a/host.yaml b/host.yaml index 0bdd95356e..4e94a9a30c 100644 --- a/host.yaml +++ b/host.yaml @@ -101,6 +101,8 @@ factorio_options: # server_settings: "factorio\\data\\server-settings.json" # Whether to filter item send messages displayed in-game to only those that involve you. filter_item_sends: false + # Whether to send chat messages from players on the Factorio server to Archipelago. + bridge_chat_out: true minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 86e83b9f4d..98c9ca621a 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -157,6 +157,7 @@ function on_player_created(event) {%- if silo == 2 %} check_spawn_silo(game.players[event.player_index].force) {%- endif %} + dumpInfo(player.force) end script.on_event(defines.events.on_player_created, on_player_created) @@ -491,6 +492,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress ["death_link"] = DEATH_LINK, ["energy"] = chain_lookup(forcedata, "energy"), ["energy_bridges"] = chain_lookup(forcedata, "energy_bridges"), + ["multiplayer"] = #game.players > 1, } for tech_name, tech in pairs(force.technologies) do @@ -600,5 +602,9 @@ commands.add_command("toggle-ap-send-filter", "Toggle filtering of item sends th log("Player command toggle-ap-send-filter") -- notifies client end) +commands.add_command("toggle-ap-chat", "Toggle sending of chat messages from players on the Factorio server to Archipelago.", function(call) + log("Player command toggle-ap-chat") -- notifies client +end) + -- data progressive_technologies = {{ dict_to_lua(progressive_technology_table) }} diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md index 8b24da13d5..73ff5c8c48 100644 --- a/worlds/factorio/docs/setup_en.md +++ b/worlds/factorio/docs/setup_en.md @@ -132,6 +132,8 @@ This allows you to host your own Factorio game. For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server, you can also issue the `!help` command to learn about additional commands like `!hint`. +For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en) and +[Other Settings](#other-settings). ## Allowing Other People to Join Your Game @@ -148,10 +150,20 @@ you can also issue the `!help` command to learn about additional commands like ` - Type `/toggle-ap-send-filter` in-game - Type `/toggle_send_filter` in the Archipelago Client - In your `host.yaml` set - ``` - factorio_options: - filter_item_sends: true - ``` +``` +factorio_options: + filter_item_sends: true +``` +- By default, in-game chat is bridged to Archipelago. If you prefer to be able to speak privately, you can disable this + feature by doing one of the following: + - Type `/toggle-ap-chat` in-game + - Type `/toggle_chat` in the Archipelago Client + - In your `host.yaml` set +``` +factorio_options: + bridge_chat_out: false +``` + Note that this will also disable `!` commands from within the game, and that it will not affect incoming chat. ## Troubleshooting @@ -159,13 +171,6 @@ In case any problems should occur, the Archipelago Client will create a file `Fa contents of this file may help you troubleshoot an issue on your own and is vital for requesting help from other people in Archipelago. -## Commands in game - -Once you have connected to the server successfully using the Archipelago Factorio Client you should see a message -stating you can get help using Archipelago commands by typing `!help`. Commands cannot currently be sent from within -the Factorio session, but you can send them from the Archipelago Factorio Client. For more information about the commands -you can use see the [commands guide](/tutorial/Archipelago/commands/en). - ## Additional Resources - Alternate Tutorial by From c09e089f9df6b0b0dae4780c7d19c8e4de0dd837 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Thu, 27 Oct 2022 16:35:18 -0700 Subject: [PATCH 101/105] Docs: Zillion: RetroArch core and `early_items` recommendation. (#1150) * add Genesis Plus GX to Zillion docs * zillion early items recommendation --- worlds/zillion/docs/setup_en.md | 17 ++++++++++++++--- worlds/zillion/options.py | 8 +------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md index 43a1296748..63a0ec5a5b 100644 --- a/worlds/zillion/docs/setup_en.md +++ b/worlds/zillion/docs/setup_en.md @@ -15,7 +15,9 @@ RetroArch 1.9.x will not work, as it is older than 1.10.3. 1. Enter the RetroArch main menu screen. -2. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and install "Sega - MS/GG (SMS Plus GX)". +2. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and install one of these cores: + - "Sega - MS/GG (SMS Plus GX)" + - "Sega - MS/GG/MD/CD (Genesis Plus GX) 3. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. 4. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default Network Command Port at 55355. @@ -47,6 +49,15 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) The [player settings page](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from them. +### Advanced settings + +The [advanced settings page](/tutorial/Archipelago/advanced_settings/en) describes more options you can put in your configuration file. + - A recommended setting for Zillion is: +``` + early_items: + Scope: 1 +``` + ### Verifying your config file If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck). @@ -63,7 +74,7 @@ If you would like to validate your config file to make sure it works, you may do - Windows - Double-click on your patch file. The Zillion Client will launch automatically, and create your ROM in the location of the patch file. -6. Open the ROM in RetroArch using the core "SMS Plus GX". +6. Open the ROM in RetroArch using the core "SMS Plus GX" or "Genesis Plus GX". - For a single player game, any emulator (or a Sega Master System) can be used, but there are additional features with RetroArch and the Zillion Client. - If you press reset or restore a save state and return to the surface in the game, the Zillion Client will keep open all the doors that you have opened. @@ -80,7 +91,7 @@ If you would like to validate your config file to make sure it works, you may do - This should automatically launch the client, and will also create your ROM in the same place as your patch file. 3. Connect to the client. - Use RetroArch to open the ROM that was generated. - - Be sure to select the **SMS Plus GX** core. This core will allow external tools to read RAM data. + - Be sure to select the **SMS Plus GX** core or the **Genesis Plus GX** core. These cores will allow external tools to read RAM data. 4. Connect to the Archipelago Server. - The patch file which launched your client should have automatically connected you to the AP Server. There are a few reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press enter. - The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 4e7d3b6a70..d24b5fd582 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -26,11 +26,6 @@ class ZillionContinues(SpecialRange): } -class ZillionEarlyScope(Toggle): - """ whether to make sure there is a scope available early """ - display_name = "early scope" - - class ZillionFloppyReq(Range): """ how many floppy disks are required """ range_start = 0 @@ -227,7 +222,6 @@ class ZillionRoomGen(Toggle): zillion_options: Dict[str, AssembleOptions] = { "continues": ZillionContinues, - # "early_scope": ZillionEarlyScope, # TODO: implement "floppy_req": ZillionFloppyReq, "gun_levels": ZillionGunLevels, "jump_levels": ZillionJumpLevels, @@ -371,7 +365,7 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": floppy_req.value, wo.continues[p].value, wo.randomize_alarms[p].value, - False, # wo.early_scope[p].value, + False, # early scope can be done with AP early_items True, # balance defense starting_cards.value, bool(room_gen.value) From 813ea6ef8b6d08ec7e89007997198b6bb9e3980c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 27 Oct 2022 19:43:02 -0400 Subject: [PATCH 102/105] [SM64] Separate Entrance Shuffle pools option and MIPS cost option improvement (#1137) * Add separate pool option for entrance shuffle and swap MIPS costs if MIPS1Cost is greater * Changes based on N00by's suggestions * split into secret_entrance_ids and course_entrance_ids --- worlds/sm64ex/Options.py | 5 +++-- worlds/sm64ex/Rules.py | 17 +++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index f29a65c58d..88d27bb3ea 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -46,7 +46,7 @@ class MIPS1Cost(Range): class MIPS2Cost(Range): - """How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost""" + """How many stars are required to spawn MIPS the second time.""" range_start = 0 range_end = 80 default = 50 @@ -72,7 +72,8 @@ class AreaRandomizer(Choice): display_name = "Entrance Randomizer" option_Off = 0 option_Courses_Only = 1 - option_Courses_and_Secrets = 2 + option_Courses_and_Secrets_Separate = 2 + option_Courses_and_Secrets = 3 class BuddyChecks(Toggle): diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 2462206246..2397f2c807 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -11,13 +11,14 @@ def fix_reg(entrance_ids, reg, invalidspot, swaplist, world): def set_rules(world, player: int, area_connections): destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions - if world.AreaRandomizer[player].value == 0: - entrance_ids = list(range(len(sm64paintings + sm64secrets))) - if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses - entrance_ids = list(range(len(sm64paintings))) - world.random.shuffle(entrance_ids) - entrance_ids = entrance_ids + list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets))) - if world.AreaRandomizer[player].value == 2: # Secret Regions as well + secret_entrance_ids = list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets))) + course_entrance_ids = list(range(len(sm64paintings))) + if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses + world.random.shuffle(course_entrance_ids) + if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well + world.random.shuffle(secret_entrance_ids) + entrance_ids = course_entrance_ids + secret_entrance_ids + if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool world.random.shuffle(entrance_ids) # Guarantee first entrance is a course swaplist = list(range(len(entrance_ids))) @@ -117,7 +118,7 @@ def set_rules(world, player: int, area_connections): 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)) if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value: - world.MIPS2Cost[player].value = world.MIPS1Cost[player].value + (world.MIPS2Cost[player].value, world.MIPS1Cost[player].value) = (world.MIPS1Cost[player].value, world.MIPS2Cost[player].value) add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value)) add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value)) From e6c6b00109c6aa6e31816bb50b9fb74e7f234a19 Mon Sep 17 00:00:00 2001 From: AkumaGath17 <98751776+AkumaGath17@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:34:46 +0200 Subject: [PATCH 103/105] Minecraft: Two by two logical requirement fix (#1152) * [Minecraft] Two by two logical requirement fix * Two by two update * Two by Two logical fix [Description in order] * Two by Two fix [Bucket only= False] Along with the others isolated items checks --- test/minecraft/TestAdvancements.py | 5 +++-- worlds/minecraft/Rules.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/minecraft/TestAdvancements.py b/test/minecraft/TestAdvancements.py index 6ddebcbfd2..f86d5a7333 100644 --- a/test/minecraft/TestAdvancements.py +++ b/test/minecraft/TestAdvancements.py @@ -312,9 +312,10 @@ class TestAdvancements(TestMinecraft): ["Two by Two", False, [], ['Flint and Steel']], ["Two by Two", False, [], ['Progressive Tools']], ["Two by Two", False, [], ['Progressive Weapons']], - ["Two by Two", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Two by Two", False, [], ['Bucket']], + ["Two by Two", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], + ["Two by Two", False, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], ["Two by Two", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons']], - ["Two by Two", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], ]) def test_42023(self): diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index ecdb459769..06576d7012 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -173,7 +173,7 @@ def set_advancement_rules(world: MultiWorld, player: int): state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots + set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; buckets of tropical fish > axolotls; nether > striders; gold carrots > horses skips ingots # set_rule(world.get_location("Stone Age", player), lambda state: True) set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state._mc_craft_crossbow(player) and state._mc_can_enchant(player)) # set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True) From 80db8a33af3d5b1556a2caf278d2c2ca64f9a6a0 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 28 Oct 2022 02:45:18 -0700 Subject: [PATCH 104/105] Don't leak info about what exists or not if player can't afford the hint (#1146) --- MultiServer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index bab762c84b..1a672afaa6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1332,6 +1332,8 @@ class ClientMessageProcessor(CommonCommandProcessor): def get_hints(self, input_text: str, for_location: bool = False) -> bool: points_available = get_client_points(self.ctx, self.client) + cost = self.ctx.get_hint_cost(self.client.slot) + if not input_text: hints = {hint.re_check(self.ctx, self.client.team) for hint in self.ctx.hints[self.client.team, self.client.slot]} @@ -1386,7 +1388,6 @@ class ClientMessageProcessor(CommonCommandProcessor): 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: @@ -1436,7 +1437,12 @@ class ClientMessageProcessor(CommonCommandProcessor): return True else: - self.output("Nothing found. Item/Location may not exist.") + if points_available >= cost: + self.output("Nothing found. Item/Location may not exist.") + 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)}.") return False @mark_raw From 5ca0249db9524be3ddc778b52acd5eb1360893e4 Mon Sep 17 00:00:00 2001 From: Marechal-l Date: Fri, 28 Oct 2022 19:16:00 +0200 Subject: [PATCH 105/105] DS3: Changed i in loop --- worlds/dark_souls_3/Items.py | 4 +--- worlds/dark_souls_3/Locations.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index bb13dc7514..e7ba2ecf00 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -11,9 +11,7 @@ class DarkSouls3Item(Item): table_offset = 100 output = {} - i = 0 - for table in item_tables: + for i, table in enumerate(item_tables): output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) - i += 1 return output diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index d7196f8295..0ba84e365b 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -11,9 +11,7 @@ class DarkSouls3Location(Location): table_offset = 100 output = {} - i = 0 - for table in location_tables: + for i, table in enumerate(location_tables): output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) - i += 1 return output