From 57b2ecf14cc21fec493bc15b9e993c5b5996d039 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 14 Apr 2024 11:10:37 -0400 Subject: [PATCH 01/70] Jak 1: Initial commit: Cell Locations, Items, and Regions modeled. --- worlds/jakanddaxter/Items.py | 25 ++++ worlds/jakanddaxter/Locations.py | 43 ++++++ worlds/jakanddaxter/Options.py | 12 ++ worlds/jakanddaxter/Regions.py | 33 +++++ worlds/jakanddaxter/Rules.py | 0 worlds/jakanddaxter/__init__.py | 0 worlds/jakanddaxter/locs/CellLocations.py | 164 +++++++++++++++++++++ worlds/jakanddaxter/locs/ScoutLocations.py | 63 ++++++++ 8 files changed, 340 insertions(+) create mode 100644 worlds/jakanddaxter/Items.py create mode 100644 worlds/jakanddaxter/Locations.py create mode 100644 worlds/jakanddaxter/Options.py create mode 100644 worlds/jakanddaxter/Regions.py create mode 100644 worlds/jakanddaxter/Rules.py create mode 100644 worlds/jakanddaxter/__init__.py create mode 100644 worlds/jakanddaxter/locs/CellLocations.py create mode 100644 worlds/jakanddaxter/locs/ScoutLocations.py diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py new file mode 100644 index 000000000000..8dd7fa5430cb --- /dev/null +++ b/worlds/jakanddaxter/Items.py @@ -0,0 +1,25 @@ +import typing +from BaseClasses import Item + +class JakAndDaxterItem(Item): + game: str = "Jak and Daxter: The Precursor Legacy" + +# Items Found Multiple Times +generic_item_table = { + "Power Cell": 1000, + "Scout Fly": 2000, + "Precursor Orb": 3000 +} + +# Items Only Found Once +special_item_table = { + "Fisherman's Boat": 0, + "Sculptor's Muse": 1, + "Flut Flut": 2, + "Blue Eco Switch": 3, + "Gladiator's Pontoons": 4, + "Yellow Eco Switch": 5 +} + +# All Items +item_table = {**generic_item_table, **special_item_table} diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py new file mode 100644 index 000000000000..2834efc505f3 --- /dev/null +++ b/worlds/jakanddaxter/Locations.py @@ -0,0 +1,43 @@ +import typing +import locs.CellLocations +import locs.ScoutLocations +from BaseClasses import Location + +class JakAndDaxterLocation(Location): + game: str = "Jak and Daxter: The Precursor Legacy" + +# All Locations +location_cellTable = { + **locGR_cellTable, \ + **locSV_cellTable, \ + **locFJ_cellTable, \ + **locSB_cellTable, \ + **locMI_cellTable, \ + **locFC_cellTable, \ + **locRV_cellTable, \ + **locPB_cellTable, \ + **locLPC_cellTable, \ + **locBS_cellTable, \ + **locMP_cellTable, \ + **locVC_cellTable, \ + **locSC_cellTable, \ + **locSM_cellTable, \ + **locLT_cellTable, \ + **locGMC_cellTable, \ + **locGR_scoutTable, \ + **locSV_scoutTable, \ + **locFJ_scoutTable, \ + **locSB_scoutTable, \ + **locMI_scoutTable, \ + **locFC_scoutTable, \ + **locRV_scoutTable, \ + **locPB_scoutTable, \ + **locLPC_scoutTable, \ + **locBS_scoutTable, \ + **locMP_scoutTable, \ + **locVC_scoutTable, \ + **locSC_scoutTable, \ + **locSM_scoutTable, \ + **locLT_scoutTable, \ + **locGMC_scoutTable +} diff --git a/worlds/jakanddaxter/Options.py b/worlds/jakanddaxter/Options.py new file mode 100644 index 000000000000..9895b0a6bc33 --- /dev/null +++ b/worlds/jakanddaxter/Options.py @@ -0,0 +1,12 @@ +import typing +from dataclasses import dataclass +from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet +from .Items import action_item_table + +class EnableScoutFlies(Toggle): + """Enable to include each Scout Fly as a check. Adds 213 checks to the pool.""" + display_name = "Enable Scout Flies" + +# class EnablePrecursorOrbs(Toggle): +# """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool.""" +# display_name = "Enable Precursor Orbs" \ No newline at end of file diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py new file mode 100644 index 000000000000..63f12f0098a5 --- /dev/null +++ b/worlds/jakanddaxter/Regions.py @@ -0,0 +1,33 @@ +import typing +from BaseClasses import Region, Location +from .Locations import JakAndDaxterLocation, location_table + +class JakAndDaxterLevel(int, Enum): + GEYSER_ROCK = 0 + SANDOVER_VILLAGE = 1 + FORBIDDEN_JUNGLE = 2 + SENTINEL_BEACH = 3 + MISTY_ISLAND = 4 + FIRE_CANYON = 5 + ROCK_VILLAGE = 6 + PRECURSOR_BASIN = 7 + LOST_PRECURSOR_CITY = 8 + BOGGY_SWAMP = 9 + MOUNTAIN_PASS = 10 + VOLCANIC_CRATER = 11 + SPIDER_CAVE = 12 + SNOWY_MOUNTAIN = 13 + LAVA_TUBE = 14 + GOL_AND_MAIAS_CITADEL = 15 + +class JakAndDaxterSubLevel(int, Enum): + MAIN = 0 + FORBIDDEN_JUNGLE_PLANT_ROOM = 1 + SENTINEL_BEACH_CANNON_TOWER = 2 + BOGGY_SWAMP_FLUT_FLUT = 3 + MOUNTAIN_PASS_SHORTCUT = 4 + SNOWY_MOUNTAIN_FLUT_FLUT = 5 + SNOWY_MOUNTAIN_LURKER_FORT = 6 + +class JakAndDaxterRegion(Region): + game: str = "Jak and Daxter: The Precursor Legacy" diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py new file mode 100644 index 000000000000..cc5e9da6ce8a --- /dev/null +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -0,0 +1,164 @@ +# Geyser Rock +locGR_cellTable = { + "GR: Find The Cell On The Path": 0, + "GR: Open The Precursor Door": 1, + "GR: Climb Up The Cliff": 2, + "GR: Free 7 Scout Flies": 3 +} + +# Sandover Village +locSV_cellTable = { + "SV: Bring 90 Orbs To The Mayor": 4, + "SV: Bring 90 Orbs to Your Uncle": 5, + "SV: Herd The Yakows Into The Pen": 6, + "SV: Bring 120 Orbs To The Oracle (1)": 7, + "SV: Bring 120 Orbs To The Oracle (2)": 8, + "SV: Free 7 Scout Flies": 9 +} + +# Forbidden Jungle +locFJ_cellTable = { + "FJ: Connect The Eco Beams": 10, + "FJ: Get To The Top Of The Temple": 11, + "FJ: Find The Blue Vent Switch": 12, + "FJ: Defeat The Dark Eco Plant": 13, + "FJ: Catch 200 Pounds Of Fish": 14, + "FJ: Follow The Canyon To The Sea": 15, + "FJ: Open The Locked Temple Door": 16, + "FJ: Free 7 Scout Flies": 17 +} + +# Sentinel Beach +locSB_cellTable = { + "SB: Unblock The Eco Harvesters": 18, + "SB: Push The Flut Flut Egg Off The Cliff": 19, + "SB: Get The Power Cell From The Pelican": 20, + "SB: Chase The Seagulls": 21, + "SB: Launch Up To The Cannon Tower": 22, + "SB: Explore The Beach": 23, + "SB: Climb The Sentinel": 24, + "SB: Free 7 Scout Flies": 25 +} + +# Misty Island +locMI_cellTable = { + "MI: Catch The Sculptor's Muse": 26, + "MI: Climb The Lurker Ship": 27, + "MI: Stop The Cannon": 28, + "MI: Return To The Dark Eco Pool": 29, + "MI: Destroy the Balloon Lurkers": 30, + "MI: Use Zoomer To Reach Power Cell": 31, + "MI: Use Blue Eco To Reach Power Cell": 32, + "MI: Free 7 Scout Flies": 33 +} + +# Fire Canyon +locFC_cellTable = { + "FC: Reach The End Of Fire Canyon": 34, + "FC: Free 7 Scout Flies": 35 +} + +# Rock Village +locRV_cellTable = { + "RV: Bring 90 Orbs To The Gambler": 36, + "RV: Bring 90 Orbs To The Geologist": 37, + "RV: Bring 90 Orbs To The Warrior": 38, + "RV: Bring 120 Orbs To The Oracle (1)": 39, + "RV: Bring 120 Orbs To The Oracle (2)": 40, + "RV: Free 7 Scout Flies": 41 +} + +# Precursor Basin +locPB_cellTable = { + "PB: Herd The Moles Into Their Hole": 42, + "PB: Catch The Flying Lurkers": 43, + "PB: Beat Record Time On The Gorge": 44, + "PB: Get The Power Cell Over The Lake": 45, + "PB: Cure Dark Eco Infected Plants": 46, + "PB: Navigate The Purple Precursor Rings": 47, + "PB: Navigate The Blue Precursor Rings": 48, + "PB: Free 7 Scout Flies": 49 +} + +# Lost Precursor City +locLPC_cellTable = { + "LPC: Raise The Chamber": 50, + "LPC: Follow The Colored Pipes": 51, + "LPC: Reach The Bottom Of The City": 52, + "LPC: Quickly Cross The Dangerous Pool": 53, + "LPC: Match The Platform Colors": 54, + "LPC: Climb The Slide Tube": 55, + "LPC: Reach The Center Of The Complex": 56, + "LPC: Free 7 Scout Flies": 57 +} + +# Boggy Swamp +locBS_cellTable = { + "BS: Ride The Flut Flut": 58, + "BS: Protect Farthy's Snacks": 59, + "BS: Defeat The Lurker Ambush": 60, + "BS: Break The Tethers To The Zeppelin (1)": 61, + "BS: Break The Tethers To The Zeppelin (2)": 62, + "BS: Break The Tethers To The Zeppelin (3)": 63, + "BS: Break The Tethers To The Zeppelin (4)": 64, + "BS: Free 7 Scout Flies": 65 +} + +# Mountain Pass +locMP_cellTable = { + "MP: Defeat Klaww": 66, + "MP: Reach The End Of The Mountain Pass": 67, + "MP: Find The Hidden Power Cell": 68, + "MP: Free 7 Scout Flies": 69 +} + +# Volcanic Crater +locVC_cellTable = { + "VC: Bring 90 Orbs To The Miners (1)": 70, + "VC: Bring 90 Orbs To The Miners (2)": 71, + "VC: Bring 90 Orbs To The Miners (3)": 72, + "VC: Bring 90 Orbs To The Miners (4)": 73, + "VC: Bring 120 Orbs To The Oracle (1)": 74, + "VC: Bring 120 Orbs To The Oracle (2)": 75, + "VC: Find The Hidden Power Cell": 76, + "VC: Free 7 Scout Flies": 77 +} + +# Spider Cave +locSC_cellTable = { + "SC: Use Your Goggles To Shoot The Gnawing Lurkers": 78, + "SC: Destroy The Dark Eco Crystals": 79, + "SC: Explore The Dark Cave": 80, + "SC: Climb The Giant Robot": 81, + "SC: Launch To The Poles": 82, + "SC: Navigate The Spider Tunnel": 83, + "SC: Climb the Precursor Platforms": 84, + "SC: Free 7 Scout Flies": 85 +} + +# Snowy Mountain +locSM_cellTable = { + "SM: Find The Yellow Vent Switch": 86, + "SM: Stop The 3 Lurker Glacier Troops": 87, + "SM: Deactivate The Precursor Blockers": 88, + "SM: Open The Frozen Crate": 89, + "SM: Get Through The Lurker Fort": 90, + "SM: Open The Lurker Fort Gate": 91, + "SM: Survive The Lurker Infested Cave": 92, + "SM: Free 7 Scout Flies": 93 +} + +# Lava Tube +locLT_cellTable = { + "LT: Cross The Lava Tube": 94, + "LT: Free 7 Scout Flies": 95 +} + +# Gol and Maias Citadel +locGMC_cellTable = { + "GMC: Free The Blue Sage": 96, + "GMC: Free The Red Sage": 97, + "GMC: Free The Yellow Sage": 98, + "GMC: Free The Green Sage": 99, + "GMC: Free 7 Scout Flies ": 100 +} diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py new file mode 100644 index 000000000000..227380a44382 --- /dev/null +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -0,0 +1,63 @@ +# Geyser Rock +locGR_scoutTable = { +} + +# Sandover Village +locSV_scoutTable = { +} + +# Forbidden Jungle +locFJ_scoutTable = { +} + +# Sentinel Beach +locSB_scoutTable = { +} + +# Misty Island +locMI_scoutTable = { +} + +# Fire Canyon +locFC_scoutTable = { +} + +# Rock Village +locRV_scoutTable = { +} + +# Precursor Basin +locPB_scoutTable = { +} + +# Lost Precursor City +locLPC_scoutTable = { +} + +# Boggy Swamp +locBS_scoutTable = { +} + +# Mountain Pass +locMP_scoutTable = { +} + +# Volcanic Crater +locVC_scoutTable = { +} + +# Spider Cave +locSC_scoutTable = { +} + +# Snowy Mountain +locSM_scoutTable = { +} + +# Lava Tube +locLT_scoutTable = { +} + +# Gol and Maias Citadel +locGMC_scoutTable = { +} From bff2d4a80bf23a1b2bcf29fa0119284f02a3c1f4 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 14 Apr 2024 22:27:06 -0400 Subject: [PATCH 02/70] Jak 1: Wrote Regions, Rules, init. Untested. --- worlds/jakanddaxter/Items.py | 19 +- worlds/jakanddaxter/Locations.py | 23 +-- worlds/jakanddaxter/Options.py | 7 +- worlds/jakanddaxter/Regions.py | 128 ++++++++++++- worlds/jakanddaxter/Rules.py | 112 +++++++++++ worlds/jakanddaxter/__init__.py | 43 +++++ worlds/jakanddaxter/locs/CellLocations.py | 204 +++++++++++---------- worlds/jakanddaxter/locs/OrbLocations.py | 65 +++++++ worlds/jakanddaxter/locs/ScoutLocations.py | 2 + 9 files changed, 470 insertions(+), 133 deletions(-) create mode 100644 worlds/jakanddaxter/locs/OrbLocations.py diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 8dd7fa5430cb..d1bac68258dc 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -6,19 +6,20 @@ class JakAndDaxterItem(Item): # Items Found Multiple Times generic_item_table = { - "Power Cell": 1000, - "Scout Fly": 2000, - "Precursor Orb": 3000 + 0: "Power Cell", + 101: "Scout Fly", + 213: "Precursor Orb" } # Items Only Found Once special_item_table = { - "Fisherman's Boat": 0, - "Sculptor's Muse": 1, - "Flut Flut": 2, - "Blue Eco Switch": 3, - "Gladiator's Pontoons": 4, - "Yellow Eco Switch": 5 + 2213: "Fisherman's Boat", + 2214: "Sculptor's Muse", + 2215: "Flut Flut", + 2216: "Blue Eco Switch", + 2217: "Gladiator's Pontoons", + 2218: "Yellow Eco Switch", + 2219: "Lurker Fort Gate" } # All Items diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index 2834efc505f3..df6ac400163d 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,13 +1,12 @@ import typing -import locs.CellLocations -import locs.ScoutLocations from BaseClasses import Location +import locs.CellLocations class JakAndDaxterLocation(Location): game: str = "Jak and Daxter: The Precursor Legacy" # All Locations -location_cellTable = { +location_table = { **locGR_cellTable, \ **locSV_cellTable, \ **locFJ_cellTable, \ @@ -23,21 +22,5 @@ class JakAndDaxterLocation(Location): **locSC_cellTable, \ **locSM_cellTable, \ **locLT_cellTable, \ - **locGMC_cellTable, \ - **locGR_scoutTable, \ - **locSV_scoutTable, \ - **locFJ_scoutTable, \ - **locSB_scoutTable, \ - **locMI_scoutTable, \ - **locFC_scoutTable, \ - **locRV_scoutTable, \ - **locPB_scoutTable, \ - **locLPC_scoutTable, \ - **locBS_scoutTable, \ - **locMP_scoutTable, \ - **locVC_scoutTable, \ - **locSC_scoutTable, \ - **locSM_scoutTable, \ - **locLT_scoutTable, \ - **locGMC_scoutTable + **locGMC_cellTable } diff --git a/worlds/jakanddaxter/Options.py b/worlds/jakanddaxter/Options.py index 9895b0a6bc33..1751916f94e4 100644 --- a/worlds/jakanddaxter/Options.py +++ b/worlds/jakanddaxter/Options.py @@ -9,4 +9,9 @@ class EnableScoutFlies(Toggle): # class EnablePrecursorOrbs(Toggle): # """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool.""" -# display_name = "Enable Precursor Orbs" \ No newline at end of file +# display_name = "Enable Precursor Orbs" + +@dataclass +class JakAndDaxterOptions(PerGameCommonOptions): + enable_scout_flies: EnableScoutFlies + # enable_precursor_orbs: EnablePrecursorOrbs diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 63f12f0098a5..578ef13c9370 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,6 +1,8 @@ import typing -from BaseClasses import Region, Location +from BaseClasses import MultiWorld, Region, Entrance, Location +from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table +import locs.CellLocations class JakAndDaxterLevel(int, Enum): GEYSER_ROCK = 0 @@ -21,13 +23,135 @@ class JakAndDaxterLevel(int, Enum): GOL_AND_MAIAS_CITADEL = 15 class JakAndDaxterSubLevel(int, Enum): - MAIN = 0 + MAIN_AREA = 0 FORBIDDEN_JUNGLE_PLANT_ROOM = 1 SENTINEL_BEACH_CANNON_TOWER = 2 BOGGY_SWAMP_FLUT_FLUT = 3 MOUNTAIN_PASS_SHORTCUT = 4 SNOWY_MOUNTAIN_FLUT_FLUT = 5 SNOWY_MOUNTAIN_LURKER_FORT = 6 + GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = 7 + +level_table: typing.Dict[JakAndDaxterLevel, str] = { + JakAndDaxterLevel.GEYSER_ROCK: "Geyser Rock", + JakAndDaxterLevel.SANDOVER_VILLAGE: "Sandover Village", + JakAndDaxterLevel.FORBIDDEN_JUNGLE: "Forbidden Jungle", + JakAndDaxterLevel.SENTINEL_BEACH: "Sentinel Beach", + JakAndDaxterLevel.MISTY_ISLAND: "Misty Island", + JakAndDaxterLevel.FIRE_CANYON: "Fire Canyon", + JakAndDaxterLevel.ROCK_VILLAGE: "Rock Village", + JakAndDaxterLevel.PRECURSOR_BASIN: "Precursor Basin", + JakAndDaxterLevel.LOST_PRECURSOR_CITY: "Lost Precursor City", + JakAndDaxterLevel.BOGGY_SWAMP: "Boggy Swamp", + JakAndDaxterLevel.MOUNTAIN_PASS: "Mountain Pass", + JakAndDaxterLevel.VOLCANIC_CRATER: "Volcanic Crater", + JakAndDaxterLevel.SPIDER_CAVE: "Spider Cave", + JakAndDaxterLevel.SNOWY_MOUNTAIN: "Snowy Mountain", + JakAndDaxterLevel.LAVA_TUBE: "Lava Tube", + JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL: "Gol and Maia's Citadel" +} + +subLevel_table: typing.Dict[JakAndDaxterSubLevel, str] = { + JakAndDaxterSubLevel.MAIN_AREA: "Main Area", + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room", + JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower", + JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut", + JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut", + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort", + JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower" +} class JakAndDaxterRegion(Region): game: str = "Jak and Daxter: The Precursor Legacy" + +def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + regionMenu = Region("Menu", player, multiworld) + + regionGR = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) + create_locations(regionGR, locGR_cellTable) + + regionSV = create_region(player, multiworld, level_table[JakAndDaxterLevel.SANDOVER_VILLAGE]) + create_locations(regionSV, locSV_cellTable) + + regionFJ = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) + create_locations(regionFJ, {k: locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}) + + subRegionFJPR = create_subregion(regionFJ, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) + create_locations(subRegionFJPR, {k: locFJ_cellTable[k] for k in {13}}) + + regionSB = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) + create_locations(regionSB, {k: locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}) + + subRegionSBCT = create_subregion(regionSB, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) + create_locations(subRegionSBCT, {k: locSB_cellTable[k] for k in {22}}) + + regionMI = create_region(player, multiworld, level_table[JakAndDaxterLevel.MISTY_ISLAND]) + create_locations(regionMI, locMI_cellTable) + + regionFC = create_region(player, multiworld, level_table[JakAndDaxterLevel.FIRE_CANYON]) + create_locations(regionFC, locFC_cellTable) + + regionRV = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) + create_locations(regionRV, locRV_cellTable) + + regionPB = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) + create_locations(regionPB, locPB_cellTable) + + regionLPC = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) + create_locations(regionLPC, locPB_cellTable) + + regionBS = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) + create_locations(regionBS, {k: locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}) + + subRegionBSFF = create_subregion(regionBS, subLevel_table[JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT]) + create_locations(subRegionBSFF, {k: locBS_cellTable[k] for k in {58, 65}}) + + regionMP = create_region(player, multiworld, level_table[JakAndDaxterLevel.MOUNTAIN_PASS]) + create_locations(regionMP, {k: locMP_cellTable[k] for k in {66, 67, 69}}) + + subRegionMPS = create_subregion(regionMP, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) + create_locations(subRegionMPS, {k: locMP_cellTable[k] for k in {68}}) + + regionVC = create_region(player, multiworld, level_table[JakAndDaxterLevel.VOLCANIC_CRATER]) + create_locations(regionVC, locVC_cellTable) + + regionSC = create_region(player, multiworld, level_table[JakAndDaxterLevel.SPIDER_CAVE]) + create_locations(regionSC, locSC_cellTable) + + regionSM = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) + create_locations(regionSM, {k: locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}) + + subRegionSMFF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) + create_locations(subRegionSMFF, {k: locSM_cellTable[k] for k in {90}}) + + subRegionSMLF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) + create_locations(subRegionSMLF, {k: locSM_cellTable[k] for k in {91, 93}}) + + regionLT = create_region(player, multiworld, level_table[JakAndDaxterLevel.LAVA_TUBE]) + create_locations(regionLT, locLT_cellTable) + + regionGMC = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) + create_locations(regionGMC, {k: locGMC_cellTable[k] for k in {96, 97, 98}}) + + subRegionGMCRT = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) + create_locations(subRegionGMCRT, {k: locGMC_cellTable[k] for k in {99, 100}}) + +def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: + region = JakAndDaxterRegion(name, player, multiworld) + + multiworld.regions.append(region) + return region + +def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: + region = JakAndDaxterRegion(name, parent.player, parent.multiworld) + + connection = Entrance(parent.player, name, parent) + connection.connect(region) + parent.entrances.append(connection) + + parent.multiworld.regions.append(region) + return region + +def create_locations(region: Region, locs: typing.Dict[int, str]): + region.locations += [JakAndDaxterLocation(region.player, loc, location_table[loc], region) for loc in locs] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index e69de29bb2d1..68767ed17b3f 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -0,0 +1,112 @@ +import typing +from BaseClasses import MultiWorld +from .Options import JakAndDaxterOptions +from .Locations import JakAndDaxterLocation, location_table +from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table +from .Items import item_table + +def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + multiworld.get_region("Menu").connect(level_table[JakAndDaxterLevel.GEYSER_ROCK]) + + connect_regions(multiworld, player, + JakAndDaxterLevel.GEYSER_ROCK, + JakAndDaxterLevel.SANDOVER_VILLAGE, + lambda state: state.has(item_table[0], player, 4)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.FORBIDDEN_JUNGLE) + + assign_subregion_access_rule(multiworld, player, + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + lambda state: state.has(item_table[2216], player)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.SENTINEL_BEACH) + + assign_subregion_access_rule(multiworld, player, + JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, + lambda state: state.has(item_table[2216], player)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.MISTY_ISLAND, + lambda state: state.has(item_table[2213], player)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.FIRE_CANYON, + lambda state: state.has(item_table[0], player, 20)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.FIRE_CANYON, + JakAndDaxterLevel.ROCK_VILLAGE) + + connect_regions(multiworld, player, + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.PRECURSOR_BASIN) + + connect_regions(multiworld, player, + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.LOST_PRECURSOR_CITY) + + connect_regions(multiworld, player, + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.BOGGY_SWAMP, + lambda state: state.has(item_table[2217], player)) + + assign_subregion_access_rule(multiworld, player, + JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, + lambda state: state.has(item_table[2215], player)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.MOUNTAIN_PASS, + lambda state: state.has(item_table[2217], player) && state.has(item_table[0], player, 45)) + + assign_subregion_access_rule(multiworld, player, + JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, + lambda state: state.has(item_table[2218], player)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.MOUNTAIN_PASS, + JakAndDaxterLevel.VOLCANIC_CRATER) + + connect_regions(multiworld, player, + JakAndDaxterLevel.VOLCANIC_CRATER, + JakAndDaxterLevel.SPIDER_CAVE) + + connect_regions(multiworld, player, + JakAndDaxterLevel.VOLCANIC_CRATER, + JakAndDaxterLevel.SNOWY_MOUNTAIN) + + assign_subregion_access_rule(multiworld, player, + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, + lambda state: state.has(item_table[2215], player)) + + assign_subregion_access_rule(multiworld, player, + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, + lambda state: state.has(item_table[2219], player)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.VOLCANIC_CRATER, + JakAndDaxterLevel.LAVA_TUBE, + lambda state: state.has(item_table[0], player, 72)) + + connect_regions(multiworld, player, + JakAndDaxterLevel.LAVA_TUBE, + JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL) + + assign_subregion_access_rule(multiworld, player, + JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, + lambda state: state.has(item_table[96], player) && state.has(item_table[97], player) && state.has(item_table[98], player)) + +def connect_regions(multiworld: MultiWorld, player: int, source: int, target: int, rule = None): + sourceRegion = multiworld.get_region(level_table[source], player) + targetRegion = multiworld.get_region(level_table[target], player) + sourceRegion.connect(targetRegion, rule = rule) + +def assign_subregion_access_rule(multiworld: MultiWorld, player: int, target: int, rule = None): + targetEntrance = multiworld.get_entrance(multiworld, player, subLevel_table[target]) + targetEntrance.access_rule = rule diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index e69de29bb2d1..6ba7598d103b 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -0,0 +1,43 @@ +import typing, os, json +from BaseClasses import Item, Region, Entrance, Location +from .Locations import JakAndDaxterLocation, location_table +from .Options import JakAndDaxterOptions +from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, create_regions +from .Rules import set_rules +from .Items import JakAndDaxterItem, item_table, generic_item_table, special_item_table + +class JakAndDaxterWorld(World): + game: str = "Jak and Daxter: The Precursor Legacy" + game_id = 74680000 # All IDs will be offset by this number. + + # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. + item_name_to_id = {item_table[k], game_id + k for k in item_table} + location_name_to_id = {location_table[k], game_id + k for k in location_table} + + options_dataclass = JakAndDaxterOptions + + def create_regions(self): + create_regions(self.multiworld, self.options, self.player) + + def set_rules(self): + set_rules(self.multiworld, self.options, self.player) + + def create_item(self, name: str) -> Item: + item_id = item_name_to_id[name] + match name: + case "Power Cell": + classification = ItemClassification.progression_skip_balancing + case "Scout Fly": + classification = ItemClassification.progression_skip_balancing + case "Precursor Orb": + classification = ItemClassification.filler # TODO + case _: + classification = ItemClassification.progression + + item = JakAndDaxterItem(name, classification, item_id, player) + return item + + def create_items(self): + self.multiworld.itempool += [self.create_item(item_table[0]) for k in range(0, 100)] + self.multiworld.itempool += [self.create_item(item_table[101]) for k in range(101, 212)] + self.multiworld.itempool += [self.create_item(item_table[k]) for k in special_item_table] diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py index cc5e9da6ce8a..4f674bff6192 100644 --- a/worlds/jakanddaxter/locs/CellLocations.py +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -1,164 +1,166 @@ +# Power Cells start at ID 0 and end at ID 100. + # Geyser Rock locGR_cellTable = { - "GR: Find The Cell On The Path": 0, - "GR: Open The Precursor Door": 1, - "GR: Climb Up The Cliff": 2, - "GR: Free 7 Scout Flies": 3 + 0: "GR: Find The Cell On The Path", + 1: "GR: Open The Precursor Door", + 2: "GR: Climb Up The Cliff", + 3: "GR: Free 7 Scout Flies" } # Sandover Village locSV_cellTable = { - "SV: Bring 90 Orbs To The Mayor": 4, - "SV: Bring 90 Orbs to Your Uncle": 5, - "SV: Herd The Yakows Into The Pen": 6, - "SV: Bring 120 Orbs To The Oracle (1)": 7, - "SV: Bring 120 Orbs To The Oracle (2)": 8, - "SV: Free 7 Scout Flies": 9 + 4: "SV: Bring 90 Orbs To The Mayor", + 5: "SV: Bring 90 Orbs to Your Uncle", + 6: "SV: Herd The Yakows Into The Pen", + 7: "SV: Bring 120 Orbs To The Oracle (1)", + 8: "SV: Bring 120 Orbs To The Oracle (2)", + 9: "SV: Free 7 Scout Flies" } # Forbidden Jungle locFJ_cellTable = { - "FJ: Connect The Eco Beams": 10, - "FJ: Get To The Top Of The Temple": 11, - "FJ: Find The Blue Vent Switch": 12, - "FJ: Defeat The Dark Eco Plant": 13, - "FJ: Catch 200 Pounds Of Fish": 14, - "FJ: Follow The Canyon To The Sea": 15, - "FJ: Open The Locked Temple Door": 16, - "FJ: Free 7 Scout Flies": 17 + 10: "FJ: Connect The Eco Beams", + 11: "FJ: Get To The Top Of The Temple", + 12: "FJ: Find The Blue Vent Switch", + 13: "FJ: Defeat The Dark Eco Plant", + 14: "FJ: Catch 200 Pounds Of Fish", + 15: "FJ: Follow The Canyon To The Sea", + 16: "FJ: Open The Locked Temple Door", + 17: "FJ: Free 7 Scout Flies" } # Sentinel Beach locSB_cellTable = { - "SB: Unblock The Eco Harvesters": 18, - "SB: Push The Flut Flut Egg Off The Cliff": 19, - "SB: Get The Power Cell From The Pelican": 20, - "SB: Chase The Seagulls": 21, - "SB: Launch Up To The Cannon Tower": 22, - "SB: Explore The Beach": 23, - "SB: Climb The Sentinel": 24, - "SB: Free 7 Scout Flies": 25 + 18: "SB: Unblock The Eco Harvesters", + 19: "SB: Push The Flut Flut Egg Off The Cliff", + 20: "SB: Get The Power Cell From The Pelican", + 21: "SB: Chase The Seagulls", + 22: "SB: Launch Up To The Cannon Tower", + 23: "SB: Explore The Beach", + 24: "SB: Climb The Sentinel", + 25: "SB: Free 7 Scout Flies" } # Misty Island locMI_cellTable = { - "MI: Catch The Sculptor's Muse": 26, - "MI: Climb The Lurker Ship": 27, - "MI: Stop The Cannon": 28, - "MI: Return To The Dark Eco Pool": 29, - "MI: Destroy the Balloon Lurkers": 30, - "MI: Use Zoomer To Reach Power Cell": 31, - "MI: Use Blue Eco To Reach Power Cell": 32, - "MI: Free 7 Scout Flies": 33 + 26: "MI: Catch The Sculptor's Muse", + 27: "MI: Climb The Lurker Ship", + 28: "MI: Stop The Cannon", + 29: "MI: Return To The Dark Eco Pool", + 30: "MI: Destroy the Balloon Lurkers", + 31: "MI: Use Zoomer To Reach Power Cell", + 32: "MI: Use Blue Eco To Reach Power Cell", + 33: "MI: Free 7 Scout Flies" } # Fire Canyon locFC_cellTable = { - "FC: Reach The End Of Fire Canyon": 34, - "FC: Free 7 Scout Flies": 35 + 34: "FC: Reach The End Of Fire Canyon", + 35: "FC: Free 7 Scout Flies" } # Rock Village locRV_cellTable = { - "RV: Bring 90 Orbs To The Gambler": 36, - "RV: Bring 90 Orbs To The Geologist": 37, - "RV: Bring 90 Orbs To The Warrior": 38, - "RV: Bring 120 Orbs To The Oracle (1)": 39, - "RV: Bring 120 Orbs To The Oracle (2)": 40, - "RV: Free 7 Scout Flies": 41 + 36: "RV: Bring 90 Orbs To The Gambler", + 37: "RV: Bring 90 Orbs To The Geologist", + 38: "RV: Bring 90 Orbs To The Warrior", + 39: "RV: Bring 120 Orbs To The Oracle (1)", + 40: "RV: Bring 120 Orbs To The Oracle (2)", + 41: "RV: Free 7 Scout Flies" } # Precursor Basin locPB_cellTable = { - "PB: Herd The Moles Into Their Hole": 42, - "PB: Catch The Flying Lurkers": 43, - "PB: Beat Record Time On The Gorge": 44, - "PB: Get The Power Cell Over The Lake": 45, - "PB: Cure Dark Eco Infected Plants": 46, - "PB: Navigate The Purple Precursor Rings": 47, - "PB: Navigate The Blue Precursor Rings": 48, - "PB: Free 7 Scout Flies": 49 + 42: "PB: Herd The Moles Into Their Hole", + 43: "PB: Catch The Flying Lurkers", + 44: "PB: Beat Record Time On The Gorge", + 45: "PB: Get The Power Cell Over The Lake", + 46: "PB: Cure Dark Eco Infected Plants", + 47: "PB: Navigate The Purple Precursor Rings", + 48: "PB: Navigate The Blue Precursor Rings", + 49: "PB: Free 7 Scout Flies" } # Lost Precursor City locLPC_cellTable = { - "LPC: Raise The Chamber": 50, - "LPC: Follow The Colored Pipes": 51, - "LPC: Reach The Bottom Of The City": 52, - "LPC: Quickly Cross The Dangerous Pool": 53, - "LPC: Match The Platform Colors": 54, - "LPC: Climb The Slide Tube": 55, - "LPC: Reach The Center Of The Complex": 56, - "LPC: Free 7 Scout Flies": 57 + 50: "LPC: Raise The Chamber", + 51: "LPC: Follow The Colored Pipes", + 52: "LPC: Reach The Bottom Of The City", + 53: "LPC: Quickly Cross The Dangerous Pool", + 54: "LPC: Match The Platform Colors", + 55: "LPC: Climb The Slide Tube", + 56: "LPC: Reach The Center Of The Complex", + 57: "LPC: Free 7 Scout Flies" } # Boggy Swamp locBS_cellTable = { - "BS: Ride The Flut Flut": 58, - "BS: Protect Farthy's Snacks": 59, - "BS: Defeat The Lurker Ambush": 60, - "BS: Break The Tethers To The Zeppelin (1)": 61, - "BS: Break The Tethers To The Zeppelin (2)": 62, - "BS: Break The Tethers To The Zeppelin (3)": 63, - "BS: Break The Tethers To The Zeppelin (4)": 64, - "BS: Free 7 Scout Flies": 65 + 58: "BS: Ride The Flut Flut", + 59: "BS: Protect Farthy's Snacks", + 60: "BS: Defeat The Lurker Ambush", + 61: "BS: Break The Tethers To The Zeppelin (1)", + 62: "BS: Break The Tethers To The Zeppelin (2)", + 63: "BS: Break The Tethers To The Zeppelin (3)", + 64: "BS: Break The Tethers To The Zeppelin (4)", + 65: "BS: Free 7 Scout Flies" } # Mountain Pass locMP_cellTable = { - "MP: Defeat Klaww": 66, - "MP: Reach The End Of The Mountain Pass": 67, - "MP: Find The Hidden Power Cell": 68, - "MP: Free 7 Scout Flies": 69 + 66: "MP: Defeat Klaww", + 67: "MP: Reach The End Of The Mountain Pass", + 68: "MP: Find The Hidden Power Cell", + 69: "MP: Free 7 Scout Flies" } # Volcanic Crater locVC_cellTable = { - "VC: Bring 90 Orbs To The Miners (1)": 70, - "VC: Bring 90 Orbs To The Miners (2)": 71, - "VC: Bring 90 Orbs To The Miners (3)": 72, - "VC: Bring 90 Orbs To The Miners (4)": 73, - "VC: Bring 120 Orbs To The Oracle (1)": 74, - "VC: Bring 120 Orbs To The Oracle (2)": 75, - "VC: Find The Hidden Power Cell": 76, - "VC: Free 7 Scout Flies": 77 + 70: "VC: Bring 90 Orbs To The Miners (1)", + 71: "VC: Bring 90 Orbs To The Miners (2)", + 72: "VC: Bring 90 Orbs To The Miners (3)", + 73: "VC: Bring 90 Orbs To The Miners (4)", + 74: "VC: Bring 120 Orbs To The Oracle (1)", + 75: "VC: Bring 120 Orbs To The Oracle (2)", + 76: "VC: Find The Hidden Power Cell", + 77: "VC: Free 7 Scout Flies" } # Spider Cave locSC_cellTable = { - "SC: Use Your Goggles To Shoot The Gnawing Lurkers": 78, - "SC: Destroy The Dark Eco Crystals": 79, - "SC: Explore The Dark Cave": 80, - "SC: Climb The Giant Robot": 81, - "SC: Launch To The Poles": 82, - "SC: Navigate The Spider Tunnel": 83, - "SC: Climb the Precursor Platforms": 84, - "SC: Free 7 Scout Flies": 85 + 78: "SC: Use Your Goggles To Shoot The Gnawing Lurkers", + 79: "SC: Destroy The Dark Eco Crystals", + 80: "SC: Explore The Dark Cave", + 81: "SC: Climb The Giant Robot", + 82: "SC: Launch To The Poles", + 83: "SC: Navigate The Spider Tunnel", + 84: "SC: Climb the Precursor Platforms", + 85: "SC: Free 7 Scout Flies" } # Snowy Mountain locSM_cellTable = { - "SM: Find The Yellow Vent Switch": 86, - "SM: Stop The 3 Lurker Glacier Troops": 87, - "SM: Deactivate The Precursor Blockers": 88, - "SM: Open The Frozen Crate": 89, - "SM: Get Through The Lurker Fort": 90, - "SM: Open The Lurker Fort Gate": 91, - "SM: Survive The Lurker Infested Cave": 92, - "SM: Free 7 Scout Flies": 93 + 86: "SM: Find The Yellow Vent Switch", + 87: "SM: Stop The 3 Lurker Glacier Troops", + 88: "SM: Deactivate The Precursor Blockers", + 89: "SM: Open The Frozen Crate", + 90: "SM: Open The Lurker Fort Gate", + 91: "SM: Get Through The Lurker Fort", + 92: "SM: Survive The Lurker Infested Cave", + 93: "SM: Free 7 Scout Flies" } # Lava Tube locLT_cellTable = { - "LT: Cross The Lava Tube": 94, - "LT: Free 7 Scout Flies": 95 + 94: "LT: Cross The Lava Tube", + 95: "LT: Free 7 Scout Flies" } # Gol and Maias Citadel locGMC_cellTable = { - "GMC: Free The Blue Sage": 96, - "GMC: Free The Red Sage": 97, - "GMC: Free The Yellow Sage": 98, - "GMC: Free The Green Sage": 99, - "GMC: Free 7 Scout Flies ": 100 + 96: "GMC: Free The Blue Sage", + 97: "GMC: Free The Red Sage", + 98: "GMC: Free The Yellow Sage", + 99: "GMC: Free The Green Sage", + 100: "GMC: Free 7 Scout Flies" } diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py new file mode 100644 index 000000000000..83037790d4ff --- /dev/null +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -0,0 +1,65 @@ +# Precursor Orbs start at ID 213 and end at ID 2212. + +# Geyser Rock +locGR_orbTable = { +} + +# Sandover Village +locSV_orbTable = { +} + +# Forbidden Jungle +locFJ_orbTable = { +} + +# Sentinel Beach +locSB_orbTable = { +} + +# Misty Island +locMI_orbTable = { +} + +# Fire Canyon +locFC_orbTable = { +} + +# Rock Village +locRV_orbTable = { +} + +# Precursor Basin +locPB_orbTable = { +} + +# Lost Precursor City +locLPC_orbTable = { +} + +# Boggy Swamp +locBS_orbTable = { +} + +# Mountain Pass +locMP_orbTable = { +} + +# Volcanic Crater +locVC_orbTable = { +} + +# Spider Cave +locSC_orbTable = { +} + +# Snowy Mountain +locSM_orbTable = { +} + +# Lava Tube +locLT_orbTable = { +} + +# Gol and Maias Citadel +locGMC_orbTable = { +} diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index 227380a44382..fd1fe2f6dc1c 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -1,3 +1,5 @@ +# Scout Flies start at ID 101 and end at ID 212. + # Geyser Rock locGR_scoutTable = { } From ab4a5e9f754838b72fa802f5bce2cc012026f099 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 15 Apr 2024 00:36:17 -0400 Subject: [PATCH 03/70] Jak 1: Fixed mistakes, need better understanding of Entrances. --- worlds/jakanddaxter/Locations.py | 34 +++++++++---------- worlds/jakanddaxter/Options.py | 1 - worlds/jakanddaxter/Regions.py | 57 +++++++++++++++++--------------- worlds/jakanddaxter/Rules.py | 11 +++--- worlds/jakanddaxter/__init__.py | 34 ++++++++++--------- 5 files changed, 74 insertions(+), 63 deletions(-) diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index df6ac400163d..d74473951aa6 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,26 +1,26 @@ import typing from BaseClasses import Location -import locs.CellLocations +from .locs import CellLocations class JakAndDaxterLocation(Location): game: str = "Jak and Daxter: The Precursor Legacy" # All Locations location_table = { - **locGR_cellTable, \ - **locSV_cellTable, \ - **locFJ_cellTable, \ - **locSB_cellTable, \ - **locMI_cellTable, \ - **locFC_cellTable, \ - **locRV_cellTable, \ - **locPB_cellTable, \ - **locLPC_cellTable, \ - **locBS_cellTable, \ - **locMP_cellTable, \ - **locVC_cellTable, \ - **locSC_cellTable, \ - **locSM_cellTable, \ - **locLT_cellTable, \ - **locGMC_cellTable + **CellLocations.locGR_cellTable, \ + **CellLocations.locSV_cellTable, \ + **CellLocations.locFJ_cellTable, \ + **CellLocations.locSB_cellTable, \ + **CellLocations.locMI_cellTable, \ + **CellLocations.locFC_cellTable, \ + **CellLocations.locRV_cellTable, \ + **CellLocations.locPB_cellTable, \ + **CellLocations.locLPC_cellTable, \ + **CellLocations.locBS_cellTable, \ + **CellLocations.locMP_cellTable, \ + **CellLocations.locVC_cellTable, \ + **CellLocations.locSC_cellTable, \ + **CellLocations.locSM_cellTable, \ + **CellLocations.locLT_cellTable, \ + **CellLocations.locGMC_cellTable } diff --git a/worlds/jakanddaxter/Options.py b/worlds/jakanddaxter/Options.py index 1751916f94e4..b33e73be402a 100644 --- a/worlds/jakanddaxter/Options.py +++ b/worlds/jakanddaxter/Options.py @@ -1,7 +1,6 @@ import typing from dataclasses import dataclass from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet -from .Items import action_item_table class EnableScoutFlies(Toggle): """Enable to include each Scout Fly as a check. Adds 213 checks to the pool.""" diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 578ef13c9370..6f29e0817286 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,8 +1,9 @@ import typing +from enum import Enum from BaseClasses import MultiWorld, Region, Entrance, Location from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table -import locs.CellLocations +from .locs import CellLocations class JakAndDaxterLevel(int, Enum): GEYSER_ROCK = 0 @@ -66,76 +67,76 @@ class JakAndDaxterRegion(Region): game: str = "Jak and Daxter: The Precursor Legacy" def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - regionMenu = Region("Menu", player, multiworld) + regionMenu = create_region(player, multiworld, "Menu") regionGR = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) - create_locations(regionGR, locGR_cellTable) + create_locations(regionGR, CellLocations.locGR_cellTable) regionSV = create_region(player, multiworld, level_table[JakAndDaxterLevel.SANDOVER_VILLAGE]) - create_locations(regionSV, locSV_cellTable) + create_locations(regionSV, CellLocations.locSV_cellTable) regionFJ = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) - create_locations(regionFJ, {k: locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}) + create_locations(regionFJ, {k: CellLocations.locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}) subRegionFJPR = create_subregion(regionFJ, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) - create_locations(subRegionFJPR, {k: locFJ_cellTable[k] for k in {13}}) + create_locations(subRegionFJPR, {k: CellLocations.locFJ_cellTable[k] for k in {13}}) regionSB = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) - create_locations(regionSB, {k: locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}) + create_locations(regionSB, {k: CellLocations.locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}) subRegionSBCT = create_subregion(regionSB, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) - create_locations(subRegionSBCT, {k: locSB_cellTable[k] for k in {22}}) + create_locations(subRegionSBCT, {k: CellLocations.locSB_cellTable[k] for k in {22}}) regionMI = create_region(player, multiworld, level_table[JakAndDaxterLevel.MISTY_ISLAND]) - create_locations(regionMI, locMI_cellTable) + create_locations(regionMI, CellLocations.locMI_cellTable) regionFC = create_region(player, multiworld, level_table[JakAndDaxterLevel.FIRE_CANYON]) - create_locations(regionFC, locFC_cellTable) + create_locations(regionFC, CellLocations.locFC_cellTable) regionRV = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) - create_locations(regionRV, locRV_cellTable) + create_locations(regionRV, CellLocations.locRV_cellTable) regionPB = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) - create_locations(regionPB, locPB_cellTable) + create_locations(regionPB, CellLocations.locPB_cellTable) regionLPC = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) - create_locations(regionLPC, locPB_cellTable) + create_locations(regionLPC, CellLocations.locLPC_cellTable) regionBS = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) - create_locations(regionBS, {k: locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}) + create_locations(regionBS, {k: CellLocations.locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}) subRegionBSFF = create_subregion(regionBS, subLevel_table[JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT]) - create_locations(subRegionBSFF, {k: locBS_cellTable[k] for k in {58, 65}}) + create_locations(subRegionBSFF, {k: CellLocations.locBS_cellTable[k] for k in {58, 65}}) regionMP = create_region(player, multiworld, level_table[JakAndDaxterLevel.MOUNTAIN_PASS]) - create_locations(regionMP, {k: locMP_cellTable[k] for k in {66, 67, 69}}) + create_locations(regionMP, {k: CellLocations.locMP_cellTable[k] for k in {66, 67, 69}}) subRegionMPS = create_subregion(regionMP, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) - create_locations(subRegionMPS, {k: locMP_cellTable[k] for k in {68}}) + create_locations(subRegionMPS, {k: CellLocations.locMP_cellTable[k] for k in {68}}) regionVC = create_region(player, multiworld, level_table[JakAndDaxterLevel.VOLCANIC_CRATER]) - create_locations(regionVC, locVC_cellTable) + create_locations(regionVC, CellLocations.locVC_cellTable) regionSC = create_region(player, multiworld, level_table[JakAndDaxterLevel.SPIDER_CAVE]) - create_locations(regionSC, locSC_cellTable) + create_locations(regionSC, CellLocations.locSC_cellTable) regionSM = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) - create_locations(regionSM, {k: locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}) + create_locations(regionSM, {k: CellLocations.locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}) subRegionSMFF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) - create_locations(subRegionSMFF, {k: locSM_cellTable[k] for k in {90}}) + create_locations(subRegionSMFF, {k: CellLocations.locSM_cellTable[k] for k in {90}}) subRegionSMLF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) - create_locations(subRegionSMLF, {k: locSM_cellTable[k] for k in {91, 93}}) + create_locations(subRegionSMLF, {k: CellLocations.locSM_cellTable[k] for k in {91, 93}}) regionLT = create_region(player, multiworld, level_table[JakAndDaxterLevel.LAVA_TUBE]) - create_locations(regionLT, locLT_cellTable) + create_locations(regionLT, CellLocations.locLT_cellTable) regionGMC = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) - create_locations(regionGMC, {k: locGMC_cellTable[k] for k in {96, 97, 98}}) + create_locations(regionGMC, {k: CellLocations.locGMC_cellTable[k] for k in {96, 97, 98}}) - subRegionGMCRT = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) - create_locations(subRegionGMCRT, {k: locGMC_cellTable[k] for k in {99, 100}}) + subRegionGMCRT = create_subregion(regionGMC, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) + create_locations(subRegionGMCRT, {k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}) def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: region = JakAndDaxterRegion(name, player, multiworld) @@ -150,6 +151,10 @@ def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: connection.connect(region) parent.entrances.append(connection) + # connection = Entrance(parent.player, parent.name + " " + subLevel_table[JakAndDaxterSubLevel.MAIN_AREA], parent) + # connection.connect(parent) + # region.entrances.append(connection) + parent.multiworld.regions.append(region) return region diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 68767ed17b3f..069e6e6e5ffa 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -6,7 +6,10 @@ from .Items import item_table def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - multiworld.get_region("Menu").connect(level_table[JakAndDaxterLevel.GEYSER_ROCK]) + + menuRegion = multiworld.get_region("Menu", player) + grRegion = multiworld.get_region(level_table[JakAndDaxterLevel.GEYSER_ROCK], player) + menuRegion.connect(grRegion) connect_regions(multiworld, player, JakAndDaxterLevel.GEYSER_ROCK, @@ -63,7 +66,7 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_regions(multiworld, player, JakAndDaxterLevel.ROCK_VILLAGE, JakAndDaxterLevel.MOUNTAIN_PASS, - lambda state: state.has(item_table[2217], player) && state.has(item_table[0], player, 45)) + lambda state: state.has(item_table[2217], player) and state.has(item_table[0], player, 45)) assign_subregion_access_rule(multiworld, player, JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, @@ -100,7 +103,7 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) assign_subregion_access_rule(multiworld, player, JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - lambda state: state.has(item_table[96], player) && state.has(item_table[97], player) && state.has(item_table[98], player)) + lambda state: state.has(item_table[96], player) and state.has(item_table[97], player) and state.has(item_table[98], player)) def connect_regions(multiworld: MultiWorld, player: int, source: int, target: int, rule = None): sourceRegion = multiworld.get_region(level_table[source], player) @@ -108,5 +111,5 @@ def connect_regions(multiworld: MultiWorld, player: int, source: int, target: in sourceRegion.connect(targetRegion, rule = rule) def assign_subregion_access_rule(multiworld: MultiWorld, player: int, target: int, rule = None): - targetEntrance = multiworld.get_entrance(multiworld, player, subLevel_table[target]) + targetEntrance = multiworld.get_entrance(subLevel_table[target], player) targetEntrance.access_rule = rule diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 6ba7598d103b..7ff173e4f4cf 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,18 +1,22 @@ import typing, os, json -from BaseClasses import Item, Region, Entrance, Location +from BaseClasses import Item, ItemClassification, Region, Entrance, Location from .Locations import JakAndDaxterLocation, location_table from .Options import JakAndDaxterOptions from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, create_regions from .Rules import set_rules from .Items import JakAndDaxterItem, item_table, generic_item_table, special_item_table +from ..AutoWorld import World + +from Utils import visualize_regions + class JakAndDaxterWorld(World): game: str = "Jak and Daxter: The Precursor Legacy" game_id = 74680000 # All IDs will be offset by this number. # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. - item_name_to_id = {item_table[k], game_id + k for k in item_table} - location_name_to_id = {location_table[k], game_id + k for k in location_table} + item_name_to_id = {item_table[k]: 74680000 + k for k in item_table} + location_name_to_id = {location_table[k]: 74680000 + k for k in location_table} options_dataclass = JakAndDaxterOptions @@ -21,20 +25,20 @@ def create_regions(self): def set_rules(self): set_rules(self.multiworld, self.options, self.player) + visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") def create_item(self, name: str) -> Item: - item_id = item_name_to_id[name] - match name: - case "Power Cell": - classification = ItemClassification.progression_skip_balancing - case "Scout Fly": - classification = ItemClassification.progression_skip_balancing - case "Precursor Orb": - classification = ItemClassification.filler # TODO - case _: - classification = ItemClassification.progression - - item = JakAndDaxterItem(name, classification, item_id, player) + item_id = self.item_name_to_id[name] + if name == "Power Cell": + classification = ItemClassification.progression_skip_balancing + elif name == "Scout Fly": + classification = ItemClassification.progression_skip_balancing + elif name == "Precursor Orb": + classification = ItemClassification.filler # TODO + else: + classification = ItemClassification.progression + + item = JakAndDaxterItem(name, classification, item_id, self.player) return item def create_items(self): From 723f6415d998d744708c3f600466421930097da2 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:31:18 -0400 Subject: [PATCH 04/70] Jak 1: Fixed bugs, refactored Regions, added missing Special Checks. First spoiler log generated. --- worlds/jakanddaxter/GameID.py | 1 + worlds/jakanddaxter/Items.py | 2 +- worlds/jakanddaxter/Locations.py | 35 +++++++++--------- worlds/jakanddaxter/Regions.py | 37 ++++++++++++-------- worlds/jakanddaxter/Rules.py | 29 ++++++++++----- worlds/jakanddaxter/__init__.py | 24 ++++++------- worlds/jakanddaxter/locs/SpecialLocations.py | 11 ++++++ 7 files changed, 85 insertions(+), 54 deletions(-) create mode 100644 worlds/jakanddaxter/GameID.py create mode 100644 worlds/jakanddaxter/locs/SpecialLocations.py diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py new file mode 100644 index 000000000000..f88a02a30abe --- /dev/null +++ b/worlds/jakanddaxter/GameID.py @@ -0,0 +1 @@ +game_id = 74680000 # All IDs will be offset by this number. \ No newline at end of file diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index d1bac68258dc..d50810b77e22 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -14,7 +14,7 @@ class JakAndDaxterItem(Item): # Items Only Found Once special_item_table = { 2213: "Fisherman's Boat", - 2214: "Sculptor's Muse", + # 2214: "Sculptor's Muse", # Unused? 2215: "Flut Flut", 2216: "Blue Eco Switch", 2217: "Gladiator's Pontoons", diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index d74473951aa6..ee21ce079e43 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,26 +1,27 @@ import typing from BaseClasses import Location -from .locs import CellLocations +from .locs import CellLocations, SpecialLocations class JakAndDaxterLocation(Location): game: str = "Jak and Daxter: The Precursor Legacy" # All Locations location_table = { - **CellLocations.locGR_cellTable, \ - **CellLocations.locSV_cellTable, \ - **CellLocations.locFJ_cellTable, \ - **CellLocations.locSB_cellTable, \ - **CellLocations.locMI_cellTable, \ - **CellLocations.locFC_cellTable, \ - **CellLocations.locRV_cellTable, \ - **CellLocations.locPB_cellTable, \ - **CellLocations.locLPC_cellTable, \ - **CellLocations.locBS_cellTable, \ - **CellLocations.locMP_cellTable, \ - **CellLocations.locVC_cellTable, \ - **CellLocations.locSC_cellTable, \ - **CellLocations.locSM_cellTable, \ - **CellLocations.locLT_cellTable, \ - **CellLocations.locGMC_cellTable + **CellLocations.locGR_cellTable, + **CellLocations.locSV_cellTable, + **CellLocations.locFJ_cellTable, + **CellLocations.locSB_cellTable, + **CellLocations.locMI_cellTable, + **CellLocations.locFC_cellTable, + **CellLocations.locRV_cellTable, + **CellLocations.locPB_cellTable, + **CellLocations.locLPC_cellTable, + **CellLocations.locBS_cellTable, + **CellLocations.locMP_cellTable, + **CellLocations.locVC_cellTable, + **CellLocations.locSC_cellTable, + **CellLocations.locSM_cellTable, + **CellLocations.locLT_cellTable, + **CellLocations.locGMC_cellTable, + **SpecialLocations.loc_specialTable } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 6f29e0817286..dda46d1b180c 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -3,7 +3,7 @@ from BaseClasses import MultiWorld, Region, Entrance, Location from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table -from .locs import CellLocations +from .locs import CellLocations, SpecialLocations class JakAndDaxterLevel(int, Enum): GEYSER_ROCK = 0 @@ -76,13 +76,19 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: create_locations(regionSV, CellLocations.locSV_cellTable) regionFJ = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) - create_locations(regionFJ, {k: CellLocations.locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}) + create_locations(regionFJ, { + **{k: CellLocations.locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}, + **{k: SpecialLocations.loc_specialTable[k] for k in {2213, 2216}} + }) subRegionFJPR = create_subregion(regionFJ, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) create_locations(subRegionFJPR, {k: CellLocations.locFJ_cellTable[k] for k in {13}}) regionSB = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) - create_locations(regionSB, {k: CellLocations.locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}) + create_locations(regionSB, { + **{k: CellLocations.locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}, + **{k: SpecialLocations.loc_specialTable[k] for k in {2215}} + }) subRegionSBCT = create_subregion(regionSB, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) create_locations(subRegionSBCT, {k: CellLocations.locSB_cellTable[k] for k in {22}}) @@ -94,7 +100,10 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: create_locations(regionFC, CellLocations.locFC_cellTable) regionRV = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) - create_locations(regionRV, CellLocations.locRV_cellTable) + create_locations(regionRV, { + **CellLocations.locRV_cellTable, + **{k: SpecialLocations.loc_specialTable[k] for k in {2217}} + }) regionPB = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) create_locations(regionPB, CellLocations.locPB_cellTable) @@ -121,10 +130,16 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: create_locations(regionSC, CellLocations.locSC_cellTable) regionSM = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) - create_locations(regionSM, {k: CellLocations.locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}) + create_locations(regionSM, { + **{k: CellLocations.locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}, + **{k: SpecialLocations.loc_specialTable[k] for k in {2218}} + }) subRegionSMFF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) - create_locations(subRegionSMFF, {k: CellLocations.locSM_cellTable[k] for k in {90}}) + create_locations(subRegionSMFF, { + **{k: CellLocations.locSM_cellTable[k] for k in {90}}, + **{k: SpecialLocations.loc_specialTable[k] for k in {2219}} + }) subRegionSMLF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) create_locations(subRegionSMLF, {k: CellLocations.locSM_cellTable[k] for k in {91, 93}}) @@ -147,16 +162,8 @@ def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxte def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: region = JakAndDaxterRegion(name, parent.player, parent.multiworld) - connection = Entrance(parent.player, name, parent) - connection.connect(region) - parent.entrances.append(connection) - - # connection = Entrance(parent.player, parent.name + " " + subLevel_table[JakAndDaxterSubLevel.MAIN_AREA], parent) - # connection.connect(parent) - # region.entrances.append(connection) - parent.multiworld.regions.append(region) return region def create_locations(region: Region, locs: typing.Dict[int, str]): - region.locations += [JakAndDaxterLocation(region.player, loc, location_table[loc], region) for loc in locs] + region.locations += [JakAndDaxterLocation(region.player, location_table[loc], loc, region) for loc in locs] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 069e6e6e5ffa..be9c16cd619c 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -20,7 +20,8 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.SANDOVER_VILLAGE, JakAndDaxterLevel.FORBIDDEN_JUNGLE) - assign_subregion_access_rule(multiworld, player, + connect_subregions(multiworld, player, + JakAndDaxterLevel.FORBIDDEN_JUNGLE, JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, lambda state: state.has(item_table[2216], player)) @@ -28,7 +29,8 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.SANDOVER_VILLAGE, JakAndDaxterLevel.SENTINEL_BEACH) - assign_subregion_access_rule(multiworld, player, + connect_subregions(multiworld, player, + JakAndDaxterLevel.SENTINEL_BEACH, JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, lambda state: state.has(item_table[2216], player)) @@ -59,7 +61,8 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.BOGGY_SWAMP, lambda state: state.has(item_table[2217], player)) - assign_subregion_access_rule(multiworld, player, + connect_subregions(multiworld, player, + JakAndDaxterLevel.BOGGY_SWAMP, JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, lambda state: state.has(item_table[2215], player)) @@ -68,7 +71,8 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.MOUNTAIN_PASS, lambda state: state.has(item_table[2217], player) and state.has(item_table[0], player, 45)) - assign_subregion_access_rule(multiworld, player, + connect_subregions(multiworld, player, + JakAndDaxterLevel.MOUNTAIN_PASS, JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, lambda state: state.has(item_table[2218], player)) @@ -84,11 +88,13 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.VOLCANIC_CRATER, JakAndDaxterLevel.SNOWY_MOUNTAIN) - assign_subregion_access_rule(multiworld, player, + connect_subregions(multiworld, player, + JakAndDaxterLevel.SNOWY_MOUNTAIN, JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, lambda state: state.has(item_table[2215], player)) - assign_subregion_access_rule(multiworld, player, + connect_subregions(multiworld, player, + JakAndDaxterLevel.SNOWY_MOUNTAIN, JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, lambda state: state.has(item_table[2219], player)) @@ -101,15 +107,22 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.LAVA_TUBE, JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL) - assign_subregion_access_rule(multiworld, player, + connect_subregions(multiworld, player, + JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - lambda state: state.has(item_table[96], player) and state.has(item_table[97], player) and state.has(item_table[98], player)) + # lambda state: state.has(item_table[96], player) and state.has(item_table[97], player) and state.has(item_table[98], player)) + lambda state: state.has(item_table[0], player, 75)) def connect_regions(multiworld: MultiWorld, player: int, source: int, target: int, rule = None): sourceRegion = multiworld.get_region(level_table[source], player) targetRegion = multiworld.get_region(level_table[target], player) sourceRegion.connect(targetRegion, rule = rule) +def connect_subregions(multiworld: MultiWorld, player: int, source: int, target: int, rule = None): + sourceRegion = multiworld.get_region(level_table[source], player) + targetRegion = multiworld.get_region(subLevel_table[target], player) + sourceRegion.connect(targetRegion, rule = rule) + def assign_subregion_access_rule(multiworld: MultiWorld, player: int, target: int, rule = None): targetEntrance = multiworld.get_entrance(subLevel_table[target], player) targetEntrance.access_rule = rule diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 7ff173e4f4cf..ecbcbd2fa037 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -5,35 +5,33 @@ from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, create_regions from .Rules import set_rules from .Items import JakAndDaxterItem, item_table, generic_item_table, special_item_table +from .GameID import game_id from ..AutoWorld import World from Utils import visualize_regions - class JakAndDaxterWorld(World): game: str = "Jak and Daxter: The Precursor Legacy" - game_id = 74680000 # All IDs will be offset by this number. # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. - item_name_to_id = {item_table[k]: 74680000 + k for k in item_table} - location_name_to_id = {location_table[k]: 74680000 + k for k in location_table} + item_name_to_id = {item_table[k]: game_id + k for k in item_table} + location_name_to_id = {location_table[k]: game_id + k for k in location_table} options_dataclass = JakAndDaxterOptions + options: JakAndDaxterOptions def create_regions(self): create_regions(self.multiworld, self.options, self.player) def set_rules(self): set_rules(self.multiworld, self.options, self.player) - visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") - def create_item(self, name: str) -> Item: - item_id = self.item_name_to_id[name] - if name == "Power Cell": + def create_item(self, name: str, item_id: int) -> Item: + if "Power Cell" in name: classification = ItemClassification.progression_skip_balancing - elif name == "Scout Fly": + elif "Scout Fly" in name: classification = ItemClassification.progression_skip_balancing - elif name == "Precursor Orb": + elif "Precursor Orb" in name: classification = ItemClassification.filler # TODO else: classification = ItemClassification.progression @@ -42,6 +40,6 @@ def create_item(self, name: str) -> Item: return item def create_items(self): - self.multiworld.itempool += [self.create_item(item_table[0]) for k in range(0, 100)] - self.multiworld.itempool += [self.create_item(item_table[101]) for k in range(101, 212)] - self.multiworld.itempool += [self.create_item(item_table[k]) for k in special_item_table] + self.multiworld.itempool += [self.create_item("Power Cell", game_id + k) for k in range(0, 101)] + # self.multiworld.itempool += [self.create_item(item_table[101]) for k in range(101, 212)] + self.multiworld.itempool += [self.create_item(item_table[k], game_id + k) for k in special_item_table] diff --git a/worlds/jakanddaxter/locs/SpecialLocations.py b/worlds/jakanddaxter/locs/SpecialLocations.py new file mode 100644 index 000000000000..de7c891db326 --- /dev/null +++ b/worlds/jakanddaxter/locs/SpecialLocations.py @@ -0,0 +1,11 @@ +# Special Locations start at ID 2213 and end at ID 2219. + +loc_specialTable = { + 2213: "Fisherman's Boat", + # 2214: "Sculptor's Muse", # Unused? + 2215: "Flut Flut", + 2216: "Blue Eco Switch", + 2217: "Gladiator's Pontoons", + 2218: "Yellow Eco Switch", + 2219: "Lurker Fort Gate" +} \ No newline at end of file From 4031f19d92f84ca8f3da863b41806ef64c75a578 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:17:25 -0400 Subject: [PATCH 05/70] Jak 1: Add Scout Fly Locations, code and style cleanup. --- worlds/jakanddaxter/GameID.py | 3 +- worlds/jakanddaxter/Items.py | 2 + worlds/jakanddaxter/Locations.py | 22 ++- worlds/jakanddaxter/Options.py | 2 + worlds/jakanddaxter/Regions.py | 114 ++++++++------- worlds/jakanddaxter/Rules.py | 155 ++++++++++----------- worlds/jakanddaxter/__init__.py | 15 +- worlds/jakanddaxter/locs/ScoutLocations.py | 112 +++++++++++++++ 8 files changed, 282 insertions(+), 143 deletions(-) diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index f88a02a30abe..63e1bb10025c 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -1 +1,2 @@ -game_id = 74680000 # All IDs will be offset by this number. \ No newline at end of file +# All Jak And Daxter IDs will be offset by this number. +game_id = 74680000 diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index d50810b77e22..7904f54fa74c 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -1,9 +1,11 @@ import typing from BaseClasses import Item + class JakAndDaxterItem(Item): game: str = "Jak and Daxter: The Precursor Legacy" + # Items Found Multiple Times generic_item_table = { 0: "Power Cell", diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index ee21ce079e43..adfb90ea1f5a 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,10 +1,12 @@ import typing from BaseClasses import Location -from .locs import CellLocations, SpecialLocations +from .locs import CellLocations, SpecialLocations, ScoutLocations + class JakAndDaxterLocation(Location): game: str = "Jak and Daxter: The Precursor Legacy" + # All Locations location_table = { **CellLocations.locGR_cellTable, @@ -23,5 +25,21 @@ class JakAndDaxterLocation(Location): **CellLocations.locSM_cellTable, **CellLocations.locLT_cellTable, **CellLocations.locGMC_cellTable, - **SpecialLocations.loc_specialTable + **SpecialLocations.loc_specialTable, + **ScoutLocations.locGR_scoutTable, + **ScoutLocations.locSV_scoutTable, + **ScoutLocations.locFJ_scoutTable, + **ScoutLocations.locSB_scoutTable, + **ScoutLocations.locMI_scoutTable, + **ScoutLocations.locFC_scoutTable, + **ScoutLocations.locRV_scoutTable, + **ScoutLocations.locPB_scoutTable, + **ScoutLocations.locLPC_scoutTable, + **ScoutLocations.locBS_scoutTable, + **ScoutLocations.locMP_scoutTable, + **ScoutLocations.locVC_scoutTable, + **ScoutLocations.locSC_scoutTable, + **ScoutLocations.locSM_scoutTable, + **ScoutLocations.locLT_scoutTable, + **ScoutLocations.locGMC_scoutTable } diff --git a/worlds/jakanddaxter/Options.py b/worlds/jakanddaxter/Options.py index b33e73be402a..9870f10049d1 100644 --- a/worlds/jakanddaxter/Options.py +++ b/worlds/jakanddaxter/Options.py @@ -2,10 +2,12 @@ from dataclasses import dataclass from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet + class EnableScoutFlies(Toggle): """Enable to include each Scout Fly as a check. Adds 213 checks to the pool.""" display_name = "Enable Scout Flies" + # class EnablePrecursorOrbs(Toggle): # """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool.""" # display_name = "Enable Precursor Orbs" diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index dda46d1b180c..7f37f138c4ee 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,10 +1,11 @@ import typing from enum import Enum -from BaseClasses import MultiWorld, Region, Entrance, Location +from BaseClasses import MultiWorld, Region from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table from .locs import CellLocations, SpecialLocations + class JakAndDaxterLevel(int, Enum): GEYSER_ROCK = 0 SANDOVER_VILLAGE = 1 @@ -23,6 +24,7 @@ class JakAndDaxterLevel(int, Enum): LAVA_TUBE = 14 GOL_AND_MAIAS_CITADEL = 15 + class JakAndDaxterSubLevel(int, Enum): MAIN_AREA = 0 FORBIDDEN_JUNGLE_PLANT_ROOM = 1 @@ -33,6 +35,7 @@ class JakAndDaxterSubLevel(int, Enum): SNOWY_MOUNTAIN_LURKER_FORT = 6 GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = 7 + level_table: typing.Dict[JakAndDaxterLevel, str] = { JakAndDaxterLevel.GEYSER_ROCK: "Geyser Rock", JakAndDaxterLevel.SANDOVER_VILLAGE: "Sandover Village", @@ -63,95 +66,98 @@ class JakAndDaxterSubLevel(int, Enum): JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower" } + class JakAndDaxterRegion(Region): game: str = "Jak and Daxter: The Precursor Legacy" + def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - regionMenu = create_region(player, multiworld, "Menu") + region_menu = create_region(player, multiworld, "Menu") - regionGR = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) - create_locations(regionGR, CellLocations.locGR_cellTable) + region_gr = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) + create_locations(region_gr, CellLocations.locGR_cellTable) - regionSV = create_region(player, multiworld, level_table[JakAndDaxterLevel.SANDOVER_VILLAGE]) - create_locations(regionSV, CellLocations.locSV_cellTable) + region_sv = create_region(player, multiworld, level_table[JakAndDaxterLevel.SANDOVER_VILLAGE]) + create_locations(region_sv, CellLocations.locSV_cellTable) - regionFJ = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) - create_locations(regionFJ, { + region_fj = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) + create_locations(region_fj, { **{k: CellLocations.locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}, **{k: SpecialLocations.loc_specialTable[k] for k in {2213, 2216}} - }) + }) - subRegionFJPR = create_subregion(regionFJ, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) - create_locations(subRegionFJPR, {k: CellLocations.locFJ_cellTable[k] for k in {13}}) + sub_region_fjpr = create_subregion(region_fj, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) + create_locations(sub_region_fjpr, {k: CellLocations.locFJ_cellTable[k] for k in {13}}) - regionSB = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) - create_locations(regionSB, { + region_sb = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) + create_locations(region_sb, { **{k: CellLocations.locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}, **{k: SpecialLocations.loc_specialTable[k] for k in {2215}} - }) + }) - subRegionSBCT = create_subregion(regionSB, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) - create_locations(subRegionSBCT, {k: CellLocations.locSB_cellTable[k] for k in {22}}) + sub_region_sbct = create_subregion(region_sb, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) + create_locations(sub_region_sbct, {k: CellLocations.locSB_cellTable[k] for k in {22}}) - regionMI = create_region(player, multiworld, level_table[JakAndDaxterLevel.MISTY_ISLAND]) - create_locations(regionMI, CellLocations.locMI_cellTable) + region_mi = create_region(player, multiworld, level_table[JakAndDaxterLevel.MISTY_ISLAND]) + create_locations(region_mi, CellLocations.locMI_cellTable) - regionFC = create_region(player, multiworld, level_table[JakAndDaxterLevel.FIRE_CANYON]) - create_locations(regionFC, CellLocations.locFC_cellTable) + region_fc = create_region(player, multiworld, level_table[JakAndDaxterLevel.FIRE_CANYON]) + create_locations(region_fc, CellLocations.locFC_cellTable) - regionRV = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) - create_locations(regionRV, { + region_rv = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) + create_locations(region_rv, { **CellLocations.locRV_cellTable, **{k: SpecialLocations.loc_specialTable[k] for k in {2217}} - }) + }) - regionPB = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) - create_locations(regionPB, CellLocations.locPB_cellTable) + region_pb = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) + create_locations(region_pb, CellLocations.locPB_cellTable) - regionLPC = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) - create_locations(regionLPC, CellLocations.locLPC_cellTable) + region_lpc = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) + create_locations(region_lpc, CellLocations.locLPC_cellTable) - regionBS = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) - create_locations(regionBS, {k: CellLocations.locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}) + region_bs = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) + create_locations(region_bs, {k: CellLocations.locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}) - subRegionBSFF = create_subregion(regionBS, subLevel_table[JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT]) - create_locations(subRegionBSFF, {k: CellLocations.locBS_cellTable[k] for k in {58, 65}}) + sub_region_bsff = create_subregion(region_bs, subLevel_table[JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT]) + create_locations(sub_region_bsff, {k: CellLocations.locBS_cellTable[k] for k in {58, 65}}) - regionMP = create_region(player, multiworld, level_table[JakAndDaxterLevel.MOUNTAIN_PASS]) - create_locations(regionMP, {k: CellLocations.locMP_cellTable[k] for k in {66, 67, 69}}) + region_mp = create_region(player, multiworld, level_table[JakAndDaxterLevel.MOUNTAIN_PASS]) + create_locations(region_mp, {k: CellLocations.locMP_cellTable[k] for k in {66, 67, 69}}) - subRegionMPS = create_subregion(regionMP, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) - create_locations(subRegionMPS, {k: CellLocations.locMP_cellTable[k] for k in {68}}) + sub_region_mps = create_subregion(region_mp, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) + create_locations(sub_region_mps, {k: CellLocations.locMP_cellTable[k] for k in {68}}) - regionVC = create_region(player, multiworld, level_table[JakAndDaxterLevel.VOLCANIC_CRATER]) - create_locations(regionVC, CellLocations.locVC_cellTable) + region_vc = create_region(player, multiworld, level_table[JakAndDaxterLevel.VOLCANIC_CRATER]) + create_locations(region_vc, CellLocations.locVC_cellTable) - regionSC = create_region(player, multiworld, level_table[JakAndDaxterLevel.SPIDER_CAVE]) - create_locations(regionSC, CellLocations.locSC_cellTable) + region_sc = create_region(player, multiworld, level_table[JakAndDaxterLevel.SPIDER_CAVE]) + create_locations(region_sc, CellLocations.locSC_cellTable) - regionSM = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) - create_locations(regionSM, { + region_sm = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) + create_locations(region_sm, { **{k: CellLocations.locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}, **{k: SpecialLocations.loc_specialTable[k] for k in {2218}} - }) + }) - subRegionSMFF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) - create_locations(subRegionSMFF, { + sub_region_smff = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) + create_locations(sub_region_smff, { **{k: CellLocations.locSM_cellTable[k] for k in {90}}, **{k: SpecialLocations.loc_specialTable[k] for k in {2219}} - }) + }) + + sub_region_smlf = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) + create_locations(sub_region_smlf, {k: CellLocations.locSM_cellTable[k] for k in {91, 93}}) - subRegionSMLF = create_subregion(regionSM, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) - create_locations(subRegionSMLF, {k: CellLocations.locSM_cellTable[k] for k in {91, 93}}) + region_lt = create_region(player, multiworld, level_table[JakAndDaxterLevel.LAVA_TUBE]) + create_locations(region_lt, CellLocations.locLT_cellTable) - regionLT = create_region(player, multiworld, level_table[JakAndDaxterLevel.LAVA_TUBE]) - create_locations(regionLT, CellLocations.locLT_cellTable) + region_gmc = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) + create_locations(region_gmc, {k: CellLocations.locGMC_cellTable[k] for k in {96, 97, 98}}) - regionGMC = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) - create_locations(regionGMC, {k: CellLocations.locGMC_cellTable[k] for k in {96, 97, 98}}) + sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) + create_locations(sub_region_gmcrt, {k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}) - subRegionGMCRT = create_subregion(regionGMC, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) - create_locations(subRegionGMCRT, {k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}) def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: region = JakAndDaxterRegion(name, player, multiworld) @@ -159,11 +165,13 @@ def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxte multiworld.regions.append(region) return region + def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: region = JakAndDaxterRegion(name, parent.player, parent.multiworld) parent.multiworld.regions.append(region) return region + def create_locations(region: Region, locs: typing.Dict[int, str]): region.locations += [JakAndDaxterLocation(region.player, location_table[loc], loc, region) for loc in locs] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index be9c16cd619c..8215e4d5d59f 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,128 +1,125 @@ import typing from BaseClasses import MultiWorld from .Options import JakAndDaxterOptions -from .Locations import JakAndDaxterLocation, location_table from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table from .Items import item_table -def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - menuRegion = multiworld.get_region("Menu", player) - grRegion = multiworld.get_region(level_table[JakAndDaxterLevel.GEYSER_ROCK], player) - menuRegion.connect(grRegion) +def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + region_menu = multiworld.get_region("Menu", player) + region_gr = multiworld.get_region(level_table[JakAndDaxterLevel.GEYSER_ROCK], player) + region_menu.connect(region_gr) connect_regions(multiworld, player, - JakAndDaxterLevel.GEYSER_ROCK, - JakAndDaxterLevel.SANDOVER_VILLAGE, - lambda state: state.has(item_table[0], player, 4)) + JakAndDaxterLevel.GEYSER_ROCK, + JakAndDaxterLevel.SANDOVER_VILLAGE, + lambda state: state.has(item_table[0], player, 4)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.FORBIDDEN_JUNGLE) + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.FORBIDDEN_JUNGLE) connect_subregions(multiworld, player, - JakAndDaxterLevel.FORBIDDEN_JUNGLE, - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - lambda state: state.has(item_table[2216], player)) + JakAndDaxterLevel.FORBIDDEN_JUNGLE, + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + lambda state: state.has(item_table[2216], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.SENTINEL_BEACH) + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.SENTINEL_BEACH) - connect_subregions(multiworld, player, - JakAndDaxterLevel.SENTINEL_BEACH, - JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, - lambda state: state.has(item_table[2216], player)) + connect_subregions(multiworld, player, + JakAndDaxterLevel.SENTINEL_BEACH, + JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, + lambda state: state.has(item_table[2216], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.MISTY_ISLAND, - lambda state: state.has(item_table[2213], player)) + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.MISTY_ISLAND, + lambda state: state.has(item_table[2213], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.FIRE_CANYON, - lambda state: state.has(item_table[0], player, 20)) + JakAndDaxterLevel.SANDOVER_VILLAGE, + JakAndDaxterLevel.FIRE_CANYON, + lambda state: state.has(item_table[0], player, 20)) connect_regions(multiworld, player, - JakAndDaxterLevel.FIRE_CANYON, - JakAndDaxterLevel.ROCK_VILLAGE) + JakAndDaxterLevel.FIRE_CANYON, + JakAndDaxterLevel.ROCK_VILLAGE) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.PRECURSOR_BASIN) + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.PRECURSOR_BASIN) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.LOST_PRECURSOR_CITY) + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.LOST_PRECURSOR_CITY) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.BOGGY_SWAMP, - lambda state: state.has(item_table[2217], player)) + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.BOGGY_SWAMP, + lambda state: state.has(item_table[2217], player)) - connect_subregions(multiworld, player, - JakAndDaxterLevel.BOGGY_SWAMP, - JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, - lambda state: state.has(item_table[2215], player)) + connect_subregions(multiworld, player, + JakAndDaxterLevel.BOGGY_SWAMP, + JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, + lambda state: state.has(item_table[2215], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.MOUNTAIN_PASS, - lambda state: state.has(item_table[2217], player) and state.has(item_table[0], player, 45)) + JakAndDaxterLevel.ROCK_VILLAGE, + JakAndDaxterLevel.MOUNTAIN_PASS, + lambda state: state.has(item_table[2217], player) and state.has(item_table[0], player, 45)) connect_subregions(multiworld, player, - JakAndDaxterLevel.MOUNTAIN_PASS, - JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, - lambda state: state.has(item_table[2218], player)) + JakAndDaxterLevel.MOUNTAIN_PASS, + JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, + lambda state: state.has(item_table[2218], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.MOUNTAIN_PASS, - JakAndDaxterLevel.VOLCANIC_CRATER) + JakAndDaxterLevel.MOUNTAIN_PASS, + JakAndDaxterLevel.VOLCANIC_CRATER) connect_regions(multiworld, player, - JakAndDaxterLevel.VOLCANIC_CRATER, - JakAndDaxterLevel.SPIDER_CAVE) + JakAndDaxterLevel.VOLCANIC_CRATER, + JakAndDaxterLevel.SPIDER_CAVE) connect_regions(multiworld, player, - JakAndDaxterLevel.VOLCANIC_CRATER, - JakAndDaxterLevel.SNOWY_MOUNTAIN) + JakAndDaxterLevel.VOLCANIC_CRATER, + JakAndDaxterLevel.SNOWY_MOUNTAIN) connect_subregions(multiworld, player, - JakAndDaxterLevel.SNOWY_MOUNTAIN, - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, - lambda state: state.has(item_table[2215], player)) + JakAndDaxterLevel.SNOWY_MOUNTAIN, + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, + lambda state: state.has(item_table[2215], player)) connect_subregions(multiworld, player, - JakAndDaxterLevel.SNOWY_MOUNTAIN, - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, - lambda state: state.has(item_table[2219], player)) + JakAndDaxterLevel.SNOWY_MOUNTAIN, + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, + lambda state: state.has(item_table[2219], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.VOLCANIC_CRATER, - JakAndDaxterLevel.LAVA_TUBE, - lambda state: state.has(item_table[0], player, 72)) + JakAndDaxterLevel.VOLCANIC_CRATER, + JakAndDaxterLevel.LAVA_TUBE, + lambda state: state.has(item_table[0], player, 72)) connect_regions(multiworld, player, - JakAndDaxterLevel.LAVA_TUBE, - JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL) + JakAndDaxterLevel.LAVA_TUBE, + JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL) connect_subregions(multiworld, player, - JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, - JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - # lambda state: state.has(item_table[96], player) and state.has(item_table[97], player) and state.has(item_table[98], player)) - lambda state: state.has(item_table[0], player, 75)) - -def connect_regions(multiworld: MultiWorld, player: int, source: int, target: int, rule = None): - sourceRegion = multiworld.get_region(level_table[source], player) - targetRegion = multiworld.get_region(level_table[target], player) - sourceRegion.connect(targetRegion, rule = rule) - -def connect_subregions(multiworld: MultiWorld, player: int, source: int, target: int, rule = None): - sourceRegion = multiworld.get_region(level_table[source], player) - targetRegion = multiworld.get_region(subLevel_table[target], player) - sourceRegion.connect(targetRegion, rule = rule) - -def assign_subregion_access_rule(multiworld: MultiWorld, player: int, target: int, rule = None): - targetEntrance = multiworld.get_entrance(subLevel_table[target], player) - targetEntrance.access_rule = rule + JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, + JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, + # lambda state: state.has(item_table[96], player) and state.has(item_table[97], player) and state.has(item_table[98], player)) + lambda state: state.has(item_table[0], player, 75)) + + +def connect_regions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterLevel, rule=None): + source_region = multiworld.get_region(level_table[source], player) + target_region = multiworld.get_region(level_table[target], player) + source_region.connect(target_region, rule=rule) + + +def connect_subregions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterSubLevel, rule=None): + source_region = multiworld.get_region(level_table[source], player) + target_region = multiworld.get_region(subLevel_table[target], player) + source_region.connect(target_region, rule=rule) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index ecbcbd2fa037..3b99456336a5 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,5 +1,4 @@ -import typing, os, json -from BaseClasses import Item, ItemClassification, Region, Entrance, Location +from BaseClasses import Item, ItemClassification from .Locations import JakAndDaxterLocation, location_table from .Options import JakAndDaxterOptions from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, create_regions @@ -8,7 +7,6 @@ from .GameID import game_id from ..AutoWorld import World -from Utils import visualize_regions class JakAndDaxterWorld(World): game: str = "Jak and Daxter: The Precursor Legacy" @@ -26,13 +24,14 @@ def create_regions(self): def set_rules(self): set_rules(self.multiworld, self.options, self.player) - def create_item(self, name: str, item_id: int) -> Item: + def create_item(self, name: str) -> Item: + item_id = self.item_name_to_id[name] if "Power Cell" in name: classification = ItemClassification.progression_skip_balancing elif "Scout Fly" in name: classification = ItemClassification.progression_skip_balancing elif "Precursor Orb" in name: - classification = ItemClassification.filler # TODO + classification = ItemClassification.filler # TODO else: classification = ItemClassification.progression @@ -40,6 +39,6 @@ def create_item(self, name: str, item_id: int) -> Item: return item def create_items(self): - self.multiworld.itempool += [self.create_item("Power Cell", game_id + k) for k in range(0, 101)] - # self.multiworld.itempool += [self.create_item(item_table[101]) for k in range(101, 212)] - self.multiworld.itempool += [self.create_item(item_table[k], game_id + k) for k in special_item_table] + self.multiworld.itempool += [self.create_item("Power Cell") for _ in range(0, 101)] + self.multiworld.itempool += [self.create_item("Scout Fly") for _ in range(101, 213)] + self.multiworld.itempool += [self.create_item(item_table[k]) for k in special_item_table] diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index fd1fe2f6dc1c..c4620a503c8c 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -2,64 +2,176 @@ # Geyser Rock locGR_scoutTable = { + 101: "GR: Scout Fly 1", + 102: "GR: Scout Fly 2", + 103: "GR: Scout Fly 3", + 104: "GR: Scout Fly 4", + 105: "GR: Scout Fly 5", + 106: "GR: Scout Fly 6", + 107: "GR: Scout Fly 7" } # Sandover Village locSV_scoutTable = { + 108: "SV: Scout Fly 1", + 109: "SV: Scout Fly 2", + 110: "SV: Scout Fly 3", + 111: "SV: Scout Fly 4", + 112: "SV: Scout Fly 5", + 113: "SV: Scout Fly 6", + 114: "SV: Scout Fly 7" } # Forbidden Jungle locFJ_scoutTable = { + 115: "FJ: Scout Fly 1", + 116: "FJ: Scout Fly 2", + 117: "FJ: Scout Fly 3", + 118: "FJ: Scout Fly 4", + 119: "FJ: Scout Fly 5", + 120: "FJ: Scout Fly 6", + 121: "FJ: Scout Fly 7" } # Sentinel Beach locSB_scoutTable = { + 122: "SB: Scout Fly 1", + 123: "SB: Scout Fly 2", + 124: "SB: Scout Fly 3", + 125: "SB: Scout Fly 4", + 126: "SB: Scout Fly 5", + 127: "SB: Scout Fly 6", + 128: "SB: Scout Fly 7" } # Misty Island locMI_scoutTable = { + 129: "MI: Scout Fly 1", + 130: "MI: Scout Fly 2", + 131: "MI: Scout Fly 3", + 132: "MI: Scout Fly 4", + 133: "MI: Scout Fly 5", + 134: "MI: Scout Fly 6", + 135: "MI: Scout Fly 7" } # Fire Canyon locFC_scoutTable = { + 136: "FC: Scout Fly 1", + 137: "FC: Scout Fly 2", + 138: "FC: Scout Fly 3", + 139: "FC: Scout Fly 4", + 140: "FC: Scout Fly 5", + 141: "FC: Scout Fly 6", + 142: "FC: Scout Fly 7" } # Rock Village locRV_scoutTable = { + 143: "RV: Scout Fly 1", + 144: "RV: Scout Fly 2", + 145: "RV: Scout Fly 3", + 146: "RV: Scout Fly 4", + 147: "RV: Scout Fly 5", + 148: "RV: Scout Fly 6", + 149: "RV: Scout Fly 7" } # Precursor Basin locPB_scoutTable = { + 150: "PB: Scout Fly 1", + 151: "PB: Scout Fly 2", + 152: "PB: Scout Fly 3", + 153: "PB: Scout Fly 4", + 154: "PB: Scout Fly 5", + 155: "PB: Scout Fly 6", + 156: "PB: Scout Fly 7" } # Lost Precursor City locLPC_scoutTable = { + 157: "LPC: Scout Fly 1", + 158: "LPC: Scout Fly 2", + 159: "LPC: Scout Fly 3", + 160: "LPC: Scout Fly 4", + 161: "LPC: Scout Fly 5", + 162: "LPC: Scout Fly 6", + 163: "LPC: Scout Fly 7" } # Boggy Swamp locBS_scoutTable = { + 164: "BS: Scout Fly 1", + 165: "BS: Scout Fly 2", + 166: "BS: Scout Fly 3", + 167: "BS: Scout Fly 4", + 168: "BS: Scout Fly 5", + 169: "BS: Scout Fly 6", + 170: "BS: Scout Fly 7" } # Mountain Pass locMP_scoutTable = { + 171: "MP: Scout Fly 1", + 172: "MP: Scout Fly 2", + 173: "MP: Scout Fly 3", + 174: "MP: Scout Fly 4", + 175: "MP: Scout Fly 5", + 176: "MP: Scout Fly 6", + 177: "MP: Scout Fly 7" } # Volcanic Crater locVC_scoutTable = { + 178: "VC: Scout Fly 1", + 179: "VC: Scout Fly 2", + 180: "VC: Scout Fly 3", + 181: "VC: Scout Fly 4", + 182: "VC: Scout Fly 5", + 183: "VC: Scout Fly 6", + 184: "VC: Scout Fly 7" } # Spider Cave locSC_scoutTable = { + 185: "SC: Scout Fly 1", + 186: "SC: Scout Fly 2", + 187: "SC: Scout Fly 3", + 188: "SC: Scout Fly 4", + 189: "SC: Scout Fly 5", + 190: "SC: Scout Fly 6", + 191: "SC: Scout Fly 7" } # Snowy Mountain locSM_scoutTable = { + 192: "SM: Scout Fly 1", + 193: "SM: Scout Fly 2", + 194: "SM: Scout Fly 3", + 195: "SM: Scout Fly 4", + 196: "SM: Scout Fly 5", + 197: "SM: Scout Fly 6", + 198: "SM: Scout Fly 7" } # Lava Tube locLT_scoutTable = { + 199: "LT: Scout Fly 1", + 200: "LT: Scout Fly 2", + 201: "LT: Scout Fly 3", + 202: "LT: Scout Fly 4", + 203: "LT: Scout Fly 5", + 204: "LT: Scout Fly 6", + 205: "LT: Scout Fly 7" } # Gol and Maias Citadel locGMC_scoutTable = { + 206: "GMC: Scout Fly 1", + 207: "GMC: Scout Fly 2", + 208: "GMC: Scout Fly 3", + 209: "GMC: Scout Fly 4", + 210: "GMC: Scout Fly 5", + 211: "GMC: Scout Fly 6", + 212: "GMC: Scout Fly 7" } From 352a26a88802427fee378f8fc6c2dbb21f85c30e Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:58:04 -0400 Subject: [PATCH 06/70] Jak 1: Add Scout Flies to Regions. --- worlds/jakanddaxter/Regions.py | 89 +++++++--- worlds/jakanddaxter/locs/ScoutLocations.py | 182 ++++++++++----------- 2 files changed, 160 insertions(+), 111 deletions(-) diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 7f37f138c4ee..f732fd861ba8 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -3,7 +3,7 @@ from BaseClasses import MultiWorld, Region from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table -from .locs import CellLocations, SpecialLocations +from .locs import CellLocations, SpecialLocations, ScoutLocations class JakAndDaxterLevel(int, Enum): @@ -75,15 +75,22 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: region_menu = create_region(player, multiworld, "Menu") region_gr = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) - create_locations(region_gr, CellLocations.locGR_cellTable) + create_locations(region_gr, { + **CellLocations.locGR_cellTable, + **ScoutLocations.locGR_scoutTable + }) region_sv = create_region(player, multiworld, level_table[JakAndDaxterLevel.SANDOVER_VILLAGE]) - create_locations(region_sv, CellLocations.locSV_cellTable) + create_locations(region_sv, { + **CellLocations.locSV_cellTable, + **ScoutLocations.locSV_scoutTable + }) region_fj = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) create_locations(region_fj, { **{k: CellLocations.locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2213, 2216}} + **{k: SpecialLocations.loc_specialTable[k] for k in {2213, 2216}}, + **ScoutLocations.locFJ_scoutTable }) sub_region_fjpr = create_subregion(region_fj, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) @@ -92,52 +99,82 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: region_sb = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) create_locations(region_sb, { **{k: CellLocations.locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2215}} + **{k: SpecialLocations.loc_specialTable[k] for k in {2215}}, + **ScoutLocations.locSB_scoutTable }) sub_region_sbct = create_subregion(region_sb, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) create_locations(sub_region_sbct, {k: CellLocations.locSB_cellTable[k] for k in {22}}) region_mi = create_region(player, multiworld, level_table[JakAndDaxterLevel.MISTY_ISLAND]) - create_locations(region_mi, CellLocations.locMI_cellTable) + create_locations(region_mi, { + **CellLocations.locMI_cellTable, + **ScoutLocations.locMI_scoutTable + }) region_fc = create_region(player, multiworld, level_table[JakAndDaxterLevel.FIRE_CANYON]) - create_locations(region_fc, CellLocations.locFC_cellTable) + create_locations(region_fc, { + **CellLocations.locFC_cellTable, + **ScoutLocations.locFC_scoutTable + }) region_rv = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) create_locations(region_rv, { **CellLocations.locRV_cellTable, - **{k: SpecialLocations.loc_specialTable[k] for k in {2217}} + **{k: SpecialLocations.loc_specialTable[k] for k in {2217}}, + **ScoutLocations.locRV_scoutTable }) region_pb = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) - create_locations(region_pb, CellLocations.locPB_cellTable) + create_locations(region_pb, { + **CellLocations.locPB_cellTable, + **ScoutLocations.locPB_scoutTable + }) region_lpc = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) - create_locations(region_lpc, CellLocations.locLPC_cellTable) + create_locations(region_lpc, { + **CellLocations.locLPC_cellTable, + **ScoutLocations.locLPC_scoutTable + }) region_bs = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) - create_locations(region_bs, {k: CellLocations.locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}) + create_locations(region_bs, { + **{k: CellLocations.locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}, + **{k: ScoutLocations.locBS_scoutTable[k] for k in {164, 165, 166, 167, 170}} + }) sub_region_bsff = create_subregion(region_bs, subLevel_table[JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT]) - create_locations(sub_region_bsff, {k: CellLocations.locBS_cellTable[k] for k in {58, 65}}) + create_locations(sub_region_bsff, { + **{k: CellLocations.locBS_cellTable[k] for k in {58, 65}}, + **{k: ScoutLocations.locBS_scoutTable[k] for k in {168, 169}} + }) region_mp = create_region(player, multiworld, level_table[JakAndDaxterLevel.MOUNTAIN_PASS]) - create_locations(region_mp, {k: CellLocations.locMP_cellTable[k] for k in {66, 67, 69}}) + create_locations(region_mp, { + **{k: CellLocations.locMP_cellTable[k] for k in {66, 67, 69}}, + **ScoutLocations.locMP_scoutTable + }) sub_region_mps = create_subregion(region_mp, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) create_locations(sub_region_mps, {k: CellLocations.locMP_cellTable[k] for k in {68}}) region_vc = create_region(player, multiworld, level_table[JakAndDaxterLevel.VOLCANIC_CRATER]) - create_locations(region_vc, CellLocations.locVC_cellTable) + create_locations(region_vc, { + **CellLocations.locVC_cellTable, + **ScoutLocations.locVC_scoutTable + }) region_sc = create_region(player, multiworld, level_table[JakAndDaxterLevel.SPIDER_CAVE]) - create_locations(region_sc, CellLocations.locSC_cellTable) + create_locations(region_sc, { + **CellLocations.locSC_cellTable, + **ScoutLocations.locSC_scoutTable + }) region_sm = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) create_locations(region_sm, { **{k: CellLocations.locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2218}} + **{k: SpecialLocations.loc_specialTable[k] for k in {2218}}, + **{k: ScoutLocations.locSM_scoutTable[k] for k in {192, 193, 194, 195, 196}} }) sub_region_smff = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) @@ -147,16 +184,28 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: }) sub_region_smlf = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) - create_locations(sub_region_smlf, {k: CellLocations.locSM_cellTable[k] for k in {91, 93}}) + create_locations(sub_region_smlf, { + **{k: CellLocations.locSM_cellTable[k] for k in {91, 93}}, + **{k: ScoutLocations.locSM_scoutTable[k] for k in {197, 198}} + }) region_lt = create_region(player, multiworld, level_table[JakAndDaxterLevel.LAVA_TUBE]) - create_locations(region_lt, CellLocations.locLT_cellTable) + create_locations(region_lt, { + **CellLocations.locLT_cellTable, + **ScoutLocations.locLT_scoutTable + }) region_gmc = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) - create_locations(region_gmc, {k: CellLocations.locGMC_cellTable[k] for k in {96, 97, 98}}) + create_locations(region_gmc, { + **{k: CellLocations.locGMC_cellTable[k] for k in {96, 97, 98}}, + **{k: ScoutLocations.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}} + }) sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) - create_locations(sub_region_gmcrt, {k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}) + create_locations(sub_region_gmcrt, { + **{k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}, + **{k: ScoutLocations.locGMC_scoutTable[k] for k in {212}} + }) def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index c4620a503c8c..a05b39bb8cbb 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -2,57 +2,57 @@ # Geyser Rock locGR_scoutTable = { - 101: "GR: Scout Fly 1", - 102: "GR: Scout Fly 2", - 103: "GR: Scout Fly 3", - 104: "GR: Scout Fly 4", - 105: "GR: Scout Fly 5", - 106: "GR: Scout Fly 6", - 107: "GR: Scout Fly 7" + 101: "GR: Scout Fly On Ground, Front", + 102: "GR: Scout Fly On Ground, Back", + 103: "GR: Scout Fly On Left Ledge", + 104: "GR: Scout Fly On Right Ledge", + 105: "GR: Scout Fly On Middle Ledge, Left", + 106: "GR: Scout Fly On Middle Ledge, Right", + 107: "GR: Scout Fly On Top Ledge" } # Sandover Village locSV_scoutTable = { - 108: "SV: Scout Fly 1", - 109: "SV: Scout Fly 2", - 110: "SV: Scout Fly 3", - 111: "SV: Scout Fly 4", - 112: "SV: Scout Fly 5", - 113: "SV: Scout Fly 6", - 114: "SV: Scout Fly 7" + 108: "SV: Scout Fly In Fisherman's House", + 109: "SV: Scout Fly In Mayor's House", + 110: "SV: Scout Fly Under Bridge", + 111: "SV: Scout Fly Behind Sculptor's House", + 112: "SV: Scout Fly Overlooking Farmer's House", + 113: "SV: Scout Fly Near Oracle", + 114: "SV: Scout Fly In Farmer's House" } # Forbidden Jungle locFJ_scoutTable = { - 115: "FJ: Scout Fly 1", - 116: "FJ: Scout Fly 2", - 117: "FJ: Scout Fly 3", - 118: "FJ: Scout Fly 4", - 119: "FJ: Scout Fly 5", - 120: "FJ: Scout Fly 6", - 121: "FJ: Scout Fly 7" + 115: "FJ: Scout Fly At End Of Path", + 116: "FJ: Scout Fly On Spiral Of Stumps", + 117: "FJ: Scout Fly Under Bridge", + 118: "FJ: Scout Fly At End Of River", + 119: "FJ: Scout Fly Behind Lurker Machine", + 120: "FJ: Scout Fly Around Temple Spire", + 121: "FJ: Scout Fly On Top Of Temple" } # Sentinel Beach locSB_scoutTable = { - 122: "SB: Scout Fly 1", - 123: "SB: Scout Fly 2", - 124: "SB: Scout Fly 3", - 125: "SB: Scout Fly 4", - 126: "SB: Scout Fly 5", - 127: "SB: Scout Fly 6", - 128: "SB: Scout Fly 7" + 122: "SB: Scout Fly At Entrance", + 123: "SB: Scout Fly Overlooking Locked Boxes", + 124: "SB: Scout Fly On Path To Flut Flut", + 125: "SB: Scout Fly Under Wood Pillars", + 126: "SB: Scout Fly Overlooking Blue Eco Vents", + 127: "SB: Scout Fly Overlooking Green Eco Vents", + 128: "SB: Scout Fly On Sentinel" } # Misty Island locMI_scoutTable = { - 129: "MI: Scout Fly 1", - 130: "MI: Scout Fly 2", - 131: "MI: Scout Fly 3", - 132: "MI: Scout Fly 4", - 133: "MI: Scout Fly 5", - 134: "MI: Scout Fly 6", - 135: "MI: Scout Fly 7" + 129: "MI: Scout Fly Overlooking Entrance", + 130: "MI: Scout Fly On Ledge Path, First", + 131: "MI: Scout Fly On Ledge Path, Second", + 132: "MI: Scout Fly Overlooking Shipyard", + 133: "MI: Scout Fly On Ship", + 134: "MI: Scout Fly On Barrel Ramps", + 135: "MI: Scout Fly On Zoomer Ramps" } # Fire Canyon @@ -68,46 +68,46 @@ # Rock Village locRV_scoutTable = { - 143: "RV: Scout Fly 1", - 144: "RV: Scout Fly 2", - 145: "RV: Scout Fly 3", - 146: "RV: Scout Fly 4", - 147: "RV: Scout Fly 5", - 148: "RV: Scout Fly 6", - 149: "RV: Scout Fly 7" + 143: "RV: Scout Fly Behind Sage's Hut", + 144: "RV: Scout Fly On Path To Village", + 145: "RV: Scout Fly Behind Geologist", + 146: "RV: Scout Fly Behind Fiery Boulder", + 147: "RV: Scout Fly On Dock", + 148: "RV: Scout Fly At Pontoon Bridge", + 149: "RV: Scout Fly At Boggy Swamp Entrance" } # Precursor Basin locPB_scoutTable = { - 150: "PB: Scout Fly 1", - 151: "PB: Scout Fly 2", - 152: "PB: Scout Fly 3", - 153: "PB: Scout Fly 4", - 154: "PB: Scout Fly 5", - 155: "PB: Scout Fly 6", - 156: "PB: Scout Fly 7" + 150: "PB: Scout Fly Overlooking Entrance", + 151: "PB: Scout Fly Near Mole Hole", + 152: "PB: Scout Fly At Purple Ring Start", + 153: "PB: Scout Fly Overlooking Dark Eco Plant", + 154: "PB: Scout Fly At Green Ring Start", + 155: "PB: Scout Fly Before Big Jump", + 156: "PB: Scout Fly Near Dark Eco Plant" } # Lost Precursor City locLPC_scoutTable = { - 157: "LPC: Scout Fly 1", - 158: "LPC: Scout Fly 2", - 159: "LPC: Scout Fly 3", - 160: "LPC: Scout Fly 4", - 161: "LPC: Scout Fly 5", - 162: "LPC: Scout Fly 6", - 163: "LPC: Scout Fly 7" + 157: "LPC: Scout Fly First Room", + 158: "LPC: Scout Fly Before Second Room", + 159: "LPC: Scout Fly Second Room, Near Orb Vent", + 160: "LPC: Scout Fly Second Room, On Path To Cell", + 161: "LPC: Scout Fly Second Room, Green Switch", + 162: "LPC: Scout Fly Second Room, Blue Switch", + 163: "LPC: Scout Fly Across Steam Vents" } # Boggy Swamp locBS_scoutTable = { - 164: "BS: Scout Fly 1", - 165: "BS: Scout Fly 2", - 166: "BS: Scout Fly 3", - 167: "BS: Scout Fly 4", - 168: "BS: Scout Fly 5", - 169: "BS: Scout Fly 6", - 170: "BS: Scout Fly 7" + 164: "BS: Scout Fly Near Entrance", + 165: "BS: Scout Fly Over First Jump Pad", + 166: "BS: Scout Fly Over Second Jump Pad", + 167: "BS: Scout Fly Across Black Swamp", + 168: "BS: Scout Fly Overlooking Flut Flut", + 169: "BS: Scout Fly On Flut Flut Platforms", + 170: "BS: Scout Fly In Field Of Boxes" } # Mountain Pass @@ -123,35 +123,35 @@ # Volcanic Crater locVC_scoutTable = { - 178: "VC: Scout Fly 1", - 179: "VC: Scout Fly 2", - 180: "VC: Scout Fly 3", - 181: "VC: Scout Fly 4", - 182: "VC: Scout Fly 5", - 183: "VC: Scout Fly 6", - 184: "VC: Scout Fly 7" + 178: "VC: Scout Fly In Miner's Cave", + 179: "VC: Scout Fly Near Oracle", + 180: "VC: Scout Fly Overlooking Minecarts", + 181: "VC: Scout Fly On First Minecart Path", + 182: "VC: Scout Fly At Minecart Junction", + 183: "VC: Scout Fly At Spider Cave Entrance", + 184: "VC: Scout Fly Under Mountain Pass Exit" } # Spider Cave locSC_scoutTable = { - 185: "SC: Scout Fly 1", - 186: "SC: Scout Fly 2", - 187: "SC: Scout Fly 3", - 188: "SC: Scout Fly 4", - 189: "SC: Scout Fly 5", - 190: "SC: Scout Fly 6", - 191: "SC: Scout Fly 7" + 185: "SC: Scout Fly Across Dark Eco Pool", + 186: "SC: Scout Fly At Dark Area Entrance", + 187: "SC: Scout Fly First Room, Overlooking Entrance", + 188: "SC: Scout Fly First Room, Near Dark Crystal", + 189: "SC: Scout Fly First Room, Near Dark Eco Pool", + 190: "SC: Scout Fly Robot Room, First Level", + 191: "SC: Scout Fly Robot Room, Second Level", } # Snowy Mountain locSM_scoutTable = { - 192: "SM: Scout Fly 1", - 193: "SM: Scout Fly 2", - 194: "SM: Scout Fly 3", - 195: "SM: Scout Fly 4", - 196: "SM: Scout Fly 5", - 197: "SM: Scout Fly 6", - 198: "SM: Scout Fly 7" + 192: "SM: Scout Fly Near Entrance", + 193: "SM: Scout Fly Near Frozen Box", + 194: "SM: Scout Fly Near Yellow Eco Switch", + 195: "SM: Scout Fly On Cliff near Flut Flut", + 196: "SM: Scout Fly Under Bridge To Fort", + 197: "SM: Scout Fly On Top Of Fort Tower", + 198: "SM: Scout Fly On Top Of Fort" } # Lava Tube @@ -167,11 +167,11 @@ # Gol and Maias Citadel locGMC_scoutTable = { - 206: "GMC: Scout Fly 1", - 207: "GMC: Scout Fly 2", - 208: "GMC: Scout Fly 3", - 209: "GMC: Scout Fly 4", - 210: "GMC: Scout Fly 5", - 211: "GMC: Scout Fly 6", - 212: "GMC: Scout Fly 7" + 206: "GMC: Scout Fly At Entrance", + 207: "GMC: Scout Fly At Jump Room Entrance", + 208: "GMC: Scout Fly On Ledge Across Rotators", + 209: "GMC: Scout Fly At Tile Color Puzzle", + 210: "GMC: Scout Fly At Blast Furnace", + 211: "GMC: Scout Fly At Path To Robot", + 212: "GMC: Scout Fly On Top Of Rotating Tower" } From 3b197bba3a70343769cda92627b76253165554f7 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:41:28 -0400 Subject: [PATCH 07/70] Jak 1: Add version info. --- worlds/jakanddaxter/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 3b99456336a5..24581cb55f30 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -10,6 +10,8 @@ class JakAndDaxterWorld(World): game: str = "Jak and Daxter: The Precursor Legacy" + data_version = 1 + required_client_version = (0, 4, 5) # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. item_name_to_id = {item_table[k]: game_id + k for k in item_table} From e1e8c04f854c55d8049a687c755cd729c80f51df Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:05:51 -0400 Subject: [PATCH 08/70] Jak 1: Reduced code smell. --- worlds/jakanddaxter/Items.py | 1 - worlds/jakanddaxter/Locations.py | 1 - worlds/jakanddaxter/Options.py | 5 ++--- worlds/jakanddaxter/Regions.py | 5 +++-- worlds/jakanddaxter/Rules.py | 13 ++++++++----- worlds/jakanddaxter/__init__.py | 3 ++- worlds/jakanddaxter/locs/SpecialLocations.py | 2 +- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 7904f54fa74c..2690d68f8557 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -1,4 +1,3 @@ -import typing from BaseClasses import Item diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index adfb90ea1f5a..156e422eeaa9 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,4 +1,3 @@ -import typing from BaseClasses import Location from .locs import CellLocations, SpecialLocations, ScoutLocations diff --git a/worlds/jakanddaxter/Options.py b/worlds/jakanddaxter/Options.py index 9870f10049d1..e585bb816fd9 100644 --- a/worlds/jakanddaxter/Options.py +++ b/worlds/jakanddaxter/Options.py @@ -1,10 +1,9 @@ -import typing from dataclasses import dataclass -from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet +from Options import Toggle, PerGameCommonOptions class EnableScoutFlies(Toggle): - """Enable to include each Scout Fly as a check. Adds 213 checks to the pool.""" + """Enable to include each Scout Fly as a check. Adds 112 checks to the pool.""" display_name = "Enable Scout Flies" diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index f732fd861ba8..627efc85c62d 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -72,7 +72,7 @@ class JakAndDaxterRegion(Region): def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - region_menu = create_region(player, multiworld, "Menu") + create_region(player, multiworld, "Menu") region_gr = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) create_locations(region_gr, { @@ -201,7 +201,8 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: **{k: ScoutLocations.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}} }) - sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) + sub_region_gmcrt = create_subregion(region_gmc, + subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) create_locations(sub_region_gmcrt, { **{k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}, **{k: ScoutLocations.locGMC_scoutTable[k] for k in {212}} diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 8215e4d5d59f..3822b42f7b4a 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,7 +1,6 @@ -import typing from BaseClasses import MultiWorld from .Options import JakAndDaxterOptions -from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table +from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, level_table, subLevel_table from .Items import item_table @@ -109,17 +108,21 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_subregions(multiworld, player, JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - # lambda state: state.has(item_table[96], player) and state.has(item_table[97], player) and state.has(item_table[98], player)) + # lambda state: state.has(item_table[96], player) + # and state.has(item_table[97], player) + # and state.has(item_table[98], player)) lambda state: state.has(item_table[0], player, 75)) -def connect_regions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterLevel, rule=None): +def connect_regions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterLevel, + rule=None): source_region = multiworld.get_region(level_table[source], player) target_region = multiworld.get_region(level_table[target], player) source_region.connect(target_region, rule=rule) -def connect_subregions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterSubLevel, rule=None): +def connect_subregions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterSubLevel, + rule=None): source_region = multiworld.get_region(level_table[source], player) target_region = multiworld.get_region(subLevel_table[target], player) source_region.connect(target_region, rule=rule) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 24581cb55f30..6e140cde563a 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,7 +1,8 @@ from BaseClasses import Item, ItemClassification from .Locations import JakAndDaxterLocation, location_table from .Options import JakAndDaxterOptions -from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, create_regions +from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, \ + create_regions from .Rules import set_rules from .Items import JakAndDaxterItem, item_table, generic_item_table, special_item_table from .GameID import game_id diff --git a/worlds/jakanddaxter/locs/SpecialLocations.py b/worlds/jakanddaxter/locs/SpecialLocations.py index de7c891db326..6a21f6db1a97 100644 --- a/worlds/jakanddaxter/locs/SpecialLocations.py +++ b/worlds/jakanddaxter/locs/SpecialLocations.py @@ -8,4 +8,4 @@ 2217: "Gladiator's Pontoons", 2218: "Yellow Eco Switch", 2219: "Lurker Fort Gate" -} \ No newline at end of file +} From 293a282ff4d2e998f3cb507175c736f1444e5660 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:34:30 -0400 Subject: [PATCH 09/70] Jak 1: Fixed UT bugs, added Free The Sages as Locations. --- worlds/jakanddaxter/GameID.py | 5 +- worlds/jakanddaxter/Items.py | 9 ++- worlds/jakanddaxter/Locations.py | 3 +- worlds/jakanddaxter/Regions.py | 20 +++-- worlds/jakanddaxter/Rules.py | 78 ++++++++++++-------- worlds/jakanddaxter/__init__.py | 4 +- worlds/jakanddaxter/locs/SpecialLocations.py | 8 +- 7 files changed, 82 insertions(+), 45 deletions(-) diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index 63e1bb10025c..12d0079483cc 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -1,2 +1,5 @@ -# All Jak And Daxter IDs will be offset by this number. +# All Jak And Daxter IDs must be offset by this number. game_id = 74680000 + +# The name of the game. +game_name = "Jak and Daxter: The Precursor Legacy" diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 2690d68f8557..ee94ca55e033 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -1,8 +1,9 @@ from BaseClasses import Item +from .GameID import game_id, game_name class JakAndDaxterItem(Item): - game: str = "Jak and Daxter: The Precursor Legacy" + game: str = game_name # Items Found Multiple Times @@ -20,7 +21,11 @@ class JakAndDaxterItem(Item): 2216: "Blue Eco Switch", 2217: "Gladiator's Pontoons", 2218: "Yellow Eco Switch", - 2219: "Lurker Fort Gate" + 2219: "Lurker Fort Gate", + 2220: "Free The Yellow Sage", + 2221: "Free The Red Sage", + 2222: "Free The Blue Sage", + 2223: "Free The Green Sage" } # All Items diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index 156e422eeaa9..9d03297e4a04 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,9 +1,10 @@ from BaseClasses import Location +from .GameID import game_id, game_name from .locs import CellLocations, SpecialLocations, ScoutLocations class JakAndDaxterLocation(Location): - game: str = "Jak and Daxter: The Precursor Legacy" + game: str = game_name # All Locations diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 627efc85c62d..59cd9e5ff777 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,6 +1,7 @@ import typing from enum import Enum from BaseClasses import MultiWorld, Region +from .GameID import game_id, game_name from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table from .locs import CellLocations, SpecialLocations, ScoutLocations @@ -34,6 +35,7 @@ class JakAndDaxterSubLevel(int, Enum): SNOWY_MOUNTAIN_FLUT_FLUT = 5 SNOWY_MOUNTAIN_LURKER_FORT = 6 GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = 7 + GOL_AND_MAIAS_CITADEL_FINAL_BOSS = 8 level_table: typing.Dict[JakAndDaxterLevel, str] = { @@ -63,12 +65,13 @@ class JakAndDaxterSubLevel(int, Enum): JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut", JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort", - JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower" + JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower", + JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss" } class JakAndDaxterRegion(Region): - game: str = "Jak and Daxter: The Precursor Legacy" + game: str = game_name def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): @@ -198,16 +201,20 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: region_gmc = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) create_locations(region_gmc, { **{k: CellLocations.locGMC_cellTable[k] for k in {96, 97, 98}}, - **{k: ScoutLocations.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}} + **{k: ScoutLocations.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}}, + **{k: SpecialLocations.loc_specialTable[k] for k in {2220, 2221, 2222}} }) sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) create_locations(sub_region_gmcrt, { **{k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}, - **{k: ScoutLocations.locGMC_scoutTable[k] for k in {212}} + **{k: ScoutLocations.locGMC_scoutTable[k] for k in {212}}, + **{k: SpecialLocations.loc_specialTable[k] for k in {2223}} }) + create_subregion(sub_region_gmcrt, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS]) + def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: region = JakAndDaxterRegion(name, player, multiworld) @@ -223,5 +230,6 @@ def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: return region -def create_locations(region: Region, locs: typing.Dict[int, str]): - region.locations += [JakAndDaxterLocation(region.player, location_table[loc], loc, region) for loc in locs] +def create_locations(region: Region, locations: typing.Dict[int, str]): + region.locations += [JakAndDaxterLocation(region.player, location_table[loc], game_id + loc, region) + for loc in locations] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 3822b42f7b4a..4165ea8d4a8b 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -18,19 +18,19 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.SANDOVER_VILLAGE, JakAndDaxterLevel.FORBIDDEN_JUNGLE) - connect_subregions(multiworld, player, - JakAndDaxterLevel.FORBIDDEN_JUNGLE, - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - lambda state: state.has(item_table[2216], player)) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.FORBIDDEN_JUNGLE, + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + lambda state: state.has(item_table[2216], player)) connect_regions(multiworld, player, JakAndDaxterLevel.SANDOVER_VILLAGE, JakAndDaxterLevel.SENTINEL_BEACH) - connect_subregions(multiworld, player, - JakAndDaxterLevel.SENTINEL_BEACH, - JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, - lambda state: state.has(item_table[2216], player)) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.SENTINEL_BEACH, + JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, + lambda state: state.has(item_table[2216], player)) connect_regions(multiworld, player, JakAndDaxterLevel.SANDOVER_VILLAGE, @@ -59,20 +59,20 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.BOGGY_SWAMP, lambda state: state.has(item_table[2217], player)) - connect_subregions(multiworld, player, - JakAndDaxterLevel.BOGGY_SWAMP, - JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, - lambda state: state.has(item_table[2215], player)) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.BOGGY_SWAMP, + JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, + lambda state: state.has(item_table[2215], player)) connect_regions(multiworld, player, JakAndDaxterLevel.ROCK_VILLAGE, JakAndDaxterLevel.MOUNTAIN_PASS, lambda state: state.has(item_table[2217], player) and state.has(item_table[0], player, 45)) - connect_subregions(multiworld, player, - JakAndDaxterLevel.MOUNTAIN_PASS, - JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, - lambda state: state.has(item_table[2218], player)) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.MOUNTAIN_PASS, + JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, + lambda state: state.has(item_table[2218], player)) connect_regions(multiworld, player, JakAndDaxterLevel.MOUNTAIN_PASS, @@ -86,15 +86,15 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.VOLCANIC_CRATER, JakAndDaxterLevel.SNOWY_MOUNTAIN) - connect_subregions(multiworld, player, - JakAndDaxterLevel.SNOWY_MOUNTAIN, - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, - lambda state: state.has(item_table[2215], player)) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.SNOWY_MOUNTAIN, + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, + lambda state: state.has(item_table[2215], player)) - connect_subregions(multiworld, player, - JakAndDaxterLevel.SNOWY_MOUNTAIN, - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, - lambda state: state.has(item_table[2219], player)) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.SNOWY_MOUNTAIN, + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, + lambda state: state.has(item_table[2219], player)) connect_regions(multiworld, player, JakAndDaxterLevel.VOLCANIC_CRATER, @@ -105,13 +105,22 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.LAVA_TUBE, JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, + JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, + lambda state: state.has(item_table[2220], player) + and state.has(item_table[2221], player) + and state.has(item_table[2222], player)) + connect_subregions(multiworld, player, - JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - # lambda state: state.has(item_table[96], player) - # and state.has(item_table[97], player) - # and state.has(item_table[98], player)) - lambda state: state.has(item_table[0], player, 75)) + JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS, + lambda state: state.has(item_table[2223], player)) + + multiworld.completion_condition[player] = lambda state: state.can_reach( + multiworld.get_region(subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player), + "Region", + player) def connect_regions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterLevel, @@ -121,8 +130,15 @@ def connect_regions(multiworld: MultiWorld, player: int, source: JakAndDaxterLev source_region.connect(target_region, rule=rule) -def connect_subregions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterSubLevel, - rule=None): +def connect_region_to_sub(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterSubLevel, + rule=None): source_region = multiworld.get_region(level_table[source], player) target_region = multiworld.get_region(subLevel_table[target], player) source_region.connect(target_region, rule=rule) + + +def connect_subregions(multiworld: MultiWorld, player: int, source: JakAndDaxterSubLevel, target: JakAndDaxterSubLevel, + rule=None): + source_region = multiworld.get_region(subLevel_table[source], player) + target_region = multiworld.get_region(subLevel_table[target], player) + source_region.connect(target_region, rule=rule) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 6e140cde563a..d2a8db9982d3 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -5,12 +5,12 @@ create_regions from .Rules import set_rules from .Items import JakAndDaxterItem, item_table, generic_item_table, special_item_table -from .GameID import game_id +from .GameID import game_id, game_name from ..AutoWorld import World class JakAndDaxterWorld(World): - game: str = "Jak and Daxter: The Precursor Legacy" + game: str = game_name data_version = 1 required_client_version = (0, 4, 5) diff --git a/worlds/jakanddaxter/locs/SpecialLocations.py b/worlds/jakanddaxter/locs/SpecialLocations.py index 6a21f6db1a97..c93c86f02b3c 100644 --- a/worlds/jakanddaxter/locs/SpecialLocations.py +++ b/worlds/jakanddaxter/locs/SpecialLocations.py @@ -1,4 +1,4 @@ -# Special Locations start at ID 2213 and end at ID 2219. +# Special Locations start at ID 2213 and end at ID 22xx. loc_specialTable = { 2213: "Fisherman's Boat", @@ -7,5 +7,9 @@ 2216: "Blue Eco Switch", 2217: "Gladiator's Pontoons", 2218: "Yellow Eco Switch", - 2219: "Lurker Fort Gate" + 2219: "Lurker Fort Gate", + 2220: "Free The Yellow Sage", + 2221: "Free The Red Sage", + 2222: "Free The Blue Sage", + 2223: "Free The Green Sage" } From db8d74e2ec6930b69af37c5f66c86378044d30eb Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:11:44 -0400 Subject: [PATCH 10/70] Jak 1: Refactor ID scheme to better fit game's scheme. Add more subregions and rules, but still missing one-way Entrances. --- worlds/jakanddaxter/GameID.py | 10 +- worlds/jakanddaxter/Items.py | 27 +-- worlds/jakanddaxter/Locations.py | 103 +++++--- worlds/jakanddaxter/Regions.py | 237 +++++++++---------- worlds/jakanddaxter/Rules.py | 100 ++++++-- worlds/jakanddaxter/__init__.py | 40 ++-- worlds/jakanddaxter/locs/CellLocations.py | 193 +++++++-------- worlds/jakanddaxter/locs/OrbLocations.py | 5 +- worlds/jakanddaxter/locs/ScoutLocations.py | 17 +- worlds/jakanddaxter/locs/SpecialLocations.py | 15 -- 10 files changed, 406 insertions(+), 341 deletions(-) delete mode 100644 worlds/jakanddaxter/locs/SpecialLocations.py diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index 12d0079483cc..e4ebb030f842 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -1,5 +1,13 @@ # All Jak And Daxter IDs must be offset by this number. -game_id = 74680000 +game_id = 746800000 # The name of the game. game_name = "Jak and Daxter: The Precursor Legacy" + +# What follows are offsets for each Location/Item type, +# necessary for Archipelago to avoid collision between +# ID numbers shared across items. See respective +# Locations files for explanations. +cell_offset = 0 +fly_offset = 1048576 # 2^20 +orb_offset = 2097152 # 2^21 diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index ee94ca55e033..4c9f16af0e5a 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -1,32 +1,7 @@ from BaseClasses import Item -from .GameID import game_id, game_name +from .GameID import game_name class JakAndDaxterItem(Item): game: str = game_name - -# Items Found Multiple Times -generic_item_table = { - 0: "Power Cell", - 101: "Scout Fly", - 213: "Precursor Orb" -} - -# Items Only Found Once -special_item_table = { - 2213: "Fisherman's Boat", - # 2214: "Sculptor's Muse", # Unused? - 2215: "Flut Flut", - 2216: "Blue Eco Switch", - 2217: "Gladiator's Pontoons", - 2218: "Yellow Eco Switch", - 2219: "Lurker Fort Gate", - 2220: "Free The Yellow Sage", - 2221: "Free The Red Sage", - 2222: "Free The Blue Sage", - 2223: "Free The Green Sage" -} - -# All Items -item_table = {**generic_item_table, **special_item_table} diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index 9d03297e4a04..84dd27f6ed92 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,6 +1,6 @@ from BaseClasses import Location -from .GameID import game_id, game_name -from .locs import CellLocations, SpecialLocations, ScoutLocations +from .GameID import game_id, game_name, cell_offset, fly_offset +from .locs import CellLocations, ScoutLocations class JakAndDaxterLocation(Location): @@ -8,38 +8,71 @@ class JakAndDaxterLocation(Location): # All Locations +# Because all items in Jak And Daxter are unique and do not regenerate, we can use this same table as our item table. +# Each Item ID == its corresponding Location ID. And then we only have to do this ugly math once. location_table = { - **CellLocations.locGR_cellTable, - **CellLocations.locSV_cellTable, - **CellLocations.locFJ_cellTable, - **CellLocations.locSB_cellTable, - **CellLocations.locMI_cellTable, - **CellLocations.locFC_cellTable, - **CellLocations.locRV_cellTable, - **CellLocations.locPB_cellTable, - **CellLocations.locLPC_cellTable, - **CellLocations.locBS_cellTable, - **CellLocations.locMP_cellTable, - **CellLocations.locVC_cellTable, - **CellLocations.locSC_cellTable, - **CellLocations.locSM_cellTable, - **CellLocations.locLT_cellTable, - **CellLocations.locGMC_cellTable, - **SpecialLocations.loc_specialTable, - **ScoutLocations.locGR_scoutTable, - **ScoutLocations.locSV_scoutTable, - **ScoutLocations.locFJ_scoutTable, - **ScoutLocations.locSB_scoutTable, - **ScoutLocations.locMI_scoutTable, - **ScoutLocations.locFC_scoutTable, - **ScoutLocations.locRV_scoutTable, - **ScoutLocations.locPB_scoutTable, - **ScoutLocations.locLPC_scoutTable, - **ScoutLocations.locBS_scoutTable, - **ScoutLocations.locMP_scoutTable, - **ScoutLocations.locVC_scoutTable, - **ScoutLocations.locSC_scoutTable, - **ScoutLocations.locSM_scoutTable, - **ScoutLocations.locLT_scoutTable, - **ScoutLocations.locGMC_scoutTable + **{game_id + cell_offset + k: CellLocations.locGR_cellTable[k] + for k in CellLocations.locGR_cellTable}, + **{game_id + cell_offset + k: CellLocations.locSV_cellTable[k] + for k in CellLocations.locSV_cellTable}, + **{game_id + cell_offset + k: CellLocations.locFJ_cellTable[k] + for k in CellLocations.locFJ_cellTable}, + **{game_id + cell_offset + k: CellLocations.locSB_cellTable[k] + for k in CellLocations.locSB_cellTable}, + **{game_id + cell_offset + k: CellLocations.locMI_cellTable[k] + for k in CellLocations.locMI_cellTable}, + **{game_id + cell_offset + k: CellLocations.locFC_cellTable[k] + for k in CellLocations.locFC_cellTable}, + **{game_id + cell_offset + k: CellLocations.locRV_cellTable[k] + for k in CellLocations.locRV_cellTable}, + **{game_id + cell_offset + k: CellLocations.locPB_cellTable[k] + for k in CellLocations.locPB_cellTable}, + **{game_id + cell_offset + k: CellLocations.locLPC_cellTable[k] + for k in CellLocations.locLPC_cellTable}, + **{game_id + cell_offset + k: CellLocations.locBS_cellTable[k] + for k in CellLocations.locBS_cellTable}, + **{game_id + cell_offset + k: CellLocations.locMP_cellTable[k] + for k in CellLocations.locMP_cellTable}, + **{game_id + cell_offset + k: CellLocations.locVC_cellTable[k] + for k in CellLocations.locVC_cellTable}, + **{game_id + cell_offset + k: CellLocations.locSC_cellTable[k] + for k in CellLocations.locSC_cellTable}, + **{game_id + cell_offset + k: CellLocations.locSM_cellTable[k] + for k in CellLocations.locSM_cellTable}, + **{game_id + cell_offset + k: CellLocations.locLT_cellTable[k] + for k in CellLocations.locLT_cellTable}, + **{game_id + cell_offset + k: CellLocations.locGMC_cellTable[k] + for k in CellLocations.locGMC_cellTable}, + **{game_id + fly_offset + k: ScoutLocations.locGR_scoutTable[k] + for k in ScoutLocations.locGR_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locSV_scoutTable[k] + for k in ScoutLocations.locSV_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locFJ_scoutTable[k] + for k in ScoutLocations.locFJ_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locSB_scoutTable[k] + for k in ScoutLocations.locSB_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locMI_scoutTable[k] + for k in ScoutLocations.locMI_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locFC_scoutTable[k] + for k in ScoutLocations.locFC_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locRV_scoutTable[k] + for k in ScoutLocations.locRV_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locPB_scoutTable[k] + for k in ScoutLocations.locPB_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locLPC_scoutTable[k] + for k in ScoutLocations.locLPC_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locBS_scoutTable[k] + for k in ScoutLocations.locBS_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locMP_scoutTable[k] + for k in ScoutLocations.locMP_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locVC_scoutTable[k] + for k in ScoutLocations.locVC_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locSC_scoutTable[k] + for k in ScoutLocations.locSC_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locSM_scoutTable[k] + for k in ScoutLocations.locSM_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locLT_scoutTable[k] + for k in ScoutLocations.locLT_scoutTable}, + **{game_id + fly_offset + k: ScoutLocations.locGMC_scoutTable[k] + for k in ScoutLocations.locGMC_scoutTable} } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 59cd9e5ff777..787ad5921425 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,41 +1,45 @@ import typing -from enum import Enum +from enum import Enum, auto from BaseClasses import MultiWorld, Region -from .GameID import game_id, game_name +from .GameID import game_id, game_name, cell_offset, fly_offset from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table -from .locs import CellLocations, SpecialLocations, ScoutLocations +from .locs import CellLocations, ScoutLocations class JakAndDaxterLevel(int, Enum): - GEYSER_ROCK = 0 - SANDOVER_VILLAGE = 1 - FORBIDDEN_JUNGLE = 2 - SENTINEL_BEACH = 3 - MISTY_ISLAND = 4 - FIRE_CANYON = 5 - ROCK_VILLAGE = 6 - PRECURSOR_BASIN = 7 - LOST_PRECURSOR_CITY = 8 - BOGGY_SWAMP = 9 - MOUNTAIN_PASS = 10 - VOLCANIC_CRATER = 11 - SPIDER_CAVE = 12 - SNOWY_MOUNTAIN = 13 - LAVA_TUBE = 14 - GOL_AND_MAIAS_CITADEL = 15 + GEYSER_ROCK = auto() + SANDOVER_VILLAGE = auto() + FORBIDDEN_JUNGLE = auto() + SENTINEL_BEACH = auto() + MISTY_ISLAND = auto() + FIRE_CANYON = auto() + ROCK_VILLAGE = auto() + PRECURSOR_BASIN = auto() + LOST_PRECURSOR_CITY = auto() + BOGGY_SWAMP = auto() + MOUNTAIN_PASS = auto() + VOLCANIC_CRATER = auto() + SPIDER_CAVE = auto() + SNOWY_MOUNTAIN = auto() + LAVA_TUBE = auto() + GOL_AND_MAIAS_CITADEL = auto() class JakAndDaxterSubLevel(int, Enum): - MAIN_AREA = 0 - FORBIDDEN_JUNGLE_PLANT_ROOM = 1 - SENTINEL_BEACH_CANNON_TOWER = 2 - BOGGY_SWAMP_FLUT_FLUT = 3 - MOUNTAIN_PASS_SHORTCUT = 4 - SNOWY_MOUNTAIN_FLUT_FLUT = 5 - SNOWY_MOUNTAIN_LURKER_FORT = 6 - GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = 7 - GOL_AND_MAIAS_CITADEL_FINAL_BOSS = 8 + MAIN_AREA = auto() + FORBIDDEN_JUNGLE_SWITCH_ROOM = auto() + FORBIDDEN_JUNGLE_PLANT_ROOM = auto() + SENTINEL_BEACH_CANNON_TOWER = auto() + PRECURSOR_BASIN_BLUE_RINGS = auto() + BOGGY_SWAMP_FLUT_FLUT = auto() + MOUNTAIN_PASS_RACE = auto() + MOUNTAIN_PASS_SHORTCUT = auto() + SNOWY_MOUNTAIN_FLUT_FLUT = auto() + SNOWY_MOUNTAIN_LURKER_FORT = auto() + SNOWY_MOUNTAIN_FROZEN_BOX = auto() + GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = auto() + GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto() level_table: typing.Dict[JakAndDaxterLevel, str] = { @@ -59,12 +63,16 @@ class JakAndDaxterSubLevel(int, Enum): subLevel_table: typing.Dict[JakAndDaxterSubLevel, str] = { JakAndDaxterSubLevel.MAIN_AREA: "Main Area", + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: "Forbidden Jungle Switch Room", JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room", JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower", + JakAndDaxterSubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings", JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut", + JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race", JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut", JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort", + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: "Snowy Mountain Frozen Box", JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower", JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss" } @@ -74,162 +82,137 @@ class JakAndDaxterRegion(Region): game: str = game_name +# Use the original ID's for each item to tell the Region which Locations are available in it. +# You do NOT need to add the item offsets, that will be handled by create_*_locations. def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): create_region(player, multiworld, "Menu") region_gr = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) - create_locations(region_gr, { - **CellLocations.locGR_cellTable, - **ScoutLocations.locGR_scoutTable - }) + create_cell_locations(region_gr, CellLocations.locGR_cellTable) + create_fly_locations(region_gr, ScoutLocations.locGR_scoutTable) region_sv = create_region(player, multiworld, level_table[JakAndDaxterLevel.SANDOVER_VILLAGE]) - create_locations(region_sv, { - **CellLocations.locSV_cellTable, - **ScoutLocations.locSV_scoutTable - }) + create_cell_locations(region_sv, CellLocations.locSV_cellTable) + create_fly_locations(region_sv, ScoutLocations.locSV_scoutTable) region_fj = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) - create_locations(region_fj, { - **{k: CellLocations.locFJ_cellTable[k] for k in {10, 11, 12, 14, 15, 16, 17}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2213, 2216}}, - **ScoutLocations.locFJ_scoutTable - }) + create_cell_locations(region_fj, {k: CellLocations.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9, 7}}) + create_fly_locations(region_fj, ScoutLocations.locFJ_scoutTable) - sub_region_fjpr = create_subregion(region_fj, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) - create_locations(sub_region_fjpr, {k: CellLocations.locFJ_cellTable[k] for k in {13}}) + sub_region_fjsr = create_subregion(region_fj, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM]) + create_cell_locations(sub_region_fjsr, {k: CellLocations.locFJ_cellTable[k] for k in {2}}) + + sub_region_fjpr = create_subregion(sub_region_fjsr, subLevel_table[JakAndDaxterSubLevel + .FORBIDDEN_JUNGLE_PLANT_ROOM]) + create_cell_locations(sub_region_fjpr, {k: CellLocations.locFJ_cellTable[k] for k in {6}}) region_sb = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) - create_locations(region_sb, { - **{k: CellLocations.locSB_cellTable[k] for k in {18, 19, 20, 21, 23, 24, 25}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2215}}, - **ScoutLocations.locSB_scoutTable - }) + create_cell_locations(region_sb, {k: CellLocations.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22, 20}}) + create_fly_locations(region_sb, ScoutLocations.locSB_scoutTable) sub_region_sbct = create_subregion(region_sb, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) - create_locations(sub_region_sbct, {k: CellLocations.locSB_cellTable[k] for k in {22}}) + create_cell_locations(sub_region_sbct, {k: CellLocations.locSB_cellTable[k] for k in {19}}) region_mi = create_region(player, multiworld, level_table[JakAndDaxterLevel.MISTY_ISLAND]) - create_locations(region_mi, { - **CellLocations.locMI_cellTable, - **ScoutLocations.locMI_scoutTable - }) + create_cell_locations(region_mi, CellLocations.locMI_cellTable) + create_fly_locations(region_mi, ScoutLocations.locMI_scoutTable) region_fc = create_region(player, multiworld, level_table[JakAndDaxterLevel.FIRE_CANYON]) - create_locations(region_fc, { - **CellLocations.locFC_cellTable, - **ScoutLocations.locFC_scoutTable - }) + create_cell_locations(region_fc, CellLocations.locFC_cellTable) + create_fly_locations(region_fc, ScoutLocations.locFC_scoutTable) region_rv = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) - create_locations(region_rv, { - **CellLocations.locRV_cellTable, - **{k: SpecialLocations.loc_specialTable[k] for k in {2217}}, - **ScoutLocations.locRV_scoutTable - }) + create_cell_locations(region_rv, CellLocations.locRV_cellTable) + create_fly_locations(region_rv, ScoutLocations.locRV_scoutTable) region_pb = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) - create_locations(region_pb, { - **CellLocations.locPB_cellTable, - **ScoutLocations.locPB_scoutTable - }) + create_cell_locations(region_pb, {k: CellLocations.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58, 57}}) + create_fly_locations(region_pb, ScoutLocations.locPB_scoutTable) + + sub_region_pbbr = create_subregion(region_pb, subLevel_table[JakAndDaxterSubLevel.PRECURSOR_BASIN_BLUE_RINGS]) + create_cell_locations(sub_region_pbbr, {k: CellLocations.locPB_cellTable[k] for k in {59}}) region_lpc = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) - create_locations(region_lpc, { - **CellLocations.locLPC_cellTable, - **ScoutLocations.locLPC_scoutTable - }) + create_cell_locations(region_lpc, CellLocations.locLPC_cellTable) + create_fly_locations(region_lpc, ScoutLocations.locLPC_scoutTable) region_bs = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) - create_locations(region_bs, { - **{k: CellLocations.locBS_cellTable[k] for k in {59, 60, 61, 62, 63, 64}}, - **{k: ScoutLocations.locBS_scoutTable[k] for k in {164, 165, 166, 167, 170}} - }) + create_cell_locations(region_bs, {k: CellLocations.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) + create_fly_locations(region_bs, {k: ScoutLocations.locBS_scoutTable[k] for k in {164, 165, 166, 167, 170}}) sub_region_bsff = create_subregion(region_bs, subLevel_table[JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT]) - create_locations(sub_region_bsff, { - **{k: CellLocations.locBS_cellTable[k] for k in {58, 65}}, - **{k: ScoutLocations.locBS_scoutTable[k] for k in {168, 169}} - }) + create_cell_locations(sub_region_bsff, {k: CellLocations.locBS_cellTable[k] for k in {43, 37}}) + create_fly_locations(sub_region_bsff, {k: ScoutLocations.locBS_scoutTable[k] for k in {168, 169}}) region_mp = create_region(player, multiworld, level_table[JakAndDaxterLevel.MOUNTAIN_PASS]) - create_locations(region_mp, { - **{k: CellLocations.locMP_cellTable[k] for k in {66, 67, 69}}, - **ScoutLocations.locMP_scoutTable - }) + create_cell_locations(region_mp, {k: CellLocations.locMP_cellTable[k] for k in {86}}) + + sub_region_mpr = create_subregion(region_mp, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE]) + create_cell_locations(sub_region_mpr, {k: CellLocations.locMP_cellTable[k] for k in {87, 88}}) + create_fly_locations(sub_region_mpr, ScoutLocations.locMP_scoutTable) - sub_region_mps = create_subregion(region_mp, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) - create_locations(sub_region_mps, {k: CellLocations.locMP_cellTable[k] for k in {68}}) + sub_region_mps = create_subregion(sub_region_mpr, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) + create_cell_locations(sub_region_mps, {k: CellLocations.locMP_cellTable[k] for k in {110}}) region_vc = create_region(player, multiworld, level_table[JakAndDaxterLevel.VOLCANIC_CRATER]) - create_locations(region_vc, { - **CellLocations.locVC_cellTable, - **ScoutLocations.locVC_scoutTable - }) + create_cell_locations(region_vc, CellLocations.locVC_cellTable) + create_fly_locations(region_vc, ScoutLocations.locVC_scoutTable) region_sc = create_region(player, multiworld, level_table[JakAndDaxterLevel.SPIDER_CAVE]) - create_locations(region_sc, { - **CellLocations.locSC_cellTable, - **ScoutLocations.locSC_scoutTable - }) + create_cell_locations(region_sc, CellLocations.locSC_cellTable) + create_fly_locations(region_sc, ScoutLocations.locSC_scoutTable) region_sm = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) - create_locations(region_sm, { - **{k: CellLocations.locSM_cellTable[k] for k in {86, 87, 88, 89, 92}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2218}}, - **{k: ScoutLocations.locSM_scoutTable[k] for k in {192, 193, 194, 195, 196}} - }) + create_cell_locations(region_sm, {k: CellLocations.locSM_cellTable[k] for k in {60, 61, 66, 64}}) + create_fly_locations(region_sm, {k: ScoutLocations.locSM_scoutTable[k] for k in {192, 193, 194, 195, 196}}) + + sub_region_smfb = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FROZEN_BOX]) + create_cell_locations(sub_region_smfb, {k: CellLocations.locSM_cellTable[k] for k in {67}}) sub_region_smff = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) - create_locations(sub_region_smff, { - **{k: CellLocations.locSM_cellTable[k] for k in {90}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2219}} - }) + create_cell_locations(sub_region_smff, {k: CellLocations.locSM_cellTable[k] for k in {63}}) sub_region_smlf = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) - create_locations(sub_region_smlf, { - **{k: CellLocations.locSM_cellTable[k] for k in {91, 93}}, - **{k: ScoutLocations.locSM_scoutTable[k] for k in {197, 198}} - }) + create_cell_locations(sub_region_smlf, {k: CellLocations.locSM_cellTable[k] for k in {62, 65}}) + create_fly_locations(sub_region_smlf, {k: ScoutLocations.locSM_scoutTable[k] for k in {197, 198}}) region_lt = create_region(player, multiworld, level_table[JakAndDaxterLevel.LAVA_TUBE]) - create_locations(region_lt, { - **CellLocations.locLT_cellTable, - **ScoutLocations.locLT_scoutTable - }) + create_cell_locations(region_lt, CellLocations.locLT_cellTable) + create_fly_locations(region_lt, ScoutLocations.locLT_scoutTable) region_gmc = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) - create_locations(region_gmc, { - **{k: CellLocations.locGMC_cellTable[k] for k in {96, 97, 98}}, - **{k: ScoutLocations.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2220, 2221, 2222}} - }) - - sub_region_gmcrt = create_subregion(region_gmc, - subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) - create_locations(sub_region_gmcrt, { - **{k: CellLocations.locGMC_cellTable[k] for k in {99, 100}}, - **{k: ScoutLocations.locGMC_scoutTable[k] for k in {212}}, - **{k: SpecialLocations.loc_specialTable[k] for k in {2223}} - }) + create_cell_locations(region_gmc, {k: CellLocations.locGMC_cellTable[k] for k in {71, 72, 73}}) + create_fly_locations(region_gmc, {k: ScoutLocations.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}}) + + sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[JakAndDaxterSubLevel + .GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) + create_cell_locations(sub_region_gmcrt, {k: CellLocations.locGMC_cellTable[k] for k in {70, 91}}) + create_fly_locations(sub_region_gmcrt, {k: ScoutLocations.locGMC_scoutTable[k] for k in {212}}) create_subregion(sub_region_gmcrt, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS]) def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: region = JakAndDaxterRegion(name, player, multiworld) - multiworld.regions.append(region) return region def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: region = JakAndDaxterRegion(name, parent.player, parent.multiworld) - parent.multiworld.regions.append(region) return region -def create_locations(region: Region, locations: typing.Dict[int, str]): - region.locations += [JakAndDaxterLocation(region.player, location_table[loc], game_id + loc, region) - for loc in locations] +def create_cell_locations(region: Region, locations: typing.Dict[int, str]): + region.locations += [JakAndDaxterLocation(region.player, + location_table[game_id + cell_offset + loc], + game_id + cell_offset + loc, + region) for loc in locations] + + +def create_fly_locations(region: Region, locations: typing.Dict[int, str]): + region.locations += [JakAndDaxterLocation(region.player, + location_table[game_id + fly_offset + loc], + game_id + fly_offset + loc, + region) for loc in locations] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 4165ea8d4a8b..deeef7c92dd8 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,18 +1,38 @@ from BaseClasses import MultiWorld +from .GameID import game_id, cell_offset, fly_offset from .Options import JakAndDaxterOptions from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, level_table, subLevel_table -from .Items import item_table +from .Locations import location_table as item_table +from .locs.CellLocations import locGR_cellTable def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - region_menu = multiworld.get_region("Menu", player) - region_gr = multiworld.get_region(level_table[JakAndDaxterLevel.GEYSER_ROCK], player) - region_menu.connect(region_gr) + # Setting up some useful variables here because the offset numbers can get confusing + # for access rules. Feel free to add more variables here to keep the code more readable. + gr_cells = {game_id + cell_offset + k for k in locGR_cellTable} + fj_temple_top = game_id + cell_offset + 4 + fj_blue_switch = game_id + cell_offset + 2 + fj_fisherman = game_id + cell_offset + 5 + pb_purple_rings = game_id + cell_offset + 58 + sb_flut_flut = game_id + cell_offset + 17 + fc_end = game_id + cell_offset + 69 + mp_klaww = game_id + cell_offset + 86 + mp_end = game_id + cell_offset + 87 + sm_yellow_switch = game_id + cell_offset + 60 + sm_fort_gate = game_id + cell_offset + 63 + lt_end = game_id + cell_offset + 89 + gmc_blue_sage = game_id + cell_offset + 71 + gmc_red_sage = game_id + cell_offset + 72 + gmc_yellow_sage = game_id + cell_offset + 73 + gmc_green_sage = game_id + cell_offset + 70 + + # Start connecting regions and set their access rules. + connect_start(multiworld, player, JakAndDaxterLevel.GEYSER_ROCK) connect_regions(multiworld, player, JakAndDaxterLevel.GEYSER_ROCK, JakAndDaxterLevel.SANDOVER_VILLAGE, - lambda state: state.has(item_table[0], player, 4)) + lambda state: state.has_all({item_table[k] for k in gr_cells}, player)) connect_regions(multiworld, player, JakAndDaxterLevel.SANDOVER_VILLAGE, @@ -20,8 +40,13 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_region_to_sub(multiworld, player, JakAndDaxterLevel.FORBIDDEN_JUNGLE, - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - lambda state: state.has(item_table[2216], player)) + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, + lambda state: state.has(item_table[fj_temple_top], player)) + + connect_subregions(multiworld, player, + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + lambda state: state.has(item_table[fj_blue_switch], player)) connect_regions(multiworld, player, JakAndDaxterLevel.SANDOVER_VILLAGE, @@ -30,53 +55,64 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_region_to_sub(multiworld, player, JakAndDaxterLevel.SENTINEL_BEACH, JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, - lambda state: state.has(item_table[2216], player)) + lambda state: state.has(item_table[fj_blue_switch], player)) connect_regions(multiworld, player, JakAndDaxterLevel.SANDOVER_VILLAGE, JakAndDaxterLevel.MISTY_ISLAND, - lambda state: state.has(item_table[2213], player)) + lambda state: state.has(item_table[fj_fisherman], player)) connect_regions(multiworld, player, JakAndDaxterLevel.SANDOVER_VILLAGE, JakAndDaxterLevel.FIRE_CANYON, - lambda state: state.has(item_table[0], player, 20)) + lambda state: state.count_group("Power Cell", player) >= 20) connect_regions(multiworld, player, JakAndDaxterLevel.FIRE_CANYON, - JakAndDaxterLevel.ROCK_VILLAGE) + JakAndDaxterLevel.ROCK_VILLAGE, + lambda state: state.has(item_table[fc_end], player)) connect_regions(multiworld, player, JakAndDaxterLevel.ROCK_VILLAGE, JakAndDaxterLevel.PRECURSOR_BASIN) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.PRECURSOR_BASIN, + JakAndDaxterSubLevel.PRECURSOR_BASIN_BLUE_RINGS, + lambda state: state.has(item_table[pb_purple_rings], player)) + connect_regions(multiworld, player, JakAndDaxterLevel.ROCK_VILLAGE, JakAndDaxterLevel.LOST_PRECURSOR_CITY) connect_regions(multiworld, player, JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.BOGGY_SWAMP, - lambda state: state.has(item_table[2217], player)) + JakAndDaxterLevel.BOGGY_SWAMP) connect_region_to_sub(multiworld, player, JakAndDaxterLevel.BOGGY_SWAMP, JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, - lambda state: state.has(item_table[2215], player)) + lambda state: state.has(item_table[sb_flut_flut], player)) connect_regions(multiworld, player, JakAndDaxterLevel.ROCK_VILLAGE, JakAndDaxterLevel.MOUNTAIN_PASS, - lambda state: state.has(item_table[2217], player) and state.has(item_table[0], player, 45)) + lambda state: state.count_group("Power Cell", player) >= 45) connect_region_to_sub(multiworld, player, JakAndDaxterLevel.MOUNTAIN_PASS, - JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, - lambda state: state.has(item_table[2218], player)) + JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE, + lambda state: state.has(item_table[mp_klaww], player)) + + connect_subregions(multiworld, player, + JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE, + JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, + lambda state: state.has(item_table[sm_yellow_switch], player)) connect_regions(multiworld, player, JakAndDaxterLevel.MOUNTAIN_PASS, - JakAndDaxterLevel.VOLCANIC_CRATER) + JakAndDaxterLevel.VOLCANIC_CRATER, + lambda state: state.has(item_table[mp_end], player)) connect_regions(multiworld, player, JakAndDaxterLevel.VOLCANIC_CRATER, @@ -86,36 +122,42 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.VOLCANIC_CRATER, JakAndDaxterLevel.SNOWY_MOUNTAIN) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.SNOWY_MOUNTAIN, + JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FROZEN_BOX, + lambda state: state.has(item_table[sm_yellow_switch], player)) + connect_region_to_sub(multiworld, player, JakAndDaxterLevel.SNOWY_MOUNTAIN, JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, - lambda state: state.has(item_table[2215], player)) + lambda state: state.has(item_table[sb_flut_flut], player)) connect_region_to_sub(multiworld, player, JakAndDaxterLevel.SNOWY_MOUNTAIN, JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, - lambda state: state.has(item_table[2219], player)) + lambda state: state.has(item_table[sm_fort_gate], player)) connect_regions(multiworld, player, JakAndDaxterLevel.VOLCANIC_CRATER, JakAndDaxterLevel.LAVA_TUBE, - lambda state: state.has(item_table[0], player, 72)) + lambda state: state.count_group("Power Cell", player) >= 72) connect_regions(multiworld, player, JakAndDaxterLevel.LAVA_TUBE, - JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL) + JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, + lambda state: state.has(item_table[lt_end], player)) connect_region_to_sub(multiworld, player, JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - lambda state: state.has(item_table[2220], player) - and state.has(item_table[2221], player) - and state.has(item_table[2222], player)) + lambda state: state.has(item_table[gmc_blue_sage], player) + and state.has(item_table[gmc_red_sage], player) + and state.has(item_table[gmc_yellow_sage], player)) connect_subregions(multiworld, player, JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS, - lambda state: state.has(item_table[2223], player)) + lambda state: state.has(item_table[gmc_green_sage], player)) multiworld.completion_condition[player] = lambda state: state.can_reach( multiworld.get_region(subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player), @@ -123,6 +165,12 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) player) +def connect_start(multiworld: MultiWorld, player: int, target: JakAndDaxterLevel): + menu_region = multiworld.get_region("Menu", player) + start_region = multiworld.get_region(level_table[target], player) + menu_region.connect(start_region) + + def connect_regions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterLevel, rule=None): source_region = multiworld.get_region(level_table[source], player) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index d2a8db9982d3..3a99126b0d3c 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,11 +1,11 @@ from BaseClasses import Item, ItemClassification -from .Locations import JakAndDaxterLocation, location_table +from .Locations import JakAndDaxterLocation, location_table as item_table from .Options import JakAndDaxterOptions from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, \ create_regions from .Rules import set_rules -from .Items import JakAndDaxterItem, item_table, generic_item_table, special_item_table -from .GameID import game_id, game_name +from .Items import JakAndDaxterItem +from .GameID import game_id, game_name, cell_offset, fly_offset, orb_offset from ..AutoWorld import World @@ -14,34 +14,42 @@ class JakAndDaxterWorld(World): data_version = 1 required_client_version = (0, 4, 5) - # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. - item_name_to_id = {item_table[k]: game_id + k for k in item_table} - location_name_to_id = {location_table[k]: game_id + k for k in location_table} - options_dataclass = JakAndDaxterOptions options: JakAndDaxterOptions + # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. + item_name_to_id = {item_table[k]: k for k in item_table} + location_name_to_id = {item_table[k]: k for k in item_table} + item_name_groups = { + "Power Cell": {item_table[k]: k + for k in item_table if k in range(game_id + cell_offset, game_id + fly_offset)}, + "Scout Fly": {item_table[k]: k + for k in item_table if k in range(game_id + fly_offset, game_id + orb_offset)}, + "Precursor Orb": {} # TODO + } + def create_regions(self): create_regions(self.multiworld, self.options, self.player) def set_rules(self): set_rules(self.multiworld, self.options, self.player) + def create_items(self): + self.multiworld.itempool += [self.create_item(item_table[k]) for k in item_table] + def create_item(self, name: str) -> Item: item_id = self.item_name_to_id[name] - if "Power Cell" in name: + if item_id in range(game_id + cell_offset, game_id + fly_offset): + # Power Cell classification = ItemClassification.progression_skip_balancing - elif "Scout Fly" in name: + elif item_id in range(game_id + fly_offset, game_id + orb_offset): + # Scout Fly classification = ItemClassification.progression_skip_balancing - elif "Precursor Orb" in name: + elif item_id > game_id + orb_offset: + # Precursor Orb classification = ItemClassification.filler # TODO else: - classification = ItemClassification.progression + classification = ItemClassification.filler item = JakAndDaxterItem(name, classification, item_id, self.player) return item - - def create_items(self): - self.multiworld.itempool += [self.create_item("Power Cell") for _ in range(0, 101)] - self.multiworld.itempool += [self.create_item("Scout Fly") for _ in range(101, 213)] - self.multiworld.itempool += [self.create_item(item_table[k]) for k in special_item_table] diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py index 4f674bff6192..a16f40ce94f5 100644 --- a/worlds/jakanddaxter/locs/CellLocations.py +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -1,128 +1,135 @@ -# Power Cells start at ID 0 and end at ID 100. +# Power Cells are given ID's between 0 and 116 by the game. + +# The game tracks all game-tasks as integers. +# 101 of these ID's correspond directly to power cells, but they are not +# necessarily ordered, nor are they the first 101 in the task list. +# The remaining ones are cutscenes and other events. + +# The ID's you see below correspond directly to that cell's game-task ID. # Geyser Rock locGR_cellTable = { - 0: "GR: Find The Cell On The Path", - 1: "GR: Open The Precursor Door", - 2: "GR: Climb Up The Cliff", - 3: "GR: Free 7 Scout Flies" + 92: "GR: Find The Cell On The Path", + 93: "GR: Open The Precursor Door", + 94: "GR: Climb Up The Cliff", + 95: "GR: Free 7 Scout Flies" } # Sandover Village locSV_cellTable = { - 4: "SV: Bring 90 Orbs To The Mayor", - 5: "SV: Bring 90 Orbs to Your Uncle", - 6: "SV: Herd The Yakows Into The Pen", - 7: "SV: Bring 120 Orbs To The Oracle (1)", - 8: "SV: Bring 120 Orbs To The Oracle (2)", - 9: "SV: Free 7 Scout Flies" + 11: "SV: Bring 90 Orbs To The Mayor", + 12: "SV: Bring 90 Orbs to Your Uncle", + 10: "SV: Herd The Yakows Into The Pen", + 13: "SV: Bring 120 Orbs To The Oracle (1)", + 14: "SV: Bring 120 Orbs To The Oracle (2)", + 75: "SV: Free 7 Scout Flies" } # Forbidden Jungle locFJ_cellTable = { - 10: "FJ: Connect The Eco Beams", - 11: "FJ: Get To The Top Of The Temple", - 12: "FJ: Find The Blue Vent Switch", - 13: "FJ: Defeat The Dark Eco Plant", - 14: "FJ: Catch 200 Pounds Of Fish", - 15: "FJ: Follow The Canyon To The Sea", - 16: "FJ: Open The Locked Temple Door", - 17: "FJ: Free 7 Scout Flies" + 3: "FJ: Connect The Eco Beams", + 4: "FJ: Get To The Top Of The Temple", + 2: "FJ: Find The Blue Vent Switch", + 6: "FJ: Defeat The Dark Eco Plant", + 5: "FJ: Catch 200 Pounds Of Fish", + 8: "FJ: Follow The Canyon To The Sea", + 9: "FJ: Open The Locked Temple Door", + 7: "FJ: Free 7 Scout Flies" } # Sentinel Beach locSB_cellTable = { - 18: "SB: Unblock The Eco Harvesters", - 19: "SB: Push The Flut Flut Egg Off The Cliff", - 20: "SB: Get The Power Cell From The Pelican", - 21: "SB: Chase The Seagulls", - 22: "SB: Launch Up To The Cannon Tower", - 23: "SB: Explore The Beach", - 24: "SB: Climb The Sentinel", - 25: "SB: Free 7 Scout Flies" + 15: "SB: Unblock The Eco Harvesters", + 17: "SB: Push The Flut Flut Egg Off The Cliff", + 16: "SB: Get The Power Cell From The Pelican", + 18: "SB: Chase The Seagulls", + 19: "SB: Launch Up To The Cannon Tower", + 21: "SB: Explore The Beach", + 22: "SB: Climb The Sentinel", + 20: "SB: Free 7 Scout Flies" } # Misty Island locMI_cellTable = { - 26: "MI: Catch The Sculptor's Muse", - 27: "MI: Climb The Lurker Ship", - 28: "MI: Stop The Cannon", - 29: "MI: Return To The Dark Eco Pool", - 30: "MI: Destroy the Balloon Lurkers", - 31: "MI: Use Zoomer To Reach Power Cell", - 32: "MI: Use Blue Eco To Reach Power Cell", - 33: "MI: Free 7 Scout Flies" + 23: "MI: Catch The Sculptor's Muse", + 24: "MI: Climb The Lurker Ship", + 26: "MI: Stop The Cannon", + 25: "MI: Return To The Dark Eco Pool", + 27: "MI: Destroy the Balloon Lurkers", + 29: "MI: Use Zoomer To Reach Power Cell", + 30: "MI: Use Blue Eco To Reach Power Cell", + 28: "MI: Free 7 Scout Flies" } # Fire Canyon locFC_cellTable = { - 34: "FC: Reach The End Of Fire Canyon", - 35: "FC: Free 7 Scout Flies" + 69: "FC: Reach The End Of Fire Canyon", + 68: "FC: Free 7 Scout Flies" } # Rock Village locRV_cellTable = { - 36: "RV: Bring 90 Orbs To The Gambler", - 37: "RV: Bring 90 Orbs To The Geologist", - 38: "RV: Bring 90 Orbs To The Warrior", - 39: "RV: Bring 120 Orbs To The Oracle (1)", - 40: "RV: Bring 120 Orbs To The Oracle (2)", - 41: "RV: Free 7 Scout Flies" + 31: "RV: Bring 90 Orbs To The Gambler", + 32: "RV: Bring 90 Orbs To The Geologist", + 33: "RV: Bring 90 Orbs To The Warrior", + 34: "RV: Bring 120 Orbs To The Oracle (1)", + 35: "RV: Bring 120 Orbs To The Oracle (2)", + 76: "RV: Free 7 Scout Flies" } # Precursor Basin locPB_cellTable = { - 42: "PB: Herd The Moles Into Their Hole", - 43: "PB: Catch The Flying Lurkers", - 44: "PB: Beat Record Time On The Gorge", - 45: "PB: Get The Power Cell Over The Lake", - 46: "PB: Cure Dark Eco Infected Plants", - 47: "PB: Navigate The Purple Precursor Rings", - 48: "PB: Navigate The Blue Precursor Rings", - 49: "PB: Free 7 Scout Flies" + 54: "PB: Herd The Moles Into Their Hole", + 53: "PB: Catch The Flying Lurkers", + 52: "PB: Beat Record Time On The Gorge", + 56: "PB: Get The Power Cell Over The Lake", + 55: "PB: Cure Dark Eco Infected Plants", + 58: "PB: Navigate The Purple Precursor Rings", + 59: "PB: Navigate The Blue Precursor Rings", + 57: "PB: Free 7 Scout Flies" } # Lost Precursor City locLPC_cellTable = { - 50: "LPC: Raise The Chamber", - 51: "LPC: Follow The Colored Pipes", - 52: "LPC: Reach The Bottom Of The City", - 53: "LPC: Quickly Cross The Dangerous Pool", - 54: "LPC: Match The Platform Colors", - 55: "LPC: Climb The Slide Tube", - 56: "LPC: Reach The Center Of The Complex", - 57: "LPC: Free 7 Scout Flies" + 47: "LPC: Raise The Chamber", + 45: "LPC: Follow The Colored Pipes", + 46: "LPC: Reach The Bottom Of The City", + 48: "LPC: Quickly Cross The Dangerous Pool", + 44: "LPC: Match The Platform Colors", + 50: "LPC: Climb The Slide Tube", + 51: "LPC: Reach The Center Of The Complex", + 49: "LPC: Free 7 Scout Flies" } # Boggy Swamp locBS_cellTable = { - 58: "BS: Ride The Flut Flut", - 59: "BS: Protect Farthy's Snacks", - 60: "BS: Defeat The Lurker Ambush", - 61: "BS: Break The Tethers To The Zeppelin (1)", - 62: "BS: Break The Tethers To The Zeppelin (2)", - 63: "BS: Break The Tethers To The Zeppelin (3)", - 64: "BS: Break The Tethers To The Zeppelin (4)", - 65: "BS: Free 7 Scout Flies" + 37: "BS: Ride The Flut Flut", + 36: "BS: Protect Farthy's Snacks", + 38: "BS: Defeat The Lurker Ambush", + 39: "BS: Break The Tethers To The Zeppelin (1)", + 40: "BS: Break The Tethers To The Zeppelin (2)", + 41: "BS: Break The Tethers To The Zeppelin (3)", + 42: "BS: Break The Tethers To The Zeppelin (4)", + 43: "BS: Free 7 Scout Flies" } # Mountain Pass locMP_cellTable = { - 66: "MP: Defeat Klaww", - 67: "MP: Reach The End Of The Mountain Pass", - 68: "MP: Find The Hidden Power Cell", - 69: "MP: Free 7 Scout Flies" + 86: "MP: Defeat Klaww", + 87: "MP: Reach The End Of The Mountain Pass", + 110: "MP: Find The Hidden Power Cell", + 88: "MP: Free 7 Scout Flies" } # Volcanic Crater locVC_cellTable = { - 70: "VC: Bring 90 Orbs To The Miners (1)", - 71: "VC: Bring 90 Orbs To The Miners (2)", - 72: "VC: Bring 90 Orbs To The Miners (3)", - 73: "VC: Bring 90 Orbs To The Miners (4)", - 74: "VC: Bring 120 Orbs To The Oracle (1)", - 75: "VC: Bring 120 Orbs To The Oracle (2)", - 76: "VC: Find The Hidden Power Cell", + 96: "VC: Bring 90 Orbs To The Miners (1)", + 97: "VC: Bring 90 Orbs To The Miners (2)", + 98: "VC: Bring 90 Orbs To The Miners (3)", + 99: "VC: Bring 90 Orbs To The Miners (4)", + 100: "VC: Bring 120 Orbs To The Oracle (1)", + 101: "VC: Bring 120 Orbs To The Oracle (2)", + 74: "VC: Find The Hidden Power Cell", 77: "VC: Free 7 Scout Flies" } @@ -140,27 +147,27 @@ # Snowy Mountain locSM_cellTable = { - 86: "SM: Find The Yellow Vent Switch", - 87: "SM: Stop The 3 Lurker Glacier Troops", - 88: "SM: Deactivate The Precursor Blockers", - 89: "SM: Open The Frozen Crate", - 90: "SM: Open The Lurker Fort Gate", - 91: "SM: Get Through The Lurker Fort", - 92: "SM: Survive The Lurker Infested Cave", - 93: "SM: Free 7 Scout Flies" + 60: "SM: Find The Yellow Vent Switch", + 61: "SM: Stop The 3 Lurker Glacier Troops", + 66: "SM: Deactivate The Precursor Blockers", + 67: "SM: Open The Frozen Crate", + 63: "SM: Open The Lurker Fort Gate", + 62: "SM: Get Through The Lurker Fort", + 64: "SM: Survive The Lurker Infested Cave", + 65: "SM: Free 7 Scout Flies" } # Lava Tube locLT_cellTable = { - 94: "LT: Cross The Lava Tube", - 95: "LT: Free 7 Scout Flies" + 89: "LT: Cross The Lava Tube", + 90: "LT: Free 7 Scout Flies" } # Gol and Maias Citadel locGMC_cellTable = { - 96: "GMC: Free The Blue Sage", - 97: "GMC: Free The Red Sage", - 98: "GMC: Free The Yellow Sage", - 99: "GMC: Free The Green Sage", - 100: "GMC: Free 7 Scout Flies" + 71: "GMC: Free The Blue Sage", + 72: "GMC: Free The Red Sage", + 73: "GMC: Free The Yellow Sage", + 70: "GMC: Free The Green Sage", + 91: "GMC: Free 7 Scout Flies" } diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index 83037790d4ff..2da5972e17ac 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -1,4 +1,7 @@ -# Precursor Orbs start at ID 213 and end at ID 2212. +# Precursor Orbs start at ??? and end at ??? + +# Given that Scout Flies are being offset by 2^20 to avoid collisions with power cells, +# I'm tentatively setting the Orb offset to 2^21, or 2,097,152. # Geyser Rock locGR_orbTable = { diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index a05b39bb8cbb..1f86416d287f 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -1,4 +1,19 @@ -# Scout Flies start at ID 101 and end at ID 212. +# Scout Flies are given ID's between 0 and 393311 by the game, explanation below. + +# Each fly is given a unique 32-bit number broken into two 16-bit numbers. +# The lower 16 bits are the game-task ID of the power cell the fly corresponds to. +# The higher 16 bits are the index of the fly itself, from 000 (0) to 110 (6). + +# Ex: The final scout fly on Geyser Rock +# 0000000000000110 0000000001011111 +# ( Index: 6 ) ( Cell: 95 ) + +# Because flies are indexed from 0, each 0th fly's full ID == the power cell's ID. +# So we need to offset all of their ID's in order for Archipelago to separate them +# from their power cells. We use 1,048,576 (2^20) for this purpose, because scout flies +# don't use more than 19 bits to describe themselves. + +# TODO - The ID's you see below correspond directly to that fly's 32-bit ID in the game. # Geyser Rock locGR_scoutTable = { diff --git a/worlds/jakanddaxter/locs/SpecialLocations.py b/worlds/jakanddaxter/locs/SpecialLocations.py deleted file mode 100644 index c93c86f02b3c..000000000000 --- a/worlds/jakanddaxter/locs/SpecialLocations.py +++ /dev/null @@ -1,15 +0,0 @@ -# Special Locations start at ID 2213 and end at ID 22xx. - -loc_specialTable = { - 2213: "Fisherman's Boat", - # 2214: "Sculptor's Muse", # Unused? - 2215: "Flut Flut", - 2216: "Blue Eco Switch", - 2217: "Gladiator's Pontoons", - 2218: "Yellow Eco Switch", - 2219: "Lurker Fort Gate", - 2220: "Free The Yellow Sage", - 2221: "Free The Red Sage", - 2222: "Free The Blue Sage", - 2223: "Free The Green Sage" -} From 3cd3e73742157e922750d69ca3f44fdc00e96d56 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:42:33 -0400 Subject: [PATCH 11/70] Jak 1: Add some one-ways, adjust scout fly offset. --- worlds/jakanddaxter/GameID.py | 2 +- worlds/jakanddaxter/Regions.py | 17 ++++++++- worlds/jakanddaxter/Rules.py | 43 +++++++++++++++++++--- worlds/jakanddaxter/locs/ScoutLocations.py | 4 +- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index e4ebb030f842..a025f2752a2a 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -9,5 +9,5 @@ # ID numbers shared across items. See respective # Locations files for explanations. cell_offset = 0 -fly_offset = 1048576 # 2^20 +fly_offset = 128 # 2^7 orb_offset = 2097152 # 2^21 diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 787ad5921425..8fb62c9eda82 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -32,6 +32,8 @@ class JakAndDaxterSubLevel(int, Enum): FORBIDDEN_JUNGLE_PLANT_ROOM = auto() SENTINEL_BEACH_CANNON_TOWER = auto() PRECURSOR_BASIN_BLUE_RINGS = auto() + LOST_PRECURSOR_CITY_SUNKEN_ROOM = auto() + LOST_PRECURSOR_CITY_HELIX_ROOM = auto() BOGGY_SWAMP_FLUT_FLUT = auto() MOUNTAIN_PASS_RACE = auto() MOUNTAIN_PASS_SHORTCUT = auto() @@ -67,6 +69,8 @@ class JakAndDaxterSubLevel(int, Enum): JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room", JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower", JakAndDaxterSubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings", + JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: "Lost Precursor City Sunken Room", + JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: "Lost Precursor City Helix Room", JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut", JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race", JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", @@ -133,8 +137,17 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: create_cell_locations(sub_region_pbbr, {k: CellLocations.locPB_cellTable[k] for k in {59}}) region_lpc = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) - create_cell_locations(region_lpc, CellLocations.locLPC_cellTable) - create_fly_locations(region_lpc, ScoutLocations.locLPC_scoutTable) + create_cell_locations(region_lpc, {k: CellLocations.locLPC_cellTable[k] for k in {45, 48, 44, 51}}) + create_fly_locations(region_lpc, {k: ScoutLocations.locLPC_scoutTable[k] for k in {157, 158, 159, 160, 161, 162}}) + + sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[JakAndDaxterSubLevel + .LOST_PRECURSOR_CITY_SUNKEN_ROOM]) + create_cell_locations(sub_region_lpcsr, {k: CellLocations.locLPC_cellTable[k] for k in {47, 49}}) + create_fly_locations(region_lpc, {k: ScoutLocations.locLPC_scoutTable[k] for k in {163}}) + + sub_region_lpchr = create_subregion(region_lpc, subLevel_table[JakAndDaxterSubLevel + .LOST_PRECURSOR_CITY_HELIX_ROOM]) + create_cell_locations(sub_region_lpchr, {k: CellLocations.locLPC_cellTable[k] for k in {46, 50}}) region_bs = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) create_cell_locations(region_bs, {k: CellLocations.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index deeef7c92dd8..58585bba18d3 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -12,10 +12,13 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) gr_cells = {game_id + cell_offset + k for k in locGR_cellTable} fj_temple_top = game_id + cell_offset + 4 fj_blue_switch = game_id + cell_offset + 2 + fj_plant_boss = game_id + cell_offset + 6 fj_fisherman = game_id + cell_offset + 5 - pb_purple_rings = game_id + cell_offset + 58 sb_flut_flut = game_id + cell_offset + 17 fc_end = game_id + cell_offset + 69 + pb_purple_rings = game_id + cell_offset + 58 + lpc_sunken = game_id + cell_offset + 47 + lpc_helix = game_id + cell_offset + 50 mp_klaww = game_id + cell_offset + 86 mp_end = game_id + cell_offset + 87 sm_yellow_switch = game_id + cell_offset + 60 @@ -48,6 +51,11 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, lambda state: state.has(item_table[fj_blue_switch], player)) + connect_sub_to_region(multiworld, player, + JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + JakAndDaxterLevel.FORBIDDEN_JUNGLE, + lambda state: state.has(item_table[fj_plant_boss], player)) + connect_regions(multiworld, player, JakAndDaxterLevel.SANDOVER_VILLAGE, JakAndDaxterLevel.SENTINEL_BEACH) @@ -85,6 +93,24 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterLevel.ROCK_VILLAGE, JakAndDaxterLevel.LOST_PRECURSOR_CITY) + connect_region_to_sub(multiworld, player, + JakAndDaxterLevel.LOST_PRECURSOR_CITY, + JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM) + + connect_subregions(multiworld, player, + JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, + JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) + + connect_sub_to_region(multiworld, player, + JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM, + JakAndDaxterLevel.LOST_PRECURSOR_CITY, + lambda state: state.has(item_table[lpc_helix], player)) + + connect_sub_to_region(multiworld, player, + JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, + JakAndDaxterLevel.ROCK_VILLAGE, + lambda state: state.has(item_table[lpc_sunken], player)) + connect_regions(multiworld, player, JakAndDaxterLevel.ROCK_VILLAGE, JakAndDaxterLevel.BOGGY_SWAMP) @@ -109,10 +135,10 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, lambda state: state.has(item_table[sm_yellow_switch], player)) - connect_regions(multiworld, player, - JakAndDaxterLevel.MOUNTAIN_PASS, - JakAndDaxterLevel.VOLCANIC_CRATER, - lambda state: state.has(item_table[mp_end], player)) + connect_sub_to_region(multiworld, player, + JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE, + JakAndDaxterLevel.VOLCANIC_CRATER, + lambda state: state.has(item_table[mp_end], player)) connect_regions(multiworld, player, JakAndDaxterLevel.VOLCANIC_CRATER, @@ -185,6 +211,13 @@ def connect_region_to_sub(multiworld: MultiWorld, player: int, source: JakAndDax source_region.connect(target_region, rule=rule) +def connect_sub_to_region(multiworld: MultiWorld, player: int, source: JakAndDaxterSubLevel, target: JakAndDaxterLevel, + rule=None): + source_region = multiworld.get_region(subLevel_table[source], player) + target_region = multiworld.get_region(level_table[target], player) + source_region.connect(target_region, rule=rule) + + def connect_subregions(multiworld: MultiWorld, player: int, source: JakAndDaxterSubLevel, target: JakAndDaxterSubLevel, rule=None): source_region = multiworld.get_region(subLevel_table[source], player) diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index 1f86416d287f..1672aadbd72d 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -10,8 +10,8 @@ # Because flies are indexed from 0, each 0th fly's full ID == the power cell's ID. # So we need to offset all of their ID's in order for Archipelago to separate them -# from their power cells. We use 1,048,576 (2^20) for this purpose, because scout flies -# don't use more than 19 bits to describe themselves. +# from their power cells. We use 128 (2^7) for this purpose, because scout flies +# never use the 8th lowest bit to describe themselves. # TODO - The ID's you see below correspond directly to that fly's 32-bit ID in the game. From d22c2ca2a19bd6c00e7516b037fff579d85e5ceb Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Fri, 19 Apr 2024 08:51:09 -0400 Subject: [PATCH 12/70] Jak 1: Found Scout Fly ID's for first 4 maps. --- worlds/jakanddaxter/locs/ScoutLocations.py | 56 +++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index 1672aadbd72d..cf8fa0842055 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -17,46 +17,46 @@ # Geyser Rock locGR_scoutTable = { - 101: "GR: Scout Fly On Ground, Front", - 102: "GR: Scout Fly On Ground, Back", - 103: "GR: Scout Fly On Left Ledge", - 104: "GR: Scout Fly On Right Ledge", - 105: "GR: Scout Fly On Middle Ledge, Left", - 106: "GR: Scout Fly On Middle Ledge, Right", - 107: "GR: Scout Fly On Top Ledge" + 95: "GR: Scout Fly On Ground, Front", + 327775: "GR: Scout Fly On Ground, Back", + 393311: "GR: Scout Fly On Left Ledge", + 65631: "GR: Scout Fly On Right Ledge", + 262239: "GR: Scout Fly On Middle Ledge, Left", + 131167: "GR: Scout Fly On Middle Ledge, Right", + 196703: "GR: Scout Fly On Top Ledge" } # Sandover Village locSV_scoutTable = { - 108: "SV: Scout Fly In Fisherman's House", - 109: "SV: Scout Fly In Mayor's House", - 110: "SV: Scout Fly Under Bridge", - 111: "SV: Scout Fly Behind Sculptor's House", - 112: "SV: Scout Fly Overlooking Farmer's House", - 113: "SV: Scout Fly Near Oracle", - 114: "SV: Scout Fly In Farmer's House" + 262219: "SV: Scout Fly In Fisherman's House", + 327755: "SV: Scout Fly In Mayor's House", + 131147: "SV: Scout Fly Under Bridge", + 65611: "SV: Scout Fly Behind Sculptor's House", + 75: "SV: Scout Fly Overlooking Farmer's House", + 393291: "SV: Scout Fly Near Oracle", + 196683: "SV: Scout Fly In Farmer's House" } # Forbidden Jungle locFJ_scoutTable = { - 115: "FJ: Scout Fly At End Of Path", - 116: "FJ: Scout Fly On Spiral Of Stumps", - 117: "FJ: Scout Fly Under Bridge", - 118: "FJ: Scout Fly At End Of River", - 119: "FJ: Scout Fly Behind Lurker Machine", - 120: "FJ: Scout Fly Around Temple Spire", - 121: "FJ: Scout Fly On Top Of Temple" + 393223: "FJ: Scout Fly At End Of Path", + 262151: "FJ: Scout Fly On Spiral Of Stumps", + 7: "FJ: Scout Fly Near Dark Eco Boxes", + 196615: "FJ: Scout Fly At End Of River", + 131079: "FJ: Scout Fly Behind Lurker Machine", + 327687: "FJ: Scout Fly Around Temple Spire", + 65543: "FJ: Scout Fly On Top Of Temple" } # Sentinel Beach locSB_scoutTable = { - 122: "SB: Scout Fly At Entrance", - 123: "SB: Scout Fly Overlooking Locked Boxes", - 124: "SB: Scout Fly On Path To Flut Flut", - 125: "SB: Scout Fly Under Wood Pillars", - 126: "SB: Scout Fly Overlooking Blue Eco Vents", - 127: "SB: Scout Fly Overlooking Green Eco Vents", - 128: "SB: Scout Fly On Sentinel" + 327700: "SB: Scout Fly At Entrance", + 20: "SB: Scout Fly Overlooking Locked Boxes", + 65556: "SB: Scout Fly On Path To Flut Flut", + 262164: "SB: Scout Fly Under Wood Pillars", + 196628: "SB: Scout Fly Overlooking Blue Eco Vent", + 131092: "SB: Scout Fly Overlooking Green Eco Vents", + 393236: "SB: Scout Fly On Sentinel" } # Misty Island From 0b26a99da0d6bbfa6f2e5d66ccb61023d9676ab9 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 20 Apr 2024 13:14:00 -0400 Subject: [PATCH 13/70] Jak 1: Add more scout fly ID's, refactor game/AP ID translation for easier reading and code reuse. --- worlds/jakanddaxter/GameID.py | 12 +- worlds/jakanddaxter/Items.py | 4 +- worlds/jakanddaxter/Locations.py | 104 +++------ worlds/jakanddaxter/Regions.py | 250 ++++++++++----------- worlds/jakanddaxter/Rules.py | 190 ++++++++-------- worlds/jakanddaxter/__init__.py | 27 +-- worlds/jakanddaxter/locs/CellLocations.py | 13 ++ worlds/jakanddaxter/locs/OrbLocations.py | 37 ++- worlds/jakanddaxter/locs/ScoutLocations.py | 88 +++++--- 9 files changed, 373 insertions(+), 352 deletions(-) diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index a025f2752a2a..d3e75826601a 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -1,13 +1,5 @@ # All Jak And Daxter IDs must be offset by this number. -game_id = 746800000 +jak1_id = 741000000 # The name of the game. -game_name = "Jak and Daxter: The Precursor Legacy" - -# What follows are offsets for each Location/Item type, -# necessary for Archipelago to avoid collision between -# ID numbers shared across items. See respective -# Locations files for explanations. -cell_offset = 0 -fly_offset = 128 # 2^7 -orb_offset = 2097152 # 2^21 +jak1_name = "Jak and Daxter: The Precursor Legacy" diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 4c9f16af0e5a..d051f15869d4 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -1,7 +1,7 @@ from BaseClasses import Item -from .GameID import game_name +from .GameID import jak1_name class JakAndDaxterItem(Item): - game: str = game_name + game: str = jak1_name diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index 84dd27f6ed92..ef56137cf176 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,78 +1,46 @@ from BaseClasses import Location -from .GameID import game_id, game_name, cell_offset, fly_offset -from .locs import CellLocations, ScoutLocations +from .GameID import jak1_name +from .locs import CellLocations as Cells, ScoutLocations as Scouts class JakAndDaxterLocation(Location): - game: str = game_name + game: str = jak1_name # All Locations # Because all items in Jak And Daxter are unique and do not regenerate, we can use this same table as our item table. -# Each Item ID == its corresponding Location ID. And then we only have to do this ugly math once. +# Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed. location_table = { - **{game_id + cell_offset + k: CellLocations.locGR_cellTable[k] - for k in CellLocations.locGR_cellTable}, - **{game_id + cell_offset + k: CellLocations.locSV_cellTable[k] - for k in CellLocations.locSV_cellTable}, - **{game_id + cell_offset + k: CellLocations.locFJ_cellTable[k] - for k in CellLocations.locFJ_cellTable}, - **{game_id + cell_offset + k: CellLocations.locSB_cellTable[k] - for k in CellLocations.locSB_cellTable}, - **{game_id + cell_offset + k: CellLocations.locMI_cellTable[k] - for k in CellLocations.locMI_cellTable}, - **{game_id + cell_offset + k: CellLocations.locFC_cellTable[k] - for k in CellLocations.locFC_cellTable}, - **{game_id + cell_offset + k: CellLocations.locRV_cellTable[k] - for k in CellLocations.locRV_cellTable}, - **{game_id + cell_offset + k: CellLocations.locPB_cellTable[k] - for k in CellLocations.locPB_cellTable}, - **{game_id + cell_offset + k: CellLocations.locLPC_cellTable[k] - for k in CellLocations.locLPC_cellTable}, - **{game_id + cell_offset + k: CellLocations.locBS_cellTable[k] - for k in CellLocations.locBS_cellTable}, - **{game_id + cell_offset + k: CellLocations.locMP_cellTable[k] - for k in CellLocations.locMP_cellTable}, - **{game_id + cell_offset + k: CellLocations.locVC_cellTable[k] - for k in CellLocations.locVC_cellTable}, - **{game_id + cell_offset + k: CellLocations.locSC_cellTable[k] - for k in CellLocations.locSC_cellTable}, - **{game_id + cell_offset + k: CellLocations.locSM_cellTable[k] - for k in CellLocations.locSM_cellTable}, - **{game_id + cell_offset + k: CellLocations.locLT_cellTable[k] - for k in CellLocations.locLT_cellTable}, - **{game_id + cell_offset + k: CellLocations.locGMC_cellTable[k] - for k in CellLocations.locGMC_cellTable}, - **{game_id + fly_offset + k: ScoutLocations.locGR_scoutTable[k] - for k in ScoutLocations.locGR_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locSV_scoutTable[k] - for k in ScoutLocations.locSV_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locFJ_scoutTable[k] - for k in ScoutLocations.locFJ_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locSB_scoutTable[k] - for k in ScoutLocations.locSB_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locMI_scoutTable[k] - for k in ScoutLocations.locMI_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locFC_scoutTable[k] - for k in ScoutLocations.locFC_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locRV_scoutTable[k] - for k in ScoutLocations.locRV_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locPB_scoutTable[k] - for k in ScoutLocations.locPB_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locLPC_scoutTable[k] - for k in ScoutLocations.locLPC_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locBS_scoutTable[k] - for k in ScoutLocations.locBS_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locMP_scoutTable[k] - for k in ScoutLocations.locMP_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locVC_scoutTable[k] - for k in ScoutLocations.locVC_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locSC_scoutTable[k] - for k in ScoutLocations.locSC_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locSM_scoutTable[k] - for k in ScoutLocations.locSM_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locLT_scoutTable[k] - for k in ScoutLocations.locLT_scoutTable}, - **{game_id + fly_offset + k: ScoutLocations.locGMC_scoutTable[k] - for k in ScoutLocations.locGMC_scoutTable} + **{Cells.to_ap_id(k): Cells.locGR_cellTable[k] for k in Cells.locGR_cellTable}, + **{Cells.to_ap_id(k): Cells.locSV_cellTable[k] for k in Cells.locSV_cellTable}, + **{Cells.to_ap_id(k): Cells.locFJ_cellTable[k] for k in Cells.locFJ_cellTable}, + **{Cells.to_ap_id(k): Cells.locSB_cellTable[k] for k in Cells.locSB_cellTable}, + **{Cells.to_ap_id(k): Cells.locMI_cellTable[k] for k in Cells.locMI_cellTable}, + **{Cells.to_ap_id(k): Cells.locFC_cellTable[k] for k in Cells.locFC_cellTable}, + **{Cells.to_ap_id(k): Cells.locRV_cellTable[k] for k in Cells.locRV_cellTable}, + **{Cells.to_ap_id(k): Cells.locPB_cellTable[k] for k in Cells.locPB_cellTable}, + **{Cells.to_ap_id(k): Cells.locLPC_cellTable[k] for k in Cells.locLPC_cellTable}, + **{Cells.to_ap_id(k): Cells.locBS_cellTable[k] for k in Cells.locBS_cellTable}, + **{Cells.to_ap_id(k): Cells.locMP_cellTable[k] for k in Cells.locMP_cellTable}, + **{Cells.to_ap_id(k): Cells.locVC_cellTable[k] for k in Cells.locVC_cellTable}, + **{Cells.to_ap_id(k): Cells.locSC_cellTable[k] for k in Cells.locSC_cellTable}, + **{Cells.to_ap_id(k): Cells.locSM_cellTable[k] for k in Cells.locSM_cellTable}, + **{Cells.to_ap_id(k): Cells.locLT_cellTable[k] for k in Cells.locLT_cellTable}, + **{Cells.to_ap_id(k): Cells.locGMC_cellTable[k] for k in Cells.locGMC_cellTable}, + **{Scouts.to_ap_id(k): Scouts.locGR_scoutTable[k] for k in Scouts.locGR_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSV_scoutTable[k] for k in Scouts.locSV_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locFJ_scoutTable[k] for k in Scouts.locFJ_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSB_scoutTable[k] for k in Scouts.locSB_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locMI_scoutTable[k] for k in Scouts.locMI_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locFC_scoutTable[k] for k in Scouts.locFC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locRV_scoutTable[k] for k in Scouts.locRV_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locPB_scoutTable[k] for k in Scouts.locPB_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locLPC_scoutTable[k] for k in Scouts.locLPC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locBS_scoutTable[k] for k in Scouts.locBS_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locMP_scoutTable[k] for k in Scouts.locMP_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locVC_scoutTable[k] for k in Scouts.locVC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSC_scoutTable[k] for k in Scouts.locSC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSM_scoutTable[k] for k in Scouts.locSM_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locLT_scoutTable[k] for k in Scouts.locLT_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable} } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 8fb62c9eda82..8b4afe790cfc 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,13 +1,13 @@ import typing from enum import Enum, auto from BaseClasses import MultiWorld, Region -from .GameID import game_id, game_name, cell_offset, fly_offset +from .GameID import jak1_name from .Options import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table -from .locs import CellLocations, ScoutLocations +from .locs import CellLocations as Cells, ScoutLocations as Scouts -class JakAndDaxterLevel(int, Enum): +class Jak1Level(int, Enum): GEYSER_ROCK = auto() SANDOVER_VILLAGE = auto() FORBIDDEN_JUNGLE = auto() @@ -26,7 +26,7 @@ class JakAndDaxterLevel(int, Enum): GOL_AND_MAIAS_CITADEL = auto() -class JakAndDaxterSubLevel(int, Enum): +class Jak1SubLevel(int, Enum): MAIN_AREA = auto() FORBIDDEN_JUNGLE_SWITCH_ROOM = auto() FORBIDDEN_JUNGLE_PLANT_ROOM = auto() @@ -44,165 +44,161 @@ class JakAndDaxterSubLevel(int, Enum): GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto() -level_table: typing.Dict[JakAndDaxterLevel, str] = { - JakAndDaxterLevel.GEYSER_ROCK: "Geyser Rock", - JakAndDaxterLevel.SANDOVER_VILLAGE: "Sandover Village", - JakAndDaxterLevel.FORBIDDEN_JUNGLE: "Forbidden Jungle", - JakAndDaxterLevel.SENTINEL_BEACH: "Sentinel Beach", - JakAndDaxterLevel.MISTY_ISLAND: "Misty Island", - JakAndDaxterLevel.FIRE_CANYON: "Fire Canyon", - JakAndDaxterLevel.ROCK_VILLAGE: "Rock Village", - JakAndDaxterLevel.PRECURSOR_BASIN: "Precursor Basin", - JakAndDaxterLevel.LOST_PRECURSOR_CITY: "Lost Precursor City", - JakAndDaxterLevel.BOGGY_SWAMP: "Boggy Swamp", - JakAndDaxterLevel.MOUNTAIN_PASS: "Mountain Pass", - JakAndDaxterLevel.VOLCANIC_CRATER: "Volcanic Crater", - JakAndDaxterLevel.SPIDER_CAVE: "Spider Cave", - JakAndDaxterLevel.SNOWY_MOUNTAIN: "Snowy Mountain", - JakAndDaxterLevel.LAVA_TUBE: "Lava Tube", - JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL: "Gol and Maia's Citadel" +level_table: typing.Dict[Jak1Level, str] = { + Jak1Level.GEYSER_ROCK: "Geyser Rock", + Jak1Level.SANDOVER_VILLAGE: "Sandover Village", + Jak1Level.FORBIDDEN_JUNGLE: "Forbidden Jungle", + Jak1Level.SENTINEL_BEACH: "Sentinel Beach", + Jak1Level.MISTY_ISLAND: "Misty Island", + Jak1Level.FIRE_CANYON: "Fire Canyon", + Jak1Level.ROCK_VILLAGE: "Rock Village", + Jak1Level.PRECURSOR_BASIN: "Precursor Basin", + Jak1Level.LOST_PRECURSOR_CITY: "Lost Precursor City", + Jak1Level.BOGGY_SWAMP: "Boggy Swamp", + Jak1Level.MOUNTAIN_PASS: "Mountain Pass", + Jak1Level.VOLCANIC_CRATER: "Volcanic Crater", + Jak1Level.SPIDER_CAVE: "Spider Cave", + Jak1Level.SNOWY_MOUNTAIN: "Snowy Mountain", + Jak1Level.LAVA_TUBE: "Lava Tube", + Jak1Level.GOL_AND_MAIAS_CITADEL: "Gol and Maia's Citadel" } -subLevel_table: typing.Dict[JakAndDaxterSubLevel, str] = { - JakAndDaxterSubLevel.MAIN_AREA: "Main Area", - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: "Forbidden Jungle Switch Room", - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room", - JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower", - JakAndDaxterSubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings", - JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: "Lost Precursor City Sunken Room", - JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: "Lost Precursor City Helix Room", - JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut", - JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race", - JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut", - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort", - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: "Snowy Mountain Frozen Box", - JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower", - JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss" +subLevel_table: typing.Dict[Jak1SubLevel, str] = { + Jak1SubLevel.MAIN_AREA: "Main Area", + Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: "Forbidden Jungle Switch Room", + Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room", + Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower", + Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings", + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: "Lost Precursor City Sunken Room", + Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: "Lost Precursor City Helix Room", + Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut", + Jak1SubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race", + Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", + Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut", + Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort", + Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: "Snowy Mountain Frozen Box", + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower", + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss" } class JakAndDaxterRegion(Region): - game: str = game_name + game: str = jak1_name -# Use the original ID's for each item to tell the Region which Locations are available in it. -# You do NOT need to add the item offsets, that will be handled by create_*_locations. +# Use the original game ID's for each item to tell the Region which Locations are available in it. +# You do NOT need to add the item offsets or game ID, that will be handled by create_*_locations. def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): create_region(player, multiworld, "Menu") - region_gr = create_region(player, multiworld, level_table[JakAndDaxterLevel.GEYSER_ROCK]) - create_cell_locations(region_gr, CellLocations.locGR_cellTable) - create_fly_locations(region_gr, ScoutLocations.locGR_scoutTable) + region_gr = create_region(player, multiworld, level_table[Jak1Level.GEYSER_ROCK]) + create_cell_locations(region_gr, Cells.locGR_cellTable) + create_fly_locations(region_gr, Scouts.locGR_scoutTable) - region_sv = create_region(player, multiworld, level_table[JakAndDaxterLevel.SANDOVER_VILLAGE]) - create_cell_locations(region_sv, CellLocations.locSV_cellTable) - create_fly_locations(region_sv, ScoutLocations.locSV_scoutTable) + region_sv = create_region(player, multiworld, level_table[Jak1Level.SANDOVER_VILLAGE]) + create_cell_locations(region_sv, Cells.locSV_cellTable) + create_fly_locations(region_sv, Scouts.locSV_scoutTable) - region_fj = create_region(player, multiworld, level_table[JakAndDaxterLevel.FORBIDDEN_JUNGLE]) - create_cell_locations(region_fj, {k: CellLocations.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9, 7}}) - create_fly_locations(region_fj, ScoutLocations.locFJ_scoutTable) + region_fj = create_region(player, multiworld, level_table[Jak1Level.FORBIDDEN_JUNGLE]) + create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9, 7}}) + create_fly_locations(region_fj, Scouts.locFJ_scoutTable) - sub_region_fjsr = create_subregion(region_fj, subLevel_table[JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM]) - create_cell_locations(sub_region_fjsr, {k: CellLocations.locFJ_cellTable[k] for k in {2}}) + sub_region_fjsr = create_subregion(region_fj, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM]) + create_cell_locations(sub_region_fjsr, {k: Cells.locFJ_cellTable[k] for k in {2}}) - sub_region_fjpr = create_subregion(sub_region_fjsr, subLevel_table[JakAndDaxterSubLevel - .FORBIDDEN_JUNGLE_PLANT_ROOM]) - create_cell_locations(sub_region_fjpr, {k: CellLocations.locFJ_cellTable[k] for k in {6}}) + sub_region_fjpr = create_subregion(sub_region_fjsr, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) + create_cell_locations(sub_region_fjpr, {k: Cells.locFJ_cellTable[k] for k in {6}}) - region_sb = create_region(player, multiworld, level_table[JakAndDaxterLevel.SENTINEL_BEACH]) - create_cell_locations(region_sb, {k: CellLocations.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22, 20}}) - create_fly_locations(region_sb, ScoutLocations.locSB_scoutTable) + region_sb = create_region(player, multiworld, level_table[Jak1Level.SENTINEL_BEACH]) + create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22, 20}}) + create_fly_locations(region_sb, Scouts.locSB_scoutTable) - sub_region_sbct = create_subregion(region_sb, subLevel_table[JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER]) - create_cell_locations(sub_region_sbct, {k: CellLocations.locSB_cellTable[k] for k in {19}}) + sub_region_sbct = create_subregion(region_sb, subLevel_table[Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER]) + create_cell_locations(sub_region_sbct, {k: Cells.locSB_cellTable[k] for k in {19}}) - region_mi = create_region(player, multiworld, level_table[JakAndDaxterLevel.MISTY_ISLAND]) - create_cell_locations(region_mi, CellLocations.locMI_cellTable) - create_fly_locations(region_mi, ScoutLocations.locMI_scoutTable) + region_mi = create_region(player, multiworld, level_table[Jak1Level.MISTY_ISLAND]) + create_cell_locations(region_mi, Cells.locMI_cellTable) + create_fly_locations(region_mi, Scouts.locMI_scoutTable) - region_fc = create_region(player, multiworld, level_table[JakAndDaxterLevel.FIRE_CANYON]) - create_cell_locations(region_fc, CellLocations.locFC_cellTable) - create_fly_locations(region_fc, ScoutLocations.locFC_scoutTable) + region_fc = create_region(player, multiworld, level_table[Jak1Level.FIRE_CANYON]) + create_cell_locations(region_fc, Cells.locFC_cellTable) + create_fly_locations(region_fc, Scouts.locFC_scoutTable) - region_rv = create_region(player, multiworld, level_table[JakAndDaxterLevel.ROCK_VILLAGE]) - create_cell_locations(region_rv, CellLocations.locRV_cellTable) - create_fly_locations(region_rv, ScoutLocations.locRV_scoutTable) + region_rv = create_region(player, multiworld, level_table[Jak1Level.ROCK_VILLAGE]) + create_cell_locations(region_rv, Cells.locRV_cellTable) + create_fly_locations(region_rv, Scouts.locRV_scoutTable) - region_pb = create_region(player, multiworld, level_table[JakAndDaxterLevel.PRECURSOR_BASIN]) - create_cell_locations(region_pb, {k: CellLocations.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58, 57}}) - create_fly_locations(region_pb, ScoutLocations.locPB_scoutTable) + region_pb = create_region(player, multiworld, level_table[Jak1Level.PRECURSOR_BASIN]) + create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58, 57}}) + create_fly_locations(region_pb, Scouts.locPB_scoutTable) - sub_region_pbbr = create_subregion(region_pb, subLevel_table[JakAndDaxterSubLevel.PRECURSOR_BASIN_BLUE_RINGS]) - create_cell_locations(sub_region_pbbr, {k: CellLocations.locPB_cellTable[k] for k in {59}}) + sub_region_pbbr = create_subregion(region_pb, subLevel_table[Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS]) + create_cell_locations(sub_region_pbbr, {k: Cells.locPB_cellTable[k] for k in {59}}) - region_lpc = create_region(player, multiworld, level_table[JakAndDaxterLevel.LOST_PRECURSOR_CITY]) - create_cell_locations(region_lpc, {k: CellLocations.locLPC_cellTable[k] for k in {45, 48, 44, 51}}) - create_fly_locations(region_lpc, {k: ScoutLocations.locLPC_scoutTable[k] for k in {157, 158, 159, 160, 161, 162}}) + region_lpc = create_region(player, multiworld, level_table[Jak1Level.LOST_PRECURSOR_CITY]) + create_cell_locations(region_lpc, {k: Cells.locLPC_cellTable[k] for k in {45, 48, 44, 51}}) + create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {157, 158, 159, 160, 161, 162}}) - sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[JakAndDaxterSubLevel - .LOST_PRECURSOR_CITY_SUNKEN_ROOM]) - create_cell_locations(sub_region_lpcsr, {k: CellLocations.locLPC_cellTable[k] for k in {47, 49}}) - create_fly_locations(region_lpc, {k: ScoutLocations.locLPC_scoutTable[k] for k in {163}}) + sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM]) + create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47, 49}}) + create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {163}}) - sub_region_lpchr = create_subregion(region_lpc, subLevel_table[JakAndDaxterSubLevel - .LOST_PRECURSOR_CITY_HELIX_ROOM]) - create_cell_locations(sub_region_lpchr, {k: CellLocations.locLPC_cellTable[k] for k in {46, 50}}) + sub_region_lpchr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM]) + create_cell_locations(sub_region_lpchr, {k: Cells.locLPC_cellTable[k] for k in {46, 50}}) - region_bs = create_region(player, multiworld, level_table[JakAndDaxterLevel.BOGGY_SWAMP]) - create_cell_locations(region_bs, {k: CellLocations.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) - create_fly_locations(region_bs, {k: ScoutLocations.locBS_scoutTable[k] for k in {164, 165, 166, 167, 170}}) + region_bs = create_region(player, multiworld, level_table[Jak1Level.BOGGY_SWAMP]) + create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) + create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {164, 165, 166, 167, 170}}) - sub_region_bsff = create_subregion(region_bs, subLevel_table[JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT]) - create_cell_locations(sub_region_bsff, {k: CellLocations.locBS_cellTable[k] for k in {43, 37}}) - create_fly_locations(sub_region_bsff, {k: ScoutLocations.locBS_scoutTable[k] for k in {168, 169}}) + sub_region_bsff = create_subregion(region_bs, subLevel_table[Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT]) + create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {43, 37}}) + create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {168, 169}}) - region_mp = create_region(player, multiworld, level_table[JakAndDaxterLevel.MOUNTAIN_PASS]) - create_cell_locations(region_mp, {k: CellLocations.locMP_cellTable[k] for k in {86}}) + region_mp = create_region(player, multiworld, level_table[Jak1Level.MOUNTAIN_PASS]) + create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86}}) - sub_region_mpr = create_subregion(region_mp, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE]) - create_cell_locations(sub_region_mpr, {k: CellLocations.locMP_cellTable[k] for k in {87, 88}}) - create_fly_locations(sub_region_mpr, ScoutLocations.locMP_scoutTable) + sub_region_mpr = create_subregion(region_mp, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_RACE]) + create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87, 88}}) + create_fly_locations(sub_region_mpr, Scouts.locMP_scoutTable) - sub_region_mps = create_subregion(sub_region_mpr, subLevel_table[JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT]) - create_cell_locations(sub_region_mps, {k: CellLocations.locMP_cellTable[k] for k in {110}}) + sub_region_mps = create_subregion(sub_region_mpr, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT]) + create_cell_locations(sub_region_mps, {k: Cells.locMP_cellTable[k] for k in {110}}) - region_vc = create_region(player, multiworld, level_table[JakAndDaxterLevel.VOLCANIC_CRATER]) - create_cell_locations(region_vc, CellLocations.locVC_cellTable) - create_fly_locations(region_vc, ScoutLocations.locVC_scoutTable) + region_vc = create_region(player, multiworld, level_table[Jak1Level.VOLCANIC_CRATER]) + create_cell_locations(region_vc, Cells.locVC_cellTable) + create_fly_locations(region_vc, Scouts.locVC_scoutTable) - region_sc = create_region(player, multiworld, level_table[JakAndDaxterLevel.SPIDER_CAVE]) - create_cell_locations(region_sc, CellLocations.locSC_cellTable) - create_fly_locations(region_sc, ScoutLocations.locSC_scoutTable) + region_sc = create_region(player, multiworld, level_table[Jak1Level.SPIDER_CAVE]) + create_cell_locations(region_sc, Cells.locSC_cellTable) + create_fly_locations(region_sc, Scouts.locSC_scoutTable) - region_sm = create_region(player, multiworld, level_table[JakAndDaxterLevel.SNOWY_MOUNTAIN]) - create_cell_locations(region_sm, {k: CellLocations.locSM_cellTable[k] for k in {60, 61, 66, 64}}) - create_fly_locations(region_sm, {k: ScoutLocations.locSM_scoutTable[k] for k in {192, 193, 194, 195, 196}}) + region_sm = create_region(player, multiworld, level_table[Jak1Level.SNOWY_MOUNTAIN]) + create_cell_locations(region_sm, {k: Cells.locSM_cellTable[k] for k in {60, 61, 66, 64}}) + create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {192, 193, 194, 195, 196}}) - sub_region_smfb = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FROZEN_BOX]) - create_cell_locations(sub_region_smfb, {k: CellLocations.locSM_cellTable[k] for k in {67}}) + sub_region_smfb = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX]) + create_cell_locations(sub_region_smfb, {k: Cells.locSM_cellTable[k] for k in {67}}) - sub_region_smff = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) - create_cell_locations(sub_region_smff, {k: CellLocations.locSM_cellTable[k] for k in {63}}) + sub_region_smff = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) + create_cell_locations(sub_region_smff, {k: Cells.locSM_cellTable[k] for k in {63}}) - sub_region_smlf = create_subregion(region_sm, subLevel_table[JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) - create_cell_locations(sub_region_smlf, {k: CellLocations.locSM_cellTable[k] for k in {62, 65}}) - create_fly_locations(sub_region_smlf, {k: ScoutLocations.locSM_scoutTable[k] for k in {197, 198}}) + sub_region_smlf = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) + create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62, 65}}) + create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {197, 198}}) - region_lt = create_region(player, multiworld, level_table[JakAndDaxterLevel.LAVA_TUBE]) - create_cell_locations(region_lt, CellLocations.locLT_cellTable) - create_fly_locations(region_lt, ScoutLocations.locLT_scoutTable) + region_lt = create_region(player, multiworld, level_table[Jak1Level.LAVA_TUBE]) + create_cell_locations(region_lt, Cells.locLT_cellTable) + create_fly_locations(region_lt, Scouts.locLT_scoutTable) - region_gmc = create_region(player, multiworld, level_table[JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL]) - create_cell_locations(region_gmc, {k: CellLocations.locGMC_cellTable[k] for k in {71, 72, 73}}) - create_fly_locations(region_gmc, {k: ScoutLocations.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}}) + region_gmc = create_region(player, multiworld, level_table[Jak1Level.GOL_AND_MAIAS_CITADEL]) + create_cell_locations(region_gmc, {k: Cells.locGMC_cellTable[k] for k in {71, 72, 73}}) + create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}}) - sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[JakAndDaxterSubLevel - .GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) - create_cell_locations(sub_region_gmcrt, {k: CellLocations.locGMC_cellTable[k] for k in {70, 91}}) - create_fly_locations(sub_region_gmcrt, {k: ScoutLocations.locGMC_scoutTable[k] for k in {212}}) + sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) + create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70, 91}}) + create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {212}}) - create_subregion(sub_region_gmcrt, subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS]) + create_subregion(sub_region_gmcrt, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS]) def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: @@ -219,13 +215,13 @@ def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: def create_cell_locations(region: Region, locations: typing.Dict[int, str]): region.locations += [JakAndDaxterLocation(region.player, - location_table[game_id + cell_offset + loc], - game_id + cell_offset + loc, + location_table[Cells.to_ap_id(loc)], + Cells.to_ap_id(loc), region) for loc in locations] def create_fly_locations(region: Region, locations: typing.Dict[int, str]): region.locations += [JakAndDaxterLocation(region.player, - location_table[game_id + fly_offset + loc], - game_id + fly_offset + loc, + location_table[Scouts.to_ap_id(loc)], + Scouts.to_ap_id(loc), region) for loc in locations] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 58585bba18d3..d0c778f9d935 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,225 +1,221 @@ from BaseClasses import MultiWorld -from .GameID import game_id, cell_offset, fly_offset from .Options import JakAndDaxterOptions -from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, level_table, subLevel_table +from .Regions import Jak1Level, Jak1SubLevel, level_table, subLevel_table from .Locations import location_table as item_table -from .locs.CellLocations import locGR_cellTable +from .locs import CellLocations as Cells, ScoutLocations as Scouts def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): # Setting up some useful variables here because the offset numbers can get confusing # for access rules. Feel free to add more variables here to keep the code more readable. - gr_cells = {game_id + cell_offset + k for k in locGR_cellTable} - fj_temple_top = game_id + cell_offset + 4 - fj_blue_switch = game_id + cell_offset + 2 - fj_plant_boss = game_id + cell_offset + 6 - fj_fisherman = game_id + cell_offset + 5 - sb_flut_flut = game_id + cell_offset + 17 - fc_end = game_id + cell_offset + 69 - pb_purple_rings = game_id + cell_offset + 58 - lpc_sunken = game_id + cell_offset + 47 - lpc_helix = game_id + cell_offset + 50 - mp_klaww = game_id + cell_offset + 86 - mp_end = game_id + cell_offset + 87 - sm_yellow_switch = game_id + cell_offset + 60 - sm_fort_gate = game_id + cell_offset + 63 - lt_end = game_id + cell_offset + 89 - gmc_blue_sage = game_id + cell_offset + 71 - gmc_red_sage = game_id + cell_offset + 72 - gmc_yellow_sage = game_id + cell_offset + 73 - gmc_green_sage = game_id + cell_offset + 70 + # You DO need to convert the game ID's to AP ID's here. + gr_cells = {Cells.to_ap_id(k) for k in Cells.locGR_cellTable} + fj_temple_top = Cells.to_ap_id(4) + fj_blue_switch = Cells.to_ap_id(2) + fj_plant_boss = Cells.to_ap_id(6) + fj_fisherman = Cells.to_ap_id(5) + sb_flut_flut = Cells.to_ap_id(17) + fc_end = Cells.to_ap_id(69) + pb_purple_rings = Cells.to_ap_id(58) + lpc_sunken = Cells.to_ap_id(47) + lpc_helix = Cells.to_ap_id(50) + mp_klaww = Cells.to_ap_id(86) + mp_end = Cells.to_ap_id(87) + sm_yellow_switch = Cells.to_ap_id(60) + sm_fort_gate = Cells.to_ap_id(63) + lt_end = Cells.to_ap_id(89) + gmc_blue_sage = Cells.to_ap_id(71) + gmc_red_sage = Cells.to_ap_id(72) + gmc_yellow_sage = Cells.to_ap_id(73) + gmc_green_sage = Cells.to_ap_id(70) # Start connecting regions and set their access rules. - connect_start(multiworld, player, JakAndDaxterLevel.GEYSER_ROCK) + connect_start(multiworld, player, Jak1Level.GEYSER_ROCK) connect_regions(multiworld, player, - JakAndDaxterLevel.GEYSER_ROCK, - JakAndDaxterLevel.SANDOVER_VILLAGE, + Jak1Level.GEYSER_ROCK, + Jak1Level.SANDOVER_VILLAGE, lambda state: state.has_all({item_table[k] for k in gr_cells}, player)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.FORBIDDEN_JUNGLE) + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.FORBIDDEN_JUNGLE) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.FORBIDDEN_JUNGLE, - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, + Jak1Level.FORBIDDEN_JUNGLE, + Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, lambda state: state.has(item_table[fj_temple_top], player)) connect_subregions(multiworld, player, - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, + Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, lambda state: state.has(item_table[fj_blue_switch], player)) connect_sub_to_region(multiworld, player, - JakAndDaxterSubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - JakAndDaxterLevel.FORBIDDEN_JUNGLE, + Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + Jak1Level.FORBIDDEN_JUNGLE, lambda state: state.has(item_table[fj_plant_boss], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.SENTINEL_BEACH) + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.SENTINEL_BEACH) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.SENTINEL_BEACH, - JakAndDaxterSubLevel.SENTINEL_BEACH_CANNON_TOWER, + Jak1Level.SENTINEL_BEACH, + Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER, lambda state: state.has(item_table[fj_blue_switch], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.MISTY_ISLAND, + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.MISTY_ISLAND, lambda state: state.has(item_table[fj_fisherman], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.SANDOVER_VILLAGE, - JakAndDaxterLevel.FIRE_CANYON, + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.FIRE_CANYON, lambda state: state.count_group("Power Cell", player) >= 20) connect_regions(multiworld, player, - JakAndDaxterLevel.FIRE_CANYON, - JakAndDaxterLevel.ROCK_VILLAGE, + Jak1Level.FIRE_CANYON, + Jak1Level.ROCK_VILLAGE, lambda state: state.has(item_table[fc_end], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.PRECURSOR_BASIN) + Jak1Level.ROCK_VILLAGE, + Jak1Level.PRECURSOR_BASIN) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.PRECURSOR_BASIN, - JakAndDaxterSubLevel.PRECURSOR_BASIN_BLUE_RINGS, + Jak1Level.PRECURSOR_BASIN, + Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS, lambda state: state.has(item_table[pb_purple_rings], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.LOST_PRECURSOR_CITY) + Jak1Level.ROCK_VILLAGE, + Jak1Level.LOST_PRECURSOR_CITY) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.LOST_PRECURSOR_CITY, - JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM) + Jak1Level.LOST_PRECURSOR_CITY, + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM) connect_subregions(multiworld, player, - JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, - JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, + Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) connect_sub_to_region(multiworld, player, - JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM, - JakAndDaxterLevel.LOST_PRECURSOR_CITY, + Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM, + Jak1Level.LOST_PRECURSOR_CITY, lambda state: state.has(item_table[lpc_helix], player)) connect_sub_to_region(multiworld, player, - JakAndDaxterSubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, - JakAndDaxterLevel.ROCK_VILLAGE, + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, + Jak1Level.ROCK_VILLAGE, lambda state: state.has(item_table[lpc_sunken], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.BOGGY_SWAMP) + Jak1Level.ROCK_VILLAGE, + Jak1Level.BOGGY_SWAMP) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.BOGGY_SWAMP, - JakAndDaxterSubLevel.BOGGY_SWAMP_FLUT_FLUT, + Jak1Level.BOGGY_SWAMP, + Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT, lambda state: state.has(item_table[sb_flut_flut], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.ROCK_VILLAGE, - JakAndDaxterLevel.MOUNTAIN_PASS, + Jak1Level.ROCK_VILLAGE, + Jak1Level.MOUNTAIN_PASS, lambda state: state.count_group("Power Cell", player) >= 45) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.MOUNTAIN_PASS, - JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE, + Jak1Level.MOUNTAIN_PASS, + Jak1SubLevel.MOUNTAIN_PASS_RACE, lambda state: state.has(item_table[mp_klaww], player)) connect_subregions(multiworld, player, - JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE, - JakAndDaxterSubLevel.MOUNTAIN_PASS_SHORTCUT, + Jak1SubLevel.MOUNTAIN_PASS_RACE, + Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT, lambda state: state.has(item_table[sm_yellow_switch], player)) connect_sub_to_region(multiworld, player, - JakAndDaxterSubLevel.MOUNTAIN_PASS_RACE, - JakAndDaxterLevel.VOLCANIC_CRATER, + Jak1SubLevel.MOUNTAIN_PASS_RACE, + Jak1Level.VOLCANIC_CRATER, lambda state: state.has(item_table[mp_end], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.VOLCANIC_CRATER, - JakAndDaxterLevel.SPIDER_CAVE) + Jak1Level.VOLCANIC_CRATER, + Jak1Level.SPIDER_CAVE) connect_regions(multiworld, player, - JakAndDaxterLevel.VOLCANIC_CRATER, - JakAndDaxterLevel.SNOWY_MOUNTAIN) + Jak1Level.VOLCANIC_CRATER, + Jak1Level.SNOWY_MOUNTAIN) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.SNOWY_MOUNTAIN, - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FROZEN_BOX, + Jak1Level.SNOWY_MOUNTAIN, + Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX, lambda state: state.has(item_table[sm_yellow_switch], player)) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.SNOWY_MOUNTAIN, - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, + Jak1Level.SNOWY_MOUNTAIN, + Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, lambda state: state.has(item_table[sb_flut_flut], player)) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.SNOWY_MOUNTAIN, - JakAndDaxterSubLevel.SNOWY_MOUNTAIN_LURKER_FORT, + Jak1Level.SNOWY_MOUNTAIN, + Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT, lambda state: state.has(item_table[sm_fort_gate], player)) connect_regions(multiworld, player, - JakAndDaxterLevel.VOLCANIC_CRATER, - JakAndDaxterLevel.LAVA_TUBE, + Jak1Level.VOLCANIC_CRATER, + Jak1Level.LAVA_TUBE, lambda state: state.count_group("Power Cell", player) >= 72) connect_regions(multiworld, player, - JakAndDaxterLevel.LAVA_TUBE, - JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, + Jak1Level.LAVA_TUBE, + Jak1Level.GOL_AND_MAIAS_CITADEL, lambda state: state.has(item_table[lt_end], player)) connect_region_to_sub(multiworld, player, - JakAndDaxterLevel.GOL_AND_MAIAS_CITADEL, - JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, + Jak1Level.GOL_AND_MAIAS_CITADEL, + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, lambda state: state.has(item_table[gmc_blue_sage], player) and state.has(item_table[gmc_red_sage], player) and state.has(item_table[gmc_yellow_sage], player)) connect_subregions(multiworld, player, - JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS, + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS, lambda state: state.has(item_table[gmc_green_sage], player)) multiworld.completion_condition[player] = lambda state: state.can_reach( - multiworld.get_region(subLevel_table[JakAndDaxterSubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player), + multiworld.get_region(subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player), "Region", player) -def connect_start(multiworld: MultiWorld, player: int, target: JakAndDaxterLevel): +def connect_start(multiworld: MultiWorld, player: int, target: Jak1Level): menu_region = multiworld.get_region("Menu", player) start_region = multiworld.get_region(level_table[target], player) menu_region.connect(start_region) -def connect_regions(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterLevel, - rule=None): +def connect_regions(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1Level, rule=None): source_region = multiworld.get_region(level_table[source], player) target_region = multiworld.get_region(level_table[target], player) source_region.connect(target_region, rule=rule) -def connect_region_to_sub(multiworld: MultiWorld, player: int, source: JakAndDaxterLevel, target: JakAndDaxterSubLevel, - rule=None): +def connect_region_to_sub(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1SubLevel, rule=None): source_region = multiworld.get_region(level_table[source], player) target_region = multiworld.get_region(subLevel_table[target], player) source_region.connect(target_region, rule=rule) -def connect_sub_to_region(multiworld: MultiWorld, player: int, source: JakAndDaxterSubLevel, target: JakAndDaxterLevel, - rule=None): +def connect_sub_to_region(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1Level, rule=None): source_region = multiworld.get_region(subLevel_table[source], player) target_region = multiworld.get_region(level_table[target], player) source_region.connect(target_region, rule=rule) -def connect_subregions(multiworld: MultiWorld, player: int, source: JakAndDaxterSubLevel, target: JakAndDaxterSubLevel, - rule=None): +def connect_subregions(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1SubLevel, rule=None): source_region = multiworld.get_region(subLevel_table[source], player) target_region = multiworld.get_region(subLevel_table[target], player) source_region.connect(target_region, rule=rule) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 3a99126b0d3c..75c498e12fd2 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,16 +1,16 @@ from BaseClasses import Item, ItemClassification -from .Locations import JakAndDaxterLocation, location_table as item_table +from .GameID import jak1_id, jak1_name from .Options import JakAndDaxterOptions -from .Regions import JakAndDaxterLevel, JakAndDaxterSubLevel, JakAndDaxterRegion, level_table, subLevel_table, \ - create_regions -from .Rules import set_rules from .Items import JakAndDaxterItem -from .GameID import game_id, game_name, cell_offset, fly_offset, orb_offset +from .Locations import JakAndDaxterLocation, location_table as item_table +from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs +from .Regions import create_regions +from .Rules import set_rules from ..AutoWorld import World class JakAndDaxterWorld(World): - game: str = game_name + game: str = jak1_name data_version = 1 required_client_version = (0, 4, 5) @@ -18,13 +18,14 @@ class JakAndDaxterWorld(World): options: JakAndDaxterOptions # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. + # Remember, the game ID and various offsets for each item type have already been calculated. item_name_to_id = {item_table[k]: k for k in item_table} location_name_to_id = {item_table[k]: k for k in item_table} item_name_groups = { - "Power Cell": {item_table[k]: k - for k in item_table if k in range(game_id + cell_offset, game_id + fly_offset)}, - "Scout Fly": {item_table[k]: k - for k in item_table if k in range(game_id + fly_offset, game_id + orb_offset)}, + "Power Cell": {item_table[k]: k for k in item_table + if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, + "Scout Fly": {item_table[k]: k for k in item_table + if k in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset)}, "Precursor Orb": {} # TODO } @@ -39,13 +40,13 @@ def create_items(self): def create_item(self, name: str) -> Item: item_id = self.item_name_to_id[name] - if item_id in range(game_id + cell_offset, game_id + fly_offset): + if item_id in range(jak1_id, jak1_id + Scouts.fly_offset): # Power Cell classification = ItemClassification.progression_skip_balancing - elif item_id in range(game_id + fly_offset, game_id + orb_offset): + elif item_id in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset): # Scout Fly classification = ItemClassification.progression_skip_balancing - elif item_id > game_id + orb_offset: + elif item_id > jak1_id + Orbs.orb_offset: # Precursor Orb classification = ItemClassification.filler # TODO else: diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py index a16f40ce94f5..1bff47716134 100644 --- a/worlds/jakanddaxter/locs/CellLocations.py +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -1,3 +1,5 @@ +from ..GameID import jak1_id + # Power Cells are given ID's between 0 and 116 by the game. # The game tracks all game-tasks as integers. @@ -5,6 +7,17 @@ # necessarily ordered, nor are they the first 101 in the task list. # The remaining ones are cutscenes and other events. + +# These helper functions do all the math required to get information about each +# power cell and translate its ID between AP and OpenGOAL. +def to_ap_id(game_id: int) -> int: + return jak1_id + game_id + + +def to_game_id(ap_id: int) -> int: + return ap_id - jak1_id + + # The ID's you see below correspond directly to that cell's game-task ID. # Geyser Rock diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index 2da5972e17ac..6b913dc11c36 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -1,7 +1,38 @@ -# Precursor Orbs start at ??? and end at ??? +from ..GameID import jak1_id -# Given that Scout Flies are being offset by 2^20 to avoid collisions with power cells, -# I'm tentatively setting the Orb offset to 2^21, or 2,097,152. +# Precursor Orbs are not necessarily given ID's by the game. + +# Of the 2000 orbs (or "money") you can pick up, only 1233 are standalone ones you find in the overworld. +# We can identify them by Actor ID's, which run from 549 to 24433. Other actors reside in this range, +# so like Power Cells these are not ordered, nor contiguous, nor exclusively orbs. + +# In fact, other ID's in this range belong to actors that spawn orbs when they are activated or when they die, +# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head. + +# These orbs that spawn from parent actors DON'T have an Actor ID themselves - the parent object keeps +# track of how many of its orbs have been picked up. If you pick up only some of its orbs, it +# will respawn when you leave the area, and only drop the remaining number of orbs when activated/killed. +# Once all the orbs are picked up, the actor will permanently "retire" and never spawn again. +# The maximum number of orbs that any actor can spawn is 30 (the orb caches in citadel). Covering +# these ID-less orbs may need to be a future enhancement. TODO ^^ + +# Standalone orbs need 15 bits to identify themselves by Actor ID, +# so we can use 2^15 to offset them from scout flies, just like we offset +# scout flies from power cells by 2^10. +orb_offset = 32768 + + +# These helper functions do all the math required to get information about each +# precursor orb and translate its ID between AP and OpenGOAL. +def to_ap_id(game_id: int) -> int: + return jak1_id + orb_offset + game_id # Add the offsets and the orb Actor ID. + + +def to_game_id(ap_id: int) -> int: + return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets. + + +# The ID's you see below correspond directly to that orb's Actor ID in the game. # Geyser Rock locGR_orbTable = { diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index cf8fa0842055..809df2d28739 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -1,6 +1,8 @@ +from ..GameID import jak1_id + # Scout Flies are given ID's between 0 and 393311 by the game, explanation below. -# Each fly is given a unique 32-bit number broken into two 16-bit numbers. +# Each fly (or "buzzer") is given a unique 32-bit number broken into two 16-bit numbers. # The lower 16 bits are the game-task ID of the power cell the fly corresponds to. # The higher 16 bits are the index of the fly itself, from 000 (0) to 110 (6). @@ -10,10 +12,32 @@ # Because flies are indexed from 0, each 0th fly's full ID == the power cell's ID. # So we need to offset all of their ID's in order for Archipelago to separate them -# from their power cells. We use 128 (2^7) for this purpose, because scout flies -# never use the 8th lowest bit to describe themselves. +# from their power cells. We can use 1024 (2^10) for this purpose, because scout flies +# only ever need 10 bits to identify themselves (3 for the index, 7 for the cell ID). +fly_offset = 1024 + + +# These helper functions do all the math required to get information about each +# scout fly and translate its ID between AP and OpenGOAL. +def to_ap_id(game_id: int) -> int: + cell_id = get_cell_id(game_id) # This is AP/OpenGOAL agnostic, works on either ID. + buzzer_index = (game_id - cell_id) >> 9 # Subtract the cell ID, bit shift the index down 9 places. + return jak1_id + fly_offset + buzzer_index + cell_id # Add the offsets, the bit-shifted index, and the cell ID. + + +def to_game_id(ap_id: int) -> int: + cell_id = get_cell_id(ap_id) # This is AP/OpenGOAL agnostic, works on either ID. + buzzer_index = ap_id - jak1_id - fly_offset - cell_id # Reverse process, subtract the offsets and the cell ID. + return (buzzer_index << 9) + cell_id # Bit shift the index up 9 places, re-add the cell ID. + + +def get_cell_id(buzzer_id: int) -> int: + return buzzer_id & 0b1111111 # Get the power cell ID from the lowest 7 bits. + -# TODO - The ID's you see below correspond directly to that fly's 32-bit ID in the game. +# The ID's you see below correspond directly to that fly's 32-bit ID in the game. +# I used the decompiled entity JSON's and Jak's X/Y coordinates in Debug Mode +# to determine which box ID is which location. # Geyser Rock locGR_scoutTable = { @@ -61,46 +85,46 @@ # Misty Island locMI_scoutTable = { - 129: "MI: Scout Fly Overlooking Entrance", - 130: "MI: Scout Fly On Ledge Path, First", - 131: "MI: Scout Fly On Ledge Path, Second", - 132: "MI: Scout Fly Overlooking Shipyard", - 133: "MI: Scout Fly On Ship", - 134: "MI: Scout Fly On Barrel Ramps", - 135: "MI: Scout Fly On Zoomer Ramps" + 327708: "MI: Scout Fly Overlooking Entrance", + 65564: "MI: Scout Fly On Ledge Near Arena Entrance", + 262172: "MI: Scout Fly Near Arena Door", + 28: "MI: Scout Fly On Ledge Near Arena Exit", + 131100: "MI: Scout Fly On Ship", + 196636: "MI: Scout Fly On Barrel Ramps", + 393244: "MI: Scout Fly On Zoomer Ramps" } # Fire Canyon locFC_scoutTable = { - 136: "FC: Scout Fly 1", - 137: "FC: Scout Fly 2", - 138: "FC: Scout Fly 3", - 139: "FC: Scout Fly 4", - 140: "FC: Scout Fly 5", - 141: "FC: Scout Fly 6", - 142: "FC: Scout Fly 7" + 393284: "FC: Scout Fly 1", + 68: "FC: Scout Fly 2", + 65604: "FC: Scout Fly 3", + 196676: "FC: Scout Fly 4", + 131140: "FC: Scout Fly 5", + 262212: "FC: Scout Fly 6", + 327748: "FC: Scout Fly 7" } # Rock Village locRV_scoutTable = { - 143: "RV: Scout Fly Behind Sage's Hut", - 144: "RV: Scout Fly On Path To Village", - 145: "RV: Scout Fly Behind Geologist", - 146: "RV: Scout Fly Behind Fiery Boulder", - 147: "RV: Scout Fly On Dock", - 148: "RV: Scout Fly At Pontoon Bridge", - 149: "RV: Scout Fly At Boggy Swamp Entrance" + 76: "RV: Scout Fly Behind Sage's Hut", + 131148: "RV: Scout Fly Near Waterfall", + 196684: "RV: Scout Fly Behind Geologist", + 262220: "RV: Scout Fly Behind Fiery Boulder", + 65612: "RV: Scout Fly On Dock", + 327756: "RV: Scout Fly At Pontoon Bridge", + 393292: "RV: Scout Fly At Boggy Swamp Entrance" } # Precursor Basin locPB_scoutTable = { - 150: "PB: Scout Fly Overlooking Entrance", - 151: "PB: Scout Fly Near Mole Hole", - 152: "PB: Scout Fly At Purple Ring Start", - 153: "PB: Scout Fly Overlooking Dark Eco Plant", - 154: "PB: Scout Fly At Green Ring Start", - 155: "PB: Scout Fly Before Big Jump", - 156: "PB: Scout Fly Near Dark Eco Plant" + 196665: "PB: Scout Fly Overlooking Entrance", + 393273: "PB: Scout Fly Near Mole Hole", + 131129: "PB: Scout Fly At Purple Ring Start", + 65593: "PB: Scout Fly Near Dark Eco Plant, Above", + 57: "PB: Scout Fly At Blue Ring Start", + 262201: "PB: Scout Fly Before Big Jump", + 327737: "PB: Scout Fly Near Dark Eco Plant, Below" } # Lost Precursor City From df31d874da3f699bcdcb7aab4fff8abd1777f7f0 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 22 Apr 2024 22:33:17 -0400 Subject: [PATCH 14/70] Jak 1: Fixed a few things. Four maps to go. --- worlds/jakanddaxter/GameID.py | 2 +- worlds/jakanddaxter/Rules.py | 20 ++++++-- worlds/jakanddaxter/locs/CellLocations.py | 2 + worlds/jakanddaxter/locs/OrbLocations.py | 2 + worlds/jakanddaxter/locs/ScoutLocations.py | 58 +++++++++++----------- 5 files changed, 52 insertions(+), 32 deletions(-) diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index d3e75826601a..37100eca4bc5 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -1,4 +1,4 @@ -# All Jak And Daxter IDs must be offset by this number. +# All Jak And Daxter Archipelago IDs must be offset by this number. jak1_id = 741000000 # The name of the game. diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index d0c778f9d935..0333fa6e5629 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,10 +1,22 @@ -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from .Options import JakAndDaxterOptions from .Regions import Jak1Level, Jak1SubLevel, level_table, subLevel_table from .Locations import location_table as item_table from .locs import CellLocations as Cells, ScoutLocations as Scouts +# Helper function for a handful of special cases +# where we need "at least any N" number of a specific set of cells. +def has_count_of(cell_list: set, required_count: int, player: int, state: CollectionState) -> bool: + c: int = 0 + for k in cell_list: + if state.has(item_table[k], player): + c += 1 + if c >= required_count: + return True + return False + + def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): # Setting up some useful variables here because the offset numbers can get confusing # for access rules. Feel free to add more variables here to keep the code more readable. @@ -21,6 +33,7 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) lpc_helix = Cells.to_ap_id(50) mp_klaww = Cells.to_ap_id(86) mp_end = Cells.to_ap_id(87) + pre_sm_cells = {Cells.to_ap_id(k) for k in {**Cells.locVC_cellTable, **Cells.locSC_cellTable}} sm_yellow_switch = Cells.to_ap_id(60) sm_fort_gate = Cells.to_ap_id(63) lt_end = Cells.to_ap_id(89) @@ -35,7 +48,7 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_regions(multiworld, player, Jak1Level.GEYSER_ROCK, Jak1Level.SANDOVER_VILLAGE, - lambda state: state.has_all({item_table[k] for k in gr_cells}, player)) + lambda state: has_count_of(gr_cells, 4, player, state)) connect_regions(multiworld, player, Jak1Level.SANDOVER_VILLAGE, @@ -146,7 +159,8 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_regions(multiworld, player, Jak1Level.VOLCANIC_CRATER, - Jak1Level.SNOWY_MOUNTAIN) + Jak1Level.SNOWY_MOUNTAIN, + lambda state: has_count_of(pre_sm_cells, 2, player, state)) connect_region_to_sub(multiworld, player, Jak1Level.SNOWY_MOUNTAIN, diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py index 1bff47716134..304810af800c 100644 --- a/worlds/jakanddaxter/locs/CellLocations.py +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -11,10 +11,12 @@ # These helper functions do all the math required to get information about each # power cell and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." return jak1_id + game_id def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." return ap_id - jak1_id diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index 6b913dc11c36..4e586a65fb47 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -25,10 +25,12 @@ # These helper functions do all the math required to get information about each # precursor orb and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." return jak1_id + orb_offset + game_id # Add the offsets and the orb Actor ID. def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets. diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index 809df2d28739..d2796f36d11f 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -20,12 +20,14 @@ # These helper functions do all the math required to get information about each # scout fly and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." cell_id = get_cell_id(game_id) # This is AP/OpenGOAL agnostic, works on either ID. buzzer_index = (game_id - cell_id) >> 9 # Subtract the cell ID, bit shift the index down 9 places. return jak1_id + fly_offset + buzzer_index + cell_id # Add the offsets, the bit-shifted index, and the cell ID. def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." cell_id = get_cell_id(ap_id) # This is AP/OpenGOAL agnostic, works on either ID. buzzer_index = ap_id - jak1_id - fly_offset - cell_id # Reverse process, subtract the offsets and the cell ID. return (buzzer_index << 9) + cell_id # Bit shift the index up 9 places, re-add the cell ID. @@ -129,46 +131,46 @@ def get_cell_id(buzzer_id: int) -> int: # Lost Precursor City locLPC_scoutTable = { - 157: "LPC: Scout Fly First Room", - 158: "LPC: Scout Fly Before Second Room", - 159: "LPC: Scout Fly Second Room, Near Orb Vent", - 160: "LPC: Scout Fly Second Room, On Path To Cell", - 161: "LPC: Scout Fly Second Room, Green Switch", - 162: "LPC: Scout Fly Second Room, Blue Switch", - 163: "LPC: Scout Fly Across Steam Vents" + 262193: "LPC: Scout Fly First Room", + 131121: "LPC: Scout Fly Before Second Room", + 393265: "LPC: Scout Fly Second Room, Near Orb Vent", + 196657: "LPC: Scout Fly Second Room, On Path To Cell", + 49: "LPC: Scout Fly Second Room, Green Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?` + 65585: "LPC: Scout Fly Second Room, Blue Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?` + 327729: "LPC: Scout Fly Across Steam Vents" } # Boggy Swamp locBS_scoutTable = { - 164: "BS: Scout Fly Near Entrance", - 165: "BS: Scout Fly Over First Jump Pad", - 166: "BS: Scout Fly Over Second Jump Pad", - 167: "BS: Scout Fly Across Black Swamp", - 168: "BS: Scout Fly Overlooking Flut Flut", - 169: "BS: Scout Fly On Flut Flut Platforms", - 170: "BS: Scout Fly In Field Of Boxes" + 43: "BS: Scout Fly Near Entrance", + 393259: "BS: Scout Fly Over First Jump Pad", + 65579: "BS: Scout Fly Over Second Jump Pad", + 262187: "BS: Scout Fly Across Black Swamp", + 327723: "BS: Scout Fly Overlooking Flut Flut", + 131115: "BS: Scout Fly On Flut Flut Platforms", + 196651: "BS: Scout Fly In Field Of Boxes" } # Mountain Pass locMP_scoutTable = { - 171: "MP: Scout Fly 1", - 172: "MP: Scout Fly 2", - 173: "MP: Scout Fly 3", - 174: "MP: Scout Fly 4", - 175: "MP: Scout Fly 5", - 176: "MP: Scout Fly 6", - 177: "MP: Scout Fly 7" + 88: "MP: Scout Fly 1", + 65624: "MP: Scout Fly 2", + 131160: "MP: Scout Fly 3", + 196696: "MP: Scout Fly 4", + 262232: "MP: Scout Fly 5", + 327768: "MP: Scout Fly 6", + 393304: "MP: Scout Fly 7" } # Volcanic Crater locVC_scoutTable = { - 178: "VC: Scout Fly In Miner's Cave", - 179: "VC: Scout Fly Near Oracle", - 180: "VC: Scout Fly Overlooking Minecarts", - 181: "VC: Scout Fly On First Minecart Path", - 182: "VC: Scout Fly At Minecart Junction", - 183: "VC: Scout Fly At Spider Cave Entrance", - 184: "VC: Scout Fly Under Mountain Pass Exit" + 262221: "VC: Scout Fly In Miner's Cave", + 393293: "VC: Scout Fly Near Oracle", + 196685: "VC: Scout Fly On Stone Platforms", + 131149: "VC: Scout Fly Near Lava Tube", + 77: "VC: Scout Fly At Minecart Junction", + 65613: "VC: Scout Fly Near Spider Cave", + 327757: "VC: Scout Fly Near Mountain Pass" } # Spider Cave From ef7b3598a22d820a07ecab8bc844a3a23875085a Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:17:55 -0400 Subject: [PATCH 15/70] Jak 1: Last of the scout flies mapped! --- worlds/jakanddaxter/Regions.py | 18 +++---- worlds/jakanddaxter/locs/ScoutLocations.py | 56 +++++++++++----------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 8b4afe790cfc..c13b48541829 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -137,22 +137,23 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: region_lpc = create_region(player, multiworld, level_table[Jak1Level.LOST_PRECURSOR_CITY]) create_cell_locations(region_lpc, {k: Cells.locLPC_cellTable[k] for k in {45, 48, 44, 51}}) - create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {157, 158, 159, 160, 161, 162}}) + create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] + for k in {262193, 131121, 393265, 196657, 49, 65585}}) sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM]) create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47, 49}}) - create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {163}}) + create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {327729}}) sub_region_lpchr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM]) create_cell_locations(sub_region_lpchr, {k: Cells.locLPC_cellTable[k] for k in {46, 50}}) region_bs = create_region(player, multiworld, level_table[Jak1Level.BOGGY_SWAMP]) create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) - create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {164, 165, 166, 167, 170}}) + create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {43, 393259, 65579, 262187, 196651}}) sub_region_bsff = create_subregion(region_bs, subLevel_table[Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT]) create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {43, 37}}) - create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {168, 169}}) + create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {327723, 131115}}) region_mp = create_region(player, multiworld, level_table[Jak1Level.MOUNTAIN_PASS]) create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86}}) @@ -174,7 +175,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: region_sm = create_region(player, multiworld, level_table[Jak1Level.SNOWY_MOUNTAIN]) create_cell_locations(region_sm, {k: Cells.locSM_cellTable[k] for k in {60, 61, 66, 64}}) - create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {192, 193, 194, 195, 196}}) + create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {65, 327745, 65601, 131137, 393281}}) sub_region_smfb = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX]) create_cell_locations(sub_region_smfb, {k: Cells.locSM_cellTable[k] for k in {67}}) @@ -184,7 +185,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: sub_region_smlf = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62, 65}}) - create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {197, 198}}) + create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {196673, 262209}}) region_lt = create_region(player, multiworld, level_table[Jak1Level.LAVA_TUBE]) create_cell_locations(region_lt, Cells.locLT_cellTable) @@ -192,11 +193,12 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: region_gmc = create_region(player, multiworld, level_table[Jak1Level.GOL_AND_MAIAS_CITADEL]) create_cell_locations(region_gmc, {k: Cells.locGMC_cellTable[k] for k in {71, 72, 73}}) - create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k] for k in {206, 207, 208, 209, 210, 211}}) + create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k] + for k in {91, 65627, 196699, 262235, 393307, 131163}}) sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70, 91}}) - create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {212}}) + create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {327771}}) create_subregion(sub_region_gmcrt, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS]) diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index d2796f36d11f..22587de2f734 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -175,44 +175,44 @@ def get_cell_id(buzzer_id: int) -> int: # Spider Cave locSC_scoutTable = { - 185: "SC: Scout Fly Across Dark Eco Pool", - 186: "SC: Scout Fly At Dark Area Entrance", - 187: "SC: Scout Fly First Room, Overlooking Entrance", - 188: "SC: Scout Fly First Room, Near Dark Crystal", - 189: "SC: Scout Fly First Room, Near Dark Eco Pool", - 190: "SC: Scout Fly Robot Room, First Level", - 191: "SC: Scout Fly Robot Room, Second Level", + 327765: "SC: Scout Fly Near Dark Dave Entrance", + 262229: "SC: Scout Fly In Dark Cave", + 393301: "SC: Scout Fly Main Cave, Overlooking Entrance", + 196693: "SC: Scout Fly Main Cave, Near Dark Crystal", + 131157: "SC: Scout Fly Main Cave, Near Robot Cave Entrance", + 85: "SC: Scout Fly Robot Cave, At Bottom Level", + 65621: "SC: Scout Fly Robot Cave, At Top Level", } # Snowy Mountain locSM_scoutTable = { - 192: "SM: Scout Fly Near Entrance", - 193: "SM: Scout Fly Near Frozen Box", - 194: "SM: Scout Fly Near Yellow Eco Switch", - 195: "SM: Scout Fly On Cliff near Flut Flut", - 196: "SM: Scout Fly Under Bridge To Fort", - 197: "SM: Scout Fly On Top Of Fort Tower", - 198: "SM: Scout Fly On Top Of Fort" + 65: "SM: Scout Fly Near Entrance", + 327745: "SM: Scout Fly Near Frozen Box", + 65601: "SM: Scout Fly Near Yellow Eco Switch", + 131137: "SM: Scout Fly On Cliff near Flut Flut", + 393281: "SM: Scout Fly Under Bridge To Fort", + 196673: "SM: Scout Fly On Top Of Fort Tower", + 262209: "SM: Scout Fly On Top Of Fort" } # Lava Tube locLT_scoutTable = { - 199: "LT: Scout Fly 1", - 200: "LT: Scout Fly 2", - 201: "LT: Scout Fly 3", - 202: "LT: Scout Fly 4", - 203: "LT: Scout Fly 5", - 204: "LT: Scout Fly 6", - 205: "LT: Scout Fly 7" + 90: "LT: Scout Fly 1", + 65626: "LT: Scout Fly 2", + 327770: "LT: Scout Fly 3", + 262234: "LT: Scout Fly 4", + 131162: "LT: Scout Fly 5", + 196698: "LT: Scout Fly 6", + 393306: "LT: Scout Fly 7" } # Gol and Maias Citadel locGMC_scoutTable = { - 206: "GMC: Scout Fly At Entrance", - 207: "GMC: Scout Fly At Jump Room Entrance", - 208: "GMC: Scout Fly On Ledge Across Rotators", - 209: "GMC: Scout Fly At Tile Color Puzzle", - 210: "GMC: Scout Fly At Blast Furnace", - 211: "GMC: Scout Fly At Path To Robot", - 212: "GMC: Scout Fly On Top Of Rotating Tower" + 91: "GMC: Scout Fly At Entrance", + 65627: "GMC: Scout Fly Main Room, Left of Robot", + 196699: "GMC: Scout Fly Main Room, Right of Robot", + 262235: "GMC: Scout Fly Before Jumping Lurkers", + 393307: "GMC: Scout Fly At Blast Furnace", + 131163: "GMC: Scout Fly At Launch Pad Room", + 327771: "GMC: Scout Fly Top Of Rotating Tower" } From c497188beb9bc2174dc29930736a7e23841f6aa2 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:19:54 -0400 Subject: [PATCH 16/70] Jak 1: simplify citadel sages logic. --- worlds/jakanddaxter/Rules.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 0333fa6e5629..467953a69748 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -37,9 +37,7 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) sm_yellow_switch = Cells.to_ap_id(60) sm_fort_gate = Cells.to_ap_id(63) lt_end = Cells.to_ap_id(89) - gmc_blue_sage = Cells.to_ap_id(71) - gmc_red_sage = Cells.to_ap_id(72) - gmc_yellow_sage = Cells.to_ap_id(73) + gmc_rby_sages = {Cells.to_ap_id(k) for k in {71, 72, 73}} gmc_green_sage = Cells.to_ap_id(70) # Start connecting regions and set their access rules. @@ -190,9 +188,7 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_region_to_sub(multiworld, player, Jak1Level.GOL_AND_MAIAS_CITADEL, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - lambda state: state.has(item_table[gmc_blue_sage], player) - and state.has(item_table[gmc_red_sage], player) - and state.has(item_table[gmc_yellow_sage], player)) + lambda state: has_count_of(gmc_rby_sages, 3, player, state)) connect_subregions(multiworld, player, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, From 7f8a41f0a6ea512b4152fda810f2dabc5b7a7594 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:36:22 -0400 Subject: [PATCH 17/70] Jak 1: WebWorld setup, some documentation. --- worlds/jakanddaxter/__init__.py | 23 ++++++- worlds/jakanddaxter/docs/en_Jak And Daxter.md | 68 +++++++++++++++++++ worlds/jakanddaxter/docs/setup_en.md | 39 +++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 worlds/jakanddaxter/docs/en_Jak And Daxter.md create mode 100644 worlds/jakanddaxter/docs/setup_en.md diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 75c498e12fd2..0930aa3c1517 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,4 +1,4 @@ -from BaseClasses import Item, ItemClassification +from BaseClasses import Item, ItemClassification, Tutorial from .GameID import jak1_id, jak1_name from .Options import JakAndDaxterOptions from .Items import JakAndDaxterItem @@ -6,17 +6,36 @@ from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs from .Regions import create_regions from .Rules import set_rules -from ..AutoWorld import World +from ..AutoWorld import World, WebWorld + + +class JakAndDaxterWebWorld(WebWorld): + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up ArchipelaGOAL (Archipelago on OpenGOAL).", + "English", + "setup_en.md", + "setup/en", + ["markustulliuscicero"] + ) + + tutorials = [setup_en] class JakAndDaxterWorld(World): + # ID, name, version game: str = jak1_name data_version = 1 required_client_version = (0, 4, 5) + # Options options_dataclass = JakAndDaxterOptions options: JakAndDaxterOptions + # Web world + web = JakAndDaxterWebWorld() + + # Items and Locations # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. # Remember, the game ID and various offsets for each item type have already been calculated. item_name_to_id = {item_table[k]: k for k in item_table} diff --git a/worlds/jakanddaxter/docs/en_Jak And Daxter.md b/worlds/jakanddaxter/docs/en_Jak And Daxter.md new file mode 100644 index 000000000000..e33459d9e34f --- /dev/null +++ b/worlds/jakanddaxter/docs/en_Jak And Daxter.md @@ -0,0 +1,68 @@ +# Jak And Daxter (ArchipelaGOAL) + +## Where is the options page? + +The [Player Options Page](../player-options) for this game contains +all the options you need to configure and export a config file. + +At this time, Scout Flies are always randomized despite the option setting... this is a work in progress. + +## What does randomization do to this game? +All 101 Power Cells and 112 Scout Flies are now Location Checks +and may contain Items for different games as well as different Items from within Jak and Daxter. + +## What is the goal of the game once randomized? +To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. + +In order to reach them, you will need at least 72 Power Cells to cross the Lava Tube. In addition, you will need +the four specific Power Cells obtained by freeing the Red, Blue, Yellow, and Green Sages. + +## What happens when I pick up an item in the game? +Jak and Daxter will perform their victory animation, if applicable, but you will not necessarily receive the item +you just picked up. The Power Cell count will not change, nor the Scout Fly count, unless you picked up a randomized +Item that just happens to belong to the game. You will however see a message saying what you did find and who it +belongs to. + +## How do I get more items to progress in the game? +You can progress by performing the tasks and completing the challenges that would normally give you Power Cells +and Scout Flies in the game. Other players may also find Power Cells and Scout Flies in their games, and those Items +will be automatically sent to your game. When you receive an Item, a Message will pop up to inform you where you +received the Item from, and which one it is. Your Item count for that type of Item will also tick up. + +If you have completed all possible tasks available to you but still cannot progress, you may have to wait for another +player to find enough of your game's Items to allow you to progress. + +## I can't reach a certain area within an accessible region, how do I get there? +Some areas are locked behind ownership of specific Power Cells. For example, you cannot access Misty Island until you +have the "Catch 200 Pounds of Fish" Power Cell. Keep in mind, your access to Misty Island is determined +_through ownership of this specific Power Cell only,_ **not** _by you completing the Fishing minigame._ + +## I got soft-locked and can't leave, how do I get out of here? +As stated before, some areas are locked behind ownership of specific Power Cells. But you may already be +past a point-of-no-return preventing you from backtracking. One example is the Forbidden Jungle temple, +where the elevator is locked at the bottom, and if you haven't unlocked the Blue Eco Switch, +you cannot access the Plant Boss's room and escape. + +In this scenario, you will need to open your menu and find the "Teleport Home" option. Selecting this option will +instantly teleport you to the nearest Sage's Hut in the last hub area you were in... or always the Green Sage's Hut, +depending on the feasibility of the former option. As stated before... it's a work in progress. + +## I think I found a bug, where should I report it? +Depending on the nature of the bug, there are a couple of different options. + +* If you found a logical error in the randomizer, please create a new Issue +[here.](https://github.com/ArchipelaGOAL/Archipelago/issues) + * Use this page if: + * For example, you are stuck on Geyser Rock because one of the four Geyser Rock Power Cells is not on Geyser Rock. + * The randomizer did not respect one of the Options you chose. + * You see a mistake, typo, etc. on this webpage. + * Please upload your config file and spoiler log file in the Issue, so we can troubleshoot the problem. + +* If you encountered an error in OpenGOAL, please create a new Issue +[here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues) + * Use this page if: + * You encounter a crash, freeze, reset, etc. + * You fail to send Items you find in the game to the Archipelago server. + * You fail to receive Items the server sends to you. + * Your game disconnects from the server and cannot reconnect. + * Please upload any log files that may have been generated. \ No newline at end of file diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md new file mode 100644 index 000000000000..61898991a744 --- /dev/null +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -0,0 +1,39 @@ +# Jak And Daxter (ArchipelaGOAL) Setup Guide + +## Required Software + +- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.* + +## Installation + +### Installation via OpenGOAL Mod Launcher + +***Windows Preparations*** + +***Linux Preparations*** + +***Using the Launcher*** + +### Manual Compilation (Linux/Windows) + +***Windows Preparations*** + +***Linux Preparations*** + +***Compiling*** + +## Starting a Game + +### Joining a MultiWorld Game + +### Playing Offline + +## Installation and Setup Troubleshooting + +### Compilation Failures + +### Runtime Failures + +## Gameplay Troubleshooting + +### Known Issues \ No newline at end of file From 3de94fb2bc4284a2f7662c74cb9fbde2ed34761b Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 25 Apr 2024 21:05:13 -0400 Subject: [PATCH 18/70] Jak 1: Initial checkin of Client. Removed the colon from the game name. --- worlds/jakanddaxter/Client.py | 168 ++++++++++++++++++ worlds/jakanddaxter/GameID.py | 2 +- worlds/jakanddaxter/__init__.py | 2 +- ...en_Jak and Daxter The Precursor Legacy.md} | 0 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 worlds/jakanddaxter/Client.py rename worlds/jakanddaxter/docs/{en_Jak And Daxter.md => en_Jak and Daxter The Precursor Legacy.md} (100%) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py new file mode 100644 index 000000000000..659f67bec743 --- /dev/null +++ b/worlds/jakanddaxter/Client.py @@ -0,0 +1,168 @@ +import time +import struct +import typing +import asyncio +from socket import socket, AF_INET, SOCK_STREAM + +import colorama + +import Utils +from GameID import jak1_name +from .locs import CellLocations as Cells, ScoutLocations as Flies +from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled + + +class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): + ctx: "JakAndDaxterContext" + + # TODO - Clean up commands related to the REPL, make them more user friendly. + # The REPL has a specific order of operations it needs to do in order to process our input: + # 1. Connect (we need to open a socket connection on ip/port to the REPL). + # 2. Listen (have the REPL compiler connect and listen on the game's REPL server's socket). + # 3. Compile (have the REPL compiler compile the game into object code it can run). + # All 3 need to be done, and in this order, for this to work. + + +class JakAndDaxterReplClient: + ip: str + port: int + socket: socket + connected: bool = False + listening: bool = False + compiled: bool = False + + def __init__(self, ip: str = "127.0.0.1", port: int = 8181): + self.ip = ip + self.port = port + self.connected = self.g_connect() + if self.connected: + self.listening = self.g_listen() + if self.connected and self.listening: + self.compiled = self.g_compile() + + # This helper function formats and sends `form` as a command to the REPL. + # ALL commands to the REPL should be sent using this function. + def send_form(self, form: str) -> None: + header = struct.pack(" bool: + if not self.ip or not self.port: + return False + + self.socket = socket(AF_INET, SOCK_STREAM) + self.socket.connect((self.ip, self.port)) + time.sleep(1) + print(self.socket.recv(1024).decode()) + return True + + def g_listen(self) -> bool: + self.send_form("(lt)") + return True + + def g_compile(self) -> bool: + # Show this visual cue when compilation is started. + # It's the version number of the OpenGOAL Compiler. + self.send_form("(set! *debug-segment* #t)") + + # Play this audio cue when compilation is started. + # It's the sound you hear when you press START + CIRCLE to open the Options menu. + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"start-options\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + + # Start compilation. This is blocking, so nothing will happen until the REPL is done. + self.send_form("(mi)") + + # Play this audio cue when compilation is complete. + # It's the sound you hear when you press START + START to close the Options menu. + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + + # Disable cheat-mode and debug (close the visual cue). + self.send_form("(set! *cheat-mode* #f)") + self.send_form("(set! *debug-segment* #f)") + return True + + def g_verify(self) -> bool: + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + return True + + # TODO - In ArchipelaGOAL, override the 'get-pickup event so that it doesn't give you the item, + # it just plays the victory animation. Then define a new event type like 'get-archipelago + # to actually give ourselves the item. See game-info.gc and target-handler.gc. + + def give_power_cell(self, ap_id: int) -> None: + cell_id = Cells.to_game_id(ap_id) + self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") + + def give_scout_fly(self, ap_id: int) -> None: + fly_id = Flies.to_game_id(ap_id) + self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))") + + +class JakAndDaxterContext(CommonContext): + tags = {"AP"} + game = jak1_name + items_handling = 0b111 # Full item handling + command_processor = JakAndDaxterClientCommandProcessor + repl: JakAndDaxterReplClient + + def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + self.repl = JakAndDaxterReplClient() + super().__init__(server_address, password) + + def on_package(self, cmd: str, args: dict): + if cmd == "": + pass + + def run_gui(self): + from kvui import GameManager + + class JakAndDaxterManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Jak and Daxter ArchipelaGOAL Client" + + self.ui = JakAndDaxterManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +def run_game(): + pass + + +async def main(): + Utils.init_logging("JakAndDaxterClient", exception_logger="Client") + + ctx = JakAndDaxterContext(None, None) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + run_game() + + await ctx.exit_event.wait() + await ctx.shutdown() + + +if __name__ == "__main__": + colorama.init() + asyncio.run(main()) + colorama.deinit() \ No newline at end of file diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index 37100eca4bc5..555be696af49 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -2,4 +2,4 @@ jak1_id = 741000000 # The name of the game. -jak1_name = "Jak and Daxter: The Precursor Legacy" +jak1_name = "Jak and Daxter The Precursor Legacy" diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 0930aa3c1517..e0985b756008 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -24,7 +24,7 @@ class JakAndDaxterWebWorld(WebWorld): class JakAndDaxterWorld(World): # ID, name, version - game: str = jak1_name + game = jak1_name data_version = 1 required_client_version = (0, 4, 5) diff --git a/worlds/jakanddaxter/docs/en_Jak And Daxter.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md similarity index 100% rename from worlds/jakanddaxter/docs/en_Jak And Daxter.md rename to worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md From 39364b38fd5664832053f1fbb6011f377195e0ff Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:56:16 -0400 Subject: [PATCH 19/70] Jak 1: Refactored client into components, working on async communication between the client and the game. --- JakAndDaxterClient.py | 9 ++ worlds/jakanddaxter/Client.py | 165 ++++++++------------- worlds/jakanddaxter/__init__.py | 16 +- worlds/jakanddaxter/client/MemoryReader.py | 59 ++++++++ worlds/jakanddaxter/client/ReplClient.py | 127 ++++++++++++++++ worlds/jakanddaxter/requirements.txt | 1 + 6 files changed, 269 insertions(+), 108 deletions(-) create mode 100644 JakAndDaxterClient.py create mode 100644 worlds/jakanddaxter/client/MemoryReader.py create mode 100644 worlds/jakanddaxter/client/ReplClient.py create mode 100644 worlds/jakanddaxter/requirements.txt diff --git a/JakAndDaxterClient.py b/JakAndDaxterClient.py new file mode 100644 index 000000000000..040f8ff389bd --- /dev/null +++ b/JakAndDaxterClient.py @@ -0,0 +1,9 @@ +import Utils +from worlds.jakanddaxter.Client import launch + +import ModuleUpdate +ModuleUpdate.update() + +if __name__ == '__main__': + Utils.init_logging("JakAndDaxterClient", exception_logger="Client") + launch() diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 659f67bec743..c451162ae90a 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,16 +1,33 @@ -import time -import struct import typing import asyncio -from socket import socket, AF_INET, SOCK_STREAM - import colorama import Utils -from GameID import jak1_name -from .locs import CellLocations as Cells, ScoutLocations as Flies from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled +from .GameID import jak1_name +from .client.ReplClient import JakAndDaxterReplClient +from .client.MemoryReader import JakAndDaxterMemoryReader + +import ModuleUpdate +ModuleUpdate.update() + + +all_tasks = set() + + +def create_task_log_exception(awaitable: typing.Awaitable) -> asyncio.Task: + async def _log_exception(a): + try: + return await a + except Exception as e: + logger.exception(e) + finally: + all_tasks.remove(task) + task = asyncio.create_task(_log_exception(awaitable)) + all_tasks.add(task) + return task + class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): ctx: "JakAndDaxterContext" @@ -23,112 +40,29 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): # All 3 need to be done, and in this order, for this to work. -class JakAndDaxterReplClient: - ip: str - port: int - socket: socket - connected: bool = False - listening: bool = False - compiled: bool = False - - def __init__(self, ip: str = "127.0.0.1", port: int = 8181): - self.ip = ip - self.port = port - self.connected = self.g_connect() - if self.connected: - self.listening = self.g_listen() - if self.connected and self.listening: - self.compiled = self.g_compile() - - # This helper function formats and sends `form` as a command to the REPL. - # ALL commands to the REPL should be sent using this function. - def send_form(self, form: str) -> None: - header = struct.pack(" bool: - if not self.ip or not self.port: - return False - - self.socket = socket(AF_INET, SOCK_STREAM) - self.socket.connect((self.ip, self.port)) - time.sleep(1) - print(self.socket.recv(1024).decode()) - return True - - def g_listen(self) -> bool: - self.send_form("(lt)") - return True - - def g_compile(self) -> bool: - # Show this visual cue when compilation is started. - # It's the version number of the OpenGOAL Compiler. - self.send_form("(set! *debug-segment* #t)") - - # Play this audio cue when compilation is started. - # It's the sound you hear when you press START + CIRCLE to open the Options menu. - self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"start-options\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") - - # Start compilation. This is blocking, so nothing will happen until the REPL is done. - self.send_form("(mi)") - - # Play this audio cue when compilation is complete. - # It's the sound you hear when you press START + START to close the Options menu. - self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") - - # Disable cheat-mode and debug (close the visual cue). - self.send_form("(set! *cheat-mode* #f)") - self.send_form("(set! *debug-segment* #f)") - return True - - def g_verify(self) -> bool: - self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") - return True - - # TODO - In ArchipelaGOAL, override the 'get-pickup event so that it doesn't give you the item, - # it just plays the victory animation. Then define a new event type like 'get-archipelago - # to actually give ourselves the item. See game-info.gc and target-handler.gc. - - def give_power_cell(self, ap_id: int) -> None: - cell_id = Cells.to_game_id(ap_id) - self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type fuel-cell) " - "(the float " + str(cell_id) + "))") - - def give_scout_fly(self, ap_id: int) -> None: - fly_id = Flies.to_game_id(ap_id) - self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type buzzer) " - "(the float " + str(fly_id) + "))") - - class JakAndDaxterContext(CommonContext): tags = {"AP"} game = jak1_name items_handling = 0b111 # Full item handling command_processor = JakAndDaxterClientCommandProcessor + + # We'll need two agents working in tandem to handle two-way communication with the game. + # The REPL Client will handle the server->game direction by issuing commands directly to the running game. + # But the REPL cannot send information back to us, it only ingests information we send it. + # Luckily OpenGOAL sets up memory addresses to write to, that AutoSplit can read from, for speedrunning. + # We'll piggyback off this system with a Memory Reader, and that will handle the game->server direction. repl: JakAndDaxterReplClient + memr: JakAndDaxterMemoryReader + + # And two associated tasks, so we have handles on them. + repl_task: asyncio.Task + memr_task: asyncio.Task def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: self.repl = JakAndDaxterReplClient() + self.memr = JakAndDaxterMemoryReader() super().__init__(server_address, password) - def on_package(self, cmd: str, args: dict): - if cmd == "": - pass - def run_gui(self): from kvui import GameManager @@ -141,28 +75,47 @@ class JakAndDaxterManager(GameManager): self.ui = JakAndDaxterManager(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + def on_package(self, cmd: str, args: dict): + if cmd == "ReceivedItems": + for index, item in enumerate(args["items"], start=args["index"]): + self.repl.item_inbox[index] = item + + async def ap_inform_location_checks(self, location_ids: typing.List[int]): + message = [{"cmd": "LocationChecks", "locations": location_ids}] + await self.send_msgs(message) -def run_game(): - pass + def on_locations(self, location_ids: typing.List[int]): + create_task_log_exception(self.ap_inform_location_checks(location_ids)) + + async def run_repl_loop(self): + await self.repl.main_tick() + await asyncio.sleep(0.1) + + async def run_memr_loop(self): + await self.memr.main_tick(self.on_locations) + await asyncio.sleep(0.1) async def main(): Utils.init_logging("JakAndDaxterClient", exception_logger="Client") ctx = JakAndDaxterContext(None, None) + + await ctx.repl.init() + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + ctx.repl_task = create_task_log_exception(ctx.run_repl_loop()) + ctx.memr_task = create_task_log_exception(ctx.run_memr_loop()) if gui_enabled: ctx.run_gui() ctx.run_cli() - run_game() - await ctx.exit_event.wait() await ctx.shutdown() -if __name__ == "__main__": +def launch(): colorama.init() asyncio.run(main()) colorama.deinit() \ No newline at end of file diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index e0985b756008..a07798475b7e 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -7,6 +7,7 @@ from .Regions import create_regions from .Rules import set_rules from ..AutoWorld import World, WebWorld +from ..LauncherComponents import components, Component, launch_subprocess, Type class JakAndDaxterWebWorld(WebWorld): @@ -44,8 +45,8 @@ class JakAndDaxterWorld(World): "Power Cell": {item_table[k]: k for k in item_table if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, "Scout Fly": {item_table[k]: k for k in item_table - if k in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset)}, - "Precursor Orb": {} # TODO + if k in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset)} + # "Precursor Orb": {} # TODO } def create_regions(self): @@ -73,3 +74,14 @@ def create_item(self, name: str) -> Item: item = JakAndDaxterItem(name, classification, item_id, self.player) return item + + +def launch_client(): + from .Client import launch + launch_subprocess(launch, name="JakAndDaxterClient") + + +components.append(Component("Jak and Daxter Client", + "JakAndDaxterClient", + func=launch_client, + component_type=Type.CLIENT)) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py new file mode 100644 index 000000000000..020805b32b2f --- /dev/null +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -0,0 +1,59 @@ +import typing +import subprocess +import pymem +from pymem import pattern +from pymem.exception import ProcessNotFound + + +class JakAndDaxterMemoryReader: + marker: typing.ByteString + connected: bool = False + marked: bool = False + + process = None + marker_address = None + goal_address = None + + location_outbox = {} + outbox_index = 0 + + def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): + self.marker = marker + self.connected = self.connect() + if self.connected and self.marker: + self.marked = self.find_marker() + pass + + async def main_tick(self, location_callback: typing.Callable): + self.read_memory() + + # Checked Locations in game. Handle 1 location per tick. + if len(self.location_outbox) > self.outbox_index: + await location_callback(self.location_outbox[self.outbox_index]) + self.outbox_index += 1 + + def connect(self) -> bool: + try: + self.process = pymem.Pymem("gk.exe") # The GOAL Kernel + return True + except ProcessNotFound: + return False + + def find_marker(self) -> bool: + + # If we don't find the marker in the first module's worth of memory, we've failed. + modules = list(self.process.list_modules()) + self.marker_address = pattern.pattern_scan_module(self.process.process_handle, modules[0], self.marker) + if self.marker_address: + # At this address is another address that contains the struct we're looking for: the game's state. + # From here we need to add the length in bytes for the marker and 4 bytes of padding, + # and the struct address is 8 bytes long (it's u64). + goal_pointer = self.marker_address + len(self.marker) + 4 + self.goal_address = int.from_bytes(self.process.read_bytes(goal_pointer, 8), + byteorder="little", + signed=False) + return True + return False + + def read_memory(self) -> typing.Dict: + pass diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py new file mode 100644 index 000000000000..0a7ca9f97cdb --- /dev/null +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -0,0 +1,127 @@ +import time +import struct +from socket import socket, AF_INET, SOCK_STREAM +from CommonClient import logger +from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, OrbLocations as Orbs +from worlds.jakanddaxter.GameID import jak1_id + + +class JakAndDaxterReplClient: + ip: str + port: int + socket: socket + connected: bool = False + listening: bool = False + compiled: bool = False + + item_inbox = {} + inbox_index = 0 + + def __init__(self, ip: str = "127.0.0.1", port: int = 8181): + self.ip = ip + self.port = port + + async def init(self): + self.connected = await self.connect() + if self.connected: + self.listening = await self.listen() + if self.connected and self.listening: + self.compiled = await self.compile() + + async def main_tick(self): + + # Receive Items from AP. Handle 1 item per tick. + if len(self.item_inbox) > self.inbox_index: + await self.receive_item() + self.inbox_index += 1 + + # This helper function formats and sends `form` as a command to the REPL. + # ALL commands to the REPL should be sent using this function. + # TODO - this needs to block on receiving an acknowledgement from the REPL server. + # Problem is, it doesn't ack anything right now. So we need that to happen first. + async def send_form(self, form: str) -> None: + header = struct.pack(" bool: + if not self.ip or not self.port: + return False + + try: + self.socket = socket(AF_INET, SOCK_STREAM) + self.socket.connect((self.ip, self.port)) + time.sleep(1) + logger.info(self.socket.recv(1024).decode()) + return True + except ConnectionRefusedError: + return False + + async def listen(self) -> bool: + await self.send_form("(lt)") + return True + + async def compile(self) -> bool: + # Show this visual cue when compilation is started. + # It's the version number of the OpenGOAL Compiler. + await self.send_form("(set! *debug-segment* #t)") + + # Play this audio cue when compilation is started. + # It's the sound you hear when you press START + CIRCLE to open the Options menu. + await self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"start-options\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + + # Start compilation. This is blocking, so nothing will happen until the REPL is done. + await self.send_form("(mi)") + + # Play this audio cue when compilation is complete. + # It's the sound you hear when you press START + START to close the Options menu. + await self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + + # Disable cheat-mode and debug (close the visual cue). + await self.send_form("(set! *cheat-mode* #f)") + # await self.send_form("(set! *debug-segment* #f)") + return True + + async def verify(self) -> bool: + await self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + return True + + async def receive_item(self): + ap_id = self.item_inbox[self.inbox_index]["item"] + + # Determine the type of item to receive. + if ap_id in range(jak1_id, jak1_id + Flies.fly_offset): + await self.receive_power_cell(ap_id) + + elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Orbs.orb_offset): + await self.receive_scout_fly(ap_id) + + elif ap_id > jak1_id + Orbs.orb_offset: + pass # TODO + + # TODO - In ArchipelaGOAL, override the 'get-pickup event so that it doesn't give you the item, + # it just plays the victory animation. Then define a new event type like 'get-archipelago + # to actually give ourselves the item. See game-info.gc and target-handler.gc. + + async def receive_power_cell(self, ap_id: int) -> None: + cell_id = Cells.to_game_id(ap_id) + await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") + + async def receive_scout_fly(self, ap_id: int) -> None: + fly_id = Flies.to_game_id(ap_id) + await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))") diff --git a/worlds/jakanddaxter/requirements.txt b/worlds/jakanddaxter/requirements.txt new file mode 100644 index 000000000000..fe25267f6705 --- /dev/null +++ b/worlds/jakanddaxter/requirements.txt @@ -0,0 +1 @@ +Pymem>=1.13.0 \ No newline at end of file From 801e50b8179e4c5cad82a281f9caed8034e200fb Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 28 Apr 2024 23:11:03 -0400 Subject: [PATCH 20/70] Jak 1: In tandem with new ArchipelaGOAL memory structure, define read_memory. --- worlds/jakanddaxter/Client.py | 21 ++++++ worlds/jakanddaxter/client/MemoryReader.py | 27 +++++-- worlds/jakanddaxter/client/ReplClient.py | 82 ++++++++++------------ 3 files changed, 83 insertions(+), 47 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index c451162ae90a..9efbd4bb2019 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,3 +1,4 @@ +import logging import typing import asyncio import colorama @@ -38,6 +39,26 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): # 2. Listen (have the REPL compiler connect and listen on the game's REPL server's socket). # 3. Compile (have the REPL compiler compile the game into object code it can run). # All 3 need to be done, and in this order, for this to work. + def _cmd_repl(self, *arguments: str): + """Sends a command to the OpenGOAL REPL. Arguments: + - connect : connect a new client to the REPL. + - listen : listen to the game's internal socket. + - compile : compile the game into executable object code. + - verify : verify successful compilation.""" + if arguments: + if arguments[0] == "connect": + if arguments[1] and arguments[2]: + self.ctx.repl.ip = str(arguments[1]) + self.ctx.repl.port = int(arguments[2]) + self.ctx.repl.connect() + else: + logging.error("You must provide the ip address and port (default 127.0.0.1 port 8181).") + if arguments[0] == "listen": + self.ctx.repl.listen() + if arguments[0] == "compile": + self.ctx.repl.compile() + if arguments[0] == "verify": + self.ctx.repl.verify() class JakAndDaxterContext(CommonContext): diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 020805b32b2f..2a3b32a259ce 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -3,6 +3,14 @@ import pymem from pymem import pattern from pymem.exception import ProcessNotFound +from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies + +# Some helpful constants. +next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. +next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. +cells_offset = 16 +buzzers_offset = 420 # cells_offset + (sizeof uint32 * 101 cells) = 16 + (4 * 101) +end_marker_offset = 868 # buzzers_offset + (sizeof uint32 * 112 flies) = 420 + (4 * 112) class JakAndDaxterMemoryReader: @@ -14,7 +22,7 @@ class JakAndDaxterMemoryReader: marker_address = None goal_address = None - location_outbox = {} + location_outbox = [] outbox_index = 0 def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): @@ -22,7 +30,6 @@ def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.connected = self.connect() if self.connected and self.marker: self.marked = self.find_marker() - pass async def main_tick(self, location_callback: typing.Callable): self.read_memory() @@ -55,5 +62,17 @@ def find_marker(self) -> bool: return True return False - def read_memory(self) -> typing.Dict: - pass + def read_memory(self) -> typing.List[int]: + next_cell_index = int.from_bytes(self.process.read_bytes(self.goal_address, 8)) + next_buzzer_index = int.from_bytes(self.process.read_bytes(self.goal_address + next_buzzer_index_offset, 8)) + next_cell = int.from_bytes(self.process.read_bytes(self.goal_address + cells_offset + (next_cell_index * 4), 4)) + next_buzzer = int.from_bytes(self.process.read_bytes(self.goal_address + cells_offset + (next_buzzer_index * 4), 4)) + + if next_cell not in self.location_outbox: + self.location_outbox.append(Cells.to_ap_id(next_cell)) + if next_buzzer not in self.location_outbox: + self.location_outbox.append(Flies.to_ap_id(next_buzzer)) + + return self.location_outbox + + diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 0a7ca9f97cdb..7d39ac866b86 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -22,29 +22,29 @@ def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.port = port async def init(self): - self.connected = await self.connect() + self.connected = self.connect() if self.connected: - self.listening = await self.listen() + self.listening = self.listen() if self.connected and self.listening: - self.compiled = await self.compile() + self.compiled = self.compile() async def main_tick(self): # Receive Items from AP. Handle 1 item per tick. if len(self.item_inbox) > self.inbox_index: - await self.receive_item() + self.receive_item() self.inbox_index += 1 # This helper function formats and sends `form` as a command to the REPL. # ALL commands to the REPL should be sent using this function. # TODO - this needs to block on receiving an acknowledgement from the REPL server. # Problem is, it doesn't ack anything right now. So we need that to happen first. - async def send_form(self, form: str) -> None: + def send_form(self, form: str) -> None: header = struct.pack(" bool: + def connect(self) -> bool: if not self.ip or not self.port: return False @@ -57,71 +57,67 @@ async def connect(self) -> bool: except ConnectionRefusedError: return False - async def listen(self) -> bool: - await self.send_form("(lt)") + def listen(self) -> bool: + self.send_form("(lt)") return True - async def compile(self) -> bool: + def compile(self) -> bool: # Show this visual cue when compilation is started. # It's the version number of the OpenGOAL Compiler. - await self.send_form("(set! *debug-segment* #t)") + self.send_form("(set! *debug-segment* #t)") # Play this audio cue when compilation is started. # It's the sound you hear when you press START + CIRCLE to open the Options menu. - await self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"start-options\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"start-options\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") # Start compilation. This is blocking, so nothing will happen until the REPL is done. - await self.send_form("(mi)") + self.send_form("(mi)") # Play this audio cue when compilation is complete. # It's the sound you hear when you press START + START to close the Options menu. - await self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu-close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") # Disable cheat-mode and debug (close the visual cue). - await self.send_form("(set! *cheat-mode* #f)") - # await self.send_form("(set! *debug-segment* #f)") + self.send_form("(set! *cheat-mode* #f)") + # self.send_form("(set! *debug-segment* #f)") return True - async def verify(self) -> bool: - await self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + def verify(self) -> bool: + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu-close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") return True - async def receive_item(self): + def receive_item(self): ap_id = self.item_inbox[self.inbox_index]["item"] # Determine the type of item to receive. if ap_id in range(jak1_id, jak1_id + Flies.fly_offset): - await self.receive_power_cell(ap_id) + self.receive_power_cell(ap_id) elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Orbs.orb_offset): - await self.receive_scout_fly(ap_id) + self.receive_scout_fly(ap_id) elif ap_id > jak1_id + Orbs.orb_offset: pass # TODO - # TODO - In ArchipelaGOAL, override the 'get-pickup event so that it doesn't give you the item, - # it just plays the victory animation. Then define a new event type like 'get-archipelago - # to actually give ourselves the item. See game-info.gc and target-handler.gc. - - async def receive_power_cell(self, ap_id: int) -> None: + def receive_power_cell(self, ap_id: int) -> None: cell_id = Cells.to_game_id(ap_id) - await self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type fuel-cell) " - "(the float " + str(cell_id) + "))") + self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") - async def receive_scout_fly(self, ap_id: int) -> None: + def receive_scout_fly(self, ap_id: int) -> None: fly_id = Flies.to_game_id(ap_id) - await self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type buzzer) " - "(the float " + str(fly_id) + "))") + self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))") From 75461d62a03c048e5327d5d659fd0d40de6eda4a Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 29 Apr 2024 22:42:52 -0400 Subject: [PATCH 21/70] Jak 1: There's magic in the air... --- worlds/jakanddaxter/Client.py | 25 ++++++--- .../{Options.py => JakAndDaxterOptions.py} | 0 worlds/jakanddaxter/Regions.py | 2 +- worlds/jakanddaxter/Rules.py | 2 +- worlds/jakanddaxter/__init__.py | 2 +- worlds/jakanddaxter/client/MemoryReader.py | 51 ++++++++++++++----- worlds/jakanddaxter/client/ReplClient.py | 5 +- 7 files changed, 61 insertions(+), 26 deletions(-) rename worlds/jakanddaxter/{Options.py => JakAndDaxterOptions.py} (100%) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 9efbd4bb2019..9d0edc8c19d1 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -6,9 +6,9 @@ import Utils from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled -from .GameID import jak1_name -from .client.ReplClient import JakAndDaxterReplClient -from .client.MemoryReader import JakAndDaxterMemoryReader +from worlds.jakanddaxter.GameID import jak1_name +from worlds.jakanddaxter.client.ReplClient import JakAndDaxterReplClient +from worlds.jakanddaxter.client.MemoryReader import JakAndDaxterMemoryReader import ModuleUpdate ModuleUpdate.update() @@ -96,9 +96,16 @@ class JakAndDaxterManager(GameManager): self.ui = JakAndDaxterManager(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(JakAndDaxterContext, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + def on_package(self, cmd: str, args: dict): if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): + logger.info(args) self.repl.item_inbox[index] = item async def ap_inform_location_checks(self, location_ids: typing.List[int]): @@ -109,12 +116,14 @@ def on_locations(self, location_ids: typing.List[int]): create_task_log_exception(self.ap_inform_location_checks(location_ids)) async def run_repl_loop(self): - await self.repl.main_tick() - await asyncio.sleep(0.1) + while True: + await self.repl.main_tick() + await asyncio.sleep(0.1) async def run_memr_loop(self): - await self.memr.main_tick(self.on_locations) - await asyncio.sleep(0.1) + while True: + await self.memr.main_tick(self.on_locations) + await asyncio.sleep(0.1) async def main(): @@ -139,4 +148,4 @@ async def main(): def launch(): colorama.init() asyncio.run(main()) - colorama.deinit() \ No newline at end of file + colorama.deinit() diff --git a/worlds/jakanddaxter/Options.py b/worlds/jakanddaxter/JakAndDaxterOptions.py similarity index 100% rename from worlds/jakanddaxter/Options.py rename to worlds/jakanddaxter/JakAndDaxterOptions.py diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index c13b48541829..fa4a6aa88015 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -2,7 +2,7 @@ from enum import Enum, auto from BaseClasses import MultiWorld, Region from .GameID import jak1_name -from .Options import JakAndDaxterOptions +from .JakAndDaxterOptions import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table from .locs import CellLocations as Cells, ScoutLocations as Scouts diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 467953a69748..6758b3c7ce56 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,5 +1,5 @@ from BaseClasses import MultiWorld, CollectionState -from .Options import JakAndDaxterOptions +from .JakAndDaxterOptions import JakAndDaxterOptions from .Regions import Jak1Level, Jak1SubLevel, level_table, subLevel_table from .Locations import location_table as item_table from .locs import CellLocations as Cells, ScoutLocations as Scouts diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index a07798475b7e..92f5549f3a1c 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,6 +1,6 @@ from BaseClasses import Item, ItemClassification, Tutorial from .GameID import jak1_id, jak1_name -from .Options import JakAndDaxterOptions +from .JakAndDaxterOptions import JakAndDaxterOptions from .Items import JakAndDaxterItem from .Locations import JakAndDaxterLocation, location_table as item_table from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 2a3b32a259ce..1c06289deaf3 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,8 +1,9 @@ import typing -import subprocess import pymem from pymem import pattern from pymem.exception import ProcessNotFound + +from CommonClient import logger from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies # Some helpful constants. @@ -18,7 +19,7 @@ class JakAndDaxterMemoryReader: connected: bool = False marked: bool = False - process = None + process: pymem.process = None marker_address = None goal_address = None @@ -33,17 +34,20 @@ def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): async def main_tick(self, location_callback: typing.Callable): self.read_memory() + location_callback(self.location_outbox) - # Checked Locations in game. Handle 1 location per tick. + # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. if len(self.location_outbox) > self.outbox_index: - await location_callback(self.location_outbox[self.outbox_index]) + location_callback(self.location_outbox) self.outbox_index += 1 def connect(self) -> bool: try: self.process = pymem.Pymem("gk.exe") # The GOAL Kernel + logger.info("Found the gk process: " + str(self.process.process_id)) return True except ProcessNotFound: + logger.error("Could not find the gk process.") return False def find_marker(self) -> bool: @@ -59,19 +63,40 @@ def find_marker(self) -> bool: self.goal_address = int.from_bytes(self.process.read_bytes(goal_pointer, 8), byteorder="little", signed=False) + logger.info("Found the archipelago memory address: " + str(self.goal_address)) return True + logger.error("Could not find the archipelago memory address.") return False def read_memory(self) -> typing.List[int]: - next_cell_index = int.from_bytes(self.process.read_bytes(self.goal_address, 8)) - next_buzzer_index = int.from_bytes(self.process.read_bytes(self.goal_address + next_buzzer_index_offset, 8)) - next_cell = int.from_bytes(self.process.read_bytes(self.goal_address + cells_offset + (next_cell_index * 4), 4)) - next_buzzer = int.from_bytes(self.process.read_bytes(self.goal_address + cells_offset + (next_buzzer_index * 4), 4)) - - if next_cell not in self.location_outbox: - self.location_outbox.append(Cells.to_ap_id(next_cell)) - if next_buzzer not in self.location_outbox: - self.location_outbox.append(Flies.to_ap_id(next_buzzer)) + next_cell_index = int.from_bytes( + self.process.read_bytes(self.goal_address, 8), + byteorder="little", + signed=False) + next_buzzer_index = int.from_bytes( + self.process.read_bytes(self.goal_address + next_buzzer_index_offset, 8), + byteorder="little", + signed=False) + + for k in range(0, next_cell_index): + next_cell = int.from_bytes( + self.process.read_bytes(self.goal_address + cells_offset + (k * 4), 4), + byteorder="little", + signed=False) + cell_ap_id = Cells.to_ap_id(next_cell) + if cell_ap_id not in self.location_outbox: + self.location_outbox.append(cell_ap_id) + logger.info("Checked power cell: " + str(next_cell)) + + for k in range(0, next_buzzer_index): + next_buzzer = int.from_bytes( + self.process.read_bytes(self.goal_address + buzzers_offset + (k * 4), 4), + byteorder="little", + signed=False) + buzzer_ap_id = Flies.to_ap_id(next_buzzer) + if buzzer_ap_id not in self.location_outbox: + self.location_outbox.append(buzzer_ap_id) + logger.info("Checked scout fly: " + str(next_buzzer)) return self.location_outbox diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 7d39ac866b86..695f78d1ef60 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -54,7 +54,8 @@ def connect(self) -> bool: time.sleep(1) logger.info(self.socket.recv(1024).decode()) return True - except ConnectionRefusedError: + except ConnectionRefusedError as e: + logger.error(e.strerror) return False def listen(self) -> bool: @@ -96,7 +97,7 @@ def verify(self) -> bool: return True def receive_item(self): - ap_id = self.item_inbox[self.inbox_index]["item"] + ap_id = getattr(self.item_inbox[self.inbox_index], "item") # Determine the type of item to receive. if ap_id in range(jak1_id, jak1_id + Flies.fly_offset): From b1f1464f6fe8b21ae39a31c91e66c75a4e41a862 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:10:52 -0400 Subject: [PATCH 22/70] Jak 1: Fixed bug translating scout fly ID's. --- worlds/jakanddaxter/locs/ScoutLocations.py | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index 22587de2f734..c1349d0d6367 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -14,6 +14,9 @@ # So we need to offset all of their ID's in order for Archipelago to separate them # from their power cells. We can use 1024 (2^10) for this purpose, because scout flies # only ever need 10 bits to identify themselves (3 for the index, 7 for the cell ID). + +# We're also going to compress the ID by bit-shifting the fly index down to lower bits, +# keeping the scout fly ID range to a smaller set of numbers (1000 -> 2000, instead of 1 -> 400000). fly_offset = 1024 @@ -21,20 +24,26 @@ # scout fly and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." - cell_id = get_cell_id(game_id) # This is AP/OpenGOAL agnostic, works on either ID. - buzzer_index = (game_id - cell_id) >> 9 # Subtract the cell ID, bit shift the index down 9 places. - return jak1_id + fly_offset + buzzer_index + cell_id # Add the offsets, the bit-shifted index, and the cell ID. + cell_id = get_cell_id(game_id) # Get the power cell ID from the lowest 7 bits. + buzzer_index = (game_id - cell_id) >> 9 # Get the index, bit shift it down 9 places. + compressed_id = fly_offset + buzzer_index + cell_id # Add the offset, the bit-shifted index, and the cell ID. + return jak1_id + compressed_id # Last thing: add the game's ID. def to_game_id(ap_id: int) -> int: assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." - cell_id = get_cell_id(ap_id) # This is AP/OpenGOAL agnostic, works on either ID. - buzzer_index = ap_id - jak1_id - fly_offset - cell_id # Reverse process, subtract the offsets and the cell ID. - return (buzzer_index << 9) + cell_id # Bit shift the index up 9 places, re-add the cell ID. + compressed_id = ap_id - jak1_id # Reverse process. First thing: subtract the game's ID. + cell_id = get_cell_id(compressed_id) # Get the power cell ID from the lowest 7 bits. + buzzer_index = compressed_id - fly_offset - cell_id # Get the bit-shifted index. + return (buzzer_index << 9) + cell_id # Return the index to its normal place, re-add the cell ID. +# Get the power cell ID from the lowest 7 bits. +# Make sure to use this function ONLY when the input argument does NOT include jak1_id, +# because that number may flip some of the bottom 7 bits, and that will throw off this bit mask. def get_cell_id(buzzer_id: int) -> int: - return buzzer_id & 0b1111111 # Get the power cell ID from the lowest 7 bits. + assert buzzer_id < jak1_id, f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}." + return buzzer_id & 0b1111111 # The ID's you see below correspond directly to that fly's 32-bit ID in the game. From f0659e36db3a603abcd40ff385f721c4309ab1e3 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Wed, 1 May 2024 17:35:52 -0400 Subject: [PATCH 23/70] Jak 1: Make the REPL a little more verbose, easier to debug. --- worlds/jakanddaxter/Client.py | 2 +- worlds/jakanddaxter/client/ReplClient.py | 140 +++++++++++++++-------- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 9d0edc8c19d1..f3c2ef6187d4 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -36,7 +36,7 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): # TODO - Clean up commands related to the REPL, make them more user friendly. # The REPL has a specific order of operations it needs to do in order to process our input: # 1. Connect (we need to open a socket connection on ip/port to the REPL). - # 2. Listen (have the REPL compiler connect and listen on the game's REPL server's socket). + # 2. Listen (have the REPL compiler connect and listen on the game's internal socket). # 3. Compile (have the REPL compiler compile the game into object code it can run). # All 3 need to be done, and in this order, for this to work. def _cmd_repl(self, *arguments: str): diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 695f78d1ef60..9149950b6c45 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -37,64 +37,94 @@ async def main_tick(self): # This helper function formats and sends `form` as a command to the REPL. # ALL commands to the REPL should be sent using this function. - # TODO - this needs to block on receiving an acknowledgement from the REPL server. - # Problem is, it doesn't ack anything right now. So we need that to happen first. - def send_form(self, form: str) -> None: + # TODO - this blocks on receiving an acknowledgement from the REPL server. But it doesn't print + # any log info in the meantime. Is that a problem? + def send_form(self, form: str, print_ok: bool = True) -> bool: header = struct.pack(" bool: + logger.info("Connecting to the OpenGOAL REPL...") if not self.ip or not self.port: + logger.error(f"Unable to connect: IP address \"{self.ip}\" or port \"{self.port}\" was not provided.") return False try: self.socket = socket(AF_INET, SOCK_STREAM) self.socket.connect((self.ip, self.port)) time.sleep(1) - logger.info(self.socket.recv(1024).decode()) - return True + welcome_message = self.socket.recv(1024).decode() + + # Should be the OpenGOAL welcome message (ignore version number). + if "Connected to OpenGOAL" and "nREPL!" in welcome_message: + logger.info(welcome_message) + return True + else: + logger.error(f"Unable to connect: unexpected welcome message \"{welcome_message}\"") + return False except ConnectionRefusedError as e: - logger.error(e.strerror) + logger.error(f"Unable to connect: {e.strerror}") return False def listen(self) -> bool: - self.send_form("(lt)") - return True + logger.info("Listening for the game...") + return self.send_form("(lt)") def compile(self) -> bool: - # Show this visual cue when compilation is started. - # It's the version number of the OpenGOAL Compiler. - self.send_form("(set! *debug-segment* #t)") - - # Play this audio cue when compilation is started. - # It's the sound you hear when you press START + CIRCLE to open the Options menu. - self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"start-options\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") - - # Start compilation. This is blocking, so nothing will happen until the REPL is done. - self.send_form("(mi)") - - # Play this audio cue when compilation is complete. - # It's the sound you hear when you press START + START to close the Options menu. - self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu-close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") - - # Disable cheat-mode and debug (close the visual cue). - self.send_form("(set! *cheat-mode* #f)") - # self.send_form("(set! *debug-segment* #f)") - return True + logger.info("Compiling the game... Wait for the success sound before continuing!") + ok_count = 0 + try: + # Show this visual cue when compilation is started. + # It's the version number of the OpenGOAL Compiler. + if self.send_form("(set! *debug-segment* #t)", print_ok=False): + ok_count += 1 + + # Play this audio cue when compilation is started. + # It's the sound you hear when you press START + CIRCLE to open the Options menu. + if self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"start-options\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False): + ok_count += 1 + + # Start compilation. This is blocking, so nothing will happen until the REPL is done. + if self.send_form("(mi)", print_ok=False): + ok_count += 1 + + # Play this audio cue when compilation is complete. + # It's the sound you hear when you press START + START to close the Options menu. + if self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu-close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False): + ok_count += 1 + + # Disable cheat-mode and debug (close the visual cue). + # self.send_form("(set! *debug-segment* #f)") + if self.send_form("(set! *cheat-mode* #f)"): + ok_count += 1 + + except: + logger.error(f"Unable to compile: commands were not sent properly.") + return False + + # Now wait until we see the success message... 5 times. + return ok_count == 5 def verify(self) -> bool: - self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu-close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") - return True + logger.info("Verifying compilation... if you don't hear the success sound, try listening and compiling again!") + return self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu-close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") def receive_item(self): ap_id = getattr(self.item_inbox[self.inbox_index], "item") @@ -109,16 +139,26 @@ def receive_item(self): elif ap_id > jak1_id + Orbs.orb_offset: pass # TODO - def receive_power_cell(self, ap_id: int) -> None: + def receive_power_cell(self, ap_id: int) -> bool: cell_id = Cells.to_game_id(ap_id) - self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type fuel-cell) " - "(the float " + str(cell_id) + "))") - - def receive_scout_fly(self, ap_id: int) -> None: + ok = self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") + if ok: + logger.info(f"Received power cell {cell_id}!") + else: + logger.error(f"Unable to receive power cell {cell_id}!") + return ok + + def receive_scout_fly(self, ap_id: int) -> bool: fly_id = Flies.to_game_id(ap_id) - self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type buzzer) " - "(the float " + str(fly_id) + "))") + ok = self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))") + if ok: + logger.info(f"Received scout fly {fly_id}!") + else: + logger.error(f"Unable to receive scout fly {fly_id}!") + return ok From f4a51a8f4a3c6f80b80e9825090b891fdab39844 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 2 May 2024 20:00:19 -0400 Subject: [PATCH 24/70] Jak 1: Did you know Snowy Mountain had such specific unlock requirements? I didn't. --- worlds/jakanddaxter/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 6758b3c7ce56..bb355e0e2cec 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -158,7 +158,8 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_regions(multiworld, player, Jak1Level.VOLCANIC_CRATER, Jak1Level.SNOWY_MOUNTAIN, - lambda state: has_count_of(pre_sm_cells, 2, player, state)) + lambda state: has_count_of(pre_sm_cells, 2, player, state) + or state.count_group("Power Cell", player) >= 71) # Yeah, this is a weird one. connect_region_to_sub(multiworld, player, Jak1Level.SNOWY_MOUNTAIN, From 6637452b64adceef33f0eb850d7d686db78c6ed3 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 9 May 2024 12:48:33 -0400 Subject: [PATCH 25/70] Jak 1: Update Documentation. --- .../en_Jak and Daxter The Precursor Legacy.md | 57 +++++++++++-------- worlds/jakanddaxter/docs/setup_en.md | 42 +++++++++++++- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index e33459d9e34f..9ca1c86079af 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -5,7 +5,8 @@ The [Player Options Page](../player-options) for this game contains all the options you need to configure and export a config file. -At this time, Scout Flies are always randomized despite the option setting... this is a work in progress. +At this time, these options don't do anything. Scout Flies are always randomized, and Precursor Orbs +are never randomized. ## What does randomization do to this game? All 101 Power Cells and 112 Scout Flies are now Location Checks @@ -14,38 +15,44 @@ and may contain Items for different games as well as different Items from within ## What is the goal of the game once randomized? To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. -In order to reach them, you will need at least 72 Power Cells to cross the Lava Tube. In addition, you will need -the four specific Power Cells obtained by freeing the Red, Blue, Yellow, and Green Sages. +In order to reach them, you will need at least 72 Power Cells to cross the Lava Tube. In addition, +you will need the four specific Power Cells obtained by freeing the Red, Blue, Yellow, and Green Sages. -## What happens when I pick up an item in the game? -Jak and Daxter will perform their victory animation, if applicable, but you will not necessarily receive the item -you just picked up. The Power Cell count will not change, nor the Scout Fly count, unless you picked up a randomized -Item that just happens to belong to the game. You will however see a message saying what you did find and who it -belongs to. +## How do I progress through the game? +You can progress by performing tasks and completing the challenges that would normally give you Power Cells and +Scout Flies in the game. If you are playing with others, those players may find Power Cells and Scout Flies +in their games, and those Items will be automatically sent to your game. -## How do I get more items to progress in the game? -You can progress by performing the tasks and completing the challenges that would normally give you Power Cells -and Scout Flies in the game. Other players may also find Power Cells and Scout Flies in their games, and those Items -will be automatically sent to your game. When you receive an Item, a Message will pop up to inform you where you -received the Item from, and which one it is. Your Item count for that type of Item will also tick up. +If you have completed all possible tasks available to you but still cannot progress, you may have to wait for +another player to find enough of your game's Items to allow you to progress. If that does not apply, +double check your spoiler log to make sure you have all the items you should have. If you don't, +you may have encountered a bug. Please see the options for bug reporting below. -If you have completed all possible tasks available to you but still cannot progress, you may have to wait for another -player to find enough of your game's Items to allow you to progress. +## What happens when I pick up an item? +Jak and Daxter will perform their victory animation, if applicable. You will not receive that item, and +the Item count for that item will not change. The pause menu will say "Task Completed" below the +picked-up Power Cell, but the icon will remain "dormant." You will see a message in your text client saying +what you found and who it belongs to. + +## What happens when I receive an item? +Jak and Daxter won't perform their victory animation, and gameplay will continue as normal. Your text client will +inform you where you received the Item from, and which one it is. Your Item count for that type of Item will also +tick up. The pause menu will not say "Task Completed" below the selected Power Cell, but the icon will be "activated." ## I can't reach a certain area within an accessible region, how do I get there? -Some areas are locked behind ownership of specific Power Cells. For example, you cannot access Misty Island until you -have the "Catch 200 Pounds of Fish" Power Cell. Keep in mind, your access to Misty Island is determined +Some areas are locked behind ownership of specific Power Cells. For example, you cannot access Misty Island +until you have the "Catch 200 Pounds of Fish" Power Cell. Keep in mind, your access to Misty Island is determined _through ownership of this specific Power Cell only,_ **not** _by you completing the Fishing minigame._ ## I got soft-locked and can't leave, how do I get out of here? -As stated before, some areas are locked behind ownership of specific Power Cells. But you may already be -past a point-of-no-return preventing you from backtracking. One example is the Forbidden Jungle temple, -where the elevator is locked at the bottom, and if you haven't unlocked the Blue Eco Switch, -you cannot access the Plant Boss's room and escape. - -In this scenario, you will need to open your menu and find the "Teleport Home" option. Selecting this option will -instantly teleport you to the nearest Sage's Hut in the last hub area you were in... or always the Green Sage's Hut, -depending on the feasibility of the former option. As stated before... it's a work in progress. +As stated before, some areas are locked behind ownership of specific Power Cells. But you may already be past +a point-of-no-return preventing you from backtracking. One example is the Forbidden Jungle temple, where +the elevator is locked at the bottom, and if you haven't unlocked the Blue Eco Switch, you cannot access +the Plant Boss's room and escape. + +In this scenario, you will need to open your menu and find the "Teleport Home" option. Selecting this option +will instantly teleport you to the nearest Sage's Hut in the last hub area you were in... or always to +the Green Sage's Hut, depending on the feasibility of the former option. This feature is a work in progress. ## I think I found a bug, where should I report it? Depending on the nature of the bug, there are a couple of different options. diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 61898991a744..807d9ac70133 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -3,11 +3,15 @@ ## Required Software - A legally purchased copy of *Jak And Daxter: The Precursor Legacy.* +- Python version 3.10 or higher. Make sure this is added to your PATH environment variable. +- [Task](https://taskfile.dev/installation/) (This makes it easier to run commands.) ## Installation ### Installation via OpenGOAL Mod Launcher +At this time, the only supported method of setup is through Manual Compilation. Aside from the legal copy of the game, all tools required to do this are free. + ***Windows Preparations*** ***Linux Preparations*** @@ -18,22 +22,58 @@ ***Windows Preparations*** +- Dump your copy of the game as an ISO file to your PC. +- Download a zipped up copy of the Archipelago Server and Client [here.](https://github.com/ArchipelaGOAL/Archipelago) +- Download a zipped up copy of the modded OpenGOAL game [here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL) +- Unzip the two projects into easily accessible directories. + + ***Linux Preparations*** ***Compiling*** ## Starting a Game +- Open 3 Powershell windows. If you have VSCode, you can run 3 terminals to consolidate this process. + - In the first window, navigate to the Archipelago folder using `cd` and run `python ./Launcher.py`. + - In the second window, navigate to the ArchipelaGOAL folder and run `task extract`. This will prompt you to tell the mod where to find your ISO file to dump its cotnents. When that is done, run `task repl`. + - In the third window, navigate to the ArchipelaGOAL folder and run `task boot-game`. At this point, Jak should be standing outside Samos's hut. +- In the Launcher, click Generate to create a new random seed. Save the resulting zip file. +- In the Launcher, click Host to host the Archipelago server. It will prompt you for the location of that zip file. +- Once the server is running, in the Launcher, find the Jak and Daxter Client and click it. You should see the `task repl` window begin to compile the game. +- When it completes, you should hear the menu closing sound effect, and the Client window should appear. +- Connect the client to the Archipelago server and enter your slot name. Once this is done, the game should be ready to play. Talk to Samos to trigger the cutscene where he sends you to Geyser Rock, and off you go! + +Once you complete the setup steps, you need to repeat most of them in order to launch a new game. You can skip a few steps: +- You never need to download the zip copies of the projects again (unless there are updates). +- You never need to dump your ISO again. +- You never need to extract the ISO assets again. + ### Joining a MultiWorld Game +MultiWorld games are untested at this time. + ### Playing Offline +Offline play is untested at this time. + ## Installation and Setup Troubleshooting ### Compilation Failures ### Runtime Failures +- If the client window appears but no sound plays, you will need to enter the repl commands into the client to connect it to the game. These are, in order: + - `/repl connect 127.0.0.1 8181` + - `/repl listen` + - `/repl compile` +- Once these are done, you can enter `/repl verify` and you should hear the menu sound again. + ## Gameplay Troubleshooting -### Known Issues \ No newline at end of file +### Known Issues + +- Needing to open so many windows to play the game is a massive pain, and I hope to streamline this process in the future. +- The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly. +- The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. +- The game relates tasks and power cells closely but separately. Some issues may result from having to tell the game to check for the power cells you own, rather than the tasks you completed. \ No newline at end of file From 240bb6c255b4454c21bcf9ce88fa8f8478e4310a Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 9 May 2024 17:23:03 -0400 Subject: [PATCH 26/70] Jak 1: Simplify user interaction with agents, make process more robust/less dependent on order of ops. --- worlds/jakanddaxter/Client.py | 35 +++--- worlds/jakanddaxter/client/MemoryReader.py | 121 +++++++++++--------- worlds/jakanddaxter/client/ReplClient.py | 124 ++++++++++++++------- 3 files changed, 167 insertions(+), 113 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index f3c2ef6187d4..bcbbc175cc7b 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -33,7 +33,6 @@ async def _log_exception(a): class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): ctx: "JakAndDaxterContext" - # TODO - Clean up commands related to the REPL, make them more user friendly. # The REPL has a specific order of operations it needs to do in order to process our input: # 1. Connect (we need to open a socket connection on ip/port to the REPL). # 2. Listen (have the REPL compiler connect and listen on the game's internal socket). @@ -41,24 +40,24 @@ class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): # All 3 need to be done, and in this order, for this to work. def _cmd_repl(self, *arguments: str): """Sends a command to the OpenGOAL REPL. Arguments: - - connect : connect a new client to the REPL. - - listen : listen to the game's internal socket. - - compile : compile the game into executable object code. - - verify : verify successful compilation.""" + - connect : connect the client to the REPL (goalc). + - status : check internal status of the REPL.""" if arguments: if arguments[0] == "connect": - if arguments[1] and arguments[2]: - self.ctx.repl.ip = str(arguments[1]) - self.ctx.repl.port = int(arguments[2]) - self.ctx.repl.connect() - else: - logging.error("You must provide the ip address and port (default 127.0.0.1 port 8181).") - if arguments[0] == "listen": - self.ctx.repl.listen() - if arguments[0] == "compile": - self.ctx.repl.compile() - if arguments[0] == "verify": - self.ctx.repl.verify() + logger.info("This may take a bit... Wait for the success audio cue before continuing!") + self.ctx.repl.user_connect = True # Will attempt to reconnect on next tick. + if arguments[0] == "status": + self.ctx.repl.print_status() + + def _cmd_memr(self, *arguments: str): + """Sends a command to the Memory Reader. Arguments: + - connect : connect the memory reader to the game process (gk). + - status : check the internal status of the Memory Reader.""" + if arguments: + if arguments[0] == "connect": + self.ctx.memr.connect() + if arguments[0] == "status": + self.ctx.memr.print_status() class JakAndDaxterContext(CommonContext): @@ -131,8 +130,6 @@ async def main(): ctx = JakAndDaxterContext(None, None) - await ctx.repl.init() - ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") ctx.repl_task = create_task_log_exception(ctx.run_repl_loop()) ctx.memr_task = create_task_log_exception(ctx.run_memr_loop()) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 1c06289deaf3..04fa0255129e 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,7 +1,7 @@ import typing import pymem from pymem import pattern -from pymem.exception import ProcessNotFound +from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError from CommonClient import logger from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies @@ -16,88 +16,107 @@ class JakAndDaxterMemoryReader: marker: typing.ByteString + goal_address = None connected: bool = False - marked: bool = False - process: pymem.process = None - marker_address = None - goal_address = None + # The memory reader just needs the game running. + gk_process: pymem.process = None location_outbox = [] outbox_index = 0 def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker - self.connected = self.connect() - if self.connected and self.marker: - self.marked = self.find_marker() + self.connect() async def main_tick(self, location_callback: typing.Callable): + if self.connected: + try: + self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. + except (ProcessError, MemoryReadError, WinAPIError): + logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") + self.connected = False + else: + return + + # Read the memory address to check the state of the game. self.read_memory() - location_callback(self.location_outbox) + location_callback(self.location_outbox) # TODO - I forgot why call this here when it's already down below... # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. if len(self.location_outbox) > self.outbox_index: location_callback(self.location_outbox) self.outbox_index += 1 - def connect(self) -> bool: + def connect(self): try: - self.process = pymem.Pymem("gk.exe") # The GOAL Kernel - logger.info("Found the gk process: " + str(self.process.process_id)) - return True + self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel + logger.info("Found the gk process: " + str(self.gk_process.process_id)) except ProcessNotFound: logger.error("Could not find the gk process.") - return False - - def find_marker(self) -> bool: + self.connected = False + return - # If we don't find the marker in the first module's worth of memory, we've failed. - modules = list(self.process.list_modules()) - self.marker_address = pattern.pattern_scan_module(self.process.process_handle, modules[0], self.marker) - if self.marker_address: + # If we don't find the marker in the first loaded module, we've failed. + modules = list(self.gk_process.list_modules()) + marker_address = pattern.pattern_scan_module(self.gk_process.process_handle, modules[0], self.marker) + if marker_address: # At this address is another address that contains the struct we're looking for: the game's state. # From here we need to add the length in bytes for the marker and 4 bytes of padding, # and the struct address is 8 bytes long (it's u64). - goal_pointer = self.marker_address + len(self.marker) + 4 - self.goal_address = int.from_bytes(self.process.read_bytes(goal_pointer, 8), + goal_pointer = marker_address + len(self.marker) + 4 + self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, 8), byteorder="little", signed=False) logger.info("Found the archipelago memory address: " + str(self.goal_address)) - return True - logger.error("Could not find the archipelago memory address.") - return False + self.connected = True + else: + logger.error("Could not find the archipelago memory address.") + self.connected = False + + if self.connected: + logger.info("The Memory Reader is ready!") + + def print_status(self): + logger.info("Memory Reader Status:") + logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None")) + logger.info(" Game state memory address: " + str(self.goal_address)) + logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index]) + if self.outbox_index else "None")) def read_memory(self) -> typing.List[int]: - next_cell_index = int.from_bytes( - self.process.read_bytes(self.goal_address, 8), - byteorder="little", - signed=False) - next_buzzer_index = int.from_bytes( - self.process.read_bytes(self.goal_address + next_buzzer_index_offset, 8), - byteorder="little", - signed=False) - - for k in range(0, next_cell_index): - next_cell = int.from_bytes( - self.process.read_bytes(self.goal_address + cells_offset + (k * 4), 4), + try: + next_cell_index = int.from_bytes( + self.gk_process.read_bytes(self.goal_address, 8), byteorder="little", signed=False) - cell_ap_id = Cells.to_ap_id(next_cell) - if cell_ap_id not in self.location_outbox: - self.location_outbox.append(cell_ap_id) - logger.info("Checked power cell: " + str(next_cell)) - - for k in range(0, next_buzzer_index): - next_buzzer = int.from_bytes( - self.process.read_bytes(self.goal_address + buzzers_offset + (k * 4), 4), + next_buzzer_index = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + next_buzzer_index_offset, 8), byteorder="little", signed=False) - buzzer_ap_id = Flies.to_ap_id(next_buzzer) - if buzzer_ap_id not in self.location_outbox: - self.location_outbox.append(buzzer_ap_id) - logger.info("Checked scout fly: " + str(next_buzzer)) - - return self.location_outbox + for k in range(0, next_cell_index): + next_cell = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + cells_offset + (k * 4), 4), + byteorder="little", + signed=False) + cell_ap_id = Cells.to_ap_id(next_cell) + if cell_ap_id not in self.location_outbox: + self.location_outbox.append(cell_ap_id) + logger.info("Checked power cell: " + str(next_cell)) + + for k in range(0, next_buzzer_index): + next_buzzer = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + buzzers_offset + (k * 4), 4), + byteorder="little", + signed=False) + buzzer_ap_id = Flies.to_ap_id(next_buzzer) + if buzzer_ap_id not in self.location_outbox: + self.location_outbox.append(buzzer_ap_id) + logger.info("Checked scout fly: " + str(next_buzzer)) + + except (ProcessError, MemoryReadError, WinAPIError): + logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") + self.connected = False + return self.location_outbox diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 9149950b6c45..cae7e6c8634e 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -1,6 +1,10 @@ import time import struct from socket import socket, AF_INET, SOCK_STREAM + +import pymem +from pymem.exception import ProcessNotFound, ProcessError + from CommonClient import logger from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, OrbLocations as Orbs from worlds.jakanddaxter.GameID import jak1_id @@ -9,10 +13,14 @@ class JakAndDaxterReplClient: ip: str port: int - socket: socket + sock: socket connected: bool = False - listening: bool = False - compiled: bool = False + user_connect: bool = False # Signals when user tells us to try reconnecting. + + # The REPL client needs the REPL/compiler process running, but that process + # also needs the game running. Therefore, the REPL client needs both running. + gk_process: pymem.process = None + goalc_process: pymem.process = None item_inbox = {} inbox_index = 0 @@ -20,15 +28,26 @@ class JakAndDaxterReplClient: def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.ip = ip self.port = port - - async def init(self): - self.connected = self.connect() - if self.connected: - self.listening = self.listen() - if self.connected and self.listening: - self.compiled = self.compile() + self.connect() async def main_tick(self): + if self.user_connect: + await self.connect() + self.user_connect = False + + if self.connected: + try: + self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. + except ProcessError: + logger.error("The gk process has died. Restart the game and run \"/repl connect\" again.") + self.connected = False + try: + self.goalc_process.read_bool(self.goalc_process.base_address) # Ping to see if it's alive. + except ProcessError: + logger.error("The goalc process has died. Restart the compiler and run \"/repl connect\" again.") + self.connected = False + else: + return # Receive Items from AP. Handle 1 item per tick. if len(self.item_inbox) > self.inbox_index: @@ -41,8 +60,8 @@ async def main_tick(self): # any log info in the meantime. Is that a problem? def send_form(self, form: str, print_ok: bool = True) -> bool: header = struct.pack(" bool: logger.error(f"Unexpected response from REPL: {response}") return False - def connect(self) -> bool: - logger.info("Connecting to the OpenGOAL REPL...") - if not self.ip or not self.port: - logger.error(f"Unable to connect: IP address \"{self.ip}\" or port \"{self.port}\" was not provided.") - return False + async def connect(self): + try: + self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel + logger.info("Found the gk process: " + str(self.gk_process.process_id)) + except ProcessNotFound: + logger.error("Could not find the gk process.") + return try: - self.socket = socket(AF_INET, SOCK_STREAM) - self.socket.connect((self.ip, self.port)) + self.goalc_process = pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL + logger.info("Found the goalc process: " + str(self.goalc_process.process_id)) + except ProcessNotFound: + logger.error("Could not find the goalc process.") + return + + try: + self.sock = socket(AF_INET, SOCK_STREAM) + self.sock.connect((self.ip, self.port)) time.sleep(1) - welcome_message = self.socket.recv(1024).decode() + welcome_message = self.sock.recv(1024).decode() # Should be the OpenGOAL welcome message (ignore version number). if "Connected to OpenGOAL" and "nREPL!" in welcome_message: logger.info(welcome_message) - return True else: - logger.error(f"Unable to connect: unexpected welcome message \"{welcome_message}\"") - return False + logger.error(f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"") except ConnectionRefusedError as e: - logger.error(f"Unable to connect: {e.strerror}") - return False + logger.error(f"Unable to connect to REPL websocket: {e.strerror}") + return - def listen(self) -> bool: - logger.info("Listening for the game...") - return self.send_form("(lt)") - - def compile(self) -> bool: - logger.info("Compiling the game... Wait for the success sound before continuing!") ok_count = 0 - try: + if self.sock: + + # Have the REPL listen to the game's internal websocket. + if self.send_form("(lt)", print_ok=False): + ok_count += 1 + # Show this visual cue when compilation is started. # It's the version number of the OpenGOAL Compiler. if self.send_form("(set! *debug-segment* #t)", print_ok=False): @@ -112,19 +137,32 @@ def compile(self) -> bool: if self.send_form("(set! *cheat-mode* #f)"): ok_count += 1 - except: - logger.error(f"Unable to compile: commands were not sent properly.") - return False + # Now wait until we see the success message... 6 times. + if ok_count == 6: + self.connected = True + else: + self.connected = False - # Now wait until we see the success message... 5 times. - return ok_count == 5 + if self.connected: + logger.info("The REPL is ready!") - def verify(self) -> bool: - logger.info("Verifying compilation... if you don't hear the success sound, try listening and compiling again!") - return self.send_form("(dotimes (i 1) " - "(sound-play-by-name " - "(static-sound-name \"menu-close\") " - "(new-sound-id) 1024 0 0 (sound-group sfx) #t))") + def print_status(self): + logger.info("REPL Status:") + logger.info(" REPL process ID: " + (str(self.goalc_process.process_id) if self.goalc_process else "None")) + logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None")) + try: + if self.sock: + ip, port = self.sock.getpeername() + logger.info(" Game websocket: " + (str(ip) + ", " + str(port) if ip else "None")) + self.send_form("(dotimes (i 1) " + "(sound-play-by-name " + "(static-sound-name \"menu-close\") " + "(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False) + except: + logger.warn(" Game websocket not found!") + logger.info(" Did you hear the success audio cue?") + logger.info(" Last item received: " + (str(getattr(self.item_inbox[self.inbox_index], "item")) + if self.inbox_index else "None")) def receive_item(self): ap_id = getattr(self.item_inbox[self.inbox_index], "item") From 4b4489a7ae8ee82bf861071e44a20ddee320f782 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 12 May 2024 15:22:53 -0400 Subject: [PATCH 27/70] Jak 1: Simplified startup process, updated docs, prayed. --- worlds/jakanddaxter/Client.py | 66 +++++++++++++++++-- worlds/jakanddaxter/JakAndDaxterOptions.py | 11 ++-- worlds/jakanddaxter/__init__.py | 9 +++ worlds/jakanddaxter/client/MemoryReader.py | 15 ++++- worlds/jakanddaxter/client/ReplClient.py | 6 +- .../en_Jak and Daxter The Precursor Legacy.md | 23 ++++--- worlds/jakanddaxter/docs/setup_en.md | 20 +++--- 7 files changed, 114 insertions(+), 36 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index bcbbc175cc7b..3b6e0f7bfe35 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,4 +1,6 @@ import logging +import os +import subprocess import typing import asyncio import colorama @@ -33,11 +35,10 @@ async def _log_exception(a): class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): ctx: "JakAndDaxterContext" - # The REPL has a specific order of operations it needs to do in order to process our input: - # 1. Connect (we need to open a socket connection on ip/port to the REPL). - # 2. Listen (have the REPL compiler connect and listen on the game's internal socket). - # 3. Compile (have the REPL compiler compile the game into object code it can run). - # All 3 need to be done, and in this order, for this to work. + # The command processor is not async and cannot use async tasks, so long-running operations + # like the /repl connect command (which takes 10-15 seconds to compile the game) have to be requested + # with user-initiated flags. The text client will hang while the operation runs, but at least we can + # inform the user to wait. The flags are checked by the agents every main_tick. def _cmd_repl(self, *arguments: str): """Sends a command to the OpenGOAL REPL. Arguments: - connect : connect the client to the REPL (goalc). @@ -45,7 +46,7 @@ def _cmd_repl(self, *arguments: str): if arguments: if arguments[0] == "connect": logger.info("This may take a bit... Wait for the success audio cue before continuing!") - self.ctx.repl.user_connect = True # Will attempt to reconnect on next tick. + self.ctx.repl.initiated_connect = True if arguments[0] == "status": self.ctx.repl.print_status() @@ -55,7 +56,7 @@ def _cmd_memr(self, *arguments: str): - status : check the internal status of the Memory Reader.""" if arguments: if arguments[0] == "connect": - self.ctx.memr.connect() + self.ctx.memr.initiated_connect = True if arguments[0] == "status": self.ctx.memr.print_status() @@ -125,6 +126,55 @@ async def run_memr_loop(self): await asyncio.sleep(0.1) +async def run_game(ctx: JakAndDaxterContext): + exec_directory = "" + try: + exec_directory = Utils.get_settings()["jakanddaxter_options"]["exec_directory"] + files_in_path = os.listdir(exec_directory) + if ".git" in files_in_path: + # Indicates the user is running from source, append expected subdirectory appropriately. + exec_directory = os.path.join(exec_directory, "out", "build", "Release", "bin") + else: + # Indicates the user is running from the official launcher, a mod launcher, or otherwise. + # We'll need to handle version numbers in the path somehow... + exec_directory = os.path.join(exec_directory, "versions", "official") + latest_version = list(reversed(os.listdir(exec_directory)))[0] + exec_directory = os.path.join(exec_directory, str(latest_version)) + except FileNotFoundError: + logger.error(f"Unable to locate directory {exec_directory}, " + f"unable to locate game executable.") + return + except KeyError as e: + logger.error(f"Hosts.yaml does not contain {e.args[0]}, " + f"unable to locate game executable.") + return + + gk = os.path.join(exec_directory, "gk.exe") + goalc = os.path.join(exec_directory, "goalc.exe") + + # Don't mind all the arguments, they are exactly what you get when you run "task boot-game" or "task repl". + await asyncio.create_subprocess_exec( + gk, + "-v", "--game jak1", "--", "-boot", "-fakeiso", "-debug", + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.DEVNULL) + + # You MUST launch goalc as a console application, so powershell/cmd/bash/etc is the program + # and goalc is just an argument. It HAS to be this way. + # TODO - Support other OS's. + await asyncio.create_subprocess_exec( + "powershell.exe", + goalc, "--user-auto", "--game jak1") + + # Auto connect the repl and memr agents. Sleep 5 because goalc takes just a little bit of time to load, + # and it's not something we can await. + logger.info("This may take a bit... Wait for the success audio cue before continuing!") + await asyncio.sleep(5) + ctx.repl.initiated_connect = True + ctx.memr.initiated_connect = True + + async def main(): Utils.init_logging("JakAndDaxterClient", exception_logger="Client") @@ -138,6 +188,8 @@ async def main(): ctx.run_gui() ctx.run_cli() + # Find and run the game (gk) and compiler/repl (goalc). + await run_game(ctx) await ctx.exit_event.wait() await ctx.shutdown() diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index e585bb816fd9..cfed39815c10 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,17 +1,20 @@ +import os from dataclasses import dataclass from Options import Toggle, PerGameCommonOptions -class EnableScoutFlies(Toggle): - """Enable to include each Scout Fly as a check. Adds 112 checks to the pool.""" - display_name = "Enable Scout Flies" +# class EnableScoutFlies(Toggle): +# """Enable to include each Scout Fly as a check. Adds 112 checks to the pool.""" +# display_name = "Enable Scout Flies" # class EnablePrecursorOrbs(Toggle): # """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool.""" # display_name = "Enable Precursor Orbs" + @dataclass class JakAndDaxterOptions(PerGameCommonOptions): - enable_scout_flies: EnableScoutFlies + # enable_scout_flies: EnableScoutFlies # enable_precursor_orbs: EnablePrecursorOrbs + pass diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 92f5549f3a1c..a7a32a76a71a 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -24,6 +24,15 @@ class JakAndDaxterWebWorld(WebWorld): class JakAndDaxterWorld(World): + """ + Jak and Daxter: The Precursor Legacy is a 2001 action platformer developed by Naughty Dog + for the PlayStation 2. The game follows the eponymous protagonists, a young boy named Jak + and his friend Daxter, who has been transformed into an "ottsel." With the help of Samos + the Sage of Green Eco and his daughter Keira, the pair travel north in search of a cure for Daxter, + discovering artifacts created by an ancient race known as the Precursors along the way. When the + rogue sages Gol and Maia Acheron plan to flood the world with Dark Eco, they must stop their evil plan + and save the world. + """ # ID, name, version game = jak1_name data_version = 1 diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 04fa0255129e..4373e9676e02 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -11,13 +11,20 @@ next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. cells_offset = 16 buzzers_offset = 420 # cells_offset + (sizeof uint32 * 101 cells) = 16 + (4 * 101) -end_marker_offset = 868 # buzzers_offset + (sizeof uint32 * 112 flies) = 420 + (4 * 112) + + +# buzzers_offset +# + (sizeof uint32 * 112 flies) <-- The buzzers themselves. +# + (sizeof uint8 * 116 tasks) <-- A "cells-received" array for the game to handle new ownership logic. +# = 420 + (4 * 112) + (1 * 116) +end_marker_offset = 984 class JakAndDaxterMemoryReader: marker: typing.ByteString goal_address = None connected: bool = False + initiated_connect: bool = False # The memory reader just needs the game running. gk_process: pymem.process = None @@ -30,6 +37,10 @@ def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.connect() async def main_tick(self, location_callback: typing.Callable): + if self.initiated_connect: + await self.connect() + self.initiated_connect = False + if self.connected: try: self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. @@ -48,7 +59,7 @@ async def main_tick(self, location_callback: typing.Callable): location_callback(self.location_outbox) self.outbox_index += 1 - def connect(self): + async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel logger.info("Found the gk process: " + str(self.gk_process.process_id)) diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index cae7e6c8634e..c70d633b00b8 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -15,7 +15,7 @@ class JakAndDaxterReplClient: port: int sock: socket connected: bool = False - user_connect: bool = False # Signals when user tells us to try reconnecting. + initiated_connect: bool = False # Signals when user tells us to try reconnecting. # The REPL client needs the REPL/compiler process running, but that process # also needs the game running. Therefore, the REPL client needs both running. @@ -31,9 +31,9 @@ def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.connect() async def main_tick(self): - if self.user_connect: + if self.initiated_connect: await self.connect() - self.user_connect = False + self.initiated_connect = False if self.connected: try: diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index 9ca1c86079af..a96d2797f239 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -5,12 +5,12 @@ The [Player Options Page](../player-options) for this game contains all the options you need to configure and export a config file. -At this time, these options don't do anything. Scout Flies are always randomized, and Precursor Orbs +At this time, Scout Flies are always randomized, and Precursor Orbs are never randomized. ## What does randomization do to this game? All 101 Power Cells and 112 Scout Flies are now Location Checks -and may contain Items for different games as well as different Items from within Jak and Daxter. +and may contain Items for different games, as well as different Items from within Jak and Daxter. ## What is the goal of the game once randomized? To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. @@ -25,7 +25,7 @@ in their games, and those Items will be automatically sent to your game. If you have completed all possible tasks available to you but still cannot progress, you may have to wait for another player to find enough of your game's Items to allow you to progress. If that does not apply, -double check your spoiler log to make sure you have all the items you should have. If you don't, +double-check your spoiler log to make sure you have all the items you should have. If you don't, you may have encountered a bug. Please see the options for bug reporting below. ## What happens when I pick up an item? @@ -40,19 +40,19 @@ inform you where you received the Item from, and which one it is. Your Item coun tick up. The pause menu will not say "Task Completed" below the selected Power Cell, but the icon will be "activated." ## I can't reach a certain area within an accessible region, how do I get there? -Some areas are locked behind ownership of specific Power Cells. For example, you cannot access Misty Island +Some areas are locked behind possession of specific Power Cells. For example, you cannot access Misty Island until you have the "Catch 200 Pounds of Fish" Power Cell. Keep in mind, your access to Misty Island is determined -_through ownership of this specific Power Cell only,_ **not** _by you completing the Fishing minigame._ +_through possession of this specific Power Cell only,_ **not** _by you completing the Fishing minigame._ ## I got soft-locked and can't leave, how do I get out of here? -As stated before, some areas are locked behind ownership of specific Power Cells. But you may already be past +As stated before, some areas are locked behind possession of specific Power Cells. But you may already be past a point-of-no-return preventing you from backtracking. One example is the Forbidden Jungle temple, where the elevator is locked at the bottom, and if you haven't unlocked the Blue Eco Switch, you cannot access the Plant Boss's room and escape. -In this scenario, you will need to open your menu and find the "Teleport Home" option. Selecting this option -will instantly teleport you to the nearest Sage's Hut in the last hub area you were in... or always to -the Green Sage's Hut, depending on the feasibility of the former option. This feature is a work in progress. +In this scenario, you will need to open your menu and find the "Warp To Home" option at the bottom of the list. +Selecting this option will instantly teleport you to Geyser Rock. From there, you can teleport back to the nearest +sage's hut to continue your journey. ## I think I found a bug, where should I report it? Depending on the nature of the bug, there are a couple of different options. @@ -60,9 +60,11 @@ Depending on the nature of the bug, there are a couple of different options. * If you found a logical error in the randomizer, please create a new Issue [here.](https://github.com/ArchipelaGOAL/Archipelago/issues) * Use this page if: - * For example, you are stuck on Geyser Rock because one of the four Geyser Rock Power Cells is not on Geyser Rock. + * You are hard-locked from progressing. For example, you are stuck on Geyser Rock because one of the four + Geyser Rock Power Cells is not on Geyser Rock. * The randomizer did not respect one of the Options you chose. * You see a mistake, typo, etc. on this webpage. + * You see an error or stack trace appear on the text client. * Please upload your config file and spoiler log file in the Issue, so we can troubleshoot the problem. * If you encountered an error in OpenGOAL, please create a new Issue @@ -72,4 +74,5 @@ Depending on the nature of the bug, there are a couple of different options. * You fail to send Items you find in the game to the Archipelago server. * You fail to receive Items the server sends to you. * Your game disconnects from the server and cannot reconnect. + * You go looking for a game item that has already disappeared before you could reach it. * Please upload any log files that may have been generated. \ No newline at end of file diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 807d9ac70133..102e5006ebe5 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -36,15 +36,16 @@ At this time, the only supported method of setup is through Manual Compilation. - Open 3 Powershell windows. If you have VSCode, you can run 3 terminals to consolidate this process. - In the first window, navigate to the Archipelago folder using `cd` and run `python ./Launcher.py`. - - In the second window, navigate to the ArchipelaGOAL folder and run `task extract`. This will prompt you to tell the mod where to find your ISO file to dump its cotnents. When that is done, run `task repl`. + - In the second window, navigate to the ArchipelaGOAL folder and run `task extract`. This will prompt you to tell the mod where to find your ISO file to dump its contents. When that is done, run `task repl`. - In the third window, navigate to the ArchipelaGOAL folder and run `task boot-game`. At this point, Jak should be standing outside Samos's hut. + - You can now close all these windows - In the Launcher, click Generate to create a new random seed. Save the resulting zip file. - In the Launcher, click Host to host the Archipelago server. It will prompt you for the location of that zip file. -- Once the server is running, in the Launcher, find the Jak and Daxter Client and click it. You should see the `task repl` window begin to compile the game. -- When it completes, you should hear the menu closing sound effect, and the Client window should appear. +- Once the server is running, in the Launcher, find the Jak and Daxter Client and click it. You should see the command window begin to compile the game. +- When it completes, you should hear the menu closing sound effect, and you should see the text client indicate that the two agents are ready to communicate with the game. - Connect the client to the Archipelago server and enter your slot name. Once this is done, the game should be ready to play. Talk to Samos to trigger the cutscene where he sends you to Geyser Rock, and off you go! -Once you complete the setup steps, you need to repeat most of them in order to launch a new game. You can skip a few steps: +Once you complete the setup steps, you should only need to run the Launcher again to generate a game, host a server, or run the client and connect to a server. - You never need to download the zip copies of the projects again (unless there are updates). - You never need to dump your ISO again. - You never need to extract the ISO assets again. @@ -63,17 +64,16 @@ Offline play is untested at this time. ### Runtime Failures -- If the client window appears but no sound plays, you will need to enter the repl commands into the client to connect it to the game. These are, in order: - - `/repl connect 127.0.0.1 8181` - - `/repl listen` - - `/repl compile` -- Once these are done, you can enter `/repl verify` and you should hear the menu sound again. +- If the client window appears but no sound plays, you will need to enter the following commands into the client to connect it to the game. + - `/repl connect` + - `/memr connect` +- Once these are done, you can enter `/repl status` and `/memr status` to check that everything is connected and ready. ## Gameplay Troubleshooting ### Known Issues -- Needing to open so many windows to play the game is a massive pain, and I hope to streamline this process in the future. +- I've streamlined the process of connecting the client's agents to the game, but they are temperamental and I am bad at asynchronous programming. - The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly. - The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. - The game relates tasks and power cells closely but separately. Some issues may result from having to tell the game to check for the power cells you own, rather than the tasks you completed. \ No newline at end of file From 89c15f5c16a23b2fff1cad19fdaf2f793462f78f Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 12 May 2024 16:05:36 -0400 Subject: [PATCH 28/70] Jak 1: quick fix to settings. --- worlds/jakanddaxter/Client.py | 2 +- worlds/jakanddaxter/__init__.py | 14 +++++++++++++- worlds/jakanddaxter/docs/setup_en.md | 12 +++++++++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 3b6e0f7bfe35..b6e982a9e3db 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -129,7 +129,7 @@ async def run_memr_loop(self): async def run_game(ctx: JakAndDaxterContext): exec_directory = "" try: - exec_directory = Utils.get_settings()["jakanddaxter_options"]["exec_directory"] + exec_directory = Utils.get_settings()["jakanddaxter_options"]["root_directory"] files_in_path = os.listdir(exec_directory) if ".git" in files_in_path: # Indicates the user is running from source, append expected subdirectory appropriately. diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index a7a32a76a71a..75a87c9dbf7b 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,3 +1,6 @@ +import typing +import settings + from BaseClasses import Item, ItemClassification, Tutorial from .GameID import jak1_id, jak1_name from .JakAndDaxterOptions import JakAndDaxterOptions @@ -10,6 +13,14 @@ from ..LauncherComponents import components, Component, launch_subprocess, Type +class JakAndDaxterSettings(settings.Group): + class RootDirectory(settings.UserFolderPath): + """Path to folder containing the ArchipelaGOAL mod.""" + description = "ArchipelaGOAL Root Directory" + + root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL") + + class JakAndDaxterWebWorld(WebWorld): setup_en = Tutorial( "Multiworld Setup Guide", @@ -27,7 +38,7 @@ class JakAndDaxterWorld(World): """ Jak and Daxter: The Precursor Legacy is a 2001 action platformer developed by Naughty Dog for the PlayStation 2. The game follows the eponymous protagonists, a young boy named Jak - and his friend Daxter, who has been transformed into an "ottsel." With the help of Samos + and his friend Daxter, who has been transformed into an ottsel. With the help of Samos the Sage of Green Eco and his daughter Keira, the pair travel north in search of a cure for Daxter, discovering artifacts created by an ancient race known as the Precursors along the way. When the rogue sages Gol and Maia Acheron plan to flood the world with Dark Eco, they must stop their evil plan @@ -39,6 +50,7 @@ class JakAndDaxterWorld(World): required_client_version = (0, 4, 5) # Options + settings: typing.ClassVar[JakAndDaxterSettings] options_dataclass = JakAndDaxterOptions options: JakAndDaxterOptions diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 102e5006ebe5..ce2b1936771b 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -35,10 +35,16 @@ At this time, the only supported method of setup is through Manual Compilation. ## Starting a Game - Open 3 Powershell windows. If you have VSCode, you can run 3 terminals to consolidate this process. - - In the first window, navigate to the Archipelago folder using `cd` and run `python ./Launcher.py`. + - In the first window, navigate to the Archipelago folder using `cd` and run `python ./Launcher.py --update_settings`. Then run it again without the `--update_settings` flag. - In the second window, navigate to the ArchipelaGOAL folder and run `task extract`. This will prompt you to tell the mod where to find your ISO file to dump its contents. When that is done, run `task repl`. - In the third window, navigate to the ArchipelaGOAL folder and run `task boot-game`. At this point, Jak should be standing outside Samos's hut. - - You can now close all these windows + - Once you confirm all those tasks succeeded, you can now close all these windows. +- Edit your host.yaml file and ensure these lines exist. And don't forget to specify your ACTUAL install path. If you're on Windows, no backslashes! +``` +jakanddaxter_options: + # Path to folder containing the ArchipelaGOAL mod. + root_directory: "D:/Files/Repositories/ArchipelaGOAL" +``` - In the Launcher, click Generate to create a new random seed. Save the resulting zip file. - In the Launcher, click Host to host the Archipelago server. It will prompt you for the location of that zip file. - Once the server is running, in the Launcher, find the Jak and Daxter Client and click it. You should see the command window begin to compile the game. @@ -73,7 +79,7 @@ Offline play is untested at this time. ### Known Issues -- I've streamlined the process of connecting the client's agents to the game, but they are temperamental and I am bad at asynchronous programming. +- I've streamlined the process of connecting the client's agents to the game, but it comes at the cost of more granular commands useful for troubleshooting. - The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly. - The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. - The game relates tasks and power cells closely but separately. Some issues may result from having to tell the game to check for the power cells you own, rather than the tasks you completed. \ No newline at end of file From 1ac53026817222a4f1087f3b158562e9687c857b Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 12 May 2024 19:23:51 -0400 Subject: [PATCH 29/70] Jak and Daxter: Implement New Game (#1) * Jak 1: Initial commit: Cell Locations, Items, and Regions modeled. * Jak 1: Wrote Regions, Rules, init. Untested. * Jak 1: Fixed mistakes, need better understanding of Entrances. * Jak 1: Fixed bugs, refactored Regions, added missing Special Checks. First spoiler log generated. * Jak 1: Add Scout Fly Locations, code and style cleanup. * Jak 1: Add Scout Flies to Regions. * Jak 1: Add version info. * Jak 1: Reduced code smell. * Jak 1: Fixed UT bugs, added Free The Sages as Locations. * Jak 1: Refactor ID scheme to better fit game's scheme. Add more subregions and rules, but still missing one-way Entrances. * Jak 1: Add some one-ways, adjust scout fly offset. * Jak 1: Found Scout Fly ID's for first 4 maps. * Jak 1: Add more scout fly ID's, refactor game/AP ID translation for easier reading and code reuse. * Jak 1: Fixed a few things. Four maps to go. * Jak 1: Last of the scout flies mapped! * Jak 1: simplify citadel sages logic. * Jak 1: WebWorld setup, some documentation. * Jak 1: Initial checkin of Client. Removed the colon from the game name. * Jak 1: Refactored client into components, working on async communication between the client and the game. * Jak 1: In tandem with new ArchipelaGOAL memory structure, define read_memory. * Jak 1: There's magic in the air... * Jak 1: Fixed bug translating scout fly ID's. * Jak 1: Make the REPL a little more verbose, easier to debug. * Jak 1: Did you know Snowy Mountain had such specific unlock requirements? I didn't. * Jak 1: Update Documentation. * Jak 1: Simplify user interaction with agents, make process more robust/less dependent on order of ops. * Jak 1: Simplified startup process, updated docs, prayed. * Jak 1: quick fix to settings. --- JakAndDaxterClient.py | 9 + worlds/jakanddaxter/Client.py | 200 +++++++++++++++ worlds/jakanddaxter/GameID.py | 5 + worlds/jakanddaxter/Items.py | 7 + worlds/jakanddaxter/JakAndDaxterOptions.py | 20 ++ worlds/jakanddaxter/Locations.py | 46 ++++ worlds/jakanddaxter/Regions.py | 229 +++++++++++++++++ worlds/jakanddaxter/Rules.py | 232 ++++++++++++++++++ worlds/jakanddaxter/__init__.py | 108 ++++++++ worlds/jakanddaxter/client/MemoryReader.py | 133 ++++++++++ worlds/jakanddaxter/client/ReplClient.py | 202 +++++++++++++++ .../en_Jak and Daxter The Precursor Legacy.md | 78 ++++++ worlds/jakanddaxter/docs/setup_en.md | 85 +++++++ worlds/jakanddaxter/locs/CellLocations.py | 188 ++++++++++++++ worlds/jakanddaxter/locs/OrbLocations.py | 101 ++++++++ worlds/jakanddaxter/locs/ScoutLocations.py | 227 +++++++++++++++++ worlds/jakanddaxter/requirements.txt | 1 + 17 files changed, 1871 insertions(+) create mode 100644 JakAndDaxterClient.py create mode 100644 worlds/jakanddaxter/Client.py create mode 100644 worlds/jakanddaxter/GameID.py create mode 100644 worlds/jakanddaxter/Items.py create mode 100644 worlds/jakanddaxter/JakAndDaxterOptions.py create mode 100644 worlds/jakanddaxter/Locations.py create mode 100644 worlds/jakanddaxter/Regions.py create mode 100644 worlds/jakanddaxter/Rules.py create mode 100644 worlds/jakanddaxter/__init__.py create mode 100644 worlds/jakanddaxter/client/MemoryReader.py create mode 100644 worlds/jakanddaxter/client/ReplClient.py create mode 100644 worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md create mode 100644 worlds/jakanddaxter/docs/setup_en.md create mode 100644 worlds/jakanddaxter/locs/CellLocations.py create mode 100644 worlds/jakanddaxter/locs/OrbLocations.py create mode 100644 worlds/jakanddaxter/locs/ScoutLocations.py create mode 100644 worlds/jakanddaxter/requirements.txt diff --git a/JakAndDaxterClient.py b/JakAndDaxterClient.py new file mode 100644 index 000000000000..040f8ff389bd --- /dev/null +++ b/JakAndDaxterClient.py @@ -0,0 +1,9 @@ +import Utils +from worlds.jakanddaxter.Client import launch + +import ModuleUpdate +ModuleUpdate.update() + +if __name__ == '__main__': + Utils.init_logging("JakAndDaxterClient", exception_logger="Client") + launch() diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py new file mode 100644 index 000000000000..b6e982a9e3db --- /dev/null +++ b/worlds/jakanddaxter/Client.py @@ -0,0 +1,200 @@ +import logging +import os +import subprocess +import typing +import asyncio +import colorama + +import Utils +from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled + +from worlds.jakanddaxter.GameID import jak1_name +from worlds.jakanddaxter.client.ReplClient import JakAndDaxterReplClient +from worlds.jakanddaxter.client.MemoryReader import JakAndDaxterMemoryReader + +import ModuleUpdate +ModuleUpdate.update() + + +all_tasks = set() + + +def create_task_log_exception(awaitable: typing.Awaitable) -> asyncio.Task: + async def _log_exception(a): + try: + return await a + except Exception as e: + logger.exception(e) + finally: + all_tasks.remove(task) + task = asyncio.create_task(_log_exception(awaitable)) + all_tasks.add(task) + return task + + +class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): + ctx: "JakAndDaxterContext" + + # The command processor is not async and cannot use async tasks, so long-running operations + # like the /repl connect command (which takes 10-15 seconds to compile the game) have to be requested + # with user-initiated flags. The text client will hang while the operation runs, but at least we can + # inform the user to wait. The flags are checked by the agents every main_tick. + def _cmd_repl(self, *arguments: str): + """Sends a command to the OpenGOAL REPL. Arguments: + - connect : connect the client to the REPL (goalc). + - status : check internal status of the REPL.""" + if arguments: + if arguments[0] == "connect": + logger.info("This may take a bit... Wait for the success audio cue before continuing!") + self.ctx.repl.initiated_connect = True + if arguments[0] == "status": + self.ctx.repl.print_status() + + def _cmd_memr(self, *arguments: str): + """Sends a command to the Memory Reader. Arguments: + - connect : connect the memory reader to the game process (gk). + - status : check the internal status of the Memory Reader.""" + if arguments: + if arguments[0] == "connect": + self.ctx.memr.initiated_connect = True + if arguments[0] == "status": + self.ctx.memr.print_status() + + +class JakAndDaxterContext(CommonContext): + tags = {"AP"} + game = jak1_name + items_handling = 0b111 # Full item handling + command_processor = JakAndDaxterClientCommandProcessor + + # We'll need two agents working in tandem to handle two-way communication with the game. + # The REPL Client will handle the server->game direction by issuing commands directly to the running game. + # But the REPL cannot send information back to us, it only ingests information we send it. + # Luckily OpenGOAL sets up memory addresses to write to, that AutoSplit can read from, for speedrunning. + # We'll piggyback off this system with a Memory Reader, and that will handle the game->server direction. + repl: JakAndDaxterReplClient + memr: JakAndDaxterMemoryReader + + # And two associated tasks, so we have handles on them. + repl_task: asyncio.Task + memr_task: asyncio.Task + + def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + self.repl = JakAndDaxterReplClient() + self.memr = JakAndDaxterMemoryReader() + super().__init__(server_address, password) + + def run_gui(self): + from kvui import GameManager + + class JakAndDaxterManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Jak and Daxter ArchipelaGOAL Client" + + self.ui = JakAndDaxterManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(JakAndDaxterContext, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + def on_package(self, cmd: str, args: dict): + if cmd == "ReceivedItems": + for index, item in enumerate(args["items"], start=args["index"]): + logger.info(args) + self.repl.item_inbox[index] = item + + async def ap_inform_location_checks(self, location_ids: typing.List[int]): + message = [{"cmd": "LocationChecks", "locations": location_ids}] + await self.send_msgs(message) + + def on_locations(self, location_ids: typing.List[int]): + create_task_log_exception(self.ap_inform_location_checks(location_ids)) + + async def run_repl_loop(self): + while True: + await self.repl.main_tick() + await asyncio.sleep(0.1) + + async def run_memr_loop(self): + while True: + await self.memr.main_tick(self.on_locations) + await asyncio.sleep(0.1) + + +async def run_game(ctx: JakAndDaxterContext): + exec_directory = "" + try: + exec_directory = Utils.get_settings()["jakanddaxter_options"]["root_directory"] + files_in_path = os.listdir(exec_directory) + if ".git" in files_in_path: + # Indicates the user is running from source, append expected subdirectory appropriately. + exec_directory = os.path.join(exec_directory, "out", "build", "Release", "bin") + else: + # Indicates the user is running from the official launcher, a mod launcher, or otherwise. + # We'll need to handle version numbers in the path somehow... + exec_directory = os.path.join(exec_directory, "versions", "official") + latest_version = list(reversed(os.listdir(exec_directory)))[0] + exec_directory = os.path.join(exec_directory, str(latest_version)) + except FileNotFoundError: + logger.error(f"Unable to locate directory {exec_directory}, " + f"unable to locate game executable.") + return + except KeyError as e: + logger.error(f"Hosts.yaml does not contain {e.args[0]}, " + f"unable to locate game executable.") + return + + gk = os.path.join(exec_directory, "gk.exe") + goalc = os.path.join(exec_directory, "goalc.exe") + + # Don't mind all the arguments, they are exactly what you get when you run "task boot-game" or "task repl". + await asyncio.create_subprocess_exec( + gk, + "-v", "--game jak1", "--", "-boot", "-fakeiso", "-debug", + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.DEVNULL) + + # You MUST launch goalc as a console application, so powershell/cmd/bash/etc is the program + # and goalc is just an argument. It HAS to be this way. + # TODO - Support other OS's. + await asyncio.create_subprocess_exec( + "powershell.exe", + goalc, "--user-auto", "--game jak1") + + # Auto connect the repl and memr agents. Sleep 5 because goalc takes just a little bit of time to load, + # and it's not something we can await. + logger.info("This may take a bit... Wait for the success audio cue before continuing!") + await asyncio.sleep(5) + ctx.repl.initiated_connect = True + ctx.memr.initiated_connect = True + + +async def main(): + Utils.init_logging("JakAndDaxterClient", exception_logger="Client") + + ctx = JakAndDaxterContext(None, None) + + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + ctx.repl_task = create_task_log_exception(ctx.run_repl_loop()) + ctx.memr_task = create_task_log_exception(ctx.run_memr_loop()) + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + # Find and run the game (gk) and compiler/repl (goalc). + await run_game(ctx) + await ctx.exit_event.wait() + await ctx.shutdown() + + +def launch(): + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py new file mode 100644 index 000000000000..555be696af49 --- /dev/null +++ b/worlds/jakanddaxter/GameID.py @@ -0,0 +1,5 @@ +# All Jak And Daxter Archipelago IDs must be offset by this number. +jak1_id = 741000000 + +# The name of the game. +jak1_name = "Jak and Daxter The Precursor Legacy" diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py new file mode 100644 index 000000000000..d051f15869d4 --- /dev/null +++ b/worlds/jakanddaxter/Items.py @@ -0,0 +1,7 @@ +from BaseClasses import Item +from .GameID import jak1_name + + +class JakAndDaxterItem(Item): + game: str = jak1_name + diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py new file mode 100644 index 000000000000..cfed39815c10 --- /dev/null +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -0,0 +1,20 @@ +import os +from dataclasses import dataclass +from Options import Toggle, PerGameCommonOptions + + +# class EnableScoutFlies(Toggle): +# """Enable to include each Scout Fly as a check. Adds 112 checks to the pool.""" +# display_name = "Enable Scout Flies" + + +# class EnablePrecursorOrbs(Toggle): +# """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool.""" +# display_name = "Enable Precursor Orbs" + + +@dataclass +class JakAndDaxterOptions(PerGameCommonOptions): + # enable_scout_flies: EnableScoutFlies + # enable_precursor_orbs: EnablePrecursorOrbs + pass diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py new file mode 100644 index 000000000000..ef56137cf176 --- /dev/null +++ b/worlds/jakanddaxter/Locations.py @@ -0,0 +1,46 @@ +from BaseClasses import Location +from .GameID import jak1_name +from .locs import CellLocations as Cells, ScoutLocations as Scouts + + +class JakAndDaxterLocation(Location): + game: str = jak1_name + + +# All Locations +# Because all items in Jak And Daxter are unique and do not regenerate, we can use this same table as our item table. +# Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed. +location_table = { + **{Cells.to_ap_id(k): Cells.locGR_cellTable[k] for k in Cells.locGR_cellTable}, + **{Cells.to_ap_id(k): Cells.locSV_cellTable[k] for k in Cells.locSV_cellTable}, + **{Cells.to_ap_id(k): Cells.locFJ_cellTable[k] for k in Cells.locFJ_cellTable}, + **{Cells.to_ap_id(k): Cells.locSB_cellTable[k] for k in Cells.locSB_cellTable}, + **{Cells.to_ap_id(k): Cells.locMI_cellTable[k] for k in Cells.locMI_cellTable}, + **{Cells.to_ap_id(k): Cells.locFC_cellTable[k] for k in Cells.locFC_cellTable}, + **{Cells.to_ap_id(k): Cells.locRV_cellTable[k] for k in Cells.locRV_cellTable}, + **{Cells.to_ap_id(k): Cells.locPB_cellTable[k] for k in Cells.locPB_cellTable}, + **{Cells.to_ap_id(k): Cells.locLPC_cellTable[k] for k in Cells.locLPC_cellTable}, + **{Cells.to_ap_id(k): Cells.locBS_cellTable[k] for k in Cells.locBS_cellTable}, + **{Cells.to_ap_id(k): Cells.locMP_cellTable[k] for k in Cells.locMP_cellTable}, + **{Cells.to_ap_id(k): Cells.locVC_cellTable[k] for k in Cells.locVC_cellTable}, + **{Cells.to_ap_id(k): Cells.locSC_cellTable[k] for k in Cells.locSC_cellTable}, + **{Cells.to_ap_id(k): Cells.locSM_cellTable[k] for k in Cells.locSM_cellTable}, + **{Cells.to_ap_id(k): Cells.locLT_cellTable[k] for k in Cells.locLT_cellTable}, + **{Cells.to_ap_id(k): Cells.locGMC_cellTable[k] for k in Cells.locGMC_cellTable}, + **{Scouts.to_ap_id(k): Scouts.locGR_scoutTable[k] for k in Scouts.locGR_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSV_scoutTable[k] for k in Scouts.locSV_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locFJ_scoutTable[k] for k in Scouts.locFJ_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSB_scoutTable[k] for k in Scouts.locSB_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locMI_scoutTable[k] for k in Scouts.locMI_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locFC_scoutTable[k] for k in Scouts.locFC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locRV_scoutTable[k] for k in Scouts.locRV_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locPB_scoutTable[k] for k in Scouts.locPB_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locLPC_scoutTable[k] for k in Scouts.locLPC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locBS_scoutTable[k] for k in Scouts.locBS_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locMP_scoutTable[k] for k in Scouts.locMP_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locVC_scoutTable[k] for k in Scouts.locVC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSC_scoutTable[k] for k in Scouts.locSC_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locSM_scoutTable[k] for k in Scouts.locSM_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locLT_scoutTable[k] for k in Scouts.locLT_scoutTable}, + **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable} +} diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py new file mode 100644 index 000000000000..fa4a6aa88015 --- /dev/null +++ b/worlds/jakanddaxter/Regions.py @@ -0,0 +1,229 @@ +import typing +from enum import Enum, auto +from BaseClasses import MultiWorld, Region +from .GameID import jak1_name +from .JakAndDaxterOptions import JakAndDaxterOptions +from .Locations import JakAndDaxterLocation, location_table +from .locs import CellLocations as Cells, ScoutLocations as Scouts + + +class Jak1Level(int, Enum): + GEYSER_ROCK = auto() + SANDOVER_VILLAGE = auto() + FORBIDDEN_JUNGLE = auto() + SENTINEL_BEACH = auto() + MISTY_ISLAND = auto() + FIRE_CANYON = auto() + ROCK_VILLAGE = auto() + PRECURSOR_BASIN = auto() + LOST_PRECURSOR_CITY = auto() + BOGGY_SWAMP = auto() + MOUNTAIN_PASS = auto() + VOLCANIC_CRATER = auto() + SPIDER_CAVE = auto() + SNOWY_MOUNTAIN = auto() + LAVA_TUBE = auto() + GOL_AND_MAIAS_CITADEL = auto() + + +class Jak1SubLevel(int, Enum): + MAIN_AREA = auto() + FORBIDDEN_JUNGLE_SWITCH_ROOM = auto() + FORBIDDEN_JUNGLE_PLANT_ROOM = auto() + SENTINEL_BEACH_CANNON_TOWER = auto() + PRECURSOR_BASIN_BLUE_RINGS = auto() + LOST_PRECURSOR_CITY_SUNKEN_ROOM = auto() + LOST_PRECURSOR_CITY_HELIX_ROOM = auto() + BOGGY_SWAMP_FLUT_FLUT = auto() + MOUNTAIN_PASS_RACE = auto() + MOUNTAIN_PASS_SHORTCUT = auto() + SNOWY_MOUNTAIN_FLUT_FLUT = auto() + SNOWY_MOUNTAIN_LURKER_FORT = auto() + SNOWY_MOUNTAIN_FROZEN_BOX = auto() + GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = auto() + GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto() + + +level_table: typing.Dict[Jak1Level, str] = { + Jak1Level.GEYSER_ROCK: "Geyser Rock", + Jak1Level.SANDOVER_VILLAGE: "Sandover Village", + Jak1Level.FORBIDDEN_JUNGLE: "Forbidden Jungle", + Jak1Level.SENTINEL_BEACH: "Sentinel Beach", + Jak1Level.MISTY_ISLAND: "Misty Island", + Jak1Level.FIRE_CANYON: "Fire Canyon", + Jak1Level.ROCK_VILLAGE: "Rock Village", + Jak1Level.PRECURSOR_BASIN: "Precursor Basin", + Jak1Level.LOST_PRECURSOR_CITY: "Lost Precursor City", + Jak1Level.BOGGY_SWAMP: "Boggy Swamp", + Jak1Level.MOUNTAIN_PASS: "Mountain Pass", + Jak1Level.VOLCANIC_CRATER: "Volcanic Crater", + Jak1Level.SPIDER_CAVE: "Spider Cave", + Jak1Level.SNOWY_MOUNTAIN: "Snowy Mountain", + Jak1Level.LAVA_TUBE: "Lava Tube", + Jak1Level.GOL_AND_MAIAS_CITADEL: "Gol and Maia's Citadel" +} + +subLevel_table: typing.Dict[Jak1SubLevel, str] = { + Jak1SubLevel.MAIN_AREA: "Main Area", + Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: "Forbidden Jungle Switch Room", + Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room", + Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower", + Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings", + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: "Lost Precursor City Sunken Room", + Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: "Lost Precursor City Helix Room", + Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut", + Jak1SubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race", + Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", + Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut", + Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort", + Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: "Snowy Mountain Frozen Box", + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower", + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss" +} + + +class JakAndDaxterRegion(Region): + game: str = jak1_name + + +# Use the original game ID's for each item to tell the Region which Locations are available in it. +# You do NOT need to add the item offsets or game ID, that will be handled by create_*_locations. +def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + create_region(player, multiworld, "Menu") + + region_gr = create_region(player, multiworld, level_table[Jak1Level.GEYSER_ROCK]) + create_cell_locations(region_gr, Cells.locGR_cellTable) + create_fly_locations(region_gr, Scouts.locGR_scoutTable) + + region_sv = create_region(player, multiworld, level_table[Jak1Level.SANDOVER_VILLAGE]) + create_cell_locations(region_sv, Cells.locSV_cellTable) + create_fly_locations(region_sv, Scouts.locSV_scoutTable) + + region_fj = create_region(player, multiworld, level_table[Jak1Level.FORBIDDEN_JUNGLE]) + create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9, 7}}) + create_fly_locations(region_fj, Scouts.locFJ_scoutTable) + + sub_region_fjsr = create_subregion(region_fj, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM]) + create_cell_locations(sub_region_fjsr, {k: Cells.locFJ_cellTable[k] for k in {2}}) + + sub_region_fjpr = create_subregion(sub_region_fjsr, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) + create_cell_locations(sub_region_fjpr, {k: Cells.locFJ_cellTable[k] for k in {6}}) + + region_sb = create_region(player, multiworld, level_table[Jak1Level.SENTINEL_BEACH]) + create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22, 20}}) + create_fly_locations(region_sb, Scouts.locSB_scoutTable) + + sub_region_sbct = create_subregion(region_sb, subLevel_table[Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER]) + create_cell_locations(sub_region_sbct, {k: Cells.locSB_cellTable[k] for k in {19}}) + + region_mi = create_region(player, multiworld, level_table[Jak1Level.MISTY_ISLAND]) + create_cell_locations(region_mi, Cells.locMI_cellTable) + create_fly_locations(region_mi, Scouts.locMI_scoutTable) + + region_fc = create_region(player, multiworld, level_table[Jak1Level.FIRE_CANYON]) + create_cell_locations(region_fc, Cells.locFC_cellTable) + create_fly_locations(region_fc, Scouts.locFC_scoutTable) + + region_rv = create_region(player, multiworld, level_table[Jak1Level.ROCK_VILLAGE]) + create_cell_locations(region_rv, Cells.locRV_cellTable) + create_fly_locations(region_rv, Scouts.locRV_scoutTable) + + region_pb = create_region(player, multiworld, level_table[Jak1Level.PRECURSOR_BASIN]) + create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58, 57}}) + create_fly_locations(region_pb, Scouts.locPB_scoutTable) + + sub_region_pbbr = create_subregion(region_pb, subLevel_table[Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS]) + create_cell_locations(sub_region_pbbr, {k: Cells.locPB_cellTable[k] for k in {59}}) + + region_lpc = create_region(player, multiworld, level_table[Jak1Level.LOST_PRECURSOR_CITY]) + create_cell_locations(region_lpc, {k: Cells.locLPC_cellTable[k] for k in {45, 48, 44, 51}}) + create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] + for k in {262193, 131121, 393265, 196657, 49, 65585}}) + + sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM]) + create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47, 49}}) + create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {327729}}) + + sub_region_lpchr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM]) + create_cell_locations(sub_region_lpchr, {k: Cells.locLPC_cellTable[k] for k in {46, 50}}) + + region_bs = create_region(player, multiworld, level_table[Jak1Level.BOGGY_SWAMP]) + create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) + create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {43, 393259, 65579, 262187, 196651}}) + + sub_region_bsff = create_subregion(region_bs, subLevel_table[Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT]) + create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {43, 37}}) + create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {327723, 131115}}) + + region_mp = create_region(player, multiworld, level_table[Jak1Level.MOUNTAIN_PASS]) + create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86}}) + + sub_region_mpr = create_subregion(region_mp, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_RACE]) + create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87, 88}}) + create_fly_locations(sub_region_mpr, Scouts.locMP_scoutTable) + + sub_region_mps = create_subregion(sub_region_mpr, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT]) + create_cell_locations(sub_region_mps, {k: Cells.locMP_cellTable[k] for k in {110}}) + + region_vc = create_region(player, multiworld, level_table[Jak1Level.VOLCANIC_CRATER]) + create_cell_locations(region_vc, Cells.locVC_cellTable) + create_fly_locations(region_vc, Scouts.locVC_scoutTable) + + region_sc = create_region(player, multiworld, level_table[Jak1Level.SPIDER_CAVE]) + create_cell_locations(region_sc, Cells.locSC_cellTable) + create_fly_locations(region_sc, Scouts.locSC_scoutTable) + + region_sm = create_region(player, multiworld, level_table[Jak1Level.SNOWY_MOUNTAIN]) + create_cell_locations(region_sm, {k: Cells.locSM_cellTable[k] for k in {60, 61, 66, 64}}) + create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {65, 327745, 65601, 131137, 393281}}) + + sub_region_smfb = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX]) + create_cell_locations(sub_region_smfb, {k: Cells.locSM_cellTable[k] for k in {67}}) + + sub_region_smff = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) + create_cell_locations(sub_region_smff, {k: Cells.locSM_cellTable[k] for k in {63}}) + + sub_region_smlf = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) + create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62, 65}}) + create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {196673, 262209}}) + + region_lt = create_region(player, multiworld, level_table[Jak1Level.LAVA_TUBE]) + create_cell_locations(region_lt, Cells.locLT_cellTable) + create_fly_locations(region_lt, Scouts.locLT_scoutTable) + + region_gmc = create_region(player, multiworld, level_table[Jak1Level.GOL_AND_MAIAS_CITADEL]) + create_cell_locations(region_gmc, {k: Cells.locGMC_cellTable[k] for k in {71, 72, 73}}) + create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k] + for k in {91, 65627, 196699, 262235, 393307, 131163}}) + + sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) + create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70, 91}}) + create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {327771}}) + + create_subregion(sub_region_gmcrt, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS]) + + +def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: + region = JakAndDaxterRegion(name, player, multiworld) + multiworld.regions.append(region) + return region + + +def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: + region = JakAndDaxterRegion(name, parent.player, parent.multiworld) + parent.multiworld.regions.append(region) + return region + + +def create_cell_locations(region: Region, locations: typing.Dict[int, str]): + region.locations += [JakAndDaxterLocation(region.player, + location_table[Cells.to_ap_id(loc)], + Cells.to_ap_id(loc), + region) for loc in locations] + + +def create_fly_locations(region: Region, locations: typing.Dict[int, str]): + region.locations += [JakAndDaxterLocation(region.player, + location_table[Scouts.to_ap_id(loc)], + Scouts.to_ap_id(loc), + region) for loc in locations] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py new file mode 100644 index 000000000000..bb355e0e2cec --- /dev/null +++ b/worlds/jakanddaxter/Rules.py @@ -0,0 +1,232 @@ +from BaseClasses import MultiWorld, CollectionState +from .JakAndDaxterOptions import JakAndDaxterOptions +from .Regions import Jak1Level, Jak1SubLevel, level_table, subLevel_table +from .Locations import location_table as item_table +from .locs import CellLocations as Cells, ScoutLocations as Scouts + + +# Helper function for a handful of special cases +# where we need "at least any N" number of a specific set of cells. +def has_count_of(cell_list: set, required_count: int, player: int, state: CollectionState) -> bool: + c: int = 0 + for k in cell_list: + if state.has(item_table[k], player): + c += 1 + if c >= required_count: + return True + return False + + +def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + # Setting up some useful variables here because the offset numbers can get confusing + # for access rules. Feel free to add more variables here to keep the code more readable. + # You DO need to convert the game ID's to AP ID's here. + gr_cells = {Cells.to_ap_id(k) for k in Cells.locGR_cellTable} + fj_temple_top = Cells.to_ap_id(4) + fj_blue_switch = Cells.to_ap_id(2) + fj_plant_boss = Cells.to_ap_id(6) + fj_fisherman = Cells.to_ap_id(5) + sb_flut_flut = Cells.to_ap_id(17) + fc_end = Cells.to_ap_id(69) + pb_purple_rings = Cells.to_ap_id(58) + lpc_sunken = Cells.to_ap_id(47) + lpc_helix = Cells.to_ap_id(50) + mp_klaww = Cells.to_ap_id(86) + mp_end = Cells.to_ap_id(87) + pre_sm_cells = {Cells.to_ap_id(k) for k in {**Cells.locVC_cellTable, **Cells.locSC_cellTable}} + sm_yellow_switch = Cells.to_ap_id(60) + sm_fort_gate = Cells.to_ap_id(63) + lt_end = Cells.to_ap_id(89) + gmc_rby_sages = {Cells.to_ap_id(k) for k in {71, 72, 73}} + gmc_green_sage = Cells.to_ap_id(70) + + # Start connecting regions and set their access rules. + connect_start(multiworld, player, Jak1Level.GEYSER_ROCK) + + connect_regions(multiworld, player, + Jak1Level.GEYSER_ROCK, + Jak1Level.SANDOVER_VILLAGE, + lambda state: has_count_of(gr_cells, 4, player, state)) + + connect_regions(multiworld, player, + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.FORBIDDEN_JUNGLE) + + connect_region_to_sub(multiworld, player, + Jak1Level.FORBIDDEN_JUNGLE, + Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, + lambda state: state.has(item_table[fj_temple_top], player)) + + connect_subregions(multiworld, player, + Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, + Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + lambda state: state.has(item_table[fj_blue_switch], player)) + + connect_sub_to_region(multiworld, player, + Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, + Jak1Level.FORBIDDEN_JUNGLE, + lambda state: state.has(item_table[fj_plant_boss], player)) + + connect_regions(multiworld, player, + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.SENTINEL_BEACH) + + connect_region_to_sub(multiworld, player, + Jak1Level.SENTINEL_BEACH, + Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER, + lambda state: state.has(item_table[fj_blue_switch], player)) + + connect_regions(multiworld, player, + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.MISTY_ISLAND, + lambda state: state.has(item_table[fj_fisherman], player)) + + connect_regions(multiworld, player, + Jak1Level.SANDOVER_VILLAGE, + Jak1Level.FIRE_CANYON, + lambda state: state.count_group("Power Cell", player) >= 20) + + connect_regions(multiworld, player, + Jak1Level.FIRE_CANYON, + Jak1Level.ROCK_VILLAGE, + lambda state: state.has(item_table[fc_end], player)) + + connect_regions(multiworld, player, + Jak1Level.ROCK_VILLAGE, + Jak1Level.PRECURSOR_BASIN) + + connect_region_to_sub(multiworld, player, + Jak1Level.PRECURSOR_BASIN, + Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS, + lambda state: state.has(item_table[pb_purple_rings], player)) + + connect_regions(multiworld, player, + Jak1Level.ROCK_VILLAGE, + Jak1Level.LOST_PRECURSOR_CITY) + + connect_region_to_sub(multiworld, player, + Jak1Level.LOST_PRECURSOR_CITY, + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM) + + connect_subregions(multiworld, player, + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, + Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) + + connect_sub_to_region(multiworld, player, + Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM, + Jak1Level.LOST_PRECURSOR_CITY, + lambda state: state.has(item_table[lpc_helix], player)) + + connect_sub_to_region(multiworld, player, + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, + Jak1Level.ROCK_VILLAGE, + lambda state: state.has(item_table[lpc_sunken], player)) + + connect_regions(multiworld, player, + Jak1Level.ROCK_VILLAGE, + Jak1Level.BOGGY_SWAMP) + + connect_region_to_sub(multiworld, player, + Jak1Level.BOGGY_SWAMP, + Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT, + lambda state: state.has(item_table[sb_flut_flut], player)) + + connect_regions(multiworld, player, + Jak1Level.ROCK_VILLAGE, + Jak1Level.MOUNTAIN_PASS, + lambda state: state.count_group("Power Cell", player) >= 45) + + connect_region_to_sub(multiworld, player, + Jak1Level.MOUNTAIN_PASS, + Jak1SubLevel.MOUNTAIN_PASS_RACE, + lambda state: state.has(item_table[mp_klaww], player)) + + connect_subregions(multiworld, player, + Jak1SubLevel.MOUNTAIN_PASS_RACE, + Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT, + lambda state: state.has(item_table[sm_yellow_switch], player)) + + connect_sub_to_region(multiworld, player, + Jak1SubLevel.MOUNTAIN_PASS_RACE, + Jak1Level.VOLCANIC_CRATER, + lambda state: state.has(item_table[mp_end], player)) + + connect_regions(multiworld, player, + Jak1Level.VOLCANIC_CRATER, + Jak1Level.SPIDER_CAVE) + + connect_regions(multiworld, player, + Jak1Level.VOLCANIC_CRATER, + Jak1Level.SNOWY_MOUNTAIN, + lambda state: has_count_of(pre_sm_cells, 2, player, state) + or state.count_group("Power Cell", player) >= 71) # Yeah, this is a weird one. + + connect_region_to_sub(multiworld, player, + Jak1Level.SNOWY_MOUNTAIN, + Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX, + lambda state: state.has(item_table[sm_yellow_switch], player)) + + connect_region_to_sub(multiworld, player, + Jak1Level.SNOWY_MOUNTAIN, + Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, + lambda state: state.has(item_table[sb_flut_flut], player)) + + connect_region_to_sub(multiworld, player, + Jak1Level.SNOWY_MOUNTAIN, + Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT, + lambda state: state.has(item_table[sm_fort_gate], player)) + + connect_regions(multiworld, player, + Jak1Level.VOLCANIC_CRATER, + Jak1Level.LAVA_TUBE, + lambda state: state.count_group("Power Cell", player) >= 72) + + connect_regions(multiworld, player, + Jak1Level.LAVA_TUBE, + Jak1Level.GOL_AND_MAIAS_CITADEL, + lambda state: state.has(item_table[lt_end], player)) + + connect_region_to_sub(multiworld, player, + Jak1Level.GOL_AND_MAIAS_CITADEL, + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, + lambda state: has_count_of(gmc_rby_sages, 3, player, state)) + + connect_subregions(multiworld, player, + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS, + lambda state: state.has(item_table[gmc_green_sage], player)) + + multiworld.completion_condition[player] = lambda state: state.can_reach( + multiworld.get_region(subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player), + "Region", + player) + + +def connect_start(multiworld: MultiWorld, player: int, target: Jak1Level): + menu_region = multiworld.get_region("Menu", player) + start_region = multiworld.get_region(level_table[target], player) + menu_region.connect(start_region) + + +def connect_regions(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1Level, rule=None): + source_region = multiworld.get_region(level_table[source], player) + target_region = multiworld.get_region(level_table[target], player) + source_region.connect(target_region, rule=rule) + + +def connect_region_to_sub(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1SubLevel, rule=None): + source_region = multiworld.get_region(level_table[source], player) + target_region = multiworld.get_region(subLevel_table[target], player) + source_region.connect(target_region, rule=rule) + + +def connect_sub_to_region(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1Level, rule=None): + source_region = multiworld.get_region(subLevel_table[source], player) + target_region = multiworld.get_region(level_table[target], player) + source_region.connect(target_region, rule=rule) + + +def connect_subregions(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1SubLevel, rule=None): + source_region = multiworld.get_region(subLevel_table[source], player) + target_region = multiworld.get_region(subLevel_table[target], player) + source_region.connect(target_region, rule=rule) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py new file mode 100644 index 000000000000..75a87c9dbf7b --- /dev/null +++ b/worlds/jakanddaxter/__init__.py @@ -0,0 +1,108 @@ +import typing +import settings + +from BaseClasses import Item, ItemClassification, Tutorial +from .GameID import jak1_id, jak1_name +from .JakAndDaxterOptions import JakAndDaxterOptions +from .Items import JakAndDaxterItem +from .Locations import JakAndDaxterLocation, location_table as item_table +from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs +from .Regions import create_regions +from .Rules import set_rules +from ..AutoWorld import World, WebWorld +from ..LauncherComponents import components, Component, launch_subprocess, Type + + +class JakAndDaxterSettings(settings.Group): + class RootDirectory(settings.UserFolderPath): + """Path to folder containing the ArchipelaGOAL mod.""" + description = "ArchipelaGOAL Root Directory" + + root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL") + + +class JakAndDaxterWebWorld(WebWorld): + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up ArchipelaGOAL (Archipelago on OpenGOAL).", + "English", + "setup_en.md", + "setup/en", + ["markustulliuscicero"] + ) + + tutorials = [setup_en] + + +class JakAndDaxterWorld(World): + """ + Jak and Daxter: The Precursor Legacy is a 2001 action platformer developed by Naughty Dog + for the PlayStation 2. The game follows the eponymous protagonists, a young boy named Jak + and his friend Daxter, who has been transformed into an ottsel. With the help of Samos + the Sage of Green Eco and his daughter Keira, the pair travel north in search of a cure for Daxter, + discovering artifacts created by an ancient race known as the Precursors along the way. When the + rogue sages Gol and Maia Acheron plan to flood the world with Dark Eco, they must stop their evil plan + and save the world. + """ + # ID, name, version + game = jak1_name + data_version = 1 + required_client_version = (0, 4, 5) + + # Options + settings: typing.ClassVar[JakAndDaxterSettings] + options_dataclass = JakAndDaxterOptions + options: JakAndDaxterOptions + + # Web world + web = JakAndDaxterWebWorld() + + # Items and Locations + # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. + # Remember, the game ID and various offsets for each item type have already been calculated. + item_name_to_id = {item_table[k]: k for k in item_table} + location_name_to_id = {item_table[k]: k for k in item_table} + item_name_groups = { + "Power Cell": {item_table[k]: k for k in item_table + if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, + "Scout Fly": {item_table[k]: k for k in item_table + if k in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset)} + # "Precursor Orb": {} # TODO + } + + def create_regions(self): + create_regions(self.multiworld, self.options, self.player) + + def set_rules(self): + set_rules(self.multiworld, self.options, self.player) + + def create_items(self): + self.multiworld.itempool += [self.create_item(item_table[k]) for k in item_table] + + def create_item(self, name: str) -> Item: + item_id = self.item_name_to_id[name] + if item_id in range(jak1_id, jak1_id + Scouts.fly_offset): + # Power Cell + classification = ItemClassification.progression_skip_balancing + elif item_id in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset): + # Scout Fly + classification = ItemClassification.progression_skip_balancing + elif item_id > jak1_id + Orbs.orb_offset: + # Precursor Orb + classification = ItemClassification.filler # TODO + else: + classification = ItemClassification.filler + + item = JakAndDaxterItem(name, classification, item_id, self.player) + return item + + +def launch_client(): + from .Client import launch + launch_subprocess(launch, name="JakAndDaxterClient") + + +components.append(Component("Jak and Daxter Client", + "JakAndDaxterClient", + func=launch_client, + component_type=Type.CLIENT)) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py new file mode 100644 index 000000000000..4373e9676e02 --- /dev/null +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -0,0 +1,133 @@ +import typing +import pymem +from pymem import pattern +from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError + +from CommonClient import logger +from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies + +# Some helpful constants. +next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. +next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. +cells_offset = 16 +buzzers_offset = 420 # cells_offset + (sizeof uint32 * 101 cells) = 16 + (4 * 101) + + +# buzzers_offset +# + (sizeof uint32 * 112 flies) <-- The buzzers themselves. +# + (sizeof uint8 * 116 tasks) <-- A "cells-received" array for the game to handle new ownership logic. +# = 420 + (4 * 112) + (1 * 116) +end_marker_offset = 984 + + +class JakAndDaxterMemoryReader: + marker: typing.ByteString + goal_address = None + connected: bool = False + initiated_connect: bool = False + + # The memory reader just needs the game running. + gk_process: pymem.process = None + + location_outbox = [] + outbox_index = 0 + + def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): + self.marker = marker + self.connect() + + async def main_tick(self, location_callback: typing.Callable): + if self.initiated_connect: + await self.connect() + self.initiated_connect = False + + if self.connected: + try: + self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. + except (ProcessError, MemoryReadError, WinAPIError): + logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") + self.connected = False + else: + return + + # Read the memory address to check the state of the game. + self.read_memory() + location_callback(self.location_outbox) # TODO - I forgot why call this here when it's already down below... + + # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. + if len(self.location_outbox) > self.outbox_index: + location_callback(self.location_outbox) + self.outbox_index += 1 + + async def connect(self): + try: + self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel + logger.info("Found the gk process: " + str(self.gk_process.process_id)) + except ProcessNotFound: + logger.error("Could not find the gk process.") + self.connected = False + return + + # If we don't find the marker in the first loaded module, we've failed. + modules = list(self.gk_process.list_modules()) + marker_address = pattern.pattern_scan_module(self.gk_process.process_handle, modules[0], self.marker) + if marker_address: + # At this address is another address that contains the struct we're looking for: the game's state. + # From here we need to add the length in bytes for the marker and 4 bytes of padding, + # and the struct address is 8 bytes long (it's u64). + goal_pointer = marker_address + len(self.marker) + 4 + self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, 8), + byteorder="little", + signed=False) + logger.info("Found the archipelago memory address: " + str(self.goal_address)) + self.connected = True + else: + logger.error("Could not find the archipelago memory address.") + self.connected = False + + if self.connected: + logger.info("The Memory Reader is ready!") + + def print_status(self): + logger.info("Memory Reader Status:") + logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None")) + logger.info(" Game state memory address: " + str(self.goal_address)) + logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index]) + if self.outbox_index else "None")) + + def read_memory(self) -> typing.List[int]: + try: + next_cell_index = int.from_bytes( + self.gk_process.read_bytes(self.goal_address, 8), + byteorder="little", + signed=False) + next_buzzer_index = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + next_buzzer_index_offset, 8), + byteorder="little", + signed=False) + + for k in range(0, next_cell_index): + next_cell = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + cells_offset + (k * 4), 4), + byteorder="little", + signed=False) + cell_ap_id = Cells.to_ap_id(next_cell) + if cell_ap_id not in self.location_outbox: + self.location_outbox.append(cell_ap_id) + logger.info("Checked power cell: " + str(next_cell)) + + for k in range(0, next_buzzer_index): + next_buzzer = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + buzzers_offset + (k * 4), 4), + byteorder="little", + signed=False) + buzzer_ap_id = Flies.to_ap_id(next_buzzer) + if buzzer_ap_id not in self.location_outbox: + self.location_outbox.append(buzzer_ap_id) + logger.info("Checked scout fly: " + str(next_buzzer)) + + except (ProcessError, MemoryReadError, WinAPIError): + logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") + self.connected = False + + return self.location_outbox diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py new file mode 100644 index 000000000000..c70d633b00b8 --- /dev/null +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -0,0 +1,202 @@ +import time +import struct +from socket import socket, AF_INET, SOCK_STREAM + +import pymem +from pymem.exception import ProcessNotFound, ProcessError + +from CommonClient import logger +from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, OrbLocations as Orbs +from worlds.jakanddaxter.GameID import jak1_id + + +class JakAndDaxterReplClient: + ip: str + port: int + sock: socket + connected: bool = False + initiated_connect: bool = False # Signals when user tells us to try reconnecting. + + # The REPL client needs the REPL/compiler process running, but that process + # also needs the game running. Therefore, the REPL client needs both running. + gk_process: pymem.process = None + goalc_process: pymem.process = None + + item_inbox = {} + inbox_index = 0 + + def __init__(self, ip: str = "127.0.0.1", port: int = 8181): + self.ip = ip + self.port = port + self.connect() + + async def main_tick(self): + if self.initiated_connect: + await self.connect() + self.initiated_connect = False + + if self.connected: + try: + self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. + except ProcessError: + logger.error("The gk process has died. Restart the game and run \"/repl connect\" again.") + self.connected = False + try: + self.goalc_process.read_bool(self.goalc_process.base_address) # Ping to see if it's alive. + except ProcessError: + logger.error("The goalc process has died. Restart the compiler and run \"/repl connect\" again.") + self.connected = False + else: + return + + # Receive Items from AP. Handle 1 item per tick. + if len(self.item_inbox) > self.inbox_index: + self.receive_item() + self.inbox_index += 1 + + # This helper function formats and sends `form` as a command to the REPL. + # ALL commands to the REPL should be sent using this function. + # TODO - this blocks on receiving an acknowledgement from the REPL server. But it doesn't print + # any log info in the meantime. Is that a problem? + def send_form(self, form: str, print_ok: bool = True) -> bool: + header = struct.pack(" jak1_id + Orbs.orb_offset: + pass # TODO + + def receive_power_cell(self, ap_id: int) -> bool: + cell_id = Cells.to_game_id(ap_id) + ok = self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") + if ok: + logger.info(f"Received power cell {cell_id}!") + else: + logger.error(f"Unable to receive power cell {cell_id}!") + return ok + + def receive_scout_fly(self, ap_id: int) -> bool: + fly_id = Flies.to_game_id(ap_id) + ok = self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))") + if ok: + logger.info(f"Received scout fly {fly_id}!") + else: + logger.error(f"Unable to receive scout fly {fly_id}!") + return ok diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md new file mode 100644 index 000000000000..a96d2797f239 --- /dev/null +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -0,0 +1,78 @@ +# Jak And Daxter (ArchipelaGOAL) + +## Where is the options page? + +The [Player Options Page](../player-options) for this game contains +all the options you need to configure and export a config file. + +At this time, Scout Flies are always randomized, and Precursor Orbs +are never randomized. + +## What does randomization do to this game? +All 101 Power Cells and 112 Scout Flies are now Location Checks +and may contain Items for different games, as well as different Items from within Jak and Daxter. + +## What is the goal of the game once randomized? +To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. + +In order to reach them, you will need at least 72 Power Cells to cross the Lava Tube. In addition, +you will need the four specific Power Cells obtained by freeing the Red, Blue, Yellow, and Green Sages. + +## How do I progress through the game? +You can progress by performing tasks and completing the challenges that would normally give you Power Cells and +Scout Flies in the game. If you are playing with others, those players may find Power Cells and Scout Flies +in their games, and those Items will be automatically sent to your game. + +If you have completed all possible tasks available to you but still cannot progress, you may have to wait for +another player to find enough of your game's Items to allow you to progress. If that does not apply, +double-check your spoiler log to make sure you have all the items you should have. If you don't, +you may have encountered a bug. Please see the options for bug reporting below. + +## What happens when I pick up an item? +Jak and Daxter will perform their victory animation, if applicable. You will not receive that item, and +the Item count for that item will not change. The pause menu will say "Task Completed" below the +picked-up Power Cell, but the icon will remain "dormant." You will see a message in your text client saying +what you found and who it belongs to. + +## What happens when I receive an item? +Jak and Daxter won't perform their victory animation, and gameplay will continue as normal. Your text client will +inform you where you received the Item from, and which one it is. Your Item count for that type of Item will also +tick up. The pause menu will not say "Task Completed" below the selected Power Cell, but the icon will be "activated." + +## I can't reach a certain area within an accessible region, how do I get there? +Some areas are locked behind possession of specific Power Cells. For example, you cannot access Misty Island +until you have the "Catch 200 Pounds of Fish" Power Cell. Keep in mind, your access to Misty Island is determined +_through possession of this specific Power Cell only,_ **not** _by you completing the Fishing minigame._ + +## I got soft-locked and can't leave, how do I get out of here? +As stated before, some areas are locked behind possession of specific Power Cells. But you may already be past +a point-of-no-return preventing you from backtracking. One example is the Forbidden Jungle temple, where +the elevator is locked at the bottom, and if you haven't unlocked the Blue Eco Switch, you cannot access +the Plant Boss's room and escape. + +In this scenario, you will need to open your menu and find the "Warp To Home" option at the bottom of the list. +Selecting this option will instantly teleport you to Geyser Rock. From there, you can teleport back to the nearest +sage's hut to continue your journey. + +## I think I found a bug, where should I report it? +Depending on the nature of the bug, there are a couple of different options. + +* If you found a logical error in the randomizer, please create a new Issue +[here.](https://github.com/ArchipelaGOAL/Archipelago/issues) + * Use this page if: + * You are hard-locked from progressing. For example, you are stuck on Geyser Rock because one of the four + Geyser Rock Power Cells is not on Geyser Rock. + * The randomizer did not respect one of the Options you chose. + * You see a mistake, typo, etc. on this webpage. + * You see an error or stack trace appear on the text client. + * Please upload your config file and spoiler log file in the Issue, so we can troubleshoot the problem. + +* If you encountered an error in OpenGOAL, please create a new Issue +[here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues) + * Use this page if: + * You encounter a crash, freeze, reset, etc. + * You fail to send Items you find in the game to the Archipelago server. + * You fail to receive Items the server sends to you. + * Your game disconnects from the server and cannot reconnect. + * You go looking for a game item that has already disappeared before you could reach it. + * Please upload any log files that may have been generated. \ No newline at end of file diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md new file mode 100644 index 000000000000..ce2b1936771b --- /dev/null +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -0,0 +1,85 @@ +# Jak And Daxter (ArchipelaGOAL) Setup Guide + +## Required Software + +- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.* +- Python version 3.10 or higher. Make sure this is added to your PATH environment variable. +- [Task](https://taskfile.dev/installation/) (This makes it easier to run commands.) + +## Installation + +### Installation via OpenGOAL Mod Launcher + +At this time, the only supported method of setup is through Manual Compilation. Aside from the legal copy of the game, all tools required to do this are free. + +***Windows Preparations*** + +***Linux Preparations*** + +***Using the Launcher*** + +### Manual Compilation (Linux/Windows) + +***Windows Preparations*** + +- Dump your copy of the game as an ISO file to your PC. +- Download a zipped up copy of the Archipelago Server and Client [here.](https://github.com/ArchipelaGOAL/Archipelago) +- Download a zipped up copy of the modded OpenGOAL game [here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL) +- Unzip the two projects into easily accessible directories. + + +***Linux Preparations*** + +***Compiling*** + +## Starting a Game + +- Open 3 Powershell windows. If you have VSCode, you can run 3 terminals to consolidate this process. + - In the first window, navigate to the Archipelago folder using `cd` and run `python ./Launcher.py --update_settings`. Then run it again without the `--update_settings` flag. + - In the second window, navigate to the ArchipelaGOAL folder and run `task extract`. This will prompt you to tell the mod where to find your ISO file to dump its contents. When that is done, run `task repl`. + - In the third window, navigate to the ArchipelaGOAL folder and run `task boot-game`. At this point, Jak should be standing outside Samos's hut. + - Once you confirm all those tasks succeeded, you can now close all these windows. +- Edit your host.yaml file and ensure these lines exist. And don't forget to specify your ACTUAL install path. If you're on Windows, no backslashes! +``` +jakanddaxter_options: + # Path to folder containing the ArchipelaGOAL mod. + root_directory: "D:/Files/Repositories/ArchipelaGOAL" +``` +- In the Launcher, click Generate to create a new random seed. Save the resulting zip file. +- In the Launcher, click Host to host the Archipelago server. It will prompt you for the location of that zip file. +- Once the server is running, in the Launcher, find the Jak and Daxter Client and click it. You should see the command window begin to compile the game. +- When it completes, you should hear the menu closing sound effect, and you should see the text client indicate that the two agents are ready to communicate with the game. +- Connect the client to the Archipelago server and enter your slot name. Once this is done, the game should be ready to play. Talk to Samos to trigger the cutscene where he sends you to Geyser Rock, and off you go! + +Once you complete the setup steps, you should only need to run the Launcher again to generate a game, host a server, or run the client and connect to a server. +- You never need to download the zip copies of the projects again (unless there are updates). +- You never need to dump your ISO again. +- You never need to extract the ISO assets again. + +### Joining a MultiWorld Game + +MultiWorld games are untested at this time. + +### Playing Offline + +Offline play is untested at this time. + +## Installation and Setup Troubleshooting + +### Compilation Failures + +### Runtime Failures + +- If the client window appears but no sound plays, you will need to enter the following commands into the client to connect it to the game. + - `/repl connect` + - `/memr connect` +- Once these are done, you can enter `/repl status` and `/memr status` to check that everything is connected and ready. + +## Gameplay Troubleshooting + +### Known Issues + +- I've streamlined the process of connecting the client's agents to the game, but it comes at the cost of more granular commands useful for troubleshooting. +- The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly. +- The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. +- The game relates tasks and power cells closely but separately. Some issues may result from having to tell the game to check for the power cells you own, rather than the tasks you completed. \ No newline at end of file diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py new file mode 100644 index 000000000000..304810af800c --- /dev/null +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -0,0 +1,188 @@ +from ..GameID import jak1_id + +# Power Cells are given ID's between 0 and 116 by the game. + +# The game tracks all game-tasks as integers. +# 101 of these ID's correspond directly to power cells, but they are not +# necessarily ordered, nor are they the first 101 in the task list. +# The remaining ones are cutscenes and other events. + + +# These helper functions do all the math required to get information about each +# power cell and translate its ID between AP and OpenGOAL. +def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + return jak1_id + game_id + + +def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + return ap_id - jak1_id + + +# The ID's you see below correspond directly to that cell's game-task ID. + +# Geyser Rock +locGR_cellTable = { + 92: "GR: Find The Cell On The Path", + 93: "GR: Open The Precursor Door", + 94: "GR: Climb Up The Cliff", + 95: "GR: Free 7 Scout Flies" +} + +# Sandover Village +locSV_cellTable = { + 11: "SV: Bring 90 Orbs To The Mayor", + 12: "SV: Bring 90 Orbs to Your Uncle", + 10: "SV: Herd The Yakows Into The Pen", + 13: "SV: Bring 120 Orbs To The Oracle (1)", + 14: "SV: Bring 120 Orbs To The Oracle (2)", + 75: "SV: Free 7 Scout Flies" +} + +# Forbidden Jungle +locFJ_cellTable = { + 3: "FJ: Connect The Eco Beams", + 4: "FJ: Get To The Top Of The Temple", + 2: "FJ: Find The Blue Vent Switch", + 6: "FJ: Defeat The Dark Eco Plant", + 5: "FJ: Catch 200 Pounds Of Fish", + 8: "FJ: Follow The Canyon To The Sea", + 9: "FJ: Open The Locked Temple Door", + 7: "FJ: Free 7 Scout Flies" +} + +# Sentinel Beach +locSB_cellTable = { + 15: "SB: Unblock The Eco Harvesters", + 17: "SB: Push The Flut Flut Egg Off The Cliff", + 16: "SB: Get The Power Cell From The Pelican", + 18: "SB: Chase The Seagulls", + 19: "SB: Launch Up To The Cannon Tower", + 21: "SB: Explore The Beach", + 22: "SB: Climb The Sentinel", + 20: "SB: Free 7 Scout Flies" +} + +# Misty Island +locMI_cellTable = { + 23: "MI: Catch The Sculptor's Muse", + 24: "MI: Climb The Lurker Ship", + 26: "MI: Stop The Cannon", + 25: "MI: Return To The Dark Eco Pool", + 27: "MI: Destroy the Balloon Lurkers", + 29: "MI: Use Zoomer To Reach Power Cell", + 30: "MI: Use Blue Eco To Reach Power Cell", + 28: "MI: Free 7 Scout Flies" +} + +# Fire Canyon +locFC_cellTable = { + 69: "FC: Reach The End Of Fire Canyon", + 68: "FC: Free 7 Scout Flies" +} + +# Rock Village +locRV_cellTable = { + 31: "RV: Bring 90 Orbs To The Gambler", + 32: "RV: Bring 90 Orbs To The Geologist", + 33: "RV: Bring 90 Orbs To The Warrior", + 34: "RV: Bring 120 Orbs To The Oracle (1)", + 35: "RV: Bring 120 Orbs To The Oracle (2)", + 76: "RV: Free 7 Scout Flies" +} + +# Precursor Basin +locPB_cellTable = { + 54: "PB: Herd The Moles Into Their Hole", + 53: "PB: Catch The Flying Lurkers", + 52: "PB: Beat Record Time On The Gorge", + 56: "PB: Get The Power Cell Over The Lake", + 55: "PB: Cure Dark Eco Infected Plants", + 58: "PB: Navigate The Purple Precursor Rings", + 59: "PB: Navigate The Blue Precursor Rings", + 57: "PB: Free 7 Scout Flies" +} + +# Lost Precursor City +locLPC_cellTable = { + 47: "LPC: Raise The Chamber", + 45: "LPC: Follow The Colored Pipes", + 46: "LPC: Reach The Bottom Of The City", + 48: "LPC: Quickly Cross The Dangerous Pool", + 44: "LPC: Match The Platform Colors", + 50: "LPC: Climb The Slide Tube", + 51: "LPC: Reach The Center Of The Complex", + 49: "LPC: Free 7 Scout Flies" +} + +# Boggy Swamp +locBS_cellTable = { + 37: "BS: Ride The Flut Flut", + 36: "BS: Protect Farthy's Snacks", + 38: "BS: Defeat The Lurker Ambush", + 39: "BS: Break The Tethers To The Zeppelin (1)", + 40: "BS: Break The Tethers To The Zeppelin (2)", + 41: "BS: Break The Tethers To The Zeppelin (3)", + 42: "BS: Break The Tethers To The Zeppelin (4)", + 43: "BS: Free 7 Scout Flies" +} + +# Mountain Pass +locMP_cellTable = { + 86: "MP: Defeat Klaww", + 87: "MP: Reach The End Of The Mountain Pass", + 110: "MP: Find The Hidden Power Cell", + 88: "MP: Free 7 Scout Flies" +} + +# Volcanic Crater +locVC_cellTable = { + 96: "VC: Bring 90 Orbs To The Miners (1)", + 97: "VC: Bring 90 Orbs To The Miners (2)", + 98: "VC: Bring 90 Orbs To The Miners (3)", + 99: "VC: Bring 90 Orbs To The Miners (4)", + 100: "VC: Bring 120 Orbs To The Oracle (1)", + 101: "VC: Bring 120 Orbs To The Oracle (2)", + 74: "VC: Find The Hidden Power Cell", + 77: "VC: Free 7 Scout Flies" +} + +# Spider Cave +locSC_cellTable = { + 78: "SC: Use Your Goggles To Shoot The Gnawing Lurkers", + 79: "SC: Destroy The Dark Eco Crystals", + 80: "SC: Explore The Dark Cave", + 81: "SC: Climb The Giant Robot", + 82: "SC: Launch To The Poles", + 83: "SC: Navigate The Spider Tunnel", + 84: "SC: Climb the Precursor Platforms", + 85: "SC: Free 7 Scout Flies" +} + +# Snowy Mountain +locSM_cellTable = { + 60: "SM: Find The Yellow Vent Switch", + 61: "SM: Stop The 3 Lurker Glacier Troops", + 66: "SM: Deactivate The Precursor Blockers", + 67: "SM: Open The Frozen Crate", + 63: "SM: Open The Lurker Fort Gate", + 62: "SM: Get Through The Lurker Fort", + 64: "SM: Survive The Lurker Infested Cave", + 65: "SM: Free 7 Scout Flies" +} + +# Lava Tube +locLT_cellTable = { + 89: "LT: Cross The Lava Tube", + 90: "LT: Free 7 Scout Flies" +} + +# Gol and Maias Citadel +locGMC_cellTable = { + 71: "GMC: Free The Blue Sage", + 72: "GMC: Free The Red Sage", + 73: "GMC: Free The Yellow Sage", + 70: "GMC: Free The Green Sage", + 91: "GMC: Free 7 Scout Flies" +} diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py new file mode 100644 index 000000000000..4e586a65fb47 --- /dev/null +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -0,0 +1,101 @@ +from ..GameID import jak1_id + +# Precursor Orbs are not necessarily given ID's by the game. + +# Of the 2000 orbs (or "money") you can pick up, only 1233 are standalone ones you find in the overworld. +# We can identify them by Actor ID's, which run from 549 to 24433. Other actors reside in this range, +# so like Power Cells these are not ordered, nor contiguous, nor exclusively orbs. + +# In fact, other ID's in this range belong to actors that spawn orbs when they are activated or when they die, +# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head. + +# These orbs that spawn from parent actors DON'T have an Actor ID themselves - the parent object keeps +# track of how many of its orbs have been picked up. If you pick up only some of its orbs, it +# will respawn when you leave the area, and only drop the remaining number of orbs when activated/killed. +# Once all the orbs are picked up, the actor will permanently "retire" and never spawn again. +# The maximum number of orbs that any actor can spawn is 30 (the orb caches in citadel). Covering +# these ID-less orbs may need to be a future enhancement. TODO ^^ + +# Standalone orbs need 15 bits to identify themselves by Actor ID, +# so we can use 2^15 to offset them from scout flies, just like we offset +# scout flies from power cells by 2^10. +orb_offset = 32768 + + +# These helper functions do all the math required to get information about each +# precursor orb and translate its ID between AP and OpenGOAL. +def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + return jak1_id + orb_offset + game_id # Add the offsets and the orb Actor ID. + + +def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets. + + +# The ID's you see below correspond directly to that orb's Actor ID in the game. + +# Geyser Rock +locGR_orbTable = { +} + +# Sandover Village +locSV_orbTable = { +} + +# Forbidden Jungle +locFJ_orbTable = { +} + +# Sentinel Beach +locSB_orbTable = { +} + +# Misty Island +locMI_orbTable = { +} + +# Fire Canyon +locFC_orbTable = { +} + +# Rock Village +locRV_orbTable = { +} + +# Precursor Basin +locPB_orbTable = { +} + +# Lost Precursor City +locLPC_orbTable = { +} + +# Boggy Swamp +locBS_orbTable = { +} + +# Mountain Pass +locMP_orbTable = { +} + +# Volcanic Crater +locVC_orbTable = { +} + +# Spider Cave +locSC_orbTable = { +} + +# Snowy Mountain +locSM_orbTable = { +} + +# Lava Tube +locLT_orbTable = { +} + +# Gol and Maias Citadel +locGMC_orbTable = { +} diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py new file mode 100644 index 000000000000..c1349d0d6367 --- /dev/null +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -0,0 +1,227 @@ +from ..GameID import jak1_id + +# Scout Flies are given ID's between 0 and 393311 by the game, explanation below. + +# Each fly (or "buzzer") is given a unique 32-bit number broken into two 16-bit numbers. +# The lower 16 bits are the game-task ID of the power cell the fly corresponds to. +# The higher 16 bits are the index of the fly itself, from 000 (0) to 110 (6). + +# Ex: The final scout fly on Geyser Rock +# 0000000000000110 0000000001011111 +# ( Index: 6 ) ( Cell: 95 ) + +# Because flies are indexed from 0, each 0th fly's full ID == the power cell's ID. +# So we need to offset all of their ID's in order for Archipelago to separate them +# from their power cells. We can use 1024 (2^10) for this purpose, because scout flies +# only ever need 10 bits to identify themselves (3 for the index, 7 for the cell ID). + +# We're also going to compress the ID by bit-shifting the fly index down to lower bits, +# keeping the scout fly ID range to a smaller set of numbers (1000 -> 2000, instead of 1 -> 400000). +fly_offset = 1024 + + +# These helper functions do all the math required to get information about each +# scout fly and translate its ID between AP and OpenGOAL. +def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + cell_id = get_cell_id(game_id) # Get the power cell ID from the lowest 7 bits. + buzzer_index = (game_id - cell_id) >> 9 # Get the index, bit shift it down 9 places. + compressed_id = fly_offset + buzzer_index + cell_id # Add the offset, the bit-shifted index, and the cell ID. + return jak1_id + compressed_id # Last thing: add the game's ID. + + +def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + compressed_id = ap_id - jak1_id # Reverse process. First thing: subtract the game's ID. + cell_id = get_cell_id(compressed_id) # Get the power cell ID from the lowest 7 bits. + buzzer_index = compressed_id - fly_offset - cell_id # Get the bit-shifted index. + return (buzzer_index << 9) + cell_id # Return the index to its normal place, re-add the cell ID. + + +# Get the power cell ID from the lowest 7 bits. +# Make sure to use this function ONLY when the input argument does NOT include jak1_id, +# because that number may flip some of the bottom 7 bits, and that will throw off this bit mask. +def get_cell_id(buzzer_id: int) -> int: + assert buzzer_id < jak1_id, f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}." + return buzzer_id & 0b1111111 + + +# The ID's you see below correspond directly to that fly's 32-bit ID in the game. +# I used the decompiled entity JSON's and Jak's X/Y coordinates in Debug Mode +# to determine which box ID is which location. + +# Geyser Rock +locGR_scoutTable = { + 95: "GR: Scout Fly On Ground, Front", + 327775: "GR: Scout Fly On Ground, Back", + 393311: "GR: Scout Fly On Left Ledge", + 65631: "GR: Scout Fly On Right Ledge", + 262239: "GR: Scout Fly On Middle Ledge, Left", + 131167: "GR: Scout Fly On Middle Ledge, Right", + 196703: "GR: Scout Fly On Top Ledge" +} + +# Sandover Village +locSV_scoutTable = { + 262219: "SV: Scout Fly In Fisherman's House", + 327755: "SV: Scout Fly In Mayor's House", + 131147: "SV: Scout Fly Under Bridge", + 65611: "SV: Scout Fly Behind Sculptor's House", + 75: "SV: Scout Fly Overlooking Farmer's House", + 393291: "SV: Scout Fly Near Oracle", + 196683: "SV: Scout Fly In Farmer's House" +} + +# Forbidden Jungle +locFJ_scoutTable = { + 393223: "FJ: Scout Fly At End Of Path", + 262151: "FJ: Scout Fly On Spiral Of Stumps", + 7: "FJ: Scout Fly Near Dark Eco Boxes", + 196615: "FJ: Scout Fly At End Of River", + 131079: "FJ: Scout Fly Behind Lurker Machine", + 327687: "FJ: Scout Fly Around Temple Spire", + 65543: "FJ: Scout Fly On Top Of Temple" +} + +# Sentinel Beach +locSB_scoutTable = { + 327700: "SB: Scout Fly At Entrance", + 20: "SB: Scout Fly Overlooking Locked Boxes", + 65556: "SB: Scout Fly On Path To Flut Flut", + 262164: "SB: Scout Fly Under Wood Pillars", + 196628: "SB: Scout Fly Overlooking Blue Eco Vent", + 131092: "SB: Scout Fly Overlooking Green Eco Vents", + 393236: "SB: Scout Fly On Sentinel" +} + +# Misty Island +locMI_scoutTable = { + 327708: "MI: Scout Fly Overlooking Entrance", + 65564: "MI: Scout Fly On Ledge Near Arena Entrance", + 262172: "MI: Scout Fly Near Arena Door", + 28: "MI: Scout Fly On Ledge Near Arena Exit", + 131100: "MI: Scout Fly On Ship", + 196636: "MI: Scout Fly On Barrel Ramps", + 393244: "MI: Scout Fly On Zoomer Ramps" +} + +# Fire Canyon +locFC_scoutTable = { + 393284: "FC: Scout Fly 1", + 68: "FC: Scout Fly 2", + 65604: "FC: Scout Fly 3", + 196676: "FC: Scout Fly 4", + 131140: "FC: Scout Fly 5", + 262212: "FC: Scout Fly 6", + 327748: "FC: Scout Fly 7" +} + +# Rock Village +locRV_scoutTable = { + 76: "RV: Scout Fly Behind Sage's Hut", + 131148: "RV: Scout Fly Near Waterfall", + 196684: "RV: Scout Fly Behind Geologist", + 262220: "RV: Scout Fly Behind Fiery Boulder", + 65612: "RV: Scout Fly On Dock", + 327756: "RV: Scout Fly At Pontoon Bridge", + 393292: "RV: Scout Fly At Boggy Swamp Entrance" +} + +# Precursor Basin +locPB_scoutTable = { + 196665: "PB: Scout Fly Overlooking Entrance", + 393273: "PB: Scout Fly Near Mole Hole", + 131129: "PB: Scout Fly At Purple Ring Start", + 65593: "PB: Scout Fly Near Dark Eco Plant, Above", + 57: "PB: Scout Fly At Blue Ring Start", + 262201: "PB: Scout Fly Before Big Jump", + 327737: "PB: Scout Fly Near Dark Eco Plant, Below" +} + +# Lost Precursor City +locLPC_scoutTable = { + 262193: "LPC: Scout Fly First Room", + 131121: "LPC: Scout Fly Before Second Room", + 393265: "LPC: Scout Fly Second Room, Near Orb Vent", + 196657: "LPC: Scout Fly Second Room, On Path To Cell", + 49: "LPC: Scout Fly Second Room, Green Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?` + 65585: "LPC: Scout Fly Second Room, Blue Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?` + 327729: "LPC: Scout Fly Across Steam Vents" +} + +# Boggy Swamp +locBS_scoutTable = { + 43: "BS: Scout Fly Near Entrance", + 393259: "BS: Scout Fly Over First Jump Pad", + 65579: "BS: Scout Fly Over Second Jump Pad", + 262187: "BS: Scout Fly Across Black Swamp", + 327723: "BS: Scout Fly Overlooking Flut Flut", + 131115: "BS: Scout Fly On Flut Flut Platforms", + 196651: "BS: Scout Fly In Field Of Boxes" +} + +# Mountain Pass +locMP_scoutTable = { + 88: "MP: Scout Fly 1", + 65624: "MP: Scout Fly 2", + 131160: "MP: Scout Fly 3", + 196696: "MP: Scout Fly 4", + 262232: "MP: Scout Fly 5", + 327768: "MP: Scout Fly 6", + 393304: "MP: Scout Fly 7" +} + +# Volcanic Crater +locVC_scoutTable = { + 262221: "VC: Scout Fly In Miner's Cave", + 393293: "VC: Scout Fly Near Oracle", + 196685: "VC: Scout Fly On Stone Platforms", + 131149: "VC: Scout Fly Near Lava Tube", + 77: "VC: Scout Fly At Minecart Junction", + 65613: "VC: Scout Fly Near Spider Cave", + 327757: "VC: Scout Fly Near Mountain Pass" +} + +# Spider Cave +locSC_scoutTable = { + 327765: "SC: Scout Fly Near Dark Dave Entrance", + 262229: "SC: Scout Fly In Dark Cave", + 393301: "SC: Scout Fly Main Cave, Overlooking Entrance", + 196693: "SC: Scout Fly Main Cave, Near Dark Crystal", + 131157: "SC: Scout Fly Main Cave, Near Robot Cave Entrance", + 85: "SC: Scout Fly Robot Cave, At Bottom Level", + 65621: "SC: Scout Fly Robot Cave, At Top Level", +} + +# Snowy Mountain +locSM_scoutTable = { + 65: "SM: Scout Fly Near Entrance", + 327745: "SM: Scout Fly Near Frozen Box", + 65601: "SM: Scout Fly Near Yellow Eco Switch", + 131137: "SM: Scout Fly On Cliff near Flut Flut", + 393281: "SM: Scout Fly Under Bridge To Fort", + 196673: "SM: Scout Fly On Top Of Fort Tower", + 262209: "SM: Scout Fly On Top Of Fort" +} + +# Lava Tube +locLT_scoutTable = { + 90: "LT: Scout Fly 1", + 65626: "LT: Scout Fly 2", + 327770: "LT: Scout Fly 3", + 262234: "LT: Scout Fly 4", + 131162: "LT: Scout Fly 5", + 196698: "LT: Scout Fly 6", + 393306: "LT: Scout Fly 7" +} + +# Gol and Maias Citadel +locGMC_scoutTable = { + 91: "GMC: Scout Fly At Entrance", + 65627: "GMC: Scout Fly Main Room, Left of Robot", + 196699: "GMC: Scout Fly Main Room, Right of Robot", + 262235: "GMC: Scout Fly Before Jumping Lurkers", + 393307: "GMC: Scout Fly At Blast Furnace", + 131163: "GMC: Scout Fly At Launch Pad Room", + 327771: "GMC: Scout Fly Top Of Rotating Tower" +} diff --git a/worlds/jakanddaxter/requirements.txt b/worlds/jakanddaxter/requirements.txt new file mode 100644 index 000000000000..fe25267f6705 --- /dev/null +++ b/worlds/jakanddaxter/requirements.txt @@ -0,0 +1 @@ +Pymem>=1.13.0 \ No newline at end of file From 7cf50b0935ef7aa14602f54f25a2fc2ec8b5b3c7 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 18 May 2024 14:37:01 -0400 Subject: [PATCH 30/70] Jak and Daxter: Genericize Items, Update Scout Fly logic, Add Victory Condition. (#3) * Jak 1: Update to 0.4.6. Decouple locations from items, support filler items. * Jak 1: Total revamp of Items. This is where everything broke. * Jak 1: Decouple 7 scout fly checks from normal checks, update regions/rules for orb counts/traders. * Jak 1: correct regions/rules, account for sequential oracle/miner locations. * Jak 1: make nicer strings. * Jak 1: Add logic for finished game. First full run complete! * Jak 1: update group names. --- worlds/jakanddaxter/Client.py | 20 +- worlds/jakanddaxter/Items.py | 57 +++++ worlds/jakanddaxter/Locations.py | 7 +- worlds/jakanddaxter/Regions.py | 226 ++++++++++++------- worlds/jakanddaxter/Rules.py | 218 +++++++++++------- worlds/jakanddaxter/__init__.py | 78 ++++--- worlds/jakanddaxter/client/MemoryReader.py | 72 ++++-- worlds/jakanddaxter/client/ReplClient.py | 39 +++- worlds/jakanddaxter/locs/CellLocations.py | 36 +-- worlds/jakanddaxter/locs/SpecialLocations.py | 47 ++++ 10 files changed, 564 insertions(+), 236 deletions(-) create mode 100644 worlds/jakanddaxter/locs/SpecialLocations.py diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index b6e982a9e3db..51a38bb3f5e9 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -6,6 +6,7 @@ import colorama import Utils +from NetUtils import ClientStatus from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled from worlds.jakanddaxter.GameID import jak1_name @@ -108,12 +109,21 @@ def on_package(self, cmd: str, args: dict): logger.info(args) self.repl.item_inbox[index] = item - async def ap_inform_location_checks(self, location_ids: typing.List[int]): + async def ap_inform_location_check(self, location_ids: typing.List[int]): message = [{"cmd": "LocationChecks", "locations": location_ids}] await self.send_msgs(message) - def on_locations(self, location_ids: typing.List[int]): - create_task_log_exception(self.ap_inform_location_checks(location_ids)) + def on_location_check(self, location_ids: typing.List[int]): + create_task_log_exception(self.ap_inform_location_check(location_ids)) + + async def ap_inform_finished_game(self): + if not self.finished_game and self.memr.finished_game: + message = [{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}] + await self.send_msgs(message) + self.finished_game = True + + def on_finish(self): + create_task_log_exception(self.ap_inform_finished_game()) async def run_repl_loop(self): while True: @@ -122,7 +132,7 @@ async def run_repl_loop(self): async def run_memr_loop(self): while True: - await self.memr.main_tick(self.on_locations) + await self.memr.main_tick(self.on_location_check, self.on_finish) await asyncio.sleep(0.1) @@ -189,7 +199,7 @@ async def main(): ctx.run_cli() # Find and run the game (gk) and compiler/repl (goalc). - await run_game(ctx) + # await run_game(ctx) await ctx.exit_event.wait() await ctx.shutdown() diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index d051f15869d4..2fa065472657 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -1,7 +1,64 @@ from BaseClasses import Item from .GameID import jak1_name +from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials class JakAndDaxterItem(Item): game: str = jak1_name + +# Power Cells are generic, fungible, interchangeable items. Every cell is indistinguishable from every other. +cell_item_table = { + 0: "Power Cell", +} + +# Scout flies are interchangeable within their respective sets of 7. Notice the abbreviated level name after each item. +# Also, notice that their Item ID equals their respective Power Cell's Location ID. This is necessary for +# game<->archipelago communication. +scout_item_table = { + 95: "Scout Fly - GR", + 75: "Scout Fly - SV", + 7: "Scout Fly - FJ", + 20: "Scout Fly - SB", + 28: "Scout Fly - MI", + 68: "Scout Fly - FC", + 76: "Scout Fly - RV", + 57: "Scout Fly - PB", + 49: "Scout Fly - LPC", + 43: "Scout Fly - BS", + 88: "Scout Fly - MP", + 77: "Scout Fly - VC", + 85: "Scout Fly - SC", + 65: "Scout Fly - SM", + 90: "Scout Fly - LT", + 91: "Scout Fly - GMC", +} + +# TODO - Orbs are also generic and interchangeable. +# orb_item_table = { +# ???: "Precursor Orb", +# } + +# These are special items representing unique unlocks in the world. Notice that their Item ID equals their +# respective Location ID. Like scout flies, this is necessary for game<->archipelago communication. +special_item_table = { + 5: "Fisherman's Boat", + 4: "Jungle Elevator", + 2: "Blue Eco Switch", + 17: "Flut Flut", + 60: "Yellow Eco Switch", + 63: "Snowy Fort Gate", + 71: "Freed The Blue Sage", + 72: "Freed The Red Sage", + 73: "Freed The Yellow Sage", + 70: "Freed The Green Sage", +} + +# All Items +# While we're here, do all the ID conversions needed. +item_table = { + **{Cells.to_ap_id(k): cell_item_table[k] for k in cell_item_table}, + **{Scouts.to_ap_id(k): scout_item_table[k] for k in scout_item_table}, + # **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table}, + **{Specials.to_ap_id(k): special_item_table[k] for k in special_item_table}, +} diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index ef56137cf176..ac18ce84c829 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,6 +1,6 @@ from BaseClasses import Location from .GameID import jak1_name -from .locs import CellLocations as Cells, ScoutLocations as Scouts +from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials class JakAndDaxterLocation(Location): @@ -8,9 +8,9 @@ class JakAndDaxterLocation(Location): # All Locations -# Because all items in Jak And Daxter are unique and do not regenerate, we can use this same table as our item table. # Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed. location_table = { + **{Cells.to_ap_id(k): Cells.loc7SF_cellTable[k] for k in Cells.loc7SF_cellTable}, **{Cells.to_ap_id(k): Cells.locGR_cellTable[k] for k in Cells.locGR_cellTable}, **{Cells.to_ap_id(k): Cells.locSV_cellTable[k] for k in Cells.locSV_cellTable}, **{Cells.to_ap_id(k): Cells.locFJ_cellTable[k] for k in Cells.locFJ_cellTable}, @@ -42,5 +42,6 @@ class JakAndDaxterLocation(Location): **{Scouts.to_ap_id(k): Scouts.locSC_scoutTable[k] for k in Scouts.locSC_scoutTable}, **{Scouts.to_ap_id(k): Scouts.locSM_scoutTable[k] for k in Scouts.locSM_scoutTable}, **{Scouts.to_ap_id(k): Scouts.locLT_scoutTable[k] for k in Scouts.locLT_scoutTable}, - **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable} + **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable}, + **{Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable}, } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index fa4a6aa88015..16bab791ffb6 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -4,10 +4,27 @@ from .GameID import jak1_name from .JakAndDaxterOptions import JakAndDaxterOptions from .Locations import JakAndDaxterLocation, location_table -from .locs import CellLocations as Cells, ScoutLocations as Scouts +from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials + + +class JakAndDaxterRegion(Region): + game: str = jak1_name + + +# Holds information like the level name, the number of orbs available there, etc. Applies to both Levels and SubLevels. +# We especially need orb_counts to be tracked here because we need to know how many orbs you have access to +# in order to know when you can afford the 90-orb and 120-orb payments for more checks. +class Jak1LevelInfo: + name: str + orb_count: int + + def __init__(self, name: str, orb_count: int): + self.name = name + self.orb_count = orb_count class Jak1Level(int, Enum): + SCOUT_FLY_POWER_CELLS = auto() # This is a virtual location to reward you receiving 7 scout flies. GEYSER_ROCK = auto() SANDOVER_VILLAGE = auto() FORBIDDEN_JUNGLE = auto() @@ -27,7 +44,6 @@ class Jak1Level(int, Enum): class Jak1SubLevel(int, Enum): - MAIN_AREA = auto() FORBIDDEN_JUNGLE_SWITCH_ROOM = auto() FORBIDDEN_JUNGLE_PLANT_ROOM = auto() SENTINEL_BEACH_CANNON_TOWER = auto() @@ -44,172 +60,213 @@ class Jak1SubLevel(int, Enum): GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto() -level_table: typing.Dict[Jak1Level, str] = { - Jak1Level.GEYSER_ROCK: "Geyser Rock", - Jak1Level.SANDOVER_VILLAGE: "Sandover Village", - Jak1Level.FORBIDDEN_JUNGLE: "Forbidden Jungle", - Jak1Level.SENTINEL_BEACH: "Sentinel Beach", - Jak1Level.MISTY_ISLAND: "Misty Island", - Jak1Level.FIRE_CANYON: "Fire Canyon", - Jak1Level.ROCK_VILLAGE: "Rock Village", - Jak1Level.PRECURSOR_BASIN: "Precursor Basin", - Jak1Level.LOST_PRECURSOR_CITY: "Lost Precursor City", - Jak1Level.BOGGY_SWAMP: "Boggy Swamp", - Jak1Level.MOUNTAIN_PASS: "Mountain Pass", - Jak1Level.VOLCANIC_CRATER: "Volcanic Crater", - Jak1Level.SPIDER_CAVE: "Spider Cave", - Jak1Level.SNOWY_MOUNTAIN: "Snowy Mountain", - Jak1Level.LAVA_TUBE: "Lava Tube", - Jak1Level.GOL_AND_MAIAS_CITADEL: "Gol and Maia's Citadel" +level_table: typing.Dict[Jak1Level, Jak1LevelInfo] = { + Jak1Level.SCOUT_FLY_POWER_CELLS: + Jak1LevelInfo("Scout Fly Power Cells", 0), # Virtual location. + Jak1Level.GEYSER_ROCK: + Jak1LevelInfo("Geyser Rock", 50), + Jak1Level.SANDOVER_VILLAGE: + Jak1LevelInfo("Sandover Village", 50), + Jak1Level.FORBIDDEN_JUNGLE: + Jak1LevelInfo("Forbidden Jungle", 99), + Jak1Level.SENTINEL_BEACH: + Jak1LevelInfo("Sentinel Beach", 128), + Jak1Level.MISTY_ISLAND: + Jak1LevelInfo("Misty Island", 150), + Jak1Level.FIRE_CANYON: + Jak1LevelInfo("Fire Canyon", 50), + Jak1Level.ROCK_VILLAGE: + Jak1LevelInfo("Rock Village", 50), + Jak1Level.PRECURSOR_BASIN: + Jak1LevelInfo("Precursor Basin", 200), + Jak1Level.LOST_PRECURSOR_CITY: + Jak1LevelInfo("Lost Precursor City", 133), + Jak1Level.BOGGY_SWAMP: + Jak1LevelInfo("Boggy Swamp", 177), + Jak1Level.MOUNTAIN_PASS: + Jak1LevelInfo("Mountain Pass", 0), + Jak1Level.VOLCANIC_CRATER: + Jak1LevelInfo("Volcanic Crater", 50), + Jak1Level.SPIDER_CAVE: + Jak1LevelInfo("Spider Cave", 200), + Jak1Level.SNOWY_MOUNTAIN: + Jak1LevelInfo("Snowy Mountain", 113), + Jak1Level.LAVA_TUBE: + Jak1LevelInfo("Lava Tube", 50), + Jak1Level.GOL_AND_MAIAS_CITADEL: + Jak1LevelInfo("Gol and Maia's Citadel", 180), } -subLevel_table: typing.Dict[Jak1SubLevel, str] = { - Jak1SubLevel.MAIN_AREA: "Main Area", - Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: "Forbidden Jungle Switch Room", - Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room", - Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower", - Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings", - Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: "Lost Precursor City Sunken Room", - Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: "Lost Precursor City Helix Room", - Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut", - Jak1SubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race", - Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut", - Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut", - Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort", - Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: "Snowy Mountain Frozen Box", - Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower", - Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss" +sub_level_table: typing.Dict[Jak1SubLevel, Jak1LevelInfo] = { + Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: + Jak1LevelInfo("Forbidden Jungle Switch Room", 24), + Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: + Jak1LevelInfo("Forbidden Jungle Plant Room", 27), + Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: + Jak1LevelInfo("Sentinel Beach Cannon Tower", 22), + Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS: + Jak1LevelInfo("Precursor Basin Blue Rings", 0), # Another virtual location, no orbs. + Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: + Jak1LevelInfo("Lost Precursor City Sunken Room", 37), + Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: + Jak1LevelInfo("Lost Precursor City Helix Room", 30), + Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: + Jak1LevelInfo("Boggy Swamp Flut Flut", 23), + Jak1SubLevel.MOUNTAIN_PASS_RACE: + Jak1LevelInfo("Mountain Pass Race", 50), + Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: + Jak1LevelInfo("Mountain Pass Shortcut", 0), + Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: + Jak1LevelInfo("Snowy Mountain Flut Flut", 15), + Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT: + Jak1LevelInfo("Snowy Mountain Lurker Fort", 72), + Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: + Jak1LevelInfo("Snowy Mountain Frozen Box", 0), + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: + Jak1LevelInfo("Gol and Maia's Citadel Rotating Tower", 20), + Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: + Jak1LevelInfo("Gol and Maia's Citadel Final Boss", 0), } -class JakAndDaxterRegion(Region): - game: str = jak1_name - - # Use the original game ID's for each item to tell the Region which Locations are available in it. # You do NOT need to add the item offsets or game ID, that will be handled by create_*_locations. def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - create_region(player, multiworld, "Menu") - region_gr = create_region(player, multiworld, level_table[Jak1Level.GEYSER_ROCK]) + # Always start with Menu. + multiworld.regions.append(JakAndDaxterRegion("Menu", player, multiworld)) + + region_7sf = create_region(player, multiworld, Jak1Level.SCOUT_FLY_POWER_CELLS) + create_cell_locations(region_7sf, Cells.loc7SF_cellTable) + + region_gr = create_region(player, multiworld, Jak1Level.GEYSER_ROCK) create_cell_locations(region_gr, Cells.locGR_cellTable) create_fly_locations(region_gr, Scouts.locGR_scoutTable) - region_sv = create_region(player, multiworld, level_table[Jak1Level.SANDOVER_VILLAGE]) + region_sv = create_region(player, multiworld, Jak1Level.SANDOVER_VILLAGE) create_cell_locations(region_sv, Cells.locSV_cellTable) create_fly_locations(region_sv, Scouts.locSV_scoutTable) - region_fj = create_region(player, multiworld, level_table[Jak1Level.FORBIDDEN_JUNGLE]) - create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9, 7}}) + region_fj = create_region(player, multiworld, Jak1Level.FORBIDDEN_JUNGLE) + create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9}}) create_fly_locations(region_fj, Scouts.locFJ_scoutTable) + create_special_locations(region_fj, {k: Specials.loc_specialTable[k] for k in {4, 5}}) - sub_region_fjsr = create_subregion(region_fj, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM]) + sub_region_fjsr = create_subregion(region_fj, Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM) create_cell_locations(sub_region_fjsr, {k: Cells.locFJ_cellTable[k] for k in {2}}) + create_special_locations(sub_region_fjsr, {k: Specials.loc_specialTable[k] for k in {2}}) - sub_region_fjpr = create_subregion(sub_region_fjsr, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM]) + sub_region_fjpr = create_subregion(sub_region_fjsr, Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM) create_cell_locations(sub_region_fjpr, {k: Cells.locFJ_cellTable[k] for k in {6}}) - region_sb = create_region(player, multiworld, level_table[Jak1Level.SENTINEL_BEACH]) - create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22, 20}}) + region_sb = create_region(player, multiworld, Jak1Level.SENTINEL_BEACH) + create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22}}) create_fly_locations(region_sb, Scouts.locSB_scoutTable) + create_special_locations(region_sb, {k: Specials.loc_specialTable[k] for k in {17}}) - sub_region_sbct = create_subregion(region_sb, subLevel_table[Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER]) + sub_region_sbct = create_subregion(region_sb, Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER) create_cell_locations(sub_region_sbct, {k: Cells.locSB_cellTable[k] for k in {19}}) - region_mi = create_region(player, multiworld, level_table[Jak1Level.MISTY_ISLAND]) + region_mi = create_region(player, multiworld, Jak1Level.MISTY_ISLAND) create_cell_locations(region_mi, Cells.locMI_cellTable) create_fly_locations(region_mi, Scouts.locMI_scoutTable) - region_fc = create_region(player, multiworld, level_table[Jak1Level.FIRE_CANYON]) + region_fc = create_region(player, multiworld, Jak1Level.FIRE_CANYON) create_cell_locations(region_fc, Cells.locFC_cellTable) create_fly_locations(region_fc, Scouts.locFC_scoutTable) - region_rv = create_region(player, multiworld, level_table[Jak1Level.ROCK_VILLAGE]) + region_rv = create_region(player, multiworld, Jak1Level.ROCK_VILLAGE) create_cell_locations(region_rv, Cells.locRV_cellTable) create_fly_locations(region_rv, Scouts.locRV_scoutTable) - region_pb = create_region(player, multiworld, level_table[Jak1Level.PRECURSOR_BASIN]) - create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58, 57}}) + region_pb = create_region(player, multiworld, Jak1Level.PRECURSOR_BASIN) + create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58}}) create_fly_locations(region_pb, Scouts.locPB_scoutTable) - sub_region_pbbr = create_subregion(region_pb, subLevel_table[Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS]) + sub_region_pbbr = create_subregion(region_pb, Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS) create_cell_locations(sub_region_pbbr, {k: Cells.locPB_cellTable[k] for k in {59}}) - region_lpc = create_region(player, multiworld, level_table[Jak1Level.LOST_PRECURSOR_CITY]) + region_lpc = create_region(player, multiworld, Jak1Level.LOST_PRECURSOR_CITY) create_cell_locations(region_lpc, {k: Cells.locLPC_cellTable[k] for k in {45, 48, 44, 51}}) create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {262193, 131121, 393265, 196657, 49, 65585}}) - sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM]) - create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47, 49}}) + sub_region_lpcsr = create_subregion(region_lpc, Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM) + create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47}}) create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {327729}}) - sub_region_lpchr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM]) + sub_region_lpchr = create_subregion(region_lpc, Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) create_cell_locations(sub_region_lpchr, {k: Cells.locLPC_cellTable[k] for k in {46, 50}}) - region_bs = create_region(player, multiworld, level_table[Jak1Level.BOGGY_SWAMP]) + region_bs = create_region(player, multiworld, Jak1Level.BOGGY_SWAMP) create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {43, 393259, 65579, 262187, 196651}}) - sub_region_bsff = create_subregion(region_bs, subLevel_table[Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT]) - create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {43, 37}}) + sub_region_bsff = create_subregion(region_bs, Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT) + create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {37}}) create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {327723, 131115}}) - region_mp = create_region(player, multiworld, level_table[Jak1Level.MOUNTAIN_PASS]) + region_mp = create_region(player, multiworld, Jak1Level.MOUNTAIN_PASS) create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86}}) - sub_region_mpr = create_subregion(region_mp, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_RACE]) - create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87, 88}}) + sub_region_mpr = create_subregion(region_mp, Jak1SubLevel.MOUNTAIN_PASS_RACE) + create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87}}) create_fly_locations(sub_region_mpr, Scouts.locMP_scoutTable) - sub_region_mps = create_subregion(sub_region_mpr, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT]) + sub_region_mps = create_subregion(sub_region_mpr, Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT) create_cell_locations(sub_region_mps, {k: Cells.locMP_cellTable[k] for k in {110}}) - region_vc = create_region(player, multiworld, level_table[Jak1Level.VOLCANIC_CRATER]) + region_vc = create_region(player, multiworld, Jak1Level.VOLCANIC_CRATER) create_cell_locations(region_vc, Cells.locVC_cellTable) create_fly_locations(region_vc, Scouts.locVC_scoutTable) - region_sc = create_region(player, multiworld, level_table[Jak1Level.SPIDER_CAVE]) + region_sc = create_region(player, multiworld, Jak1Level.SPIDER_CAVE) create_cell_locations(region_sc, Cells.locSC_cellTable) create_fly_locations(region_sc, Scouts.locSC_scoutTable) - region_sm = create_region(player, multiworld, level_table[Jak1Level.SNOWY_MOUNTAIN]) + region_sm = create_region(player, multiworld, Jak1Level.SNOWY_MOUNTAIN) create_cell_locations(region_sm, {k: Cells.locSM_cellTable[k] for k in {60, 61, 66, 64}}) create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {65, 327745, 65601, 131137, 393281}}) + create_special_locations(region_sm, {k: Specials.loc_specialTable[k] for k in {60}}) - sub_region_smfb = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX]) + sub_region_smfb = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX) create_cell_locations(sub_region_smfb, {k: Cells.locSM_cellTable[k] for k in {67}}) - sub_region_smff = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT]) + sub_region_smff = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT) create_cell_locations(sub_region_smff, {k: Cells.locSM_cellTable[k] for k in {63}}) + create_special_locations(sub_region_smff, {k: Specials.loc_specialTable[k] for k in {63}}) - sub_region_smlf = create_subregion(region_sm, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT]) - create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62, 65}}) + sub_region_smlf = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT) + create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62}}) create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {196673, 262209}}) - region_lt = create_region(player, multiworld, level_table[Jak1Level.LAVA_TUBE]) + region_lt = create_region(player, multiworld, Jak1Level.LAVA_TUBE) create_cell_locations(region_lt, Cells.locLT_cellTable) create_fly_locations(region_lt, Scouts.locLT_scoutTable) - region_gmc = create_region(player, multiworld, level_table[Jak1Level.GOL_AND_MAIAS_CITADEL]) + region_gmc = create_region(player, multiworld, Jak1Level.GOL_AND_MAIAS_CITADEL) create_cell_locations(region_gmc, {k: Cells.locGMC_cellTable[k] for k in {71, 72, 73}}) create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k] for k in {91, 65627, 196699, 262235, 393307, 131163}}) + create_special_locations(region_gmc, {k: Specials.loc_specialTable[k] for k in {71, 72, 73}}) - sub_region_gmcrt = create_subregion(region_gmc, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER]) - create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70, 91}}) + sub_region_gmcrt = create_subregion(region_gmc, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER) + create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70}}) create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {327771}}) + create_special_locations(sub_region_gmcrt, {k: Specials.loc_specialTable[k] for k in {70}}) - create_subregion(sub_region_gmcrt, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS]) + create_subregion(sub_region_gmcrt, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS) -def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion: +def create_region(player: int, multiworld: MultiWorld, level: Jak1Level) -> JakAndDaxterRegion: + name = level_table[level].name region = JakAndDaxterRegion(name, player, multiworld) multiworld.regions.append(region) return region -def create_subregion(parent: Region, name: str) -> JakAndDaxterRegion: +def create_subregion(parent: Region, sub_level: Jak1SubLevel) -> JakAndDaxterRegion: + name = sub_level_table[sub_level].name region = JakAndDaxterRegion(name, parent.player, parent.multiworld) parent.multiworld.regions.append(region) return region @@ -227,3 +284,12 @@ def create_fly_locations(region: Region, locations: typing.Dict[int, str]): location_table[Scouts.to_ap_id(loc)], Scouts.to_ap_id(loc), region) for loc in locations] + + +# Special Locations should be matched alongside their respective Power Cell Locations, +# so you get 2 unlocks for these rather than 1. +def create_special_locations(region: Region, locations: typing.Dict[int, str]): + region.locations += [JakAndDaxterLocation(region.player, + location_table[Specials.to_ap_id(loc)], + Specials.to_ap_id(loc), + region) for loc in locations] diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index bb355e0e2cec..f0c4a7e694be 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,52 +1,52 @@ +from typing import List + from BaseClasses import MultiWorld, CollectionState from .JakAndDaxterOptions import JakAndDaxterOptions -from .Regions import Jak1Level, Jak1SubLevel, level_table, subLevel_table -from .Locations import location_table as item_table -from .locs import CellLocations as Cells, ScoutLocations as Scouts - - -# Helper function for a handful of special cases -# where we need "at least any N" number of a specific set of cells. -def has_count_of(cell_list: set, required_count: int, player: int, state: CollectionState) -> bool: - c: int = 0 - for k in cell_list: - if state.has(item_table[k], player): - c += 1 - if c >= required_count: - return True - return False +from .Regions import Jak1Level, Jak1SubLevel, level_table, sub_level_table +from .Items import item_table +from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials +from worlds.jakanddaxter.Locations import location_table def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + # Setting up some useful variables here because the offset numbers can get confusing # for access rules. Feel free to add more variables here to keep the code more readable. # You DO need to convert the game ID's to AP ID's here. - gr_cells = {Cells.to_ap_id(k) for k in Cells.locGR_cellTable} - fj_temple_top = Cells.to_ap_id(4) - fj_blue_switch = Cells.to_ap_id(2) - fj_plant_boss = Cells.to_ap_id(6) - fj_fisherman = Cells.to_ap_id(5) - sb_flut_flut = Cells.to_ap_id(17) - fc_end = Cells.to_ap_id(69) - pb_purple_rings = Cells.to_ap_id(58) - lpc_sunken = Cells.to_ap_id(47) - lpc_helix = Cells.to_ap_id(50) - mp_klaww = Cells.to_ap_id(86) - mp_end = Cells.to_ap_id(87) - pre_sm_cells = {Cells.to_ap_id(k) for k in {**Cells.locVC_cellTable, **Cells.locSC_cellTable}} - sm_yellow_switch = Cells.to_ap_id(60) - sm_fort_gate = Cells.to_ap_id(63) - lt_end = Cells.to_ap_id(89) - gmc_rby_sages = {Cells.to_ap_id(k) for k in {71, 72, 73}} - gmc_green_sage = Cells.to_ap_id(70) + power_cell = item_table[Cells.to_ap_id(0)] + + # The int/list structure here is intentional, see `set_trade_requirements` for how we handle these. + sv_traders = [11, 12, [13, 14]] # Mayor, Uncle, Oracle 1 and 2 + rv_traders = [31, 32, 33, [34, 35]] # Geologist, Gambler, Warrior, Oracle 3 and 4 + vc_traders = [[96, 97, 98, 99], [100, 101]] # Miners 1-4, Oracle 5 and 6 + + fj_jungle_elevator = item_table[Specials.to_ap_id(4)] + fj_blue_switch = item_table[Specials.to_ap_id(2)] + fj_fisherman = item_table[Specials.to_ap_id(5)] + + sb_flut_flut = item_table[Specials.to_ap_id(17)] + sm_yellow_switch = item_table[Specials.to_ap_id(60)] + sm_fort_gate = item_table[Specials.to_ap_id(63)] + + gmc_blue_sage = item_table[Specials.to_ap_id(71)] + gmc_red_sage = item_table[Specials.to_ap_id(72)] + gmc_yellow_sage = item_table[Specials.to_ap_id(73)] + gmc_green_sage = item_table[Specials.to_ap_id(70)] # Start connecting regions and set their access rules. - connect_start(multiworld, player, Jak1Level.GEYSER_ROCK) + # Scout Fly Power Cells is a virtual region, not a physical one, so connect it to Menu. + connect_start(multiworld, player, Jak1Level.SCOUT_FLY_POWER_CELLS) + set_fly_requirements(multiworld, player) + + # You start the game in front of Green Sage's Hut, so you don't get stuck on Geyser Rock in the first 5 minutes. + connect_start(multiworld, player, Jak1Level.SANDOVER_VILLAGE) + set_trade_requirements(multiworld, player, Jak1Level.SANDOVER_VILLAGE, sv_traders, 1530) + + # Geyser Rock is accessible at any time, just check the 3 naked cell Locations to return. connect_regions(multiworld, player, - Jak1Level.GEYSER_ROCK, Jak1Level.SANDOVER_VILLAGE, - lambda state: has_count_of(gr_cells, 4, player, state)) + Jak1Level.GEYSER_ROCK) connect_regions(multiworld, player, Jak1Level.SANDOVER_VILLAGE, @@ -55,50 +55,52 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) connect_region_to_sub(multiworld, player, Jak1Level.FORBIDDEN_JUNGLE, Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, - lambda state: state.has(item_table[fj_temple_top], player)) + lambda state: state.has(fj_jungle_elevator, player)) connect_subregions(multiworld, player, Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - lambda state: state.has(item_table[fj_blue_switch], player)) + lambda state: state.has(fj_blue_switch, player)) + # You just need to defeat the plant boss to escape this subregion, no specific Item required. connect_sub_to_region(multiworld, player, Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - Jak1Level.FORBIDDEN_JUNGLE, - lambda state: state.has(item_table[fj_plant_boss], player)) + Jak1Level.FORBIDDEN_JUNGLE) connect_regions(multiworld, player, Jak1Level.SANDOVER_VILLAGE, Jak1Level.SENTINEL_BEACH) + # Just jump off the tower to escape this subregion. connect_region_to_sub(multiworld, player, Jak1Level.SENTINEL_BEACH, Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER, - lambda state: state.has(item_table[fj_blue_switch], player)) + lambda state: state.has(fj_blue_switch, player)) connect_regions(multiworld, player, Jak1Level.SANDOVER_VILLAGE, Jak1Level.MISTY_ISLAND, - lambda state: state.has(item_table[fj_fisherman], player)) + lambda state: state.has(fj_fisherman, player)) connect_regions(multiworld, player, Jak1Level.SANDOVER_VILLAGE, Jak1Level.FIRE_CANYON, - lambda state: state.count_group("Power Cell", player) >= 20) + lambda state: state.has(power_cell, player, 20)) connect_regions(multiworld, player, Jak1Level.FIRE_CANYON, - Jak1Level.ROCK_VILLAGE, - lambda state: state.has(item_table[fc_end], player)) + Jak1Level.ROCK_VILLAGE) + set_trade_requirements(multiworld, player, Jak1Level.ROCK_VILLAGE, rv_traders, 1530) connect_regions(multiworld, player, Jak1Level.ROCK_VILLAGE, Jak1Level.PRECURSOR_BASIN) + # This is another virtual location that shares it's "borders" with its parent location. + # You can do blue rings as soon as you finish purple rings. connect_region_to_sub(multiworld, player, Jak1Level.PRECURSOR_BASIN, - Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS, - lambda state: state.has(item_table[pb_purple_rings], player)) + Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS) connect_regions(multiworld, player, Jak1Level.ROCK_VILLAGE, @@ -112,121 +114,179 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) + # LPC is such a mess logistically... once you complete the climb up the helix room, + # you are back to the room before the first slide, which is still the "main area" of LPC. connect_sub_to_region(multiworld, player, Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM, - Jak1Level.LOST_PRECURSOR_CITY, - lambda state: state.has(item_table[lpc_helix], player)) + Jak1Level.LOST_PRECURSOR_CITY) + # Once you raise the sunken room to the surface, you can access Rock Village directly. + # You just need to complete the Location check to do this, you don't need to receive the power cell Item. connect_sub_to_region(multiworld, player, Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, - Jak1Level.ROCK_VILLAGE, - lambda state: state.has(item_table[lpc_sunken], player)) + Jak1Level.ROCK_VILLAGE) connect_regions(multiworld, player, Jak1Level.ROCK_VILLAGE, Jak1Level.BOGGY_SWAMP) + # Flut Flut only has one landing pad here, so leaving this subregion is as easy + # as dismounting Flut Flut right where you found her. connect_region_to_sub(multiworld, player, Jak1Level.BOGGY_SWAMP, Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT, - lambda state: state.has(item_table[sb_flut_flut], player)) + lambda state: state.has(sb_flut_flut, player)) + # Klaww is considered the "main area" of MP, and the "race" is a subregion. + # It's not really intended to get back up the ledge overlooking Klaww's lava pit. connect_regions(multiworld, player, Jak1Level.ROCK_VILLAGE, Jak1Level.MOUNTAIN_PASS, - lambda state: state.count_group("Power Cell", player) >= 45) + lambda state: state.has(power_cell, player, 45)) connect_region_to_sub(multiworld, player, Jak1Level.MOUNTAIN_PASS, - Jak1SubLevel.MOUNTAIN_PASS_RACE, - lambda state: state.has(item_table[mp_klaww], player)) + Jak1SubLevel.MOUNTAIN_PASS_RACE) connect_subregions(multiworld, player, Jak1SubLevel.MOUNTAIN_PASS_RACE, Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT, - lambda state: state.has(item_table[sm_yellow_switch], player)) + lambda state: state.has(sm_yellow_switch, player)) connect_sub_to_region(multiworld, player, Jak1SubLevel.MOUNTAIN_PASS_RACE, - Jak1Level.VOLCANIC_CRATER, - lambda state: state.has(item_table[mp_end], player)) + Jak1Level.VOLCANIC_CRATER) + set_trade_requirements(multiworld, player, Jak1Level.VOLCANIC_CRATER, vc_traders, 1530) connect_regions(multiworld, player, Jak1Level.VOLCANIC_CRATER, Jak1Level.SPIDER_CAVE) + # TODO - Yeah, this is a weird one. You technically need either 71 power cells OR + # any 2 power cells after arriving at Volcanic Crater. Not sure how to model this... connect_regions(multiworld, player, Jak1Level.VOLCANIC_CRATER, - Jak1Level.SNOWY_MOUNTAIN, - lambda state: has_count_of(pre_sm_cells, 2, player, state) - or state.count_group("Power Cell", player) >= 71) # Yeah, this is a weird one. + Jak1Level.SNOWY_MOUNTAIN) connect_region_to_sub(multiworld, player, Jak1Level.SNOWY_MOUNTAIN, Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX, - lambda state: state.has(item_table[sm_yellow_switch], player)) + lambda state: state.has(sm_yellow_switch, player)) + # Flut Flut has both a start and end landing pad here, but there's an elevator that takes you up + # from the end pad to the entrance of the fort, so you're back to the "main area." connect_region_to_sub(multiworld, player, Jak1Level.SNOWY_MOUNTAIN, Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, - lambda state: state.has(item_table[sb_flut_flut], player)) + lambda state: state.has(sb_flut_flut, player)) connect_region_to_sub(multiworld, player, Jak1Level.SNOWY_MOUNTAIN, Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT, - lambda state: state.has(item_table[sm_fort_gate], player)) + lambda state: state.has(sm_fort_gate, player)) connect_regions(multiworld, player, Jak1Level.VOLCANIC_CRATER, Jak1Level.LAVA_TUBE, - lambda state: state.count_group("Power Cell", player) >= 72) + lambda state: state.has(power_cell, player, 72)) connect_regions(multiworld, player, Jak1Level.LAVA_TUBE, - Jak1Level.GOL_AND_MAIAS_CITADEL, - lambda state: state.has(item_table[lt_end], player)) + Jak1Level.GOL_AND_MAIAS_CITADEL) + # The stairs up to Samos's cage is only activated when you get the Items for freeing the other 3 Sages. + # But you can climb back down that staircase (or fall down from the top) to escape this subregion. connect_region_to_sub(multiworld, player, Jak1Level.GOL_AND_MAIAS_CITADEL, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - lambda state: has_count_of(gmc_rby_sages, 3, player, state)) + lambda state: state.has(gmc_blue_sage, player) and + state.has(gmc_red_sage, player) and + state.has(gmc_yellow_sage, player)) + # This is the final elevator, only active when you get the Item for freeing the Green Sage. connect_subregions(multiworld, player, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS, - lambda state: state.has(item_table[gmc_green_sage], player)) + lambda state: state.has(gmc_green_sage, player)) multiworld.completion_condition[player] = lambda state: state.can_reach( - multiworld.get_region(subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player), + multiworld.get_region(sub_level_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS].name, player), "Region", player) def connect_start(multiworld: MultiWorld, player: int, target: Jak1Level): menu_region = multiworld.get_region("Menu", player) - start_region = multiworld.get_region(level_table[target], player) + start_region = multiworld.get_region(level_table[target].name, player) menu_region.connect(start_region) def connect_regions(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1Level, rule=None): - source_region = multiworld.get_region(level_table[source], player) - target_region = multiworld.get_region(level_table[target], player) + source_region = multiworld.get_region(level_table[source].name, player) + target_region = multiworld.get_region(level_table[target].name, player) source_region.connect(target_region, rule=rule) def connect_region_to_sub(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1SubLevel, rule=None): - source_region = multiworld.get_region(level_table[source], player) - target_region = multiworld.get_region(subLevel_table[target], player) + source_region = multiworld.get_region(level_table[source].name, player) + target_region = multiworld.get_region(sub_level_table[target].name, player) source_region.connect(target_region, rule=rule) def connect_sub_to_region(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1Level, rule=None): - source_region = multiworld.get_region(subLevel_table[source], player) - target_region = multiworld.get_region(level_table[target], player) + source_region = multiworld.get_region(sub_level_table[source].name, player) + target_region = multiworld.get_region(level_table[target].name, player) source_region.connect(target_region, rule=rule) def connect_subregions(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1SubLevel, rule=None): - source_region = multiworld.get_region(subLevel_table[source], player) - target_region = multiworld.get_region(subLevel_table[target], player) + source_region = multiworld.get_region(sub_level_table[source].name, player) + target_region = multiworld.get_region(sub_level_table[target].name, player) source_region.connect(target_region, rule=rule) + + +# The "Free 7 Scout Fly" Locations are automatically checked when you receive the 7th scout fly Item. +def set_fly_requirements(multiworld: MultiWorld, player: int): + region = multiworld.get_region(level_table[Jak1Level.SCOUT_FLY_POWER_CELLS].name, player) + for loc in region.locations: + scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(loc.address)) # Translate using game ID as an intermediary. + loc.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7) + + +# TODO - Until we come up with a better progressive system for the traders (that avoids hard-locking if you pay the +# wrong ones and can't afford the right ones) just make all the traders locked behind the total amount to pay them all. +def set_trade_requirements(multiworld: MultiWorld, player: int, level: Jak1Level, traders: List, orb_count: int): + + def count_accessible_orbs(state) -> int: + accessible_orbs = 0 + for level_info in [*level_table.values(), *sub_level_table.values()]: + reg = multiworld.get_region(level_info.name, player) + if reg.can_reach(state): + accessible_orbs += level_info.orb_count + return accessible_orbs + + region = multiworld.get_region(level_table[level].name, player) + names_to_index = {region.locations[i].name: i for i in range(0, len(region.locations))} + for trader in traders: + + # Singleton integers indicate a trader who has only one Location to check. + # (Mayor, Uncle, etc) + if type(trader) is int: + loc = region.locations[names_to_index[location_table[Cells.to_ap_id(trader)]]] + loc.access_rule = lambda state, orbs=orb_count: ( + count_accessible_orbs(state) >= orbs) + + # Lists of integers indicate a trader who has sequential Locations to check, each dependent on the last. + # (Oracles and Miners) + elif type(trader) is list: + previous_loc = None + for trade in trader: + loc = region.locations[names_to_index[location_table[Cells.to_ap_id(trade)]]] + loc.access_rule = lambda state, orbs=orb_count, prev=previous_loc: ( + count_accessible_orbs(state) >= orbs and + (state.can_reach(prev, player) if prev else True)) # TODO - Can Reach or Has Reached? + previous_loc = loc + + # Any other type of element in the traders list is wrong. + else: + raise TypeError(f"Tried to set trade requirements on an unknown type {trader}.") diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 75a87c9dbf7b..8bdfffc5ba5a 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -5,12 +5,13 @@ from .GameID import jak1_id, jak1_name from .JakAndDaxterOptions import JakAndDaxterOptions from .Items import JakAndDaxterItem -from .Locations import JakAndDaxterLocation, location_table as item_table -from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs +from .Locations import JakAndDaxterLocation, location_table +from .Items import JakAndDaxterItem, item_table +from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs, SpecialLocations as Specials from .Regions import create_regions from .Rules import set_rules -from ..AutoWorld import World, WebWorld -from ..LauncherComponents import components, Component, launch_subprocess, Type +from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import components, Component, launch_subprocess, Type class JakAndDaxterSettings(settings.Group): @@ -46,8 +47,7 @@ class JakAndDaxterWorld(World): """ # ID, name, version game = jak1_name - data_version = 1 - required_client_version = (0, 4, 5) + required_client_version = (0, 4, 6) # Options settings: typing.ClassVar[JakAndDaxterSettings] @@ -61,13 +61,17 @@ class JakAndDaxterWorld(World): # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. # Remember, the game ID and various offsets for each item type have already been calculated. item_name_to_id = {item_table[k]: k for k in item_table} - location_name_to_id = {item_table[k]: k for k in item_table} + location_name_to_id = {location_table[k]: k for k in location_table} item_name_groups = { - "Power Cell": {item_table[k]: k for k in item_table - if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, - "Scout Fly": {item_table[k]: k for k in item_table - if k in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset)} - # "Precursor Orb": {} # TODO + "Power Cells": {item_table[k]: k for k in item_table + if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, + "Scout Flies": {item_table[k]: k for k in item_table + if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)}, + "Specials": {item_table[k]: k for k in item_table + if k in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset)}, + # TODO - Make group for Precursor Orbs. + # "Precursor Orbs": {item_table[k]: k for k in item_table + # if k in range(jak1_id + Orbs.orb_offset, ???)}, } def create_regions(self): @@ -76,25 +80,47 @@ def create_regions(self): def set_rules(self): set_rules(self.multiworld, self.options, self.player) + # Helper function to reuse some nasty if/else trees. + @staticmethod + def item_type_helper(item) -> (int, ItemClassification): + # Make 101 Power Cells. + if item in range(jak1_id, jak1_id + Scouts.fly_offset): + classification = ItemClassification.progression_skip_balancing + count = 101 + + # Make 7 Scout Flies per level. + elif item in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset): + classification = ItemClassification.progression_skip_balancing + count = 7 + + # Make only 1 of each Special Item. + elif item in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset): + classification = ItemClassification.progression + count = 1 + + # TODO - Make ??? Precursor Orbs. + # elif item in range(jak1_id + Orbs.orb_offset, ???): + # classification = ItemClassification.filler + # count = ??? + + # If we try to make items with ID's higher than we've defined, something has gone wrong. + else: + raise KeyError(f"Tried to fill item pool with unknown ID {item}.") + + return count, classification + def create_items(self): - self.multiworld.itempool += [self.create_item(item_table[k]) for k in item_table] + for item_id in item_table: + count, _ = self.item_type_helper(item_id) + self.multiworld.itempool += [self.create_item(item_table[item_id]) for k in range(0, count)] def create_item(self, name: str) -> Item: item_id = self.item_name_to_id[name] - if item_id in range(jak1_id, jak1_id + Scouts.fly_offset): - # Power Cell - classification = ItemClassification.progression_skip_balancing - elif item_id in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset): - # Scout Fly - classification = ItemClassification.progression_skip_balancing - elif item_id > jak1_id + Orbs.orb_offset: - # Precursor Orb - classification = ItemClassification.filler # TODO - else: - classification = ItemClassification.filler + _, classification = self.item_type_helper(item_id) + return JakAndDaxterItem(name, classification, item_id, self.player) - item = JakAndDaxterItem(name, classification, item_id, self.player) - return item + def get_filler_item_name(self) -> str: + return "Power Cell" # TODO - Make Precursor Orb the filler item. Until then, enjoy the free progression. def launch_client(): diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 4373e9676e02..64a37352bcbf 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -4,20 +4,25 @@ from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError from CommonClient import logger -from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies +from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials # Some helpful constants. -next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. -next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. -cells_offset = 16 -buzzers_offset = 420 # cells_offset + (sizeof uint32 * 101 cells) = 16 + (4 * 101) +sizeof_uint64 = 8 +sizeof_uint32 = 4 +sizeof_uint8 = 1 +next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. +next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. +next_special_index_offset = 16 # Each of these is an uint64, so 8 bytes. -# buzzers_offset -# + (sizeof uint32 * 112 flies) <-- The buzzers themselves. -# + (sizeof uint8 * 116 tasks) <-- A "cells-received" array for the game to handle new ownership logic. -# = 420 + (4 * 112) + (1 * 116) -end_marker_offset = 984 +cells_checked_offset = 24 +buzzers_checked_offset = 428 # cells_checked_offset + (sizeof uint32 * 101 cells) +specials_checked_offset = 876 # buzzers_checked_offset + (sizeof uint32 * 112 buzzers) + +buzzers_received_offset = 1004 # specials_checked_offset + (sizeof uint32 * 32 specials) +specials_received_offset = 1020 # buzzers_received_offset + (sizeof uint8 * 16 levels (for scout fly groups)) + +end_marker_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials) class JakAndDaxterMemoryReader: @@ -31,12 +36,13 @@ class JakAndDaxterMemoryReader: location_outbox = [] outbox_index = 0 + finished_game = False def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker self.connect() - async def main_tick(self, location_callback: typing.Callable): + async def main_tick(self, location_callback: typing.Callable, finish_callback: typing.Callable): if self.initiated_connect: await self.connect() self.initiated_connect = False @@ -59,6 +65,9 @@ async def main_tick(self, location_callback: typing.Callable): location_callback(self.location_outbox) self.outbox_index += 1 + if self.finished_game: + finish_callback() + async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel @@ -74,9 +83,9 @@ async def connect(self): if marker_address: # At this address is another address that contains the struct we're looking for: the game's state. # From here we need to add the length in bytes for the marker and 4 bytes of padding, - # and the struct address is 8 bytes long (it's u64). + # and the struct address is 8 bytes long (it's a uint64). goal_pointer = marker_address + len(self.marker) + 4 - self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, 8), + self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, sizeof_uint64), byteorder="little", signed=False) logger.info("Found the archipelago memory address: " + str(self.goal_address)) @@ -98,17 +107,23 @@ def print_status(self): def read_memory(self) -> typing.List[int]: try: next_cell_index = int.from_bytes( - self.gk_process.read_bytes(self.goal_address, 8), + self.gk_process.read_bytes(self.goal_address, sizeof_uint64), byteorder="little", signed=False) next_buzzer_index = int.from_bytes( - self.gk_process.read_bytes(self.goal_address + next_buzzer_index_offset, 8), + self.gk_process.read_bytes(self.goal_address + next_buzzer_index_offset, sizeof_uint64), + byteorder="little", + signed=False) + next_special_index = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + next_special_index_offset, sizeof_uint64), byteorder="little", signed=False) for k in range(0, next_cell_index): next_cell = int.from_bytes( - self.gk_process.read_bytes(self.goal_address + cells_offset + (k * 4), 4), + self.gk_process.read_bytes( + self.goal_address + cells_checked_offset + (k * sizeof_uint32), + sizeof_uint32), byteorder="little", signed=False) cell_ap_id = Cells.to_ap_id(next_cell) @@ -118,7 +133,9 @@ def read_memory(self) -> typing.List[int]: for k in range(0, next_buzzer_index): next_buzzer = int.from_bytes( - self.gk_process.read_bytes(self.goal_address + buzzers_offset + (k * 4), 4), + self.gk_process.read_bytes( + self.goal_address + buzzers_checked_offset + (k * sizeof_uint32), + sizeof_uint32), byteorder="little", signed=False) buzzer_ap_id = Flies.to_ap_id(next_buzzer) @@ -126,6 +143,27 @@ def read_memory(self) -> typing.List[int]: self.location_outbox.append(buzzer_ap_id) logger.info("Checked scout fly: " + str(next_buzzer)) + for k in range(0, next_special_index): + next_special = int.from_bytes( + self.gk_process.read_bytes( + self.goal_address + specials_checked_offset + (k * sizeof_uint32), + sizeof_uint32), + byteorder="little", + signed=False) + + # 112 is the game-task ID of `finalboss-movies`, which is written to this array when you grab + # the white eco. This is our victory condition, so we need to catch it and act on it. + if next_special == 112 and not self.finished_game: + self.finished_game = True + logger.info("Congratulations! You finished the game!") + else: + + # All other special checks handled as normal. + special_ap_id = Specials.to_ap_id(next_special) + if special_ap_id not in self.location_outbox: + self.location_outbox.append(special_ap_id) + logger.info("Checked special: " + str(next_special)) + except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") self.connected = False diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index c70d633b00b8..e5c8030c3ed9 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -6,8 +6,13 @@ from pymem.exception import ProcessNotFound, ProcessError from CommonClient import logger -from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, OrbLocations as Orbs from worlds.jakanddaxter.GameID import jak1_id +from worlds.jakanddaxter.Items import item_table +from worlds.jakanddaxter.locs import ( + CellLocations as Cells, + ScoutLocations as Flies, + OrbLocations as Orbs, + SpecialLocations as Specials) class JakAndDaxterReplClient: @@ -170,12 +175,14 @@ def receive_item(self): # Determine the type of item to receive. if ap_id in range(jak1_id, jak1_id + Flies.fly_offset): self.receive_power_cell(ap_id) - - elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Orbs.orb_offset): + elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Specials.special_offset): self.receive_scout_fly(ap_id) - - elif ap_id > jak1_id + Orbs.orb_offset: - pass # TODO + elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset): + self.receive_special(ap_id) + # elif ap_id in range(jak1_id + Orbs.orb_offset, ???): + # self.receive_precursor_orb(ap_id) # TODO -- Ponder the Orbs. + else: + raise KeyError(f"Tried to receive item with unknown AP ID {ap_id}.") def receive_power_cell(self, ap_id: int) -> bool: cell_id = Cells.to_game_id(ap_id) @@ -184,9 +191,9 @@ def receive_power_cell(self, ap_id: int) -> bool: "(pickup-type fuel-cell) " "(the float " + str(cell_id) + "))") if ok: - logger.info(f"Received power cell {cell_id}!") + logger.info(f"Received a Power Cell!") else: - logger.error(f"Unable to receive power cell {cell_id}!") + logger.error(f"Unable to receive a Power Cell!") return ok def receive_scout_fly(self, ap_id: int) -> bool: @@ -196,7 +203,19 @@ def receive_scout_fly(self, ap_id: int) -> bool: "(pickup-type buzzer) " "(the float " + str(fly_id) + "))") if ok: - logger.info(f"Received scout fly {fly_id}!") + logger.info(f"Received a {item_table[ap_id]}!") + else: + logger.error(f"Unable to receive a {item_table[ap_id]}!") + return ok + + def receive_special(self, ap_id: int) -> bool: + special_id = Specials.to_game_id(ap_id) + ok = self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type ap-special) " + "(the float " + str(special_id) + "))") + if ok: + logger.info(f"Received special unlock {item_table[ap_id]}!") else: - logger.error(f"Unable to receive scout fly {fly_id}!") + logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py index 304810af800c..109e18de2208 100644 --- a/worlds/jakanddaxter/locs/CellLocations.py +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -22,12 +22,31 @@ def to_game_id(ap_id: int) -> int: # The ID's you see below correspond directly to that cell's game-task ID. +# The "Free 7 Scout Flies" Power Cells will be unlocked separately from their respective levels. +loc7SF_cellTable = { + 95: "GR: Free 7 Scout Flies", + 75: "SV: Free 7 Scout Flies", + 7: "FJ: Free 7 Scout Flies", + 20: "SB: Free 7 Scout Flies", + 28: "MI: Free 7 Scout Flies", + 68: "FC: Free 7 Scout Flies", + 76: "RV: Free 7 Scout Flies", + 57: "PB: Free 7 Scout Flies", + 49: "LPC: Free 7 Scout Flies", + 43: "BS: Free 7 Scout Flies", + 88: "MP: Free 7 Scout Flies", + 77: "VC: Free 7 Scout Flies", + 85: "SC: Free 7 Scout Flies", + 65: "SM: Free 7 Scout Flies", + 90: "LT: Free 7 Scout Flies", + 91: "GMC: Free 7 Scout Flies", +} + # Geyser Rock locGR_cellTable = { 92: "GR: Find The Cell On The Path", 93: "GR: Open The Precursor Door", 94: "GR: Climb Up The Cliff", - 95: "GR: Free 7 Scout Flies" } # Sandover Village @@ -37,7 +56,6 @@ def to_game_id(ap_id: int) -> int: 10: "SV: Herd The Yakows Into The Pen", 13: "SV: Bring 120 Orbs To The Oracle (1)", 14: "SV: Bring 120 Orbs To The Oracle (2)", - 75: "SV: Free 7 Scout Flies" } # Forbidden Jungle @@ -49,7 +67,6 @@ def to_game_id(ap_id: int) -> int: 5: "FJ: Catch 200 Pounds Of Fish", 8: "FJ: Follow The Canyon To The Sea", 9: "FJ: Open The Locked Temple Door", - 7: "FJ: Free 7 Scout Flies" } # Sentinel Beach @@ -61,7 +78,6 @@ def to_game_id(ap_id: int) -> int: 19: "SB: Launch Up To The Cannon Tower", 21: "SB: Explore The Beach", 22: "SB: Climb The Sentinel", - 20: "SB: Free 7 Scout Flies" } # Misty Island @@ -73,13 +89,11 @@ def to_game_id(ap_id: int) -> int: 27: "MI: Destroy the Balloon Lurkers", 29: "MI: Use Zoomer To Reach Power Cell", 30: "MI: Use Blue Eco To Reach Power Cell", - 28: "MI: Free 7 Scout Flies" } # Fire Canyon locFC_cellTable = { 69: "FC: Reach The End Of Fire Canyon", - 68: "FC: Free 7 Scout Flies" } # Rock Village @@ -89,7 +103,6 @@ def to_game_id(ap_id: int) -> int: 33: "RV: Bring 90 Orbs To The Warrior", 34: "RV: Bring 120 Orbs To The Oracle (1)", 35: "RV: Bring 120 Orbs To The Oracle (2)", - 76: "RV: Free 7 Scout Flies" } # Precursor Basin @@ -101,7 +114,6 @@ def to_game_id(ap_id: int) -> int: 55: "PB: Cure Dark Eco Infected Plants", 58: "PB: Navigate The Purple Precursor Rings", 59: "PB: Navigate The Blue Precursor Rings", - 57: "PB: Free 7 Scout Flies" } # Lost Precursor City @@ -113,7 +125,6 @@ def to_game_id(ap_id: int) -> int: 44: "LPC: Match The Platform Colors", 50: "LPC: Climb The Slide Tube", 51: "LPC: Reach The Center Of The Complex", - 49: "LPC: Free 7 Scout Flies" } # Boggy Swamp @@ -125,7 +136,6 @@ def to_game_id(ap_id: int) -> int: 40: "BS: Break The Tethers To The Zeppelin (2)", 41: "BS: Break The Tethers To The Zeppelin (3)", 42: "BS: Break The Tethers To The Zeppelin (4)", - 43: "BS: Free 7 Scout Flies" } # Mountain Pass @@ -133,7 +143,6 @@ def to_game_id(ap_id: int) -> int: 86: "MP: Defeat Klaww", 87: "MP: Reach The End Of The Mountain Pass", 110: "MP: Find The Hidden Power Cell", - 88: "MP: Free 7 Scout Flies" } # Volcanic Crater @@ -145,7 +154,6 @@ def to_game_id(ap_id: int) -> int: 100: "VC: Bring 120 Orbs To The Oracle (1)", 101: "VC: Bring 120 Orbs To The Oracle (2)", 74: "VC: Find The Hidden Power Cell", - 77: "VC: Free 7 Scout Flies" } # Spider Cave @@ -157,7 +165,6 @@ def to_game_id(ap_id: int) -> int: 82: "SC: Launch To The Poles", 83: "SC: Navigate The Spider Tunnel", 84: "SC: Climb the Precursor Platforms", - 85: "SC: Free 7 Scout Flies" } # Snowy Mountain @@ -169,13 +176,11 @@ def to_game_id(ap_id: int) -> int: 63: "SM: Open The Lurker Fort Gate", 62: "SM: Get Through The Lurker Fort", 64: "SM: Survive The Lurker Infested Cave", - 65: "SM: Free 7 Scout Flies" } # Lava Tube locLT_cellTable = { 89: "LT: Cross The Lava Tube", - 90: "LT: Free 7 Scout Flies" } # Gol and Maias Citadel @@ -184,5 +189,4 @@ def to_game_id(ap_id: int) -> int: 72: "GMC: Free The Red Sage", 73: "GMC: Free The Yellow Sage", 70: "GMC: Free The Green Sage", - 91: "GMC: Free 7 Scout Flies" } diff --git a/worlds/jakanddaxter/locs/SpecialLocations.py b/worlds/jakanddaxter/locs/SpecialLocations.py new file mode 100644 index 000000000000..4ce5b63812a6 --- /dev/null +++ b/worlds/jakanddaxter/locs/SpecialLocations.py @@ -0,0 +1,47 @@ +from ..GameID import jak1_id + +# These are special checks that the game normally does not track. They are not game entities and thus +# don't have game ID's. + +# Normally, for example, completing the fishing minigame is what gives you access to the +# fisherman's boat to get to Misty Island. The game treats completion of the fishing minigame as well as the +# power cell you receive as one and the same. The fisherman only gives you one item, a power cell. + +# We're significantly altering the game logic here to decouple these concepts. First, completing the fishing minigame +# now counts as 2 Location checks. Second, the fisherman should give you a power cell (a generic item) as well as +# the "keys" to his boat (a special item). It is the "keys" that we are defining in this file, and the respective +# Item representing those keys will be defined in Items.py. These aren't real in the sense that +# they have a model and texture, they are just the logical representation of the boat unlock. + +# We can use 2^11 to offset these from scout flies, just like we offset scout flies from power cells +# by 2^10. Even with the high-16 reminder bits, scout flies don't exceed an ID of (jak1_id + 1887). +special_offset = 2048 + + +# These helper functions do all the math required to get information about each +# special check and translate its ID between AP and OpenGOAL. +def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + return jak1_id + special_offset + game_id # Add the offsets and the orb Actor ID. + + +def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + return ap_id - jak1_id - special_offset # Reverse process, subtract the offsets. + + +# The ID's you see below correlate to each of their respective game-tasks, even though they are separate. +# This makes it easier for the new game logic to know what relates to what. I hope. God I hope. + +loc_specialTable = { + 5: "Fisherman's Boat", + 4: "Jungle Elevator", + 2: "Blue Eco Switch", + 17: "Flut Flut", + 60: "Yellow Eco Switch", + 63: "Snowy Fort Gate", + 71: "Freed The Blue Sage", + 72: "Freed The Red Sage", + 73: "Freed The Yellow Sage", + 70: "Freed The Green Sage", +} From e76df684b58501597439bd6d73c4710da4fae2c5 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 21 May 2024 22:12:01 -0400 Subject: [PATCH 31/70] Jak and Daxter - Gondola, Pontoons, Rules, Regions, and Client Update * Jak 1: Overhaul of regions, rules, and special locations. Updated game info page. * Jak 1: Preparations for Alpha. Reintroducing automatic startup in client. Updating docs, readme, codeowners. --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/jakanddaxter/Client.py | 84 ++++++++------ worlds/jakanddaxter/Items.py | 22 ++-- worlds/jakanddaxter/Regions.py | 55 +++------ worlds/jakanddaxter/Rules.py | 66 ++++------- worlds/jakanddaxter/__init__.py | 4 +- .../en_Jak and Daxter The Precursor Legacy.md | 109 +++++++++++------- worlds/jakanddaxter/locs/SpecialLocations.py | 2 + 9 files changed, 176 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 4633c99c664d..f5965bd9bef8 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Currently, the following games are supported: * Super Mario World * Pokémon Red and Blue * Hylics 2 +* Jak and Daxter: The Precursor Legacy * Overcooked! 2 * Zillion * Lufia II Ancient Cave diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index c34046d5dc30..9e26ce0487c0 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -82,6 +82,9 @@ # Hylics 2 /worlds/hylics2/ @TRPG0 +# Jak and Daxter: The Precursor Legacy +/worlds/jakanddaxter/ @massimilianodelliubaldini + # Kirby's Dream Land 3 /worlds/kdl3/ @Silvris diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 51a38bb3f5e9..c5c36b610e9e 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -4,6 +4,8 @@ import typing import asyncio import colorama +import pymem +from pymem.exception import ProcessNotFound, ProcessError import Utils from NetUtils import ClientStatus @@ -137,45 +139,52 @@ async def run_memr_loop(self): async def run_game(ctx: JakAndDaxterContext): - exec_directory = "" + + # If you're running the game through the mod launcher, these may already be running. + # If they are not running, try to start them. + gk_running = False + try: + pymem.Pymem("gk.exe") # The GOAL Kernel + gk_running = True + except ProcessNotFound: + logger.info("Game not running, attempting to start.") + + goalc_running = False try: - exec_directory = Utils.get_settings()["jakanddaxter_options"]["root_directory"] - files_in_path = os.listdir(exec_directory) - if ".git" in files_in_path: - # Indicates the user is running from source, append expected subdirectory appropriately. - exec_directory = os.path.join(exec_directory, "out", "build", "Release", "bin") - else: - # Indicates the user is running from the official launcher, a mod launcher, or otherwise. - # We'll need to handle version numbers in the path somehow... - exec_directory = os.path.join(exec_directory, "versions", "official") - latest_version = list(reversed(os.listdir(exec_directory)))[0] - exec_directory = os.path.join(exec_directory, str(latest_version)) - except FileNotFoundError: - logger.error(f"Unable to locate directory {exec_directory}, " - f"unable to locate game executable.") - return - except KeyError as e: - logger.error(f"Hosts.yaml does not contain {e.args[0]}, " - f"unable to locate game executable.") - return - - gk = os.path.join(exec_directory, "gk.exe") - goalc = os.path.join(exec_directory, "goalc.exe") + pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL + goalc_running = True + except ProcessNotFound: + logger.info("Compiler not running, attempting to start.") # Don't mind all the arguments, they are exactly what you get when you run "task boot-game" or "task repl". - await asyncio.create_subprocess_exec( - gk, - "-v", "--game jak1", "--", "-boot", "-fakeiso", "-debug", - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - stdin=subprocess.DEVNULL) - - # You MUST launch goalc as a console application, so powershell/cmd/bash/etc is the program - # and goalc is just an argument. It HAS to be this way. - # TODO - Support other OS's. - await asyncio.create_subprocess_exec( - "powershell.exe", - goalc, "--user-auto", "--game jak1") + # TODO - Support other OS's. cmd for some reason does not work with goalc. + if not gk_running: + try: + gk_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] + gk_path = os.path.normpath(gk_path) + gk_path = os.path.join(gk_path, "gk.exe") + except AttributeError as e: + logger.error(f"Hosts.yaml does not contain {e.args[0]}, unable to locate game executables.") + return + + if gk_path: + gk_process = subprocess.Popen( + ["powershell.exe", gk_path, "--game jak1", "--", "-v", "-boot", "-fakeiso", "-debug"], + creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. + + if not goalc_running: + try: + goalc_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] + goalc_path = os.path.normpath(goalc_path) + goalc_path = os.path.join(goalc_path, "goalc.exe") + except AttributeError as e: + logger.error(f"Hosts.yaml does not contain {e.args[0]}, unable to locate game executables.") + return + + if goalc_path: + goalc_process = subprocess.Popen( + ["powershell.exe", goalc_path, "--game jak1"], + creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. # Auto connect the repl and memr agents. Sleep 5 because goalc takes just a little bit of time to load, # and it's not something we can await. @@ -189,7 +198,6 @@ async def main(): Utils.init_logging("JakAndDaxterClient", exception_logger="Client") ctx = JakAndDaxterContext(None, None) - ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") ctx.repl_task = create_task_log_exception(ctx.run_repl_loop()) ctx.memr_task = create_task_log_exception(ctx.run_memr_loop()) @@ -199,7 +207,7 @@ async def main(): ctx.run_cli() # Find and run the game (gk) and compiler/repl (goalc). - # await run_game(ctx) + await run_game(ctx) await ctx.exit_event.wait() await ctx.shutdown() diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 2fa065472657..56743b7cef0b 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -42,16 +42,18 @@ class JakAndDaxterItem(Item): # These are special items representing unique unlocks in the world. Notice that their Item ID equals their # respective Location ID. Like scout flies, this is necessary for game<->archipelago communication. special_item_table = { - 5: "Fisherman's Boat", - 4: "Jungle Elevator", - 2: "Blue Eco Switch", - 17: "Flut Flut", - 60: "Yellow Eco Switch", - 63: "Snowy Fort Gate", - 71: "Freed The Blue Sage", - 72: "Freed The Red Sage", - 73: "Freed The Yellow Sage", - 70: "Freed The Green Sage", + 5: "Fisherman's Boat", # Unlocks 14 checks in Misty Island + 4: "Jungle Elevator", # Unlocks 2 checks in Forbidden Jungle + 2: "Blue Eco Switch", # Unlocks 1 check in Jungle and 1 in Beach + 17: "Flut Flut", # Unlocks 2 checks in Swamp and 2 in Snowy + 33: "Warrior's Pontoons", # Unlocks 14 checks in Swamp and everything post-Rock Village + 105: "Snowy Mountain Gondola", # Unlocks 15 checks in Snowy Mountain + 60: "Yellow Eco Switch", # Unlocks 1 check in Pass and 1 in Snowy + 63: "Snowy Fort Gate", # Unlocks 3 checks in Snowy Mountain + 71: "Freed The Blue Sage", # 1 of 3 unlocks for the final staircase and 2 checks in Citadel + 72: "Freed The Red Sage", # 1 of 3 unlocks for the final staircase and 2 checks in Citadel + 73: "Freed The Yellow Sage", # 1 of 3 unlocks for the final staircase and 2 checks in Citadel + 70: "Freed The Green Sage", # Unlocks the final elevator } # All Items diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 16bab791ffb6..44490c9bc4ed 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -47,11 +47,8 @@ class Jak1SubLevel(int, Enum): FORBIDDEN_JUNGLE_SWITCH_ROOM = auto() FORBIDDEN_JUNGLE_PLANT_ROOM = auto() SENTINEL_BEACH_CANNON_TOWER = auto() - PRECURSOR_BASIN_BLUE_RINGS = auto() - LOST_PRECURSOR_CITY_SUNKEN_ROOM = auto() - LOST_PRECURSOR_CITY_HELIX_ROOM = auto() + ROCK_VILLAGE_PONTOON_BRIDGE = auto() BOGGY_SWAMP_FLUT_FLUT = auto() - MOUNTAIN_PASS_RACE = auto() MOUNTAIN_PASS_SHORTCUT = auto() SNOWY_MOUNTAIN_FLUT_FLUT = auto() SNOWY_MOUNTAIN_LURKER_FORT = auto() @@ -76,15 +73,15 @@ class Jak1SubLevel(int, Enum): Jak1Level.FIRE_CANYON: Jak1LevelInfo("Fire Canyon", 50), Jak1Level.ROCK_VILLAGE: - Jak1LevelInfo("Rock Village", 50), + Jak1LevelInfo("Rock Village", 43), Jak1Level.PRECURSOR_BASIN: Jak1LevelInfo("Precursor Basin", 200), Jak1Level.LOST_PRECURSOR_CITY: - Jak1LevelInfo("Lost Precursor City", 133), + Jak1LevelInfo("Lost Precursor City", 200), Jak1Level.BOGGY_SWAMP: Jak1LevelInfo("Boggy Swamp", 177), Jak1Level.MOUNTAIN_PASS: - Jak1LevelInfo("Mountain Pass", 0), + Jak1LevelInfo("Mountain Pass", 50), Jak1Level.VOLCANIC_CRATER: Jak1LevelInfo("Volcanic Crater", 50), Jak1Level.SPIDER_CAVE: @@ -104,16 +101,10 @@ class Jak1SubLevel(int, Enum): Jak1LevelInfo("Forbidden Jungle Plant Room", 27), Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: Jak1LevelInfo("Sentinel Beach Cannon Tower", 22), - Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS: - Jak1LevelInfo("Precursor Basin Blue Rings", 0), # Another virtual location, no orbs. - Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: - Jak1LevelInfo("Lost Precursor City Sunken Room", 37), - Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: - Jak1LevelInfo("Lost Precursor City Helix Room", 30), + Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE: + Jak1LevelInfo("Rock Village Pontoon Bridge", 7), Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: Jak1LevelInfo("Boggy Swamp Flut Flut", 23), - Jak1SubLevel.MOUNTAIN_PASS_RACE: - Jak1LevelInfo("Mountain Pass Race", 50), Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: Jak1LevelInfo("Mountain Pass Shortcut", 0), Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: @@ -177,26 +168,20 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: region_rv = create_region(player, multiworld, Jak1Level.ROCK_VILLAGE) create_cell_locations(region_rv, Cells.locRV_cellTable) - create_fly_locations(region_rv, Scouts.locRV_scoutTable) + create_fly_locations(region_rv, {k: Scouts.locRV_scoutTable[k] + for k in {76, 131148, 196684, 262220, 65612, 327756}}) + create_special_locations(region_rv, {k: Specials.loc_specialTable[k] for k in {33}}) + + sub_region_rvpb = create_subregion(region_rv, Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE) + create_fly_locations(sub_region_rvpb, {k: Scouts.locRV_scoutTable[k] for k in {393292}}) region_pb = create_region(player, multiworld, Jak1Level.PRECURSOR_BASIN) - create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58}}) + create_cell_locations(region_pb, Cells.locPB_cellTable) create_fly_locations(region_pb, Scouts.locPB_scoutTable) - sub_region_pbbr = create_subregion(region_pb, Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS) - create_cell_locations(sub_region_pbbr, {k: Cells.locPB_cellTable[k] for k in {59}}) - region_lpc = create_region(player, multiworld, Jak1Level.LOST_PRECURSOR_CITY) - create_cell_locations(region_lpc, {k: Cells.locLPC_cellTable[k] for k in {45, 48, 44, 51}}) - create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] - for k in {262193, 131121, 393265, 196657, 49, 65585}}) - - sub_region_lpcsr = create_subregion(region_lpc, Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM) - create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47}}) - create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {327729}}) - - sub_region_lpchr = create_subregion(region_lpc, Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) - create_cell_locations(sub_region_lpchr, {k: Cells.locLPC_cellTable[k] for k in {46, 50}}) + create_cell_locations(region_lpc, Cells.locLPC_cellTable) + create_fly_locations(region_lpc, Scouts.locLPC_scoutTable) region_bs = create_region(player, multiworld, Jak1Level.BOGGY_SWAMP) create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) @@ -207,18 +192,16 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {327723, 131115}}) region_mp = create_region(player, multiworld, Jak1Level.MOUNTAIN_PASS) - create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86}}) - - sub_region_mpr = create_subregion(region_mp, Jak1SubLevel.MOUNTAIN_PASS_RACE) - create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87}}) - create_fly_locations(sub_region_mpr, Scouts.locMP_scoutTable) + create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86, 87}}) + create_fly_locations(region_mp, Scouts.locMP_scoutTable) - sub_region_mps = create_subregion(sub_region_mpr, Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT) + sub_region_mps = create_subregion(region_mp, Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT) create_cell_locations(sub_region_mps, {k: Cells.locMP_cellTable[k] for k in {110}}) region_vc = create_region(player, multiworld, Jak1Level.VOLCANIC_CRATER) create_cell_locations(region_vc, Cells.locVC_cellTable) create_fly_locations(region_vc, Scouts.locVC_scoutTable) + create_special_locations(region_vc, {k: Specials.loc_specialTable[k] for k in {105}}) region_sc = create_region(player, multiworld, Jak1Level.SPIDER_CAVE) create_cell_locations(region_sc, Cells.locSC_cellTable) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index f0c4a7e694be..8ce4d5c25526 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -25,8 +25,11 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) fj_fisherman = item_table[Specials.to_ap_id(5)] sb_flut_flut = item_table[Specials.to_ap_id(17)] + rv_pontoon_bridge = item_table[Specials.to_ap_id(33)] + sm_yellow_switch = item_table[Specials.to_ap_id(60)] sm_fort_gate = item_table[Specials.to_ap_id(63)] + sm_gondola = item_table[Specials.to_ap_id(105)] gmc_blue_sage = item_table[Specials.to_ap_id(71)] gmc_red_sage = item_table[Specials.to_ap_id(72)] @@ -96,39 +99,20 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) Jak1Level.ROCK_VILLAGE, Jak1Level.PRECURSOR_BASIN) - # This is another virtual location that shares it's "borders" with its parent location. - # You can do blue rings as soon as you finish purple rings. - connect_region_to_sub(multiworld, player, - Jak1Level.PRECURSOR_BASIN, - Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS) - connect_regions(multiworld, player, Jak1Level.ROCK_VILLAGE, Jak1Level.LOST_PRECURSOR_CITY) + # This pontoon bridge locks out Boggy Swamp and Mountain Pass, + # effectively making it required to complete the game. connect_region_to_sub(multiworld, player, - Jak1Level.LOST_PRECURSOR_CITY, - Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM) + Jak1Level.ROCK_VILLAGE, + Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE, + lambda state: state.has(rv_pontoon_bridge, player)) - connect_subregions(multiworld, player, - Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, - Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM) - - # LPC is such a mess logistically... once you complete the climb up the helix room, - # you are back to the room before the first slide, which is still the "main area" of LPC. connect_sub_to_region(multiworld, player, - Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM, - Jak1Level.LOST_PRECURSOR_CITY) - - # Once you raise the sunken room to the surface, you can access Rock Village directly. - # You just need to complete the Location check to do this, you don't need to receive the power cell Item. - connect_sub_to_region(multiworld, player, - Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM, - Jak1Level.ROCK_VILLAGE) - - connect_regions(multiworld, player, - Jak1Level.ROCK_VILLAGE, - Jak1Level.BOGGY_SWAMP) + Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE, + Jak1Level.BOGGY_SWAMP) # Flut Flut only has one landing pad here, so leaving this subregion is as easy # as dismounting Flut Flut right where you found her. @@ -137,36 +121,30 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT, lambda state: state.has(sb_flut_flut, player)) - # Klaww is considered the "main area" of MP, and the "race" is a subregion. - # It's not really intended to get back up the ledge overlooking Klaww's lava pit. - connect_regions(multiworld, player, - Jak1Level.ROCK_VILLAGE, - Jak1Level.MOUNTAIN_PASS, - lambda state: state.has(power_cell, player, 45)) + connect_sub_to_region(multiworld, player, + Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE, + Jak1Level.MOUNTAIN_PASS, + lambda state: state.has(power_cell, player, 45)) connect_region_to_sub(multiworld, player, Jak1Level.MOUNTAIN_PASS, - Jak1SubLevel.MOUNTAIN_PASS_RACE) - - connect_subregions(multiworld, player, - Jak1SubLevel.MOUNTAIN_PASS_RACE, - Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT, - lambda state: state.has(sm_yellow_switch, player)) + Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT, + lambda state: state.has(sm_yellow_switch, player)) - connect_sub_to_region(multiworld, player, - Jak1SubLevel.MOUNTAIN_PASS_RACE, - Jak1Level.VOLCANIC_CRATER) + connect_regions(multiworld, player, + Jak1Level.MOUNTAIN_PASS, + Jak1Level.VOLCANIC_CRATER) set_trade_requirements(multiworld, player, Jak1Level.VOLCANIC_CRATER, vc_traders, 1530) connect_regions(multiworld, player, Jak1Level.VOLCANIC_CRATER, Jak1Level.SPIDER_CAVE) - # TODO - Yeah, this is a weird one. You technically need either 71 power cells OR - # any 2 power cells after arriving at Volcanic Crater. Not sure how to model this... + # Custom-added unlock for snowy mountain's gondola. connect_regions(multiworld, player, Jak1Level.VOLCANIC_CRATER, - Jak1Level.SNOWY_MOUNTAIN) + Jak1Level.SNOWY_MOUNTAIN, + lambda state: state.has(sm_gondola, player)) connect_region_to_sub(multiworld, player, Jak1Level.SNOWY_MOUNTAIN, diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 8bdfffc5ba5a..82e588214e88 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -16,10 +16,10 @@ class JakAndDaxterSettings(settings.Group): class RootDirectory(settings.UserFolderPath): - """Path to folder containing the ArchipelaGOAL mod.""" + """Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe).""" description = "ArchipelaGOAL Root Directory" - root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL") + root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL/out/build/Release/bin") class JakAndDaxterWebWorld(WebWorld): diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index a96d2797f239..54e0a1a56350 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -5,52 +5,82 @@ The [Player Options Page](../player-options) for this game contains all the options you need to configure and export a config file. -At this time, Scout Flies are always randomized, and Precursor Orbs -are never randomized. +At this time, there are several caveats and restrictions: +- Power Cells and Scout Flies are **always** randomized. +- Precursor Orbs are **never** randomized. +- **All** of the traders in the game become in-logic checks **if and only if** you have enough Orbs (1530) to pay them all at once. + - This is to prevent hard locks, where an item required for progression is locked behind a trade you can't afford. ## What does randomization do to this game? -All 101 Power Cells and 112 Scout Flies are now Location Checks -and may contain Items for different games, as well as different Items from within Jak and Daxter. +All 101 Power Cells and 112 Scout Flies are now Location Checks and may contain Items for different games, +as well as different Items from within Jak and Daxter. Additionally, several special checks and corresponding items +have been added that are required to complete the game. + +## What are the special checks and how do I check them? +| Check Name | How To Check | +|------------------------|------------------------------------------------------------------------------| +| Fisherman's Boat | Complete the fishing minigame in Forbidden Jungle | +| Jungle Elevator | Collect the power cell at the top of the temple in Forbidden Jungle | +| Blue Eco Switch | Collect the power cell on the blue vent switch in Forbidden Jungle | +| Flut Flut | Push the egg off the cliff in Sentinel Beach and talk to the bird lady | +| Warrior's Pontoons | Talk to the Warrior in Rock Village once (you do NOT have to trade with him) | +| Snowy Mountain Gondola | Approach the gondola in Volcanic Crater | +| Yellow Eco Switch | Collect the power cell on the yellow vent switch in Snowy Mountain | +| Snowy Fort Gate | Ride the Flut Flut in Snowy Mountain and press the fort gate switch | +| Freed The Blue Sage | Free the Blue Sage in Gol and Maia's Citadel | +| Freed The Red Sage | Free the Red Sage in Gol and Maia's Citadel | +| Freed The Yellow Sage | Free the Yellow Sage in Gol and Maia's Citadel | +| Freed The Green Sage | Free the Green Sage in Gol and Maia's Citadel | + +## What are the special items and what do they unlock? +| Item Name | What It Unlocks | +|--------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| Fisherman's Boat | Misty Island | +| Jungle Elevator | The blue vent switch inside the temple in Forbidden Jungle | +| Blue Eco Switch | The plant boss inside the temple in Forbidden Jungle
The cannon tower in Sentinel Beach | +| Flut Flut | The upper platforms in Boggy Swamp
The fort gate switch in Snowy Mountain | +| Warrior's Pontoons | Boggy Swamp and Mountain Pass | +| Snowy Mountain Gondola | Snowy Mountain | +| Yellow Eco Switch | The frozen box in Snowy Mountain
The shortcut in Mountain Pass | +| Snowy Fort Gate | The fort in Snowy Mountain | +| Freed The Blue Sage
Freed The Red Sage
Freed The Yellow Sage | The final staircase in Gol and Maia's Citadel | +| Freed The Green Sage | The final elevator in Gol and Maia's Citadel | ## What is the goal of the game once randomized? To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. In order to reach them, you will need at least 72 Power Cells to cross the Lava Tube. In addition, -you will need the four specific Power Cells obtained by freeing the Red, Blue, Yellow, and Green Sages. - -## How do I progress through the game? -You can progress by performing tasks and completing the challenges that would normally give you Power Cells and -Scout Flies in the game. If you are playing with others, those players may find Power Cells and Scout Flies -in their games, and those Items will be automatically sent to your game. - -If you have completed all possible tasks available to you but still cannot progress, you may have to wait for -another player to find enough of your game's Items to allow you to progress. If that does not apply, -double-check your spoiler log to make sure you have all the items you should have. If you don't, -you may have encountered a bug. Please see the options for bug reporting below. - -## What happens when I pick up an item? -Jak and Daxter will perform their victory animation, if applicable. You will not receive that item, and -the Item count for that item will not change. The pause menu will say "Task Completed" below the -picked-up Power Cell, but the icon will remain "dormant." You will see a message in your text client saying -what you found and who it belongs to. - -## What happens when I receive an item? -Jak and Daxter won't perform their victory animation, and gameplay will continue as normal. Your text client will -inform you where you received the Item from, and which one it is. Your Item count for that type of Item will also -tick up. The pause menu will not say "Task Completed" below the selected Power Cell, but the icon will be "activated." - -## I can't reach a certain area within an accessible region, how do I get there? -Some areas are locked behind possession of specific Power Cells. For example, you cannot access Misty Island -until you have the "Catch 200 Pounds of Fish" Power Cell. Keep in mind, your access to Misty Island is determined -_through possession of this specific Power Cell only,_ **not** _by you completing the Fishing minigame._ +you will need the four special items that free the Red, Blue, Yellow, and Green Sages. -## I got soft-locked and can't leave, how do I get out of here? -As stated before, some areas are locked behind possession of specific Power Cells. But you may already be past -a point-of-no-return preventing you from backtracking. One example is the Forbidden Jungle temple, where -the elevator is locked at the bottom, and if you haven't unlocked the Blue Eco Switch, you cannot access -the Plant Boss's room and escape. +## What happens when I pick up or receive a power cell? +When you pick up a power cell, Jak and Daxter will perform their victory animation. Your power cell count will +NOT change. The pause menu will say "Task Completed" below the picked-up Power Cell. If your power cell was related +to one of the special checks listed above, you will automatically check that location as well - a 2 for 1 deal! +Finally, your text client will inform you what you found and who it belongs to. + +When you receive a power cell, your power cell count will tick up by 1. Gameplay will otherwise continue as normal. +Finally, your text client will inform you where you received the power cell from. + +## What happens when I pick up or receive a scout fly? +When you pick up a scout fly, your scout fly count will NOT change. The pause menu will show you the number of +scout flies you picked up per-region, and this number will have ticked up by 1 for the region that scout fly belongs to. +Finally, your text client will inform you what you found and who it belongs to. + +When you receive a scout fly, your total scout fly count will tick up by 1. The pause menu will show you the number of +scout flies you received per-region, and this number will have ticked up by 1 for the region that scout fly belongs to. +Finally, your text client will inform you where you received the scout fly from, and which one it is. -In this scenario, you will need to open your menu and find the "Warp To Home" option at the bottom of the list. +## How do I check the "Free 7 Scout Flies" power cell? +You will automatically check this power cell when you _receive_ your 7th scout fly, NOT when you _pick up_ your 7th +scout fly. So in short: + +- When you _pick up_ your 7th fly, the normal rules apply. +- When you _receive_ your 7th fly, 2 things will happen in quick succession. + - First, you will receive that scout fly, as normal. + - Second, you will immediately complete the "Free 7 Scout Flies" check, which will send out another item. + +## I got soft-locked and can't leave, how do I get out of here? +Open the game's menu, navigate to Options, and find the "Warp To Home" option at the bottom of the list. Selecting this option will instantly teleport you to Geyser Rock. From there, you can teleport back to the nearest sage's hut to continue your journey. @@ -60,8 +90,7 @@ Depending on the nature of the bug, there are a couple of different options. * If you found a logical error in the randomizer, please create a new Issue [here.](https://github.com/ArchipelaGOAL/Archipelago/issues) * Use this page if: - * You are hard-locked from progressing. For example, you are stuck on Geyser Rock because one of the four - Geyser Rock Power Cells is not on Geyser Rock. + * An item required for progression is unreachable. * The randomizer did not respect one of the Options you chose. * You see a mistake, typo, etc. on this webpage. * You see an error or stack trace appear on the text client. @@ -70,7 +99,7 @@ Depending on the nature of the bug, there are a couple of different options. * If you encountered an error in OpenGOAL, please create a new Issue [here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues) * Use this page if: - * You encounter a crash, freeze, reset, etc. + * You encounter a crash, freeze, reset, etc in the game. * You fail to send Items you find in the game to the Archipelago server. * You fail to receive Items the server sends to you. * Your game disconnects from the server and cannot reconnect. diff --git a/worlds/jakanddaxter/locs/SpecialLocations.py b/worlds/jakanddaxter/locs/SpecialLocations.py index 4ce5b63812a6..e9ebecbfd9e9 100644 --- a/worlds/jakanddaxter/locs/SpecialLocations.py +++ b/worlds/jakanddaxter/locs/SpecialLocations.py @@ -38,6 +38,8 @@ def to_game_id(ap_id: int) -> int: 4: "Jungle Elevator", 2: "Blue Eco Switch", 17: "Flut Flut", + 33: "Warrior's Pontoons", + 105: "Snowy Mountain Gondola", 60: "Yellow Eco Switch", 63: "Snowy Fort Gate", 71: "Freed The Blue Sage", From 50f56064eb3991fbf3f602aaec013028ba611c0b Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 28 May 2024 18:02:23 -0400 Subject: [PATCH 32/70] Alpha Updates (#15) * Jak 1: Consolidate client into apworld, create launcher icon, improve setup docs. * Jak 1: Update setup guide. * Jak 1: Load title screen, save states of in/outboxes. --- JakAndDaxterClient.py | 9 -- worlds/jakanddaxter/Client.py | 9 +- worlds/jakanddaxter/__init__.py | 23 ++-- worlds/jakanddaxter/client/MemoryReader.py | 20 ++- worlds/jakanddaxter/client/ReplClient.py | 42 ++++++- worlds/jakanddaxter/docs/setup_en.md | 138 ++++++++++++--------- worlds/jakanddaxter/icons/egg.ico | Bin 0 -> 165662 bytes worlds/jakanddaxter/icons/egg.png | Bin 0 -> 63687 bytes 8 files changed, 160 insertions(+), 81 deletions(-) delete mode 100644 JakAndDaxterClient.py create mode 100644 worlds/jakanddaxter/icons/egg.ico create mode 100644 worlds/jakanddaxter/icons/egg.png diff --git a/JakAndDaxterClient.py b/JakAndDaxterClient.py deleted file mode 100644 index 040f8ff389bd..000000000000 --- a/JakAndDaxterClient.py +++ /dev/null @@ -1,9 +0,0 @@ -import Utils -from worlds.jakanddaxter.Client import launch - -import ModuleUpdate -ModuleUpdate.update() - -if __name__ == '__main__': - Utils.init_logging("JakAndDaxterClient", exception_logger="Client") - launch() diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index c5c36b610e9e..2b2a83795f45 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -85,6 +85,8 @@ class JakAndDaxterContext(CommonContext): def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: self.repl = JakAndDaxterReplClient() self.memr = JakAndDaxterMemoryReader() + # self.memr.load_data() + # self.repl.load_data() super().__init__(server_address, password) def run_gui(self): @@ -110,6 +112,8 @@ def on_package(self, cmd: str, args: dict): for index, item in enumerate(args["items"], start=args["index"]): logger.info(args) self.repl.item_inbox[index] = item + self.memr.save_data() + self.repl.save_data() async def ap_inform_location_check(self, location_ids: typing.List[int]): message = [{"cmd": "LocationChecks", "locations": location_ids}] @@ -140,8 +144,7 @@ async def run_memr_loop(self): async def run_game(ctx: JakAndDaxterContext): - # If you're running the game through the mod launcher, these may already be running. - # If they are not running, try to start them. + # These may already be running. If they are not running, try to start them. gk_running = False try: pymem.Pymem("gk.exe") # The GOAL Kernel @@ -157,7 +160,7 @@ async def run_game(ctx: JakAndDaxterContext): logger.info("Compiler not running, attempting to start.") # Don't mind all the arguments, they are exactly what you get when you run "task boot-game" or "task repl". - # TODO - Support other OS's. cmd for some reason does not work with goalc. + # TODO - Support other OS's. cmd for some reason does not work with goalc. Pymem is Windows-only. if not gk_running: try: gk_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 82e588214e88..5cbf3ca4e4c1 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,6 +1,7 @@ import typing import settings +from Utils import local_path from BaseClasses import Item, ItemClassification, Tutorial from .GameID import jak1_id, jak1_name from .JakAndDaxterOptions import JakAndDaxterOptions @@ -11,7 +12,21 @@ from .Regions import create_regions from .Rules import set_rules from worlds.AutoWorld import World, WebWorld -from worlds.LauncherComponents import components, Component, launch_subprocess, Type +from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths + + +def launch_client(): + from .Client import launch + launch_subprocess(launch, name="JakAndDaxterClient") + + +components.append(Component("Jak and Daxter Client", + "JakAndDaxterClient", + func=launch_client, + component_type=Type.CLIENT, + icon="egg")) + +icon_paths["egg"] = local_path("worlds", "jakanddaxter", "icons", "egg.png") class JakAndDaxterSettings(settings.Group): @@ -126,9 +141,3 @@ def get_filler_item_name(self) -> str: def launch_client(): from .Client import launch launch_subprocess(launch, name="JakAndDaxterClient") - - -components.append(Component("Jak and Daxter Client", - "JakAndDaxterClient", - func=launch_client, - component_type=Type.CLIENT)) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 64a37352bcbf..30ed927bb130 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -2,6 +2,7 @@ import pymem from pymem import pattern from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError +import json from CommonClient import logger from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials @@ -58,7 +59,7 @@ async def main_tick(self, location_callback: typing.Callable, finish_callback: t # Read the memory address to check the state of the game. self.read_memory() - location_callback(self.location_outbox) # TODO - I forgot why call this here when it's already down below... + # location_callback(self.location_outbox) # TODO - I forgot why call this here when it's already down below... # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. if len(self.location_outbox) > self.outbox_index: @@ -169,3 +170,20 @@ def read_memory(self) -> typing.List[int]: self.connected = False return self.location_outbox + + def save_data(self): + with open("jakanddaxter_location_outbox.json", "w+") as f: + dump = { + "outbox_index": self.outbox_index, + "location_outbox": self.location_outbox + } + json.dump(dump, f, indent=4) + + def load_data(self): + try: + with open("jakanddaxter_location_outbox.json", "r") as f: + load = json.load(f) + self.outbox_index = load["outbox_index"] + self.location_outbox = load["location_outbox"] + except FileNotFoundError: + pass diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index e5c8030c3ed9..8b6c935d750c 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -1,11 +1,14 @@ +import json import time import struct +import typing from socket import socket, AF_INET, SOCK_STREAM import pymem from pymem.exception import ProcessNotFound, ProcessError from CommonClient import logger +from NetUtils import NetworkItem from worlds.jakanddaxter.GameID import jak1_id from worlds.jakanddaxter.Items import item_table from worlds.jakanddaxter.locs import ( @@ -27,7 +30,7 @@ class JakAndDaxterReplClient: gk_process: pymem.process = None goalc_process: pymem.process = None - item_inbox = {} + item_inbox: typing.Dict[int, NetworkItem] = {} inbox_index = 0 def __init__(self, ip: str = "127.0.0.1", port: int = 8181): @@ -139,11 +142,15 @@ async def connect(self): # Disable cheat-mode and debug (close the visual cue). # self.send_form("(set! *debug-segment* #f)") - if self.send_form("(set! *cheat-mode* #f)"): + if self.send_form("(set! *cheat-mode* #f)", print_ok=False): + ok_count += 1 + + # Run the retail game start sequence (while still in debug). + if self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"): ok_count += 1 # Now wait until we see the success message... 6 times. - if ok_count == 6: + if ok_count == 7: self.connected = True else: self.connected = False @@ -219,3 +226,32 @@ def receive_special(self, ap_id: int) -> bool: else: logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok + + def save_data(self): + with open("jakanddaxter_item_inbox.json", "w+") as f: + dump = { + "inbox_index": self.inbox_index, + "item_inbox": [{ + "item": self.item_inbox[k].item, + "location": self.item_inbox[k].location, + "player": self.item_inbox[k].player, + "flags": self.item_inbox[k].flags + } for k in self.item_inbox + ] + } + json.dump(dump, f, indent=4) + + def load_data(self): + try: + with open("jakanddaxter_item_inbox.json", "r") as f: + load = json.load(f) + self.inbox_index = load["inbox_index"] + self.item_inbox = {k: NetworkItem( + item=load["item_inbox"][k]["item"], + location=load["item_inbox"][k]["location"], + player=load["item_inbox"][k]["player"], + flags=load["item_inbox"][k]["flags"] + ) for k in range(0, len(load["item_inbox"])) + } + except FileNotFoundError: + pass diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index ce2b1936771b..db1385a4f8c7 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -3,83 +3,105 @@ ## Required Software - A legally purchased copy of *Jak And Daxter: The Precursor Legacy.* -- Python version 3.10 or higher. Make sure this is added to your PATH environment variable. -- [Task](https://taskfile.dev/installation/) (This makes it easier to run commands.) +- [The OpenGOAL Mod Launcher](https://jakmods.dev/) +- [The Jak and Daxter .APWORLD package](https://github.com/ArchipelaGOAL/Archipelago/releases) -## Installation - -### Installation via OpenGOAL Mod Launcher - -At this time, the only supported method of setup is through Manual Compilation. Aside from the legal copy of the game, all tools required to do this are free. - -***Windows Preparations*** +At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future. +(OpenGOAL itself supports Linux, and the mod launcher is runnable with Python.) -***Linux Preparations*** - -***Using the Launcher*** - -### Manual Compilation (Linux/Windows) - -***Windows Preparations*** +## Preparations - Dump your copy of the game as an ISO file to your PC. -- Download a zipped up copy of the Archipelago Server and Client [here.](https://github.com/ArchipelaGOAL/Archipelago) -- Download a zipped up copy of the modded OpenGOAL game [here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL) -- Unzip the two projects into easily accessible directories. - +- Install the Mod Launcher. +- If you are prompted by the Mod Launcher at any time during setup, provide the path to your ISO file. -***Linux Preparations*** +## Installation -***Compiling*** +***OpenGOAL Mod Launcher*** + +- Run the Mod Launcher and click `ArchipelaGOAL` in the mod list. +- Click `Install` and wait for it to complete. + - If you have yet to be prompted for the ISO, click `Re-Extract` and provide the path to your ISO file. +- Click `Recompile`. This may take between 30-60 seconds. It should run to 100% completion. If it does not, see the Troubleshooting section. +- Click `View Folder`. + - In the new file explorer window, take note of the current path. It should contain `gk.exe` and `goalc.exe`. +- Verify that the mod launcher copied the extracted ISO files to the mod directory: + - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\archipelagoal\iso_data` should have *all* the same files as + - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\_iso_data`, if it doesn't, copy those files over manually. + - And then `Recompile` if you needed to copy the files over. +- **DO NOT LAUNCH THE GAME FROM THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). + +***Archipelago Launcher*** + +- Copy the `jakanddaxter.apworld` file into your `Archipelago/lib/worlds` directory. + - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. +- Run the Archipelago Launcher. +- From the left-most list, click `Generate Template Options`. +- Select `Jak and Daxter The Precursor Legacy.yaml`. In the text file that opens, enter the name you want and remember it for later. +- Save this file in `Archipelago/players`. You can now close the file. +- Back in the Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. +- If you plan to host the game yourself, from the left-most list, click `Host`. + - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. + - You can sort by Date Modified to make it easy to find. ## Starting a Game -- Open 3 Powershell windows. If you have VSCode, you can run 3 terminals to consolidate this process. - - In the first window, navigate to the Archipelago folder using `cd` and run `python ./Launcher.py --update_settings`. Then run it again without the `--update_settings` flag. - - In the second window, navigate to the ArchipelaGOAL folder and run `task extract`. This will prompt you to tell the mod where to find your ISO file to dump its contents. When that is done, run `task repl`. - - In the third window, navigate to the ArchipelaGOAL folder and run `task boot-game`. At this point, Jak should be standing outside Samos's hut. - - Once you confirm all those tasks succeeded, you can now close all these windows. -- Edit your host.yaml file and ensure these lines exist. And don't forget to specify your ACTUAL install path. If you're on Windows, no backslashes! -``` -jakanddaxter_options: - # Path to folder containing the ArchipelaGOAL mod. - root_directory: "D:/Files/Repositories/ArchipelaGOAL" -``` -- In the Launcher, click Generate to create a new random seed. Save the resulting zip file. -- In the Launcher, click Host to host the Archipelago server. It will prompt you for the location of that zip file. -- Once the server is running, in the Launcher, find the Jak and Daxter Client and click it. You should see the command window begin to compile the game. -- When it completes, you should hear the menu closing sound effect, and you should see the text client indicate that the two agents are ready to communicate with the game. -- Connect the client to the Archipelago server and enter your slot name. Once this is done, the game should be ready to play. Talk to Samos to trigger the cutscene where he sends you to Geyser Rock, and off you go! - -Once you complete the setup steps, you should only need to run the Launcher again to generate a game, host a server, or run the client and connect to a server. -- You never need to download the zip copies of the projects again (unless there are updates). -- You never need to dump your ISO again. -- You never need to extract the ISO assets again. - -### Joining a MultiWorld Game +***New Game*** -MultiWorld games are untested at this time. +- Run the Archipelago Launcher. +- From the right-most list, find and click `Jak and Daxter Client`. +- 4 new windows should appear: + - A powershell window will open to run the OpenGOAL compiler. It should take about 30 seconds to compile the game. + - As before, it should run to 100% completion, and you should hear a musical cue to indicate it is done. If it does not run to 100%, or you do not hear the musical cue, see the Troubleshooting section. + - Another powershell window will open to run the game. + - The game window itself will launch, and Jak will be standing outside Samos's Hut. + - Finally, the Archipelago text client will open. + - You should see several messages appear after the compiler has run to 100% completion. If you see `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. + - The game should then load in the title screen. +- You can *minimize* the 2 powershell windows, **BUT DO NOT CLOSE THEM.** They are required for Archipelago and the game to communicate with each other. +- Start a new game in the title screen, and play through the cutscenes. +- Once you reach Geyser Rock, you can connect to the Archipelago server. + - Provide your slot/player name and hit Enter, and then start the game! + - You can leave Geyser Rock immediately if you so choose - just step on the warp gate button. -### Playing Offline +***Returning / Async Game*** -Offline play is untested at this time. +- One important note is to connect to the Archipelago server **AFTER** you load your save file. This is to allow AP to give you all the items you had previously. +- Otherwise, the same steps as New Game apply. -## Installation and Setup Troubleshooting +## Troubleshooting -### Compilation Failures +***Installation Failure*** -### Runtime Failures - -- If the client window appears but no sound plays, you will need to enter the following commands into the client to connect it to the game. +- If you encounter errors during extraction or compilation of the game when using the Mod Launcher, you may see errors like this: +``` +-- Compilation Error! -- +Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. +``` + - If this occurs, you may need to copy the extracted data to the mod folder manually. + - From a location like this: `C:\Users\\AppData\Roaming\OpenGOAL-Mods\_iso_data` + - To a location like this: `C:\Users\\AppData\Roaming\OpenGOAL-Mods\archipelagoal\iso_data` + - Then try clicking `Recompile` in the Mod Launcher (ensure you have selected the right mod first!) + +***Game Failure*** + +- If at any point the text client says `The process has died`, you will need to restart the appropriate + application: + - Open a new powershell window. + - Navigating to the directory containing `gk.exe` and `goalc.exe` via `cd`. + - Run the command corresponding to the dead process: + - `.\gk.exe --game jak1 -- -v -boot -fakeiso -debug` + - `.\goalc.exe --game jak1` + - Then enter the following commands into the text client to reconnect everything to the game. - `/repl connect` - `/memr connect` -- Once these are done, you can enter `/repl status` and `/memr status` to check that everything is connected and ready. - -## Gameplay Troubleshooting + - Once these are done, you can enter `/repl status` and `/memr status` to verify. +- If the game freezes by replaying the same two frames over and over, but the music still runs in the background, you may have accidentally interacted with the powershell windows in the background - they halt the game if you:scroll up in them, highlight text in them, etc. + - To unfreeze the game, scroll to the very bottom of the log output and right click. That will release powershell from your control and allow the game to continue. + - It is recommended to keep these windows minimized and out of your way. ### Known Issues -- I've streamlined the process of connecting the client's agents to the game, but it comes at the cost of more granular commands useful for troubleshooting. - The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly. - The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. -- The game relates tasks and power cells closely but separately. Some issues may result from having to tell the game to check for the power cells you own, rather than the tasks you completed. \ No newline at end of file +- The game relates tasks and power cells closely but separately. Some issues may result from custom code to add distinct items to the game (like the Fisherman's Boat, the Pontoons, or the Gondola). diff --git a/worlds/jakanddaxter/icons/egg.ico b/worlds/jakanddaxter/icons/egg.ico new file mode 100644 index 0000000000000000000000000000000000000000..5579bad24f630cc52d710c2da94ddd8ab12cbbc3 GIT binary patch literal 165662 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 M07*qoM6N<$f}G#@cK`qY literal 0 HcmV?d00001 From 33a858e5261099ae48d81edb738d0b75879073b2 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 1 Jun 2024 11:55:17 -0400 Subject: [PATCH 33/70] Logging Update (#16) * Jak 1: Separate info and debug logs. * Jak 1: Update world info to refer to Archipelago Options menu. --- worlds/jakanddaxter/Client.py | 2 +- worlds/jakanddaxter/client/MemoryReader.py | 6 +++--- worlds/jakanddaxter/client/ReplClient.py | 10 +++++----- .../docs/en_Jak and Daxter The Precursor Legacy.md | 11 ++++++++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 2b2a83795f45..4417c49b10cc 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -110,7 +110,7 @@ async def server_auth(self, password_requested: bool = False): def on_package(self, cmd: str, args: dict): if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): - logger.info(args) + logger.debug(f"index: {str(index)}, item: {str(item)}") self.repl.item_inbox[index] = item self.memr.save_data() self.repl.save_data() diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 30ed927bb130..c1b10ff3a6a3 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -130,7 +130,7 @@ def read_memory(self) -> typing.List[int]: cell_ap_id = Cells.to_ap_id(next_cell) if cell_ap_id not in self.location_outbox: self.location_outbox.append(cell_ap_id) - logger.info("Checked power cell: " + str(next_cell)) + logger.debug("Checked power cell: " + str(next_cell)) for k in range(0, next_buzzer_index): next_buzzer = int.from_bytes( @@ -142,7 +142,7 @@ def read_memory(self) -> typing.List[int]: buzzer_ap_id = Flies.to_ap_id(next_buzzer) if buzzer_ap_id not in self.location_outbox: self.location_outbox.append(buzzer_ap_id) - logger.info("Checked scout fly: " + str(next_buzzer)) + logger.debug("Checked scout fly: " + str(next_buzzer)) for k in range(0, next_special_index): next_special = int.from_bytes( @@ -163,7 +163,7 @@ def read_memory(self) -> typing.List[int]: special_ap_id = Specials.to_ap_id(next_special) if special_ap_id not in self.location_outbox: self.location_outbox.append(special_ap_id) - logger.info("Checked special: " + str(next_special)) + logger.debug("Checked special: " + str(next_special)) except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 8b6c935d750c..2360601d5f9f 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -72,7 +72,7 @@ def send_form(self, form: str, print_ok: bool = True) -> bool: response = self.sock.recv(1024).decode() if "OK!" in response: if print_ok: - logger.info(response) + logger.debug(response) return True else: logger.error(f"Unexpected response from REPL: {response}") @@ -101,7 +101,7 @@ async def connect(self): # Should be the OpenGOAL welcome message (ignore version number). if "Connected to OpenGOAL" and "nREPL!" in welcome_message: - logger.info(welcome_message) + logger.debug(welcome_message) else: logger.error(f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"") except ConnectionRefusedError as e: @@ -198,7 +198,7 @@ def receive_power_cell(self, ap_id: int) -> bool: "(pickup-type fuel-cell) " "(the float " + str(cell_id) + "))") if ok: - logger.info(f"Received a Power Cell!") + logger.debug(f"Received a Power Cell!") else: logger.error(f"Unable to receive a Power Cell!") return ok @@ -210,7 +210,7 @@ def receive_scout_fly(self, ap_id: int) -> bool: "(pickup-type buzzer) " "(the float " + str(fly_id) + "))") if ok: - logger.info(f"Received a {item_table[ap_id]}!") + logger.debug(f"Received a {item_table[ap_id]}!") else: logger.error(f"Unable to receive a {item_table[ap_id]}!") return ok @@ -222,7 +222,7 @@ def receive_special(self, ap_id: int) -> bool: "(pickup-type ap-special) " "(the float " + str(special_id) + "))") if ok: - logger.info(f"Received special unlock {item_table[ap_id]}!") + logger.debug(f"Received special unlock {item_table[ap_id]}!") else: logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index 54e0a1a56350..9160701be759 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -80,9 +80,14 @@ scout fly. So in short: - Second, you will immediately complete the "Free 7 Scout Flies" check, which will send out another item. ## I got soft-locked and can't leave, how do I get out of here? -Open the game's menu, navigate to Options, and find the "Warp To Home" option at the bottom of the list. -Selecting this option will instantly teleport you to Geyser Rock. From there, you can teleport back to the nearest -sage's hut to continue your journey. +Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Warp To Home`. +Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back +to the nearest sage's hut to continue your journey. + +## How do I know which special items I have? +Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Item Tracker`. +This will show you a list of all the special items in the game, ones not normally tracked as power cells or scout flies. +Grayed-out items indicate you do not possess that item, light blue items indicate you possess that item. ## I think I found a bug, where should I report it? Depending on the nature of the bug, there are a couple of different options. From 8293b9d436f2285dc1bf59c9c0df93b504739aba Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:07:31 -0400 Subject: [PATCH 34/70] Deathlink (#18) * Jak 1: Implement Deathlink. TODO: make it optional... * Jak 1: Issue a proper send-event for deathlink deaths. * Jak 1: Added cause of death to deathlink, fixed typo. * Jak 1: Make Deathlink toggleable. * Jak 1: Added player name to death text, added zoomer/flut/fishing text, simplified GOAL call for deathlink. * Jak 1: Fix death text in client logger. --- worlds/jakanddaxter/Client.py | 33 ++++++- worlds/jakanddaxter/client/MemoryReader.py | 102 ++++++++++++++++++--- worlds/jakanddaxter/client/ReplClient.py | 38 ++++++++ 3 files changed, 159 insertions(+), 14 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 4417c49b10cc..36129465489d 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -65,7 +65,6 @@ def _cmd_memr(self, *arguments: str): class JakAndDaxterContext(CommonContext): - tags = {"AP"} game = jak1_name items_handling = 0b111 # Full item handling command_processor = JakAndDaxterClientCommandProcessor @@ -115,6 +114,11 @@ def on_package(self, cmd: str, args: dict): self.memr.save_data() self.repl.save_data() + def on_deathlink(self, data: dict): + if self.memr.deathlink_enabled: + self.repl.received_deathlink = True + super().on_deathlink(data) + async def ap_inform_location_check(self, location_ids: typing.List[int]): message = [{"cmd": "LocationChecks", "locations": location_ids}] await self.send_msgs(message) @@ -128,9 +132,29 @@ async def ap_inform_finished_game(self): await self.send_msgs(message) self.finished_game = True - def on_finish(self): + def on_finish_check(self): create_task_log_exception(self.ap_inform_finished_game()) + async def ap_inform_deathlink(self): + if self.memr.deathlink_enabled: + death_text = self.memr.cause_of_death.replace("Jak", self.player_names[self.slot]) + await self.send_death(death_text) + logger.info(death_text) + + # Reset all flags. + self.memr.send_deathlink = False + self.memr.cause_of_death = "" + self.repl.reset_deathlink() + + def on_deathlink_check(self): + create_task_log_exception(self.ap_inform_deathlink()) + + async def ap_inform_deathlink_toggle(self): + await self.update_death_link(self.memr.deathlink_enabled) + + def on_deathlink_toggle(self): + create_task_log_exception(self.ap_inform_deathlink_toggle()) + async def run_repl_loop(self): while True: await self.repl.main_tick() @@ -138,7 +162,10 @@ async def run_repl_loop(self): async def run_memr_loop(self): while True: - await self.memr.main_tick(self.on_location_check, self.on_finish) + await self.memr.main_tick(self.on_location_check, + self.on_finish_check, + self.on_deathlink_check, + self.on_deathlink_toggle) await asyncio.sleep(0.1) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index c1b10ff3a6a3..c91eba8286d1 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,3 +1,4 @@ +import random import typing import pymem from pymem import pattern @@ -12,18 +13,62 @@ sizeof_uint32 = 4 sizeof_uint8 = 1 -next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. -next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. -next_special_index_offset = 16 # Each of these is an uint64, so 8 bytes. +next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. +next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. +next_special_index_offset = 16 # Each of these is an uint64, so 8 bytes. cells_checked_offset = 24 -buzzers_checked_offset = 428 # cells_checked_offset + (sizeof uint32 * 101 cells) -specials_checked_offset = 876 # buzzers_checked_offset + (sizeof uint32 * 112 buzzers) +buzzers_checked_offset = 428 # cells_checked_offset + (sizeof uint32 * 101 cells) +specials_checked_offset = 876 # buzzers_checked_offset + (sizeof uint32 * 112 buzzers) -buzzers_received_offset = 1004 # specials_checked_offset + (sizeof uint32 * 32 specials) +buzzers_received_offset = 1004 # specials_checked_offset + (sizeof uint32 * 32 specials) specials_received_offset = 1020 # buzzers_received_offset + (sizeof uint8 * 16 levels (for scout fly groups)) -end_marker_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials) +died_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials) + +deathlink_enabled_offset = 1053 # died_offset + sizeof uint8 + +end_marker_offset = 1054 # deathlink_enabled_offset + sizeof uint8 + + +# "Jak" to be replaced by player name in the Client. +def autopsy(died: int) -> str: + assert died > 0, f"Tried to find Jak's cause of death, but he's still alive!" + if died in [1, 2, 3, 4]: + return random.choice(["Jak said goodnight.", + "Jak stepped into the light.", + "Jak gave Daxter his insect collection.", + "Jak did not follow Step 1."]) + if died == 5: + return "Jak fell into an endless pit." + if died == 6: + return "Jak drowned in the spicy water." + if died == 7: + return "Jak tried to tackle a Lurker Shark." + if died == 8: + return "Jak hit 500 degrees." + if died == 9: + return "Jak took a bath in a pool of dark eco." + if died == 10: + return "Jak got bombarded with flaming 30-ton boulders." + if died == 11: + return "Jak hit 800 degrees." + if died == 12: + return "Jak ceased to be." + if died == 13: + return "Jak got eaten by the dark eco plant." + if died == 14: + return "Jak burned up." + if died == 15: + return "Jak hit the ground hard." + if died == 16: + return "Jak crashed the zoomer." + if died == 17: + return "Jak got Flut Flut hurt." + if died == 18: + return "Jak poisoned the whole darn catch." + + return "Jak died." class JakAndDaxterMemoryReader: @@ -36,14 +81,23 @@ class JakAndDaxterMemoryReader: gk_process: pymem.process = None location_outbox = [] - outbox_index = 0 - finished_game = False + outbox_index: int = 0 + finished_game: bool = False + + # Deathlink handling + deathlink_enabled: bool = False + send_deathlink: bool = False + cause_of_death: str = "" def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker self.connect() - async def main_tick(self, location_callback: typing.Callable, finish_callback: typing.Callable): + async def main_tick(self, + location_callback: typing.Callable, + finish_callback: typing.Callable, + deathlink_callback: typing.Callable, + deathlink_toggle: typing.Callable): if self.initiated_connect: await self.connect() self.initiated_connect = False @@ -57,9 +111,11 @@ async def main_tick(self, location_callback: typing.Callable, finish_callback: t else: return + # Save some state variables temporarily. + old_deathlink_enabled = self.deathlink_enabled + # Read the memory address to check the state of the game. self.read_memory() - # location_callback(self.location_outbox) # TODO - I forgot why call this here when it's already down below... # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. if len(self.location_outbox) > self.outbox_index: @@ -69,6 +125,13 @@ async def main_tick(self, location_callback: typing.Callable, finish_callback: t if self.finished_game: finish_callback() + if old_deathlink_enabled != self.deathlink_enabled: + deathlink_toggle() + logger.debug("Toggled DeathLink " + ("ON" if self.deathlink_enabled else "OFF")) + + if self.send_deathlink: + deathlink_callback() + async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel @@ -165,6 +228,23 @@ def read_memory(self) -> typing.List[int]: self.location_outbox.append(special_ap_id) logger.debug("Checked special: " + str(next_special)) + died = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + died_offset, sizeof_uint8), + byteorder="little", + signed=False) + + if died > 0: + self.send_deathlink = True + self.cause_of_death = autopsy(died) + + deathlink_flag = int.from_bytes( + self.gk_process.read_bytes(self.goal_address + deathlink_enabled_offset, sizeof_uint8), + byteorder="little", + signed=False) + + # Listen for any changes to this setting. + self.deathlink_enabled = bool(deathlink_flag) + except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") self.connected = False diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 2360601d5f9f..7c176c1224bf 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -2,6 +2,7 @@ import time import struct import typing +import random from socket import socket, AF_INET, SOCK_STREAM import pymem @@ -24,6 +25,7 @@ class JakAndDaxterReplClient: sock: socket connected: bool = False initiated_connect: bool = False # Signals when user tells us to try reconnecting. + received_deathlink: bool = False # The REPL client needs the REPL/compiler process running, but that process # also needs the game running. Therefore, the REPL client needs both running. @@ -62,6 +64,14 @@ async def main_tick(self): self.receive_item() self.inbox_index += 1 + if self.received_deathlink: + self.receive_deathlink() + + # Reset all flags. + # As a precaution, we should reset our own deathlink flag as well. + self.reset_deathlink() + self.received_deathlink = False + # This helper function formats and sends `form` as a command to the REPL. # ALL commands to the REPL should be sent using this function. # TODO - this blocks on receiving an acknowledgement from the REPL server. But it doesn't print @@ -227,6 +237,34 @@ def receive_special(self, ap_id: int) -> bool: logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok + def receive_deathlink(self) -> bool: + + # Because it should at least be funny sometimes. + death_types = ["\'death", + "\'death", + "\'death", + "\'death", + "\'endlessfall", + "\'drown-death", + "\'melt", + "\'dark-eco-pool"] + chosen_death = random.choice(death_types) + + ok = self.send_form("(ap-deathlink-received! " + chosen_death + ")") + if ok: + logger.debug(f"Received deathlink signal!") + else: + logger.error(f"Unable to receive deathlink signal!") + return ok + + def reset_deathlink(self) -> bool: + ok = self.send_form("(set! (-> *ap-info-jak1* died) 0)") + if ok: + logger.debug(f"Reset deathlink flag!") + else: + logger.error(f"Unable to reset deathlink flag!") + return ok + def save_data(self): with open("jakanddaxter_item_inbox.json", "w+") as f: dump = { From 1c42bdb3537f59ab25e5af171d1baadbde74e926 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:12:48 -0400 Subject: [PATCH 35/70] Move Randomizer (#26) * Finally remove debug-segment text, update Python imports to relative paths. * HUGE refactor to Regions/Rules to support move rando, first hub area coded. * More refactoring. * Another refactor - may squash. * Fix some Rules, reuse some code by returning key regions from build_regions. * More regions added. A couple of TODOs. * Fixed trade logic, added LPC regions. * Added Spider, Snowy, Boggy. Fixed Misty's orbs. * Fix circular import, assert orb counts per level, fix a few naming errors. * Citadel added, missing locs and connections fixed. First move rando seed generated. * Add Move Rando to Options class. * Fixed rules for prerequisite moves. * Implement client functionality for move rando, add blurbs to game info page. * Fix wrong address for cache checks. * Fix byte alignment of offsets, refactor read_memory for better code reuse. * Refactor memory offsets and add some unit tests. * Make green eco the filler item, also define a maximum ID. Fix Boggy tether locations. --- worlds/jakanddaxter/Client.py | 6 +- worlds/jakanddaxter/GameID.py | 3 + worlds/jakanddaxter/Items.py | 40 +- worlds/jakanddaxter/JakAndDaxterOptions.py | 21 +- worlds/jakanddaxter/Locations.py | 6 +- worlds/jakanddaxter/Regions.py | 345 ++++-------------- worlds/jakanddaxter/Rules.py | 298 ++------------- worlds/jakanddaxter/__init__.py | 65 ++-- worlds/jakanddaxter/client/MemoryReader.py | 132 ++++--- worlds/jakanddaxter/client/ReplClient.py | 67 +++- .../en_Jak and Daxter The Precursor Legacy.md | 60 ++- worlds/jakanddaxter/docs/setup_en.md | 4 +- worlds/jakanddaxter/locs/OrbCacheLocations.py | 50 +++ worlds/jakanddaxter/locs/OrbLocations.py | 4 +- worlds/jakanddaxter/regs/BoggySwampRegions.py | 154 ++++++++ worlds/jakanddaxter/regs/FireCanyonRegions.py | 17 + .../regs/ForbiddenJungleRegions.py | 83 +++++ worlds/jakanddaxter/regs/GeyserRockRegions.py | 26 ++ .../regs/GolAndMaiasCitadelRegions.py | 122 +++++++ worlds/jakanddaxter/regs/LavaTubeRegions.py | 17 + .../regs/LostPrecursorCityRegions.py | 130 +++++++ .../jakanddaxter/regs/MistyIslandRegions.py | 116 ++++++ .../jakanddaxter/regs/MountainPassRegions.py | 34 ++ .../regs/PrecursorBasinRegions.py | 17 + worlds/jakanddaxter/regs/RegionBase.py | 69 ++++ .../jakanddaxter/regs/RockVillageRegions.py | 63 ++++ .../regs/SandoverVillageRegions.py | 71 ++++ .../jakanddaxter/regs/SentinelBeachRegions.py | 85 +++++ .../jakanddaxter/regs/SnowyMountainRegions.py | 181 +++++++++ worlds/jakanddaxter/regs/SpiderCaveRegions.py | 110 ++++++ .../regs/VolcanicCraterRegions.py | 38 ++ worlds/jakanddaxter/test/__init__.py | 91 +++++ worlds/jakanddaxter/test/test_locations.py | 42 +++ 33 files changed, 1912 insertions(+), 655 deletions(-) create mode 100644 worlds/jakanddaxter/locs/OrbCacheLocations.py create mode 100644 worlds/jakanddaxter/regs/BoggySwampRegions.py create mode 100644 worlds/jakanddaxter/regs/FireCanyonRegions.py create mode 100644 worlds/jakanddaxter/regs/ForbiddenJungleRegions.py create mode 100644 worlds/jakanddaxter/regs/GeyserRockRegions.py create mode 100644 worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py create mode 100644 worlds/jakanddaxter/regs/LavaTubeRegions.py create mode 100644 worlds/jakanddaxter/regs/LostPrecursorCityRegions.py create mode 100644 worlds/jakanddaxter/regs/MistyIslandRegions.py create mode 100644 worlds/jakanddaxter/regs/MountainPassRegions.py create mode 100644 worlds/jakanddaxter/regs/PrecursorBasinRegions.py create mode 100644 worlds/jakanddaxter/regs/RegionBase.py create mode 100644 worlds/jakanddaxter/regs/RockVillageRegions.py create mode 100644 worlds/jakanddaxter/regs/SandoverVillageRegions.py create mode 100644 worlds/jakanddaxter/regs/SentinelBeachRegions.py create mode 100644 worlds/jakanddaxter/regs/SnowyMountainRegions.py create mode 100644 worlds/jakanddaxter/regs/SpiderCaveRegions.py create mode 100644 worlds/jakanddaxter/regs/VolcanicCraterRegions.py create mode 100644 worlds/jakanddaxter/test/__init__.py create mode 100644 worlds/jakanddaxter/test/test_locations.py diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 36129465489d..397f63e7cab7 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -11,9 +11,9 @@ from NetUtils import ClientStatus from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled -from worlds.jakanddaxter.GameID import jak1_name -from worlds.jakanddaxter.client.ReplClient import JakAndDaxterReplClient -from worlds.jakanddaxter.client.MemoryReader import JakAndDaxterMemoryReader +from .GameID import jak1_name +from .client.ReplClient import JakAndDaxterReplClient +from .client.MemoryReader import JakAndDaxterMemoryReader import ModuleUpdate ModuleUpdate.update() diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py index 555be696af49..85dd32a21344 100644 --- a/worlds/jakanddaxter/GameID.py +++ b/worlds/jakanddaxter/GameID.py @@ -1,5 +1,8 @@ # All Jak And Daxter Archipelago IDs must be offset by this number. jak1_id = 741000000 +# This is maximum ID we will allow. +jak1_max = jak1_id + 999999 + # The name of the game. jak1_name = "Jak and Daxter The Precursor Legacy" diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 56743b7cef0b..4f94e7b85c06 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -1,6 +1,10 @@ from BaseClasses import Item -from .GameID import jak1_name -from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials +from .GameID import jak1_name, jak1_max +from .locs import (OrbLocations as Orbs, + CellLocations as Cells, + ScoutLocations as Scouts, + SpecialLocations as Specials, + OrbCacheLocations as Caches) class JakAndDaxterItem(Item): @@ -34,13 +38,14 @@ class JakAndDaxterItem(Item): 91: "Scout Fly - GMC", } -# TODO - Orbs are also generic and interchangeable. -# orb_item_table = { -# ???: "Precursor Orb", -# } +# Orbs are also generic and interchangeable. +orb_item_table = { + 1: "Precursor Orb", +} # These are special items representing unique unlocks in the world. Notice that their Item ID equals their # respective Location ID. Like scout flies, this is necessary for game<->archipelago communication. +# TODO - These numbers of checks may be inaccurate post-region refactor. special_item_table = { 5: "Fisherman's Boat", # Unlocks 14 checks in Misty Island 4: "Jungle Elevator", # Unlocks 2 checks in Forbidden Jungle @@ -56,11 +61,32 @@ class JakAndDaxterItem(Item): 70: "Freed The Green Sage", # Unlocks the final elevator } +# These are the move items for move randomizer. Notice that their Item ID equals some of the Orb Cache Location ID's. +# This was 100% arbitrary. There's no reason to tie moves to orb caches except that I need a place to put them. ;_; +move_item_table = { + 10344: "Crouch", + 10369: "Crouch Jump", + 11072: "Crouch Uppercut", + 12634: "Roll", + 12635: "Roll Jump", + 10945: "Double Jump", + 14507: "Jump Dive", + 14838: "Jump Kick", + 23348: "Punch", + 23349: "Punch Uppercut", + 23350: "Kick", + # 24038: "Orb Cache at End of Blast Furnace", # TODO - IDK, we didn't need all of the orb caches for move rando. + # 24039: "Orb Cache at End of Launch Pad Room", + # 24040: "Orb Cache at Start of Launch Pad Room", +} + # All Items # While we're here, do all the ID conversions needed. item_table = { **{Cells.to_ap_id(k): cell_item_table[k] for k in cell_item_table}, **{Scouts.to_ap_id(k): scout_item_table[k] for k in scout_item_table}, - # **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table}, + **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table}, **{Specials.to_ap_id(k): special_item_table[k] for k in special_item_table}, + **{Caches.to_ap_id(k): move_item_table[k] for k in move_item_table}, + jak1_max: "Green Eco Pill" # Filler item. } diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index cfed39815c10..de5581f4c959 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -3,18 +3,21 @@ from Options import Toggle, PerGameCommonOptions -# class EnableScoutFlies(Toggle): -# """Enable to include each Scout Fly as a check. Adds 112 checks to the pool.""" -# display_name = "Enable Scout Flies" +class EnableMoveRandomizer(Toggle): + """Enable to include movement options as items in the randomizer. + Jak is only able to run, swim, and single jump, until you find his other moves. + Adds 11 items to the pool.""" + display_name = "Enable Move Randomizer" -# class EnablePrecursorOrbs(Toggle): -# """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool.""" -# display_name = "Enable Precursor Orbs" +# class EnableOrbsanity(Toggle): +# """Enable to include Precursor Orbs as an ordered list of progressive checks. +# Each orb you collect triggers the next release in the list. +# Adds 2000 items to the pool.""" +# display_name = "Enable Orbsanity" @dataclass class JakAndDaxterOptions(PerGameCommonOptions): - # enable_scout_flies: EnableScoutFlies - # enable_precursor_orbs: EnablePrecursorOrbs - pass + enable_move_randomizer: EnableMoveRandomizer + # enable_orbsanity: EnableOrbsanity diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index ac18ce84c829..3cea4f2aced2 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,6 +1,9 @@ from BaseClasses import Location from .GameID import jak1_name -from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials +from .locs import (CellLocations as Cells, + ScoutLocations as Scouts, + SpecialLocations as Specials, + OrbCacheLocations as Caches) class JakAndDaxterLocation(Location): @@ -44,4 +47,5 @@ class JakAndDaxterLocation(Location): **{Scouts.to_ap_id(k): Scouts.locLT_scoutTable[k] for k in Scouts.locLT_scoutTable}, **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable}, **{Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable}, + **{Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable}, } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 44490c9bc4ed..4e4ca73b3245 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,278 +1,81 @@ import typing -from enum import Enum, auto -from BaseClasses import MultiWorld, Region -from .GameID import jak1_name +from BaseClasses import MultiWorld +from .Items import item_table from .JakAndDaxterOptions import JakAndDaxterOptions -from .Locations import JakAndDaxterLocation, location_table -from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials +from .locs import (CellLocations as Cells, + ScoutLocations as Scouts) +from .regs.RegionBase import JakAndDaxterRegion +from .regs import (GeyserRockRegions as GeyserRock, + SandoverVillageRegions as SandoverVillage, + ForbiddenJungleRegions as ForbiddenJungle, + SentinelBeachRegions as SentinelBeach, + MistyIslandRegions as MistyIsland, + FireCanyonRegions as FireCanyon, + RockVillageRegions as RockVillage, + PrecursorBasinRegions as PrecursorBasin, + LostPrecursorCityRegions as LostPrecursorCity, + BoggySwampRegions as BoggySwamp, + MountainPassRegions as MountainPass, + VolcanicCraterRegions as VolcanicCrater, + SpiderCaveRegions as SpiderCave, + SnowyMountainRegions as SnowyMountain, + LavaTubeRegions as LavaTube, + GolAndMaiasCitadelRegions as GolAndMaiasCitadel) -class JakAndDaxterRegion(Region): - game: str = jak1_name - - -# Holds information like the level name, the number of orbs available there, etc. Applies to both Levels and SubLevels. -# We especially need orb_counts to be tracked here because we need to know how many orbs you have access to -# in order to know when you can afford the 90-orb and 120-orb payments for more checks. -class Jak1LevelInfo: - name: str - orb_count: int - - def __init__(self, name: str, orb_count: int): - self.name = name - self.orb_count = orb_count - - -class Jak1Level(int, Enum): - SCOUT_FLY_POWER_CELLS = auto() # This is a virtual location to reward you receiving 7 scout flies. - GEYSER_ROCK = auto() - SANDOVER_VILLAGE = auto() - FORBIDDEN_JUNGLE = auto() - SENTINEL_BEACH = auto() - MISTY_ISLAND = auto() - FIRE_CANYON = auto() - ROCK_VILLAGE = auto() - PRECURSOR_BASIN = auto() - LOST_PRECURSOR_CITY = auto() - BOGGY_SWAMP = auto() - MOUNTAIN_PASS = auto() - VOLCANIC_CRATER = auto() - SPIDER_CAVE = auto() - SNOWY_MOUNTAIN = auto() - LAVA_TUBE = auto() - GOL_AND_MAIAS_CITADEL = auto() - - -class Jak1SubLevel(int, Enum): - FORBIDDEN_JUNGLE_SWITCH_ROOM = auto() - FORBIDDEN_JUNGLE_PLANT_ROOM = auto() - SENTINEL_BEACH_CANNON_TOWER = auto() - ROCK_VILLAGE_PONTOON_BRIDGE = auto() - BOGGY_SWAMP_FLUT_FLUT = auto() - MOUNTAIN_PASS_SHORTCUT = auto() - SNOWY_MOUNTAIN_FLUT_FLUT = auto() - SNOWY_MOUNTAIN_LURKER_FORT = auto() - SNOWY_MOUNTAIN_FROZEN_BOX = auto() - GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = auto() - GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto() - - -level_table: typing.Dict[Jak1Level, Jak1LevelInfo] = { - Jak1Level.SCOUT_FLY_POWER_CELLS: - Jak1LevelInfo("Scout Fly Power Cells", 0), # Virtual location. - Jak1Level.GEYSER_ROCK: - Jak1LevelInfo("Geyser Rock", 50), - Jak1Level.SANDOVER_VILLAGE: - Jak1LevelInfo("Sandover Village", 50), - Jak1Level.FORBIDDEN_JUNGLE: - Jak1LevelInfo("Forbidden Jungle", 99), - Jak1Level.SENTINEL_BEACH: - Jak1LevelInfo("Sentinel Beach", 128), - Jak1Level.MISTY_ISLAND: - Jak1LevelInfo("Misty Island", 150), - Jak1Level.FIRE_CANYON: - Jak1LevelInfo("Fire Canyon", 50), - Jak1Level.ROCK_VILLAGE: - Jak1LevelInfo("Rock Village", 43), - Jak1Level.PRECURSOR_BASIN: - Jak1LevelInfo("Precursor Basin", 200), - Jak1Level.LOST_PRECURSOR_CITY: - Jak1LevelInfo("Lost Precursor City", 200), - Jak1Level.BOGGY_SWAMP: - Jak1LevelInfo("Boggy Swamp", 177), - Jak1Level.MOUNTAIN_PASS: - Jak1LevelInfo("Mountain Pass", 50), - Jak1Level.VOLCANIC_CRATER: - Jak1LevelInfo("Volcanic Crater", 50), - Jak1Level.SPIDER_CAVE: - Jak1LevelInfo("Spider Cave", 200), - Jak1Level.SNOWY_MOUNTAIN: - Jak1LevelInfo("Snowy Mountain", 113), - Jak1Level.LAVA_TUBE: - Jak1LevelInfo("Lava Tube", 50), - Jak1Level.GOL_AND_MAIAS_CITADEL: - Jak1LevelInfo("Gol and Maia's Citadel", 180), -} - -sub_level_table: typing.Dict[Jak1SubLevel, Jak1LevelInfo] = { - Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: - Jak1LevelInfo("Forbidden Jungle Switch Room", 24), - Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: - Jak1LevelInfo("Forbidden Jungle Plant Room", 27), - Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: - Jak1LevelInfo("Sentinel Beach Cannon Tower", 22), - Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE: - Jak1LevelInfo("Rock Village Pontoon Bridge", 7), - Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: - Jak1LevelInfo("Boggy Swamp Flut Flut", 23), - Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: - Jak1LevelInfo("Mountain Pass Shortcut", 0), - Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: - Jak1LevelInfo("Snowy Mountain Flut Flut", 15), - Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT: - Jak1LevelInfo("Snowy Mountain Lurker Fort", 72), - Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: - Jak1LevelInfo("Snowy Mountain Frozen Box", 0), - Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: - Jak1LevelInfo("Gol and Maia's Citadel Rotating Tower", 20), - Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: - Jak1LevelInfo("Gol and Maia's Citadel Final Boss", 0), -} - - -# Use the original game ID's for each item to tell the Region which Locations are available in it. -# You do NOT need to add the item offsets or game ID, that will be handled by create_*_locations. def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): # Always start with Menu. - multiworld.regions.append(JakAndDaxterRegion("Menu", player, multiworld)) - - region_7sf = create_region(player, multiworld, Jak1Level.SCOUT_FLY_POWER_CELLS) - create_cell_locations(region_7sf, Cells.loc7SF_cellTable) - - region_gr = create_region(player, multiworld, Jak1Level.GEYSER_ROCK) - create_cell_locations(region_gr, Cells.locGR_cellTable) - create_fly_locations(region_gr, Scouts.locGR_scoutTable) - - region_sv = create_region(player, multiworld, Jak1Level.SANDOVER_VILLAGE) - create_cell_locations(region_sv, Cells.locSV_cellTable) - create_fly_locations(region_sv, Scouts.locSV_scoutTable) - - region_fj = create_region(player, multiworld, Jak1Level.FORBIDDEN_JUNGLE) - create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9}}) - create_fly_locations(region_fj, Scouts.locFJ_scoutTable) - create_special_locations(region_fj, {k: Specials.loc_specialTable[k] for k in {4, 5}}) - - sub_region_fjsr = create_subregion(region_fj, Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM) - create_cell_locations(sub_region_fjsr, {k: Cells.locFJ_cellTable[k] for k in {2}}) - create_special_locations(sub_region_fjsr, {k: Specials.loc_specialTable[k] for k in {2}}) - - sub_region_fjpr = create_subregion(sub_region_fjsr, Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM) - create_cell_locations(sub_region_fjpr, {k: Cells.locFJ_cellTable[k] for k in {6}}) - - region_sb = create_region(player, multiworld, Jak1Level.SENTINEL_BEACH) - create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22}}) - create_fly_locations(region_sb, Scouts.locSB_scoutTable) - create_special_locations(region_sb, {k: Specials.loc_specialTable[k] for k in {17}}) - - sub_region_sbct = create_subregion(region_sb, Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER) - create_cell_locations(sub_region_sbct, {k: Cells.locSB_cellTable[k] for k in {19}}) - - region_mi = create_region(player, multiworld, Jak1Level.MISTY_ISLAND) - create_cell_locations(region_mi, Cells.locMI_cellTable) - create_fly_locations(region_mi, Scouts.locMI_scoutTable) - - region_fc = create_region(player, multiworld, Jak1Level.FIRE_CANYON) - create_cell_locations(region_fc, Cells.locFC_cellTable) - create_fly_locations(region_fc, Scouts.locFC_scoutTable) - - region_rv = create_region(player, multiworld, Jak1Level.ROCK_VILLAGE) - create_cell_locations(region_rv, Cells.locRV_cellTable) - create_fly_locations(region_rv, {k: Scouts.locRV_scoutTable[k] - for k in {76, 131148, 196684, 262220, 65612, 327756}}) - create_special_locations(region_rv, {k: Specials.loc_specialTable[k] for k in {33}}) - - sub_region_rvpb = create_subregion(region_rv, Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE) - create_fly_locations(sub_region_rvpb, {k: Scouts.locRV_scoutTable[k] for k in {393292}}) - - region_pb = create_region(player, multiworld, Jak1Level.PRECURSOR_BASIN) - create_cell_locations(region_pb, Cells.locPB_cellTable) - create_fly_locations(region_pb, Scouts.locPB_scoutTable) - - region_lpc = create_region(player, multiworld, Jak1Level.LOST_PRECURSOR_CITY) - create_cell_locations(region_lpc, Cells.locLPC_cellTable) - create_fly_locations(region_lpc, Scouts.locLPC_scoutTable) - - region_bs = create_region(player, multiworld, Jak1Level.BOGGY_SWAMP) - create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}}) - create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {43, 393259, 65579, 262187, 196651}}) - - sub_region_bsff = create_subregion(region_bs, Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT) - create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {37}}) - create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {327723, 131115}}) - - region_mp = create_region(player, multiworld, Jak1Level.MOUNTAIN_PASS) - create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86, 87}}) - create_fly_locations(region_mp, Scouts.locMP_scoutTable) - - sub_region_mps = create_subregion(region_mp, Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT) - create_cell_locations(sub_region_mps, {k: Cells.locMP_cellTable[k] for k in {110}}) - - region_vc = create_region(player, multiworld, Jak1Level.VOLCANIC_CRATER) - create_cell_locations(region_vc, Cells.locVC_cellTable) - create_fly_locations(region_vc, Scouts.locVC_scoutTable) - create_special_locations(region_vc, {k: Specials.loc_specialTable[k] for k in {105}}) - - region_sc = create_region(player, multiworld, Jak1Level.SPIDER_CAVE) - create_cell_locations(region_sc, Cells.locSC_cellTable) - create_fly_locations(region_sc, Scouts.locSC_scoutTable) - - region_sm = create_region(player, multiworld, Jak1Level.SNOWY_MOUNTAIN) - create_cell_locations(region_sm, {k: Cells.locSM_cellTable[k] for k in {60, 61, 66, 64}}) - create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {65, 327745, 65601, 131137, 393281}}) - create_special_locations(region_sm, {k: Specials.loc_specialTable[k] for k in {60}}) - - sub_region_smfb = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX) - create_cell_locations(sub_region_smfb, {k: Cells.locSM_cellTable[k] for k in {67}}) - - sub_region_smff = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT) - create_cell_locations(sub_region_smff, {k: Cells.locSM_cellTable[k] for k in {63}}) - create_special_locations(sub_region_smff, {k: Specials.loc_specialTable[k] for k in {63}}) - - sub_region_smlf = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT) - create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62}}) - create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {196673, 262209}}) - - region_lt = create_region(player, multiworld, Jak1Level.LAVA_TUBE) - create_cell_locations(region_lt, Cells.locLT_cellTable) - create_fly_locations(region_lt, Scouts.locLT_scoutTable) - - region_gmc = create_region(player, multiworld, Jak1Level.GOL_AND_MAIAS_CITADEL) - create_cell_locations(region_gmc, {k: Cells.locGMC_cellTable[k] for k in {71, 72, 73}}) - create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k] - for k in {91, 65627, 196699, 262235, 393307, 131163}}) - create_special_locations(region_gmc, {k: Specials.loc_specialTable[k] for k in {71, 72, 73}}) - - sub_region_gmcrt = create_subregion(region_gmc, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER) - create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70}}) - create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {327771}}) - create_special_locations(sub_region_gmcrt, {k: Specials.loc_specialTable[k] for k in {70}}) - - create_subregion(sub_region_gmcrt, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS) - - -def create_region(player: int, multiworld: MultiWorld, level: Jak1Level) -> JakAndDaxterRegion: - name = level_table[level].name - region = JakAndDaxterRegion(name, player, multiworld) - multiworld.regions.append(region) - return region - - -def create_subregion(parent: Region, sub_level: Jak1SubLevel) -> JakAndDaxterRegion: - name = sub_level_table[sub_level].name - region = JakAndDaxterRegion(name, parent.player, parent.multiworld) - parent.multiworld.regions.append(region) - return region - - -def create_cell_locations(region: Region, locations: typing.Dict[int, str]): - region.locations += [JakAndDaxterLocation(region.player, - location_table[Cells.to_ap_id(loc)], - Cells.to_ap_id(loc), - region) for loc in locations] - - -def create_fly_locations(region: Region, locations: typing.Dict[int, str]): - region.locations += [JakAndDaxterLocation(region.player, - location_table[Scouts.to_ap_id(loc)], - Scouts.to_ap_id(loc), - region) for loc in locations] - - -# Special Locations should be matched alongside their respective Power Cell Locations, -# so you get 2 unlocks for these rather than 1. -def create_special_locations(region: Region, locations: typing.Dict[int, str]): - region.locations += [JakAndDaxterLocation(region.player, - location_table[Specials.to_ap_id(loc)], - Specials.to_ap_id(loc), - region) for loc in locations] + menu = JakAndDaxterRegion("Menu", player, multiworld) + multiworld.regions.append(menu) + + # Build the special "Free 7 Scout Flies" Region. This is a virtual region always accessible to Menu. + # The Power Cells within it are automatically checked when you receive the 7th scout fly for the corresponding cell. + free7 = JakAndDaxterRegion("'Free 7 Scout Flies' Power Cells", player, multiworld) + free7.add_cell_locations(Cells.loc7SF_cellTable.keys()) + for scout_fly_cell in free7.locations: + + # Translate from Cell AP ID to Scout AP ID using game ID as an intermediary. + scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(scout_fly_cell.address)) + scout_fly_cell.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7) + multiworld.regions.append(free7) + + # Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules. + [gr] = GeyserRock.build_regions("Geyser Rock", player, multiworld) + [sv] = SandoverVillage.build_regions("Sandover Village", player, multiworld) + [fj] = ForbiddenJungle.build_regions("Forbidden Jungle", player, multiworld) + [sb] = SentinelBeach.build_regions("Sentinel Beach", player, multiworld) + [mi] = MistyIsland.build_regions("Misty Island", player, multiworld) + [fc] = FireCanyon.build_regions("Fire Canyon", player, multiworld) + [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", player, multiworld) + [pb] = PrecursorBasin.build_regions("Precursor Basin", player, multiworld) + [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", player, multiworld) + [bs] = BoggySwamp.build_regions("Boggy Swamp", player, multiworld) + [mp, mpr] = MountainPass.build_regions("Mountain Pass", player, multiworld) + [vc] = VolcanicCrater.build_regions("Volcanic Crater", player, multiworld) + [sc] = SpiderCave.build_regions("Spider Cave", player, multiworld) + [sm] = SnowyMountain.build_regions("Snowy Mountain", player, multiworld) + [lt] = LavaTube.build_regions("Lava Tube", player, multiworld) + [gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", player, multiworld) + + # Define the interconnecting rules. + menu.connect(free7) + menu.connect(gr) + gr.connect(sv) # Geyser Rock modified to let you leave at any time. + sv.connect(fj) + sv.connect(sb) + sv.connect(mi, rule=lambda state: state.has("Fisherman's Boat", player)) + sv.connect(fc, rule=lambda state: state.has("Power Cell", player, 20)) + fc.connect(rv) + rv.connect(pb) + rv.connect(lpc) + rvp.connect(bs) # rv->rvp/rvc connections defined internally by RockVillageRegions. + rvc.connect(mp, rule=lambda state: state.has("Power Cell", player, 45)) + mpr.connect(vc) # mp->mpr connection defined internally by MountainPassRegions. + vc.connect(sc) + vc.connect(sm, rule=lambda state: state.has("Snowy Mountain Gondola", player)) + vc.connect(lt, rule=lambda state: state.has("Power Cell", player, 72)) + lt.connect(gmc) # gmc->fb connection defined internally by GolAndMaiasCitadelRegions. + + # Finally, set the completion condition. + multiworld.completion_condition[player] = lambda state: state.can_reach(fb, "Region", player) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 8ce4d5c25526..9ff3d29d3ecc 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,270 +1,40 @@ -from typing import List - +import typing from BaseClasses import MultiWorld, CollectionState from .JakAndDaxterOptions import JakAndDaxterOptions -from .Regions import Jak1Level, Jak1SubLevel, level_table, sub_level_table -from .Items import item_table -from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials -from worlds.jakanddaxter.Locations import location_table - - -def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - - # Setting up some useful variables here because the offset numbers can get confusing - # for access rules. Feel free to add more variables here to keep the code more readable. - # You DO need to convert the game ID's to AP ID's here. - power_cell = item_table[Cells.to_ap_id(0)] - - # The int/list structure here is intentional, see `set_trade_requirements` for how we handle these. - sv_traders = [11, 12, [13, 14]] # Mayor, Uncle, Oracle 1 and 2 - rv_traders = [31, 32, 33, [34, 35]] # Geologist, Gambler, Warrior, Oracle 3 and 4 - vc_traders = [[96, 97, 98, 99], [100, 101]] # Miners 1-4, Oracle 5 and 6 - - fj_jungle_elevator = item_table[Specials.to_ap_id(4)] - fj_blue_switch = item_table[Specials.to_ap_id(2)] - fj_fisherman = item_table[Specials.to_ap_id(5)] - - sb_flut_flut = item_table[Specials.to_ap_id(17)] - rv_pontoon_bridge = item_table[Specials.to_ap_id(33)] - - sm_yellow_switch = item_table[Specials.to_ap_id(60)] - sm_fort_gate = item_table[Specials.to_ap_id(63)] - sm_gondola = item_table[Specials.to_ap_id(105)] - - gmc_blue_sage = item_table[Specials.to_ap_id(71)] - gmc_red_sage = item_table[Specials.to_ap_id(72)] - gmc_yellow_sage = item_table[Specials.to_ap_id(73)] - gmc_green_sage = item_table[Specials.to_ap_id(70)] - - # Start connecting regions and set their access rules. - - # Scout Fly Power Cells is a virtual region, not a physical one, so connect it to Menu. - connect_start(multiworld, player, Jak1Level.SCOUT_FLY_POWER_CELLS) - set_fly_requirements(multiworld, player) - - # You start the game in front of Green Sage's Hut, so you don't get stuck on Geyser Rock in the first 5 minutes. - connect_start(multiworld, player, Jak1Level.SANDOVER_VILLAGE) - set_trade_requirements(multiworld, player, Jak1Level.SANDOVER_VILLAGE, sv_traders, 1530) - - # Geyser Rock is accessible at any time, just check the 3 naked cell Locations to return. - connect_regions(multiworld, player, - Jak1Level.SANDOVER_VILLAGE, - Jak1Level.GEYSER_ROCK) - - connect_regions(multiworld, player, - Jak1Level.SANDOVER_VILLAGE, - Jak1Level.FORBIDDEN_JUNGLE) - - connect_region_to_sub(multiworld, player, - Jak1Level.FORBIDDEN_JUNGLE, - Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, - lambda state: state.has(fj_jungle_elevator, player)) - - connect_subregions(multiworld, player, - Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM, - Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - lambda state: state.has(fj_blue_switch, player)) - - # You just need to defeat the plant boss to escape this subregion, no specific Item required. - connect_sub_to_region(multiworld, player, - Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM, - Jak1Level.FORBIDDEN_JUNGLE) - - connect_regions(multiworld, player, - Jak1Level.SANDOVER_VILLAGE, - Jak1Level.SENTINEL_BEACH) - - # Just jump off the tower to escape this subregion. - connect_region_to_sub(multiworld, player, - Jak1Level.SENTINEL_BEACH, - Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER, - lambda state: state.has(fj_blue_switch, player)) - - connect_regions(multiworld, player, - Jak1Level.SANDOVER_VILLAGE, - Jak1Level.MISTY_ISLAND, - lambda state: state.has(fj_fisherman, player)) - - connect_regions(multiworld, player, - Jak1Level.SANDOVER_VILLAGE, - Jak1Level.FIRE_CANYON, - lambda state: state.has(power_cell, player, 20)) - - connect_regions(multiworld, player, - Jak1Level.FIRE_CANYON, - Jak1Level.ROCK_VILLAGE) - set_trade_requirements(multiworld, player, Jak1Level.ROCK_VILLAGE, rv_traders, 1530) - - connect_regions(multiworld, player, - Jak1Level.ROCK_VILLAGE, - Jak1Level.PRECURSOR_BASIN) - - connect_regions(multiworld, player, - Jak1Level.ROCK_VILLAGE, - Jak1Level.LOST_PRECURSOR_CITY) - - # This pontoon bridge locks out Boggy Swamp and Mountain Pass, - # effectively making it required to complete the game. - connect_region_to_sub(multiworld, player, - Jak1Level.ROCK_VILLAGE, - Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE, - lambda state: state.has(rv_pontoon_bridge, player)) - - connect_sub_to_region(multiworld, player, - Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE, - Jak1Level.BOGGY_SWAMP) - - # Flut Flut only has one landing pad here, so leaving this subregion is as easy - # as dismounting Flut Flut right where you found her. - connect_region_to_sub(multiworld, player, - Jak1Level.BOGGY_SWAMP, - Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT, - lambda state: state.has(sb_flut_flut, player)) - - connect_sub_to_region(multiworld, player, - Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE, - Jak1Level.MOUNTAIN_PASS, - lambda state: state.has(power_cell, player, 45)) - - connect_region_to_sub(multiworld, player, - Jak1Level.MOUNTAIN_PASS, - Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT, - lambda state: state.has(sm_yellow_switch, player)) - - connect_regions(multiworld, player, - Jak1Level.MOUNTAIN_PASS, - Jak1Level.VOLCANIC_CRATER) - set_trade_requirements(multiworld, player, Jak1Level.VOLCANIC_CRATER, vc_traders, 1530) - - connect_regions(multiworld, player, - Jak1Level.VOLCANIC_CRATER, - Jak1Level.SPIDER_CAVE) - - # Custom-added unlock for snowy mountain's gondola. - connect_regions(multiworld, player, - Jak1Level.VOLCANIC_CRATER, - Jak1Level.SNOWY_MOUNTAIN, - lambda state: state.has(sm_gondola, player)) - - connect_region_to_sub(multiworld, player, - Jak1Level.SNOWY_MOUNTAIN, - Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX, - lambda state: state.has(sm_yellow_switch, player)) - - # Flut Flut has both a start and end landing pad here, but there's an elevator that takes you up - # from the end pad to the entrance of the fort, so you're back to the "main area." - connect_region_to_sub(multiworld, player, - Jak1Level.SNOWY_MOUNTAIN, - Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT, - lambda state: state.has(sb_flut_flut, player)) - - connect_region_to_sub(multiworld, player, - Jak1Level.SNOWY_MOUNTAIN, - Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT, - lambda state: state.has(sm_fort_gate, player)) - - connect_regions(multiworld, player, - Jak1Level.VOLCANIC_CRATER, - Jak1Level.LAVA_TUBE, - lambda state: state.has(power_cell, player, 72)) - - connect_regions(multiworld, player, - Jak1Level.LAVA_TUBE, - Jak1Level.GOL_AND_MAIAS_CITADEL) - - # The stairs up to Samos's cage is only activated when you get the Items for freeing the other 3 Sages. - # But you can climb back down that staircase (or fall down from the top) to escape this subregion. - connect_region_to_sub(multiworld, player, - Jak1Level.GOL_AND_MAIAS_CITADEL, - Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - lambda state: state.has(gmc_blue_sage, player) and - state.has(gmc_red_sage, player) and - state.has(gmc_yellow_sage, player)) - - # This is the final elevator, only active when you get the Item for freeing the Green Sage. - connect_subregions(multiworld, player, - Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER, - Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS, - lambda state: state.has(gmc_green_sage, player)) - - multiworld.completion_condition[player] = lambda state: state.can_reach( - multiworld.get_region(sub_level_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS].name, player), - "Region", - player) - - -def connect_start(multiworld: MultiWorld, player: int, target: Jak1Level): - menu_region = multiworld.get_region("Menu", player) - start_region = multiworld.get_region(level_table[target].name, player) - menu_region.connect(start_region) - - -def connect_regions(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1Level, rule=None): - source_region = multiworld.get_region(level_table[source].name, player) - target_region = multiworld.get_region(level_table[target].name, player) - source_region.connect(target_region, rule=rule) - - -def connect_region_to_sub(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1SubLevel, rule=None): - source_region = multiworld.get_region(level_table[source].name, player) - target_region = multiworld.get_region(sub_level_table[target].name, player) - source_region.connect(target_region, rule=rule) - - -def connect_sub_to_region(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1Level, rule=None): - source_region = multiworld.get_region(sub_level_table[source].name, player) - target_region = multiworld.get_region(level_table[target].name, player) - source_region.connect(target_region, rule=rule) - - -def connect_subregions(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1SubLevel, rule=None): - source_region = multiworld.get_region(sub_level_table[source].name, player) - target_region = multiworld.get_region(sub_level_table[target].name, player) - source_region.connect(target_region, rule=rule) - - -# The "Free 7 Scout Fly" Locations are automatically checked when you receive the 7th scout fly Item. -def set_fly_requirements(multiworld: MultiWorld, player: int): - region = multiworld.get_region(level_table[Jak1Level.SCOUT_FLY_POWER_CELLS].name, player) - for loc in region.locations: - scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(loc.address)) # Translate using game ID as an intermediary. - loc.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7) +from .locs import CellLocations as Cells +from .Locations import location_table +from .Regions import JakAndDaxterRegion # TODO - Until we come up with a better progressive system for the traders (that avoids hard-locking if you pay the # wrong ones and can't afford the right ones) just make all the traders locked behind the total amount to pay them all. -def set_trade_requirements(multiworld: MultiWorld, player: int, level: Jak1Level, traders: List, orb_count: int): - - def count_accessible_orbs(state) -> int: - accessible_orbs = 0 - for level_info in [*level_table.values(), *sub_level_table.values()]: - reg = multiworld.get_region(level_info.name, player) - if reg.can_reach(state): - accessible_orbs += level_info.orb_count - return accessible_orbs - - region = multiworld.get_region(level_table[level].name, player) - names_to_index = {region.locations[i].name: i for i in range(0, len(region.locations))} - for trader in traders: - - # Singleton integers indicate a trader who has only one Location to check. - # (Mayor, Uncle, etc) - if type(trader) is int: - loc = region.locations[names_to_index[location_table[Cells.to_ap_id(trader)]]] - loc.access_rule = lambda state, orbs=orb_count: ( - count_accessible_orbs(state) >= orbs) - - # Lists of integers indicate a trader who has sequential Locations to check, each dependent on the last. - # (Oracles and Miners) - elif type(trader) is list: - previous_loc = None - for trade in trader: - loc = region.locations[names_to_index[location_table[Cells.to_ap_id(trade)]]] - loc.access_rule = lambda state, orbs=orb_count, prev=previous_loc: ( - count_accessible_orbs(state) >= orbs and - (state.can_reach(prev, player) if prev else True)) # TODO - Can Reach or Has Reached? - previous_loc = loc - - # Any other type of element in the traders list is wrong. - else: - raise TypeError(f"Tried to set trade requirements on an unknown type {trader}.") +def can_trade(state: CollectionState, + player: int, + multiworld: MultiWorld, + required_orbs: int, + required_previous_trade: int = None) -> bool: + + accessible_orbs = 0 + for region in multiworld.get_regions(player): + if state.can_reach(region, "Region", player): + accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count + + if required_previous_trade: + name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)] + return (accessible_orbs >= required_orbs + and state.can_reach(name_of_previous_trade, "Location", player=player)) + else: + return accessible_orbs >= required_orbs + + +def can_free_scout_flies(state: CollectionState, player: int) -> bool: + return (state.has("Jump Dive", player) + or (state.has("Crouch", player) + and state.has("Crouch Uppercut", player))) + + +def can_fight(state: CollectionState, player: int) -> bool: + return (state.has("Jump Dive", player) + or state.has("Jump Kick", player) + or state.has("Punch", player) + or state.has("Kick", player)) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 5cbf3ca4e4c1..2ba611d76b2e 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,16 +1,18 @@ import typing import settings -from Utils import local_path +from Utils import local_path, visualize_regions from BaseClasses import Item, ItemClassification, Tutorial -from .GameID import jak1_id, jak1_name +from .GameID import jak1_id, jak1_name, jak1_max from .JakAndDaxterOptions import JakAndDaxterOptions -from .Items import JakAndDaxterItem from .Locations import JakAndDaxterLocation, location_table from .Items import JakAndDaxterItem, item_table -from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs, SpecialLocations as Specials +from .locs import (CellLocations as Cells, + ScoutLocations as Scouts, + SpecialLocations as Specials, + OrbCacheLocations as Caches, + OrbLocations as Orbs) from .Regions import create_regions -from .Rules import set_rules from worlds.AutoWorld import World, WebWorld from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths @@ -34,7 +36,7 @@ class RootDirectory(settings.UserFolderPath): """Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe).""" description = "ArchipelaGOAL Root Directory" - root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL/out/build/Release/bin") + root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL/out/build/Release/bin") class JakAndDaxterWebWorld(WebWorld): @@ -83,17 +85,18 @@ class JakAndDaxterWorld(World): "Scout Flies": {item_table[k]: k for k in item_table if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)}, "Specials": {item_table[k]: k for k in item_table - if k in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset)}, - # TODO - Make group for Precursor Orbs. - # "Precursor Orbs": {item_table[k]: k for k in item_table - # if k in range(jak1_id + Orbs.orb_offset, ???)}, + if k in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset)}, + "Moves": {item_table[k]: k for k in item_table + if k in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset)}, + "Precursor Orbs": {item_table[k]: k for k in item_table + if k in range(jak1_id + Orbs.orb_offset, jak1_max)}, } + # Regions and Rules + # This will also set Locations, Location access rules, Region access rules, etc. def create_regions(self): create_regions(self.multiworld, self.options, self.player) - - def set_rules(self): - set_rules(self.multiworld, self.options, self.player) + # visualize_regions(self.multiworld.get_region("Menu", self.player), "jak.puml") # Helper function to reuse some nasty if/else trees. @staticmethod @@ -109,14 +112,25 @@ def item_type_helper(item) -> (int, ItemClassification): count = 7 # Make only 1 of each Special Item. - elif item in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset): + elif item in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset): classification = ItemClassification.progression count = 1 - # TODO - Make ??? Precursor Orbs. - # elif item in range(jak1_id + Orbs.orb_offset, ???): - # classification = ItemClassification.filler - # count = ??? + # Make only 1 of each Move Item. + elif item in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset): + classification = ItemClassification.progression + count = 1 + + # TODO - Make 2000 Precursor Orbs, ONLY IF Orbsanity is enabled. + elif item in range(jak1_id + Orbs.orb_offset, jak1_max): + classification = ItemClassification.progression_skip_balancing + count = 0 + + # Under normal circumstances, we will create 0 filler items. + # We will manually create filler items as needed. + elif item == jak1_max: + classification = ItemClassification.filler + count = 0 # If we try to make items with ID's higher than we've defined, something has gone wrong. else: @@ -126,8 +140,17 @@ def item_type_helper(item) -> (int, ItemClassification): def create_items(self): for item_id in item_table: - count, _ = self.item_type_helper(item_id) - self.multiworld.itempool += [self.create_item(item_table[item_id]) for k in range(0, count)] + + # Handle Move Randomizer option. + # If it is OFF, put all moves in your starting inventory instead of the item pool, + # then fill the item pool with a corresponding amount of filler items. + if not self.options.enable_move_randomizer and item_table[item_id] in self.item_name_groups["Moves"]: + self.multiworld.push_precollected(self.create_item(item_table[item_id])) + self.multiworld.itempool += [self.create_item(self.get_filler_item_name())] + else: + count, classification = self.item_type_helper(item_id) + self.multiworld.itempool += [JakAndDaxterItem(item_table[item_id], classification, item_id, self.player) + for _ in range(count)] def create_item(self, name: str) -> Item: item_id = self.item_name_to_id[name] @@ -135,7 +158,7 @@ def create_item(self, name: str) -> Item: return JakAndDaxterItem(name, classification, item_id, self.player) def get_filler_item_name(self) -> str: - return "Power Cell" # TODO - Make Precursor Orb the filler item. Until then, enjoy the free progression. + return "Green Eco Pill" def launch_client(): diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index c91eba8286d1..a4468364579f 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,34 +1,72 @@ import random import typing +import json import pymem from pymem import pattern from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError -import json +from dataclasses import dataclass from CommonClient import logger -from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials +from ..locs import (CellLocations as Cells, + ScoutLocations as Flies, + SpecialLocations as Specials, + OrbCacheLocations as Caches) # Some helpful constants. sizeof_uint64 = 8 sizeof_uint32 = 4 sizeof_uint8 = 1 -next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes. -next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes. -next_special_index_offset = 16 # Each of these is an uint64, so 8 bytes. -cells_checked_offset = 24 -buzzers_checked_offset = 428 # cells_checked_offset + (sizeof uint32 * 101 cells) -specials_checked_offset = 876 # buzzers_checked_offset + (sizeof uint32 * 112 buzzers) +# IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to +# their size in bits. The address for an N-bit field must be divisible by N. Use this class to define the memory offsets +# of important values in the struct. It will also do the byte alignment properly for you. +# See https://opengoal.dev/docs/reference/type_system/#arrays +@dataclass +class OffsetFactory: + current_offset: int = 0 + + def define(self, size: int, length: int = 1) -> int: + + # If necessary, align current_offset to the current size first. + bytes_to_alignment = self.current_offset % size + if bytes_to_alignment != 0: + self.current_offset += (size - bytes_to_alignment) + + # Increment current_offset so the next definition can be made. + offset_to_use = self.current_offset + self.current_offset += (size * length) + return offset_to_use + + +# Start defining important memory address offsets here. They must be in the same order, have the same sizes, and have +# the same lengths, as defined in `ap-info-jak1`. +offsets = OffsetFactory() + +# Cell, Buzzer, and Special information. +next_cell_index_offset = offsets.define(sizeof_uint64) +next_buzzer_index_offset = offsets.define(sizeof_uint64) +next_special_index_offset = offsets.define(sizeof_uint64) + +cells_checked_offset = offsets.define(sizeof_uint32, 101) +buzzers_checked_offset = offsets.define(sizeof_uint32, 112) +specials_checked_offset = offsets.define(sizeof_uint32, 32) -buzzers_received_offset = 1004 # specials_checked_offset + (sizeof uint32 * 32 specials) -specials_received_offset = 1020 # buzzers_received_offset + (sizeof uint8 * 16 levels (for scout fly groups)) +buzzers_received_offset = offsets.define(sizeof_uint8, 16) +specials_received_offset = offsets.define(sizeof_uint8, 32) -died_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials) +# Deathlink information. +died_offset = offsets.define(sizeof_uint8) +deathlink_enabled_offset = offsets.define(sizeof_uint8) -deathlink_enabled_offset = 1053 # died_offset + sizeof uint8 +# Move Rando information. +next_orb_cache_index_offset = offsets.define(sizeof_uint64) +orb_caches_checked_offset = offsets.define(sizeof_uint32, 16) +moves_received_offset = offsets.define(sizeof_uint8, 16) +moverando_enabled_offset = offsets.define(sizeof_uint8) -end_marker_offset = 1054 # deathlink_enabled_offset + sizeof uint8 +# The End. +end_marker_offset = offsets.define(sizeof_uint8, 4) # "Jak" to be replaced by player name in the Client. @@ -67,7 +105,6 @@ def autopsy(died: int) -> str: return "Jak got Flut Flut hurt." if died == 18: return "Jak poisoned the whole darn catch." - return "Jak died." @@ -170,50 +207,26 @@ def print_status(self): def read_memory(self) -> typing.List[int]: try: - next_cell_index = int.from_bytes( - self.gk_process.read_bytes(self.goal_address, sizeof_uint64), - byteorder="little", - signed=False) - next_buzzer_index = int.from_bytes( - self.gk_process.read_bytes(self.goal_address + next_buzzer_index_offset, sizeof_uint64), - byteorder="little", - signed=False) - next_special_index = int.from_bytes( - self.gk_process.read_bytes(self.goal_address + next_special_index_offset, sizeof_uint64), - byteorder="little", - signed=False) + next_cell_index = self.read_goal_address(0, sizeof_uint64) + next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64) + next_special_index = self.read_goal_address(next_special_index_offset, sizeof_uint64) for k in range(0, next_cell_index): - next_cell = int.from_bytes( - self.gk_process.read_bytes( - self.goal_address + cells_checked_offset + (k * sizeof_uint32), - sizeof_uint32), - byteorder="little", - signed=False) + next_cell = self.read_goal_address(cells_checked_offset + (k * sizeof_uint32), sizeof_uint32) cell_ap_id = Cells.to_ap_id(next_cell) if cell_ap_id not in self.location_outbox: self.location_outbox.append(cell_ap_id) logger.debug("Checked power cell: " + str(next_cell)) for k in range(0, next_buzzer_index): - next_buzzer = int.from_bytes( - self.gk_process.read_bytes( - self.goal_address + buzzers_checked_offset + (k * sizeof_uint32), - sizeof_uint32), - byteorder="little", - signed=False) + next_buzzer = self.read_goal_address(buzzers_checked_offset + (k * sizeof_uint32), sizeof_uint32) buzzer_ap_id = Flies.to_ap_id(next_buzzer) if buzzer_ap_id not in self.location_outbox: self.location_outbox.append(buzzer_ap_id) logger.debug("Checked scout fly: " + str(next_buzzer)) for k in range(0, next_special_index): - next_special = int.from_bytes( - self.gk_process.read_bytes( - self.goal_address + specials_checked_offset + (k * sizeof_uint32), - sizeof_uint32), - byteorder="little", - signed=False) + next_special = self.read_goal_address(specials_checked_offset + (k * sizeof_uint32), sizeof_uint32) # 112 is the game-task ID of `finalboss-movies`, which is written to this array when you grab # the white eco. This is our victory condition, so we need to catch it and act on it. @@ -228,29 +241,40 @@ def read_memory(self) -> typing.List[int]: self.location_outbox.append(special_ap_id) logger.debug("Checked special: " + str(next_special)) - died = int.from_bytes( - self.gk_process.read_bytes(self.goal_address + died_offset, sizeof_uint8), - byteorder="little", - signed=False) - + died = self.read_goal_address(died_offset, sizeof_uint8) if died > 0: self.send_deathlink = True self.cause_of_death = autopsy(died) - deathlink_flag = int.from_bytes( - self.gk_process.read_bytes(self.goal_address + deathlink_enabled_offset, sizeof_uint8), - byteorder="little", - signed=False) - # Listen for any changes to this setting. + deathlink_flag = self.read_goal_address(deathlink_enabled_offset, sizeof_uint8) self.deathlink_enabled = bool(deathlink_flag) + next_cache_index = self.read_goal_address(next_orb_cache_index_offset, sizeof_uint64) + + for k in range(0, next_cache_index): + next_cache = self.read_goal_address(orb_caches_checked_offset + (k * sizeof_uint32), sizeof_uint32) + cache_ap_id = Caches.to_ap_id(next_cache) + if cache_ap_id not in self.location_outbox: + self.location_outbox.append(cache_ap_id) + logger.debug("Checked orb cache: " + str(next_cache)) + + # Listen for any changes to this setting. + moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8) + self.moverando_enabled = bool(moverando_flag) + except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") self.connected = False return self.location_outbox + def read_goal_address(self, offset: int, length: int) -> int: + return int.from_bytes( + self.gk_process.read_bytes(self.goal_address + offset, length), + byteorder="little", + signed=False) + def save_data(self): with open("jakanddaxter_location_outbox.json", "w+") as f: dump = { diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 7c176c1224bf..c1fe78d3b221 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -10,13 +10,14 @@ from CommonClient import logger from NetUtils import NetworkItem -from worlds.jakanddaxter.GameID import jak1_id -from worlds.jakanddaxter.Items import item_table -from worlds.jakanddaxter.locs import ( +from ..GameID import jak1_id, jak1_max +from ..Items import item_table +from ..locs import ( + OrbLocations as Orbs, CellLocations as Cells, ScoutLocations as Flies, - OrbLocations as Orbs, - SpecialLocations as Specials) + SpecialLocations as Specials, + OrbCacheLocations as Caches) class JakAndDaxterReplClient: @@ -150,8 +151,10 @@ async def connect(self): "(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False): ok_count += 1 - # Disable cheat-mode and debug (close the visual cue). - # self.send_form("(set! *debug-segment* #f)") + # Disable cheat-mode and debug (close the visual cues). + if self.send_form("(set! *debug-segment* #f)", print_ok=False): + ok_count += 1 + if self.send_form("(set! *cheat-mode* #f)", print_ok=False): ok_count += 1 @@ -159,8 +162,8 @@ async def connect(self): if self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"): ok_count += 1 - # Now wait until we see the success message... 6 times. - if ok_count == 7: + # Now wait until we see the success message... 8 times. + if ok_count == 8: self.connected = True else: self.connected = False @@ -194,10 +197,14 @@ def receive_item(self): self.receive_power_cell(ap_id) elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Specials.special_offset): self.receive_scout_fly(ap_id) - elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset): + elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset): self.receive_special(ap_id) - # elif ap_id in range(jak1_id + Orbs.orb_offset, ???): - # self.receive_precursor_orb(ap_id) # TODO -- Ponder the Orbs. + elif ap_id in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset): + self.receive_move(ap_id) + elif ap_id in range(jak1_id + Orbs.orb_offset, jak1_max): + self.receive_precursor_orb(ap_id) # Ponder the Orbs. + elif ap_id == jak1_max: + self.receive_green_eco() # Ponder why I chose to do ID's this way. else: raise KeyError(f"Tried to receive item with unknown AP ID {ap_id}.") @@ -237,6 +244,42 @@ def receive_special(self, ap_id: int) -> bool: logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok + def receive_move(self, ap_id: int) -> bool: + move_id = Caches.to_game_id(ap_id) + ok = self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type ap-move) " + "(the float " + str(move_id) + "))") + if ok: + logger.debug(f"Received the ability to {item_table[ap_id]}!") + else: + logger.error(f"Unable to receive the ability to {item_table[ap_id]}!") + return ok + + def receive_precursor_orb(self, ap_id: int) -> bool: + orb_id = Orbs.to_game_id(ap_id) + ok = self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type money) " + "(the float " + str(orb_id) + "))") + if ok: + logger.debug(f"Received a Precursor Orb!") + else: + logger.error(f"Unable to receive a Precursor Orb!") + return ok + + # Green eco pills are our filler item. Use the get-pickup event instead to handle being full health. + def receive_green_eco(self) -> bool: + ok = self.send_form("(send-event " + "*target* \'get-pickup " + "(pickup-type eco-pill) " + "(the float 1))") + if ok: + logger.debug(f"Received a green eco pill!") + else: + logger.error(f"Unable to receive a green eco pill!") + return ok + def receive_deathlink(self) -> bool: # Because it should at least be funny sometimes. diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index 9160701be759..740afb326240 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -12,9 +12,13 @@ At this time, there are several caveats and restrictions: - This is to prevent hard locks, where an item required for progression is locked behind a trade you can't afford. ## What does randomization do to this game? -All 101 Power Cells and 112 Scout Flies are now Location Checks and may contain Items for different games, -as well as different Items from within Jak and Daxter. Additionally, several special checks and corresponding items -have been added that are required to complete the game. +The game now contains the following Location checks: +- All 101 Power Cells +- All 112 Scout Flies +- All the Orb Caches that are not in Gol and Maia's Citadel (a total of 11) + +These may contain Items for different games, as well as different Items from within Jak and Daxter. +Additionally, several special checks and corresponding items have been added that are required to complete the game. ## What are the special checks and how do I check them? | Check Name | How To Check | @@ -46,6 +50,11 @@ have been added that are required to complete the game. | Freed The Blue Sage
Freed The Red Sage
Freed The Yellow Sage | The final staircase in Gol and Maia's Citadel | | Freed The Green Sage | The final elevator in Gol and Maia's Citadel | +## How do I know which special items I have? +Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Item Tracker`. +This will show you a list of all the special items in the game, ones not normally tracked as power cells or scout flies. +Gray items indicate you do not possess that item, light blue items indicate you possess that item. + ## What is the goal of the game once randomized? To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. @@ -79,16 +88,51 @@ scout fly. So in short: - First, you will receive that scout fly, as normal. - Second, you will immediately complete the "Free 7 Scout Flies" check, which will send out another item. +## What does Deathlink do? +If you enable Deathlink, all the other players in your Multiworld who also have it enabled will be linked on death. +That means when Jak dies in your game, the players in your Deathlink group also die. Likewise, if any of the other +players die, Jak will also die in a random fashion. + +You can turn off Deathlink at any time in the game by opening the game's menu, navigate to `Options`, +then `Archipelago Options`, then `Deathlink`. + +## What does Move Randomizer do? +If you enable Move Randomizer, most of Jak's movement set will be added to the randomized item pool, and you will need +to receive the move in order to use it (i.e. you must find it, or another player must send it to you). Some moves have +prerequisite moves that you must also have in order to use them (e.g. Crouch Jump is dependent on Crouch). Jak will only +be able to run, swim (including underwater), and perform single jumps. Note that Flut Flut will have access to her full +movement set at all times. + +You can turn off Move Rando at any time in the game by opening the game's menu, navigate to `Options`, +then `Archipelago Options`, then `Move Randomizer`. This will give you access to the full movement set again. + +## What are the movement options in Move Randomizer? +| Move Name | Prerequisite Moves | +|-----------------|--------------------| +| Crouch | | +| Crouch Jump | Crouch | +| Crouch Uppercut | Crouch | +| Roll | | +| Roll Jump | Roll | +| Double Jump | | +| Jump Dive | | +| Jump Kick | | +| Punch | | +| Punch Uppercut | Punch | +| Kick | | + +## How do I know which moves I have? +Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Move Tracker`. +This will show you a list of all the moves in the game. +- Gray items indicate you do not possess that move. +- Yellow items indicate you possess that move, but you are missing its prerequisites. +- Light blue items indicate you possess that move, as well as its prerequisites. + ## I got soft-locked and can't leave, how do I get out of here? Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Warp To Home`. Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back to the nearest sage's hut to continue your journey. -## How do I know which special items I have? -Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Item Tracker`. -This will show you a list of all the special items in the game, ones not normally tracked as power cells or scout flies. -Grayed-out items indicate you do not possess that item, light blue items indicate you possess that item. - ## I think I found a bug, where should I report it? Depending on the nature of the bug, there are a couple of different options. diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index db1385a4f8c7..cdd2bcad7bf9 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -102,6 +102,6 @@ Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. ### Known Issues -- The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly. +- The game needs to run in debug mode in order to allow the repl to connect to it. We hide the debug text on screen and play the game's introductory cutscenes properly. +- The powershell windows cannot be run as background processes due to how the repl works, so the best we can do is minimize them. - The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. -- The game relates tasks and power cells closely but separately. Some issues may result from custom code to add distinct items to the game (like the Fisherman's Boat, the Pontoons, or the Gondola). diff --git a/worlds/jakanddaxter/locs/OrbCacheLocations.py b/worlds/jakanddaxter/locs/OrbCacheLocations.py new file mode 100644 index 000000000000..5d237c797b55 --- /dev/null +++ b/worlds/jakanddaxter/locs/OrbCacheLocations.py @@ -0,0 +1,50 @@ +from ..GameID import jak1_id + +# These are the locations of Orb Caches throughout the game, unlockable only with blue eco. +# They are not game collectables and thus don't have the same kinds of game ID's. They do, however, have actor ID's. +# There are a total of 14 in the game. + +# When these are opened, we can execute a hook in the mod that might be able to tell us which orb cache we opened, +# by ID, and that will allow us to map a Location object to it. We'll be using these for Move Randomizer, +# where each move is "mapped" to an Orb Cache being unlocked. Obviously, they will then be randomized, but with moves +# not being considered Items by the game, we need to conjure SOME kind of Location for them, and Orb Caches is the best +# we can do. + +# We can use 2^12 to offset these from special checks, just like we offset those from scout flies +# by 2^11. Special checks don't exceed an ID of (jak1_id + 2153). +orb_cache_offset = 4096 + + +# These helper functions do all the math required to get information about each +# special check and translate its ID between AP and OpenGOAL. Similar to Scout Flies, these large numbers are not +# necessary, and we can flatten out the range in which these numbers lie. +def to_ap_id(game_id: int) -> int: + assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + uncompressed_id = jak1_id + orb_cache_offset + game_id # Add the offsets and the orb cache Actor ID. + return uncompressed_id - 10344 # Subtract the smallest Actor ID. + + +def to_game_id(ap_id: int) -> int: + assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + uncompressed_id = ap_id + 10344 # Reverse process, add back the smallest Actor ID. + return uncompressed_id - jak1_id - orb_cache_offset # Subtract the offsets. + + +# The ID's you see below correlate to the Actor ID of each Orb Cache. + +loc_orbCacheTable = { + 10344: "Orb Cache in Sandover Village", + 10369: "Orb Cache in Forbidden Jungle", + 11072: "Orb Cache on Misty Island", + 12634: "Orb Cache near Flut Flut Egg", + 12635: "Orb Cache near Pelican's Nest", + 10945: "Orb Cache in Rock Village", + 14507: "Orb Cache in First Sunken Chamber", + 14838: "Orb Cache in Second Sunken Chamber", + 23348: "Orb Cache in Snowy Fort (1)", + 23349: "Orb Cache in Snowy Fort (2)", + 23350: "Orb Cache in Snowy Fort (3)", + # 24038: "Orb Cache at End of Blast Furnace", # TODO - IDK, we didn't need all of the orb caches for move rando. + # 24039: "Orb Cache at End of Launch Pad Room", + # 24040: "Orb Cache at Start of Launch Pad Room", +} diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index 4e586a65fb47..6e45a300d022 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -16,9 +16,7 @@ # The maximum number of orbs that any actor can spawn is 30 (the orb caches in citadel). Covering # these ID-less orbs may need to be a future enhancement. TODO ^^ -# Standalone orbs need 15 bits to identify themselves by Actor ID, -# so we can use 2^15 to offset them from scout flies, just like we offset -# scout flies from power cells by 2^10. +# We can use 2^15 to offset them from Orb Caches, because Orb Cache ID's max out at (jak1_id + 17792). orb_offset = 32768 diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py new file mode 100644 index 000000000000..9f5600f5abca --- /dev/null +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -0,0 +1,154 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_fight + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # This level is full of short-medium gaps that cannot be crossed by single jump alone. + # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) + def can_jump_farther(state: CollectionState, p: int) -> bool: + return (state.has("Double Jump", p) + or state.has("Jump Kick", p) + or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + + def can_jump_higher(state: CollectionState, p: int) -> bool: + return (state.has("Double Jump", p) + or (state.has("Crouch", p) and state.has("Crouch Jump", p)) + or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) + or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + + # Orb crates and fly box in this area can be gotten with yellow eco and goggles. + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) + main_area.add_fly_locations([43]) + + # Includes 4 orbs collectable with the blue eco vent. + first_bats = JakAndDaxterRegion("First Bats Area", player, multiworld, level_name, 4) + + first_jump_pad = JakAndDaxterRegion("First Jump Pad", player, multiworld, level_name, 0) + first_jump_pad.add_fly_locations([393259]) + + # The tethers in this level are all out of order... a casual playthrough has the following order for the cell ID's: + # 42, 39, 40, 41. So that is the order we're calling "first, second, third, fourth". + + # First tether cell is collectable with yellow eco and goggles. + first_tether = JakAndDaxterRegion("First Tether", player, multiworld, level_name, 7) + first_tether.add_cell_locations([42]) + + # This rat colony has 3 orbs on top of it, requires special movement. + first_tether_rat_colony = JakAndDaxterRegion("First Tether Rat Colony", player, multiworld, level_name, 3) + + # If quick enough, combat not required. + second_jump_pad = JakAndDaxterRegion("Second Jump Pad", player, multiworld, level_name, 0) + second_jump_pad.add_fly_locations([65579]) + + first_pole_course = JakAndDaxterRegion("First Pole Course", player, multiworld, level_name, 28) + + # You can break this tether with a yellow eco vent and goggles, + # but you can't reach the platform unless you can jump high. + second_tether = JakAndDaxterRegion("Second Tether", player, multiworld, level_name, 0) + second_tether.add_cell_locations([39], access_rule=lambda state: can_jump_higher(state, player)) + + # Fly and orbs are collectable with nearby blue eco cluster. + second_bats = JakAndDaxterRegion("Second Bats Area", player, multiworld, level_name, 27) + second_bats.add_fly_locations([262187], access_rule=lambda state: can_jump_farther(state, player)) + + third_jump_pad = JakAndDaxterRegion("Third Jump Pad (Arena)", player, multiworld, level_name, 0) + third_jump_pad.add_cell_locations([38], access_rule=lambda state: can_fight(state, player)) + + # The platform for the third tether might look high, but you can get a boost from the yellow eco vent. + fourth_jump_pad = JakAndDaxterRegion("Fourth Jump Pad (Third Tether)", player, multiworld, level_name, 9) + fourth_jump_pad.add_cell_locations([40]) + + # Orbs collectable here with yellow eco and goggles. + flut_flut_pad = JakAndDaxterRegion("Flut Flut Pad", player, multiworld, level_name, 36) + + flut_flut_course = JakAndDaxterRegion("Flut Flut Course", player, multiworld, level_name, 23) + flut_flut_course.add_cell_locations([37]) + flut_flut_course.add_fly_locations([327723, 131115]) + + # Includes some orbs on the way to the cabin, blue+yellow eco to collect. + farthy_snacks = JakAndDaxterRegion("Farthy's Snacks", player, multiworld, level_name, 7) + farthy_snacks.add_cell_locations([36]) + + # Scout fly in this field can be broken with yellow eco. + box_field = JakAndDaxterRegion("Field of Boxes", player, multiworld, level_name, 10) + box_field.add_fly_locations([196651]) + + last_tar_pit = JakAndDaxterRegion("Last Tar Pit", player, multiworld, level_name, 12) + + fourth_tether = JakAndDaxterRegion("Fourth Tether", player, multiworld, level_name, 11) + fourth_tether.add_cell_locations([41], access_rule=lambda state: can_jump_higher(state, player)) + + main_area.connect(first_bats, rule=lambda state: can_jump_farther(state, player)) + + first_bats.connect(main_area) + first_bats.connect(first_jump_pad) + first_bats.connect(first_tether) + + first_jump_pad.connect(first_bats) + + first_tether.connect(first_bats) + first_tether.connect(first_tether_rat_colony, rule=lambda state: + (state.has("Roll", player) and state.has("Roll Jump", player)) + or (state.has("Double Jump", player) + and state.has("Jump Kick", player))) + first_tether.connect(second_jump_pad) + first_tether.connect(first_pole_course) + + first_tether_rat_colony.connect(first_tether) + + second_jump_pad.connect(first_tether) + + first_pole_course.connect(first_tether) + first_pole_course.connect(second_tether) + + second_tether.connect(first_pole_course, rule=lambda state: can_jump_higher(state, player)) + second_tether.connect(second_bats) + + second_bats.connect(second_tether) + second_bats.connect(third_jump_pad) + second_bats.connect(fourth_jump_pad) + second_bats.connect(flut_flut_pad) + + third_jump_pad.connect(second_bats) + fourth_jump_pad.connect(second_bats) + + flut_flut_pad.connect(second_bats) + flut_flut_pad.connect(flut_flut_course, rule=lambda state: state.has("Flut Flut", player)) # Naturally. + flut_flut_pad.connect(farthy_snacks) + + flut_flut_course.connect(flut_flut_pad) + + farthy_snacks.connect(flut_flut_pad) + farthy_snacks.connect(box_field, rule=lambda state: can_jump_higher(state, player)) + + box_field.connect(farthy_snacks, rule=lambda state: can_jump_higher(state, player)) + box_field.connect(last_tar_pit, rule=lambda state: can_jump_farther(state, player)) + + last_tar_pit.connect(box_field, rule=lambda state: can_jump_farther(state, player)) + last_tar_pit.connect(fourth_tether, rule=lambda state: can_jump_farther(state, player)) + + fourth_tether.connect(last_tar_pit, rule=lambda state: can_jump_farther(state, player)) + fourth_tether.connect(main_area) # Fall down. + + multiworld.regions.append(main_area) + multiworld.regions.append(first_bats) + multiworld.regions.append(first_jump_pad) + multiworld.regions.append(first_tether) + multiworld.regions.append(first_tether_rat_colony) + multiworld.regions.append(second_jump_pad) + multiworld.regions.append(first_pole_course) + multiworld.regions.append(second_tether) + multiworld.regions.append(second_bats) + multiworld.regions.append(third_jump_pad) + multiworld.regions.append(fourth_jump_pad) + multiworld.regions.append(flut_flut_pad) + multiworld.regions.append(flut_flut_course) + multiworld.regions.append(farthy_snacks) + multiworld.regions.append(box_field) + multiworld.regions.append(last_tar_pit) + multiworld.regions.append(fourth_tether) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/FireCanyonRegions.py b/worlds/jakanddaxter/regs/FireCanyonRegions.py new file mode 100644 index 000000000000..b77d28b7d626 --- /dev/null +++ b/worlds/jakanddaxter/regs/FireCanyonRegions.py @@ -0,0 +1,17 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..locs import CellLocations as Cells, ScoutLocations as Scouts + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) + + # Everything is accessible by making contact with the zoomer. + main_area.add_cell_locations(Cells.locFC_cellTable.keys()) + main_area.add_fly_locations(Scouts.locFC_scoutTable.keys()) + + multiworld.regions.append(main_area) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py new file mode 100644 index 000000000000..33e49a9d04b1 --- /dev/null +++ b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py @@ -0,0 +1,83 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_fight + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 25) + + # You can get this scout fly by running from the blue eco vent across the temple bridge, + # falling onto the river, collecting the 3 blue clusters, using the jump pad, and running straight to the box. + main_area.add_fly_locations([393223]) + + lurker_machine = JakAndDaxterRegion("Lurker Machine", player, multiworld, level_name, 5) + lurker_machine.add_cell_locations([3], access_rule=lambda state: can_fight(state, player)) + + # This cell and this scout fly can both be gotten with the blue eco clusters near the jump pad. + lurker_machine.add_cell_locations([9]) + lurker_machine.add_fly_locations([131079]) + + river = JakAndDaxterRegion("River", player, multiworld, level_name, 42) + + # All of these can be gotten with blue eco, hitting the dark eco boxes, or by running. + river.add_cell_locations([5, 8]) + river.add_fly_locations([7, 196615]) + river.add_special_locations([5]) + river.add_cache_locations([10369]) + + temple_exit = JakAndDaxterRegion("Temple Exit", player, multiworld, level_name, 12) + + # This fly is too far from accessible blue eco sources. + temple_exit.add_fly_locations([262151], access_rule=lambda state: can_free_scout_flies(state, player)) + + temple_exterior = JakAndDaxterRegion("Temple Exterior", player, multiworld, level_name, 10) + + # All of these can be gotten with blue eco and running. + temple_exterior.add_cell_locations([4]) + temple_exterior.add_fly_locations([327687, 65543]) + temple_exterior.add_special_locations([4]) + + temple_int_pre_blue = JakAndDaxterRegion("Temple Interior (Pre Blue Eco)", player, multiworld, level_name, 17) + temple_int_pre_blue.add_cell_locations([2]) + temple_int_pre_blue.add_special_locations([2]) + + temple_int_post_blue = JakAndDaxterRegion("Temple Interior (Post Blue Eco)", player, multiworld, level_name, 39) + temple_int_post_blue.add_cell_locations([6], access_rule=lambda state: can_fight(state, player)) + + main_area.connect(lurker_machine) # Run and jump (tree stump platforms). + main_area.connect(river) # Jump down. + main_area.connect(temple_exit) # Run and jump (bridges). + + lurker_machine.connect(main_area) # Jump down. + lurker_machine.connect(river) # Jump down. + lurker_machine.connect(temple_exterior) # Jump down (ledge). + + river.connect(main_area) # Jump up (ledges near fisherman). + river.connect(lurker_machine) # Jump pad (aim toward machine). + river.connect(temple_exit) # Run and jump (trampolines). + river.connect(temple_exterior) # Jump pad (aim toward temple door). + + temple_exit.connect(main_area) # Run and jump (bridges). + temple_exit.connect(river) # Jump down. + temple_exit.connect(temple_exterior) # Run and jump (bridges, dodge spikes). + + # Requires Jungle Elevator. + temple_exterior.connect(temple_int_pre_blue, rule=lambda state: state.has("Jungle Elevator", player)) + + # Requires Blue Eco Switch. + temple_int_pre_blue.connect(temple_int_post_blue, rule=lambda state: state.has("Blue Eco Switch", player)) + + # Requires defeating the plant boss (combat). + temple_int_post_blue.connect(temple_exit, rule=lambda state: can_fight(state, player)) + + multiworld.regions.append(main_area) + multiworld.regions.append(lurker_machine) + multiworld.regions.append(river) + multiworld.regions.append(temple_exit) + multiworld.regions.append(temple_exterior) + multiworld.regions.append(temple_int_pre_blue) + multiworld.regions.append(temple_int_post_blue) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py new file mode 100644 index 000000000000..ed4c4daaf325 --- /dev/null +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -0,0 +1,26 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..locs import ScoutLocations as Scouts + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) + main_area.add_cell_locations([92, 93]) + main_area.add_fly_locations(Scouts.locGR_scoutTable.keys()) # All Flies here are accessible with blue eco. + + cliff = JakAndDaxterRegion("Cliff", player, multiworld, level_name, 0) + cliff.add_cell_locations([94]) + + main_area.connect(cliff, rule=lambda state: + ((state.has("Crouch", player) and state.has("Crouch Jump", player)) + or (state.has("Crouch", player) and state.has("Crouch Uppercut", player)) + or state.has("Double Jump", player))) + + cliff.connect(main_area) # Jump down or ride blue eco elevator. + + multiworld.regions.append(main_area) + multiworld.regions.append(cliff) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py new file mode 100644 index 000000000000..d9439e0be377 --- /dev/null +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -0,0 +1,122 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_fight + + +# God help me... here we go. +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # This level is full of short-medium gaps that cannot be crossed by single jump alone. + # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) + def can_jump_farther(state: CollectionState, p: int) -> bool: + return (state.has("Double Jump", p) + or state.has("Jump Kick", p) + or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + + def can_uppercut_spin(state: CollectionState, p: int) -> bool: + return (state.has("Punch", p) + and state.has("Punch Uppercut", p) + and state.has("Jump Kick", p)) + + def can_triple_jump(state: CollectionState, p: int) -> bool: + return state.has("Double Jump", p) and state.has("Jump Kick", p) + + # Don't @ me on the name. + def can_move_fancy(state: CollectionState, p: int) -> bool: + return can_uppercut_spin(state, p) or can_triple_jump(state, p) + + def can_jump_stairs(state: CollectionState, p: int) -> bool: + return (state.has("Double Jump", p) + or (state.has("Crouch", p) and state.has("Crouch Jump", p)) + or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) + or state.has("Jump Dive", p)) + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) + main_area.add_fly_locations([91], access_rule=lambda state: can_free_scout_flies(state, player)) + + robot_scaffolding = JakAndDaxterRegion("Scaffolding Around Robot", player, multiworld, level_name, 8) + robot_scaffolding.add_fly_locations([196699], access_rule=lambda state: + can_free_scout_flies(state, player)) + + jump_pad_room = JakAndDaxterRegion("Jump Pad Chamber", player, multiworld, level_name, 88) + jump_pad_room.add_cell_locations([73], access_rule=lambda state: can_fight(state, player)) + jump_pad_room.add_special_locations([73], access_rule=lambda state: can_fight(state, player)) + jump_pad_room.add_fly_locations([131163]) # Blue eco vent is right next to it. + jump_pad_room.add_fly_locations([65627], access_rule=lambda state: + can_free_scout_flies(state, player) + and can_jump_farther(state, player)) + + blast_furnace = JakAndDaxterRegion("Blast Furnace", player, multiworld, level_name, 39) + blast_furnace.add_cell_locations([71], access_rule=lambda state: can_fight(state, player)) + blast_furnace.add_special_locations([71], access_rule=lambda state: can_fight(state, player)) + blast_furnace.add_fly_locations([393307]) # Blue eco vent nearby. + + bunny_room = JakAndDaxterRegion("Bunny Chamber", player, multiworld, level_name, 45) + bunny_room.add_cell_locations([72], access_rule=lambda state: can_fight(state, player)) + bunny_room.add_special_locations([72], access_rule=lambda state: can_fight(state, player)) + bunny_room.add_fly_locations([262235], access_rule=lambda state: + can_free_scout_flies(state, player)) + + rotating_tower = JakAndDaxterRegion("Rotating Tower", player, multiworld, level_name, 20) + rotating_tower.add_cell_locations([70], access_rule=lambda state: can_fight(state, player)) + rotating_tower.add_special_locations([70], access_rule=lambda state: can_fight(state, player)) + rotating_tower.add_fly_locations([327771], access_rule=lambda state: + can_free_scout_flies(state, player)) + + final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0) + + # Jump Dive required for a lot of buttons, prepare yourself. + main_area.connect(robot_scaffolding, rule=lambda state: + state.has("Jump Dive", player) + or (state.has("Roll", player) and state.has("Roll Jump", player))) + main_area.connect(jump_pad_room) + + robot_scaffolding.connect(main_area, rule=lambda state: state.has("Jump Dive", player)) + robot_scaffolding.connect(blast_furnace, rule=lambda state: + state.has("Jump Dive", player) + and ((state.has("Roll", player) and state.has("Roll Jump", player)) + or can_uppercut_spin(state, player))) + robot_scaffolding.connect(bunny_room, rule=lambda state: + can_fight(state, player) + and (can_move_fancy(state, player) + or (state.has("Roll", player) and state.has("Roll Jump", player)))) + + jump_pad_room.connect(main_area) + jump_pad_room.connect(robot_scaffolding, rule=lambda state: + state.has("Jump Dive", player) + and ((state.has("Roll", player) and state.has("Roll Jump", player)) + or can_triple_jump(state, player))) + + blast_furnace.connect(robot_scaffolding) # Blue eco elevator takes you right back. + + bunny_room.connect(robot_scaffolding, rule=lambda state: + state.has("Jump Dive", player) + and ((state.has("Roll", player) and state.has("Roll Jump", player)) + or can_triple_jump(state, player))) + + # Final climb. + robot_scaffolding.connect(rotating_tower, rule=lambda state: + state.has("Freed The Blue Sage", player) + and state.has("Freed The Red Sage", player) + and state.has("Freed The Yellow Sage", player) + and can_jump_stairs(state, player)) + + rotating_tower.connect(main_area) # Take stairs back down. + + # You're going to need free-shooting yellow eco to defeat the robot. + rotating_tower.connect(final_boss, rule=lambda state: + state.has("Freed The Green Sage", player) + and state.has("Punch", player)) + + final_boss.connect(rotating_tower) # Take elevator back down. + + multiworld.regions.append(main_area) + multiworld.regions.append(robot_scaffolding) + multiworld.regions.append(jump_pad_room) + multiworld.regions.append(blast_furnace) + multiworld.regions.append(bunny_room) + multiworld.regions.append(rotating_tower) + multiworld.regions.append(final_boss) + + return [main_area, final_boss] diff --git a/worlds/jakanddaxter/regs/LavaTubeRegions.py b/worlds/jakanddaxter/regs/LavaTubeRegions.py new file mode 100644 index 000000000000..d8c8a7ec41d8 --- /dev/null +++ b/worlds/jakanddaxter/regs/LavaTubeRegions.py @@ -0,0 +1,17 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..locs import CellLocations as Cells, ScoutLocations as Scouts + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) + + # Everything is accessible by making contact with the zoomer. + main_area.add_cell_locations(Cells.locLT_cellTable.keys()) + main_area.add_fly_locations(Scouts.locLT_scoutTable.keys()) + + multiworld.regions.append(main_area) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py new file mode 100644 index 000000000000..de5251a78717 --- /dev/null +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -0,0 +1,130 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_fight + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # Just the starting area. + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 4) + + first_room_upper = JakAndDaxterRegion("First Chamber (Upper)", player, multiworld, level_name, 21) + + first_room_lower = JakAndDaxterRegion("First Chamber (Lower)", player, multiworld, level_name, 0) + first_room_lower.add_fly_locations([262193], access_rule=lambda state: can_free_scout_flies(state, player)) + + first_room_orb_cache = JakAndDaxterRegion("First Chamber Orb Cache", player, multiworld, level_name, 22) + + # Need jump dive to activate button, double jump to reach blue eco to unlock cache. + first_room_orb_cache.add_cache_locations([14507], access_rule=lambda state: + state.has("Jump Dive", player) + and state.has("Double Jump", player)) + + first_hallway = JakAndDaxterRegion("First Hallway", player, multiworld, level_name, 10) + first_hallway.add_fly_locations([131121], access_rule=lambda state: can_free_scout_flies(state, player)) + + # This entire room is accessible with floating platforms and single jump. + second_room = JakAndDaxterRegion("Second Chamber", player, multiworld, level_name, 28) + + # These items can only be gotten with jump dive to activate a button. + second_room.add_cell_locations([45], access_rule=lambda state: state.has("Jump Dive", player)) + second_room.add_fly_locations([49, 65585], access_rule=lambda state: state.has("Jump Dive", player)) + + # This is the scout fly on the way to the pipe cell, requires normal breaking moves. + second_room.add_fly_locations([196657], access_rule=lambda state: can_free_scout_flies(state, player)) + + # This orb vent and scout fly are right next to each other, can be gotten with blue eco and the floating platforms. + second_room.add_fly_locations([393265]) + second_room.add_cache_locations([14838]) + + # Named after the cell, includes the armored lurker room. + center_complex = JakAndDaxterRegion("Center of the Complex", player, multiworld, level_name, 17) + center_complex.add_cell_locations([51]) + + color_platforms = JakAndDaxterRegion("Color Platforms", player, multiworld, level_name, 6) + color_platforms.add_cell_locations([44], access_rule=lambda state: can_fight(state, player)) + + quick_platforms = JakAndDaxterRegion("Quick Platforms", player, multiworld, level_name, 3) + + # Jump dive to activate button. + quick_platforms.add_cell_locations([48], access_rule=lambda state: state.has("Jump Dive", player)) + + first_slide = JakAndDaxterRegion("First Slide", player, multiworld, level_name, 22) + + # Raised chamber room, includes vent room with scout fly prior to second slide. + capsule_room = JakAndDaxterRegion("Capsule Chamber", player, multiworld, level_name, 6) + + # Use jump dive to activate button inside the capsule. Blue eco vent can ready the chamber and get the scout fly. + capsule_room.add_cell_locations([47], access_rule=lambda state: state.has("Jump Dive", player)) + capsule_room.add_fly_locations([327729]) + + second_slide = JakAndDaxterRegion("Second Slide", player, multiworld, level_name, 31) + + helix_room = JakAndDaxterRegion("Helix Chamber", player, multiworld, level_name, 30) + helix_room.add_cell_locations([46], access_rule=lambda state: + state.has("Double Jump", player) + or state.has("Jump Kick", player) + or (state.has("Punch", player) and state.has("Punch Uppercut", player))) + helix_room.add_cell_locations([50], access_rule=lambda state: + state.has("Double Jump", player) + or can_fight(state, player)) + + main_area.connect(first_room_upper) # Run. + + first_room_upper.connect(main_area) # Run. + first_room_upper.connect(first_hallway) # Run and jump (floating platforms). + first_room_upper.connect(first_room_lower) # Run and jump down. + + first_room_lower.connect(first_room_upper) # Run and jump (floating platforms). + + # Needs some movement to reach these orbs and orb cache. + first_room_lower.connect(first_room_orb_cache, rule=lambda state: + state.has("Jump Dive", player) + and state.has("Double Jump", player)) + first_room_orb_cache.connect(first_room_lower, rule=lambda state: + state.has("Jump Dive", player) + and state.has("Double Jump", player)) + + first_hallway.connect(first_room_upper) # Run and jump down. + first_hallway.connect(second_room) # Run and jump (floating platforms). + + second_room.connect(first_hallway) # Run and jump. + second_room.connect(center_complex) # Run and jump down. + + center_complex.connect(second_room) # Run and jump (swim). + center_complex.connect(color_platforms) # Run and jump (swim). + center_complex.connect(quick_platforms) # Run and jump (swim). + + color_platforms.connect(center_complex) # Run and jump (swim). + + quick_platforms.connect(center_complex) # Run and jump (swim). + quick_platforms.connect(first_slide) # Slide. + + first_slide.connect(capsule_room) # Slide. + + capsule_room.connect(second_slide) # Slide. + capsule_room.connect(main_area, rule=lambda state: # Chamber goes back to surface. + state.has("Jump Dive", player)) # (Assume one-way for sanity.) + + second_slide.connect(helix_room) # Slide. + + helix_room.connect(quick_platforms, rule=lambda state: # Escape to get back to here. + state.has("Double Jump", player) # Capsule is a convenient exit to the level. + or can_fight(state, player)) + + multiworld.regions.append(main_area) + multiworld.regions.append(first_room_upper) + multiworld.regions.append(first_room_lower) + multiworld.regions.append(first_room_orb_cache) + multiworld.regions.append(first_hallway) + multiworld.regions.append(second_room) + multiworld.regions.append(center_complex) + multiworld.regions.append(color_platforms) + multiworld.regions.append(quick_platforms) + multiworld.regions.append(first_slide) + multiworld.regions.append(capsule_room) + multiworld.regions.append(second_slide) + multiworld.regions.append(helix_room) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py new file mode 100644 index 000000000000..259e9c5c23e9 --- /dev/null +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -0,0 +1,116 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_fight + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 9) + + muse_course = JakAndDaxterRegion("Muse Course", player, multiworld, level_name, 21) + muse_course.add_cell_locations([23]) + muse_course.add_fly_locations([327708], access_rule=lambda state: can_free_scout_flies(state, player)) + + zoomer = JakAndDaxterRegion("Zoomer", player, multiworld, level_name, 32) + zoomer.add_cell_locations([27, 29]) + zoomer.add_fly_locations([393244]) + + ship = JakAndDaxterRegion("Ship", player, multiworld, level_name, 10) + ship.add_cell_locations([24]) + ship.add_fly_locations([131100], access_rule=lambda state: can_free_scout_flies(state, player)) + + far_side = JakAndDaxterRegion("Far Side", player, multiworld, level_name, 16) + + # In order to even reach this fly, you must use the seesaw or crouch jump. + far_side_cliff = JakAndDaxterRegion("Far Side Cliff", player, multiworld, level_name, 5) + far_side_cliff.add_fly_locations([28], access_rule=lambda state: can_free_scout_flies(state, player)) + + # To carry the blue eco fast enough to open this cache, you need to break the bone bridges along the way. + far_side_cache = JakAndDaxterRegion("Far Side Orb Cache", player, multiworld, level_name, 15) + far_side_cache.add_cache_locations([11072], access_rule=lambda state: can_fight(state, player)) + + barrel_course = JakAndDaxterRegion("Barrel Course", player, multiworld, level_name, 10) + barrel_course.add_fly_locations([196636], access_rule=lambda state: can_free_scout_flies(state, player)) + + # 14 orbs for the boxes you can only break with the cannon. + cannon = JakAndDaxterRegion("Cannon", player, multiworld, level_name, 14) + cannon.add_cell_locations([26], access_rule=lambda state: can_fight(state, player)) + + upper_approach = JakAndDaxterRegion("Upper Arena Approach", player, multiworld, level_name, 6) + upper_approach.add_fly_locations([65564, 262172], access_rule=lambda state: + can_free_scout_flies(state, player)) + + lower_approach = JakAndDaxterRegion("Lower Arena Approach", player, multiworld, level_name, 7) + lower_approach.add_cell_locations([30]) + + arena = JakAndDaxterRegion("Arena", player, multiworld, level_name, 5) + arena.add_cell_locations([25], access_rule=lambda state: can_fight(state, player)) + + main_area.connect(muse_course) # TODO - What do you need to chase the muse the whole way around? + main_area.connect(zoomer) # Run and jump down. + main_area.connect(ship) # Run and jump. + main_area.connect(lower_approach) # Run and jump. + + # Need to break the bone bridge to access. + main_area.connect(upper_approach, rule=lambda state: can_fight(state, player)) + + muse_course.connect(main_area) # Run and jump down. + + # The zoomer pad is low enough that it requires Crouch Jump specifically. + zoomer.connect(main_area, rule=lambda state: + (state.has("Crouch", player) + and state.has("Crouch Jump", player))) + + ship.connect(main_area) # Run and jump down. + ship.connect(far_side) # Run and jump down. + ship.connect(barrel_course) # Run and jump (dodge barrels). + + far_side.connect(ship) # Run and jump. + far_side.connect(arena) # Run and jump. + + # Only if you can use the seesaw or Crouch Jump from the seesaw's edge. + far_side.connect(far_side_cliff, rule=lambda state: + (state.has("Crouch", player) + and state.has("Crouch Jump", player)) + or state.has("Jump Dive", player)) + + # Only if you can break the bone bridges to carry blue eco over the mud pit. + far_side.connect(far_side_cache, rule=lambda state: can_fight(state, player)) + + far_side_cliff.connect(far_side) # Run and jump down. + + barrel_course.connect(cannon) # Run and jump (dodge barrels). + + cannon.connect(barrel_course) # Run and jump (dodge barrels). + cannon.connect(arena) # Run and jump down. + cannon.connect(upper_approach) # Run and jump down. + + upper_approach.connect(lower_approach) # Jump down. + upper_approach.connect(arena) # Jump down. + + # One cliff is accessible, but only via Crouch Jump. + lower_approach.connect(upper_approach, rule=lambda state: + (state.has("Crouch", player) + and state.has("Crouch Jump", player))) + + # Requires breaking bone bridges. + lower_approach.connect(arena, rule=lambda state: can_fight(state, player)) + + arena.connect(lower_approach) # Run. + arena.connect(far_side) # Run. + + multiworld.regions.append(main_area) + multiworld.regions.append(muse_course) + multiworld.regions.append(zoomer) + multiworld.regions.append(ship) + multiworld.regions.append(far_side) + multiworld.regions.append(far_side_cliff) + multiworld.regions.append(far_side_cache) + multiworld.regions.append(barrel_course) + multiworld.regions.append(cannon) + multiworld.regions.append(upper_approach) + multiworld.regions.append(lower_approach) + multiworld.regions.append(arena) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py new file mode 100644 index 000000000000..b1eaea1019ad --- /dev/null +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -0,0 +1,34 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..locs import ScoutLocations as Scouts + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # This is basically just Klaww. + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) + main_area.add_cell_locations([86]) + + race = JakAndDaxterRegion("Race", player, multiworld, level_name, 50) + race.add_cell_locations([87]) + + # All scout flies can be broken with the zoomer. + race.add_fly_locations(Scouts.locMP_scoutTable.keys()) + + shortcut = JakAndDaxterRegion("Shortcut", player, multiworld, level_name, 0) + shortcut.add_cell_locations([110]) + + main_area.connect(race) + + # You cannot go backwards from Klaww. + race.connect(shortcut, rule=lambda state: state.has("Yellow Eco Switch", player)) + + shortcut.connect(race) + + multiworld.regions.append(main_area) + multiworld.regions.append(race) + multiworld.regions.append(shortcut) + + # Return race required for inter-level connections. + return [main_area, race] diff --git a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py new file mode 100644 index 000000000000..7b1ea8a883fd --- /dev/null +++ b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py @@ -0,0 +1,17 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..locs import CellLocations as Cells, ScoutLocations as Scouts + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 200) + + # Everything is accessible by making contact with the zoomer. + main_area.add_cell_locations(Cells.locPB_cellTable.keys()) + main_area.add_fly_locations(Scouts.locPB_scoutTable.keys()) + + multiworld.regions.append(main_area) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/RegionBase.py b/worlds/jakanddaxter/regs/RegionBase.py new file mode 100644 index 000000000000..40e4cb92735d --- /dev/null +++ b/worlds/jakanddaxter/regs/RegionBase.py @@ -0,0 +1,69 @@ +from typing import List, Callable +from BaseClasses import MultiWorld, Region +from ..GameID import jak1_name +from ..JakAndDaxterOptions import JakAndDaxterOptions +from ..Locations import JakAndDaxterLocation, location_table +from ..locs import (CellLocations as Cells, + ScoutLocations as Scouts, + SpecialLocations as Specials, + OrbCacheLocations as Caches) + + +class JakAndDaxterRegion(Region): + """ + Holds region information such as name, level name, number of orbs available, etc. + We especially need orb counts to be tracked because we need to know when you can + afford the 90-orb and 120-orb payments for more checks. + """ + game: str = jak1_name + level_name: str + orb_count: int + + def __init__(self, name: str, player: int, multiworld: MultiWorld, level_name: str = "", orb_count: int = 0): + formatted_name = f"{level_name} {name}".strip() + super().__init__(formatted_name, player, multiworld) + self.level_name = level_name + self.orb_count = orb_count + + def add_cell_locations(self, locations: List[int], access_rule: Callable = None): + """ + Adds a Power Cell Location to this region with the given access rule. + Converts Game ID's to AP ID's for you. + """ + for loc in locations: + self.add_jak_locations(Cells.to_ap_id(loc), access_rule) + + def add_fly_locations(self, locations: List[int], access_rule: Callable = None): + """ + Adds a Scout Fly Location to this region with the given access rule. + Converts Game ID's to AP ID's for you. + """ + for loc in locations: + self.add_jak_locations(Scouts.to_ap_id(loc), access_rule) + + def add_special_locations(self, locations: List[int], access_rule: Callable = None): + """ + Adds a Special Location to this region with the given access rule. + Converts Game ID's to AP ID's for you. + Special Locations should be matched alongside their respective + Power Cell Locations, so you get 2 unlocks for these rather than 1. + """ + for loc in locations: + self.add_jak_locations(Specials.to_ap_id(loc), access_rule) + + def add_cache_locations(self, locations: List[int], access_rule: Callable = None): + """ + Adds an Orb Cache Location to this region with the given access rule. + Converts Game ID's to AP ID's for you. + """ + for loc in locations: + self.add_jak_locations(Caches.to_ap_id(loc), access_rule) + + def add_jak_locations(self, ap_id: int, access_rule: Callable = None): + """ + Helper function to add Locations. Not to be used directly. + """ + location = JakAndDaxterLocation(self.player, location_table[ap_id], ap_id, self) + if access_rule: + location.access_rule = access_rule + self.locations.append(location) diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py new file mode 100644 index 000000000000..09b0858ff3fa --- /dev/null +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -0,0 +1,63 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_trade + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump. + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) + main_area.add_cell_locations([31], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + main_area.add_cell_locations([32], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + main_area.add_cell_locations([33], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + main_area.add_cell_locations([34], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + main_area.add_cell_locations([35], access_rule=lambda state: + can_trade(state, player, multiworld, 1530, 34)) + + # These 2 scout fly boxes can be broken by running with nearby blue eco. + main_area.add_fly_locations([196684, 262220]) + main_area.add_fly_locations([76, 131148, 65612, 327756], access_rule=lambda state: + can_free_scout_flies(state, player)) + + # Warrior Pontoon check. You just talk to him and get his introduction. + main_area.add_special_locations([33]) + + orb_cache = JakAndDaxterRegion("Orb Cache", player, multiworld, level_name, 20) + + # You need roll jump to be able to reach this before the blue eco runs out. + orb_cache.add_cache_locations([10945], access_rule=lambda state: + (state.has("Roll", player) and state.has("Roll Jump", player))) + + pontoon_bridge = JakAndDaxterRegion("Pontoon Bridge", player, multiworld, level_name, 7) + pontoon_bridge.add_fly_locations([393292], access_rule=lambda state: can_free_scout_flies(state, player)) + + klaww_cliff = JakAndDaxterRegion("Klaww's Cliff", player, multiworld, level_name, 0) + + main_area.connect(orb_cache, rule=lambda state: (state.has("Roll", player) and state.has("Roll Jump", player))) + main_area.connect(pontoon_bridge, rule=lambda state: state.has("Warrior's Pontoons", player)) + + orb_cache.connect(main_area) + + pontoon_bridge.connect(main_area, rule=lambda state: state.has("Warrior's Pontoons", player)) + pontoon_bridge.connect(klaww_cliff, rule=lambda state: + state.has("Double Jump", player) + or (state.has("Crouch", player) + and state.has("Crouch Jump", player)) + or (state.has("Crouch", player) + and state.has("Crouch Uppercut", player) + and state.has("Jump Kick", player))) + + klaww_cliff.connect(pontoon_bridge) # Just jump back down. + + multiworld.regions.append(main_area) + multiworld.regions.append(orb_cache) + multiworld.regions.append(pontoon_bridge) + multiworld.regions.append(klaww_cliff) + + # Return klaww_cliff required for inter-level connections. + return [main_area, pontoon_bridge, klaww_cliff] diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py new file mode 100644 index 000000000000..ac4d904a4516 --- /dev/null +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -0,0 +1,71 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_trade + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 26) + + # Yakows requires no combat. + main_area.add_cell_locations([10]) + main_area.add_cell_locations([11], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + main_area.add_cell_locations([12], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + + # These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach. + main_area.add_fly_locations([262219, 327755, 131147, 65611]) + + # The farmer's scout fly. You can either get the Orb Cache Cliff blue eco, or break it normally. + main_area.add_fly_locations([196683], access_rule=lambda state: + (state.has("Crouch", player) and state.has("Crouch Jump", player)) + or state.has("Double Jump", player) + or can_free_scout_flies(state, player)) + + orb_cache_cliff = JakAndDaxterRegion("Orb Cache Cliff", player, multiworld, level_name, 15) + orb_cache_cliff.add_cache_locations([10344]) + + yakow_cliff = JakAndDaxterRegion("Yakow Cliff", player, multiworld, level_name, 3) + yakow_cliff.add_fly_locations([75], access_rule=lambda state: can_free_scout_flies(state, player)) + + oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6) + oracle_platforms.add_cell_locations([13], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + oracle_platforms.add_cell_locations([14], access_rule=lambda state: + can_trade(state, player, multiworld, 1530, 13)) + oracle_platforms.add_fly_locations([393291], access_rule=lambda state: + can_free_scout_flies(state, player)) + + main_area.connect(orb_cache_cliff, rule=lambda state: + state.has("Double Jump", player) + or (state.has("Crouch", player) + and state.has("Crouch Jump", player)) + or (state.has("Crouch", player) + and state.has("Crouch Uppercut", player) + and state.has("Jump Kick", player))) + + main_area.connect(yakow_cliff, rule=lambda state: + state.has("Double Jump", player) + or (state.has("Crouch", player) + and state.has("Crouch Jump", player)) + or (state.has("Crouch", player) + and state.has("Crouch Uppercut", player) + and state.has("Jump Kick", player))) + + main_area.connect(oracle_platforms, rule=lambda state: + (state.has("Roll", player) and state.has("Roll Jump", player)) + or (state.has("Double Jump", player) and state.has("Jump Kick", player))) + + # All these can go back to main_area immediately. + orb_cache_cliff.connect(main_area) + yakow_cliff.connect(main_area) + oracle_platforms.connect(main_area) + + multiworld.regions.append(main_area) + multiworld.regions.append(orb_cache_cliff) + multiworld.regions.append(yakow_cliff) + multiworld.regions.append(oracle_platforms) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py new file mode 100644 index 000000000000..0e85dc0573a7 --- /dev/null +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -0,0 +1,85 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_fight + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 128) + main_area.add_cell_locations([18, 21, 22]) + + # These 3 scout fly boxes can be broken by running with freely accessible blue eco. + main_area.add_fly_locations([327700, 20, 65556]) + + # These 2 scout fly boxes can be broken with the locked blue eco vent, or by normal combat tricks. + main_area.add_fly_locations([262164, 393236], access_rule=lambda state: + state.has("Blue Eco Switch", player) + or can_free_scout_flies(state, player)) + + # No need for the blue eco vent for the orb caches. + main_area.add_cache_locations([12634, 12635]) + + pelican = JakAndDaxterRegion("Pelican", player, multiworld, level_name, 0) + pelican.add_cell_locations([16], access_rule=lambda state: can_fight(state, player)) + + # Only these specific attacks can push the flut flut egg off the cliff. + flut_flut_egg = JakAndDaxterRegion("Flut Flut Egg", player, multiworld, level_name, 0) + flut_flut_egg.add_cell_locations([17], access_rule=lambda state: + state.has("Punch", player) + or state.has("Kick", player) + or state.has("Jump Kick", player)) + flut_flut_egg.add_special_locations([17], access_rule=lambda state: + state.has("Punch", player) + or state.has("Kick", player) + or state.has("Jump Kick", player)) + + eco_harvesters = JakAndDaxterRegion("Eco Harvesters", player, multiworld, level_name, 0) + eco_harvesters.add_cell_locations([15], access_rule=lambda state: can_fight(state, player)) + + green_ridge = JakAndDaxterRegion("Ridge Near Green Vents", player, multiworld, level_name, 5) + green_ridge.add_fly_locations([131092], access_rule=lambda state: can_free_scout_flies(state, player)) + + blue_ridge = JakAndDaxterRegion("Ridge Near Blue Vent", player, multiworld, level_name, 5) + blue_ridge.add_fly_locations([196628], access_rule=lambda state: + state.has("Blue Eco Switch", player) + or can_free_scout_flies(state, player)) + + cannon_tower = JakAndDaxterRegion("Cannon Tower", player, multiworld, level_name, 12) + cannon_tower.add_cell_locations([19], access_rule=lambda state: can_fight(state, player)) + + main_area.connect(pelican) # Swim and jump. + main_area.connect(flut_flut_egg) # Run and jump. + main_area.connect(eco_harvesters) # Run. + + # You don't need any kind of uppercut to reach this place, just a high jump from a convenient nearby ledge. + main_area.connect(green_ridge, rule=lambda state: + (state.has("Crouch", player) and state.has("Crouch Jump", player)) + or state.has("Double Jump", player)) + + # Can either uppercut the log and jump from it, or use the blue eco jump pad. + main_area.connect(blue_ridge, rule=lambda state: + state.has("Blue Eco Switch", player) + or (state.has("Double Jump", player) + and ((state.has("Crouch", player) and state.has("Crouch Uppercut", player)) + or (state.has("Punch", player) and state.has("Punch Uppercut", player))))) + + main_area.connect(cannon_tower, rule=lambda state: state.has("Blue Eco Switch", player)) + + # All these can go back to main_area immediately. + pelican.connect(main_area) + flut_flut_egg.connect(main_area) + eco_harvesters.connect(main_area) + green_ridge.connect(main_area) + blue_ridge.connect(main_area) + cannon_tower.connect(main_area) + + multiworld.regions.append(main_area) + multiworld.regions.append(pelican) + multiworld.regions.append(flut_flut_egg) + multiworld.regions.append(eco_harvesters) + multiworld.regions.append(green_ridge) + multiworld.regions.append(blue_ridge) + multiworld.regions.append(cannon_tower) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py new file mode 100644 index 000000000000..0c1ffe94284f --- /dev/null +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -0,0 +1,181 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_fight + + +# God help me... here we go. +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # We need a few helper functions. + def can_uppercut_spin(state: CollectionState, p: int) -> bool: + return (state.has("Punch", p) + and state.has("Punch Uppercut", p) + and state.has("Jump Kick", p)) + + def can_triple_jump(state: CollectionState, p: int) -> bool: + return state.has("Double Jump", p) and state.has("Jump Kick", p) + + # Don't @ me on the name. + def can_move_fancy(state: CollectionState, p: int) -> bool: + return can_uppercut_spin(state, p) or can_triple_jump(state, p) + + def can_jump_blockers(state: CollectionState, p: int) -> bool: + return (state.has("Double Jump", p) + or (state.has("Crouch", p) and state.has("Crouch Jump", p)) + or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) + or (state.has("Punch", p) and state.has("Punch Uppercut", p)) + or state.has("Jump Dive", p)) + + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) + main_area.add_fly_locations([65], access_rule=lambda state: can_free_scout_flies(state, player)) + + # We need a few virtual regions like we had for Dark Crystals in Spider Cave. + # First, a virtual region for the glacier lurkers, who all require combat. + glacier_lurkers = JakAndDaxterRegion("Glacier Lurkers", player, multiworld, level_name, 0) + glacier_lurkers.add_cell_locations([61], access_rule=lambda state: can_fight(state, player)) + + # Second, a virtual region for the precursor blockers. Unlike the others, this contains orbs: + # the total number of orbs that sit on top of the blockers. Yes, there are only 8. + blockers = JakAndDaxterRegion("Precursor Blockers", player, multiworld, level_name, 8) + blockers.add_cell_locations([66], access_rule=lambda state: can_fight(state, player)) + + snowball_canyon = JakAndDaxterRegion("Snowball Canyon", player, multiworld, level_name, 28) + + frozen_box_cave = JakAndDaxterRegion("Frozen Box Cave", player, multiworld, level_name, 12) + frozen_box_cave.add_cell_locations([67], access_rule=lambda state: state.has("Yellow Eco Switch", player)) + frozen_box_cave.add_fly_locations([327745], access_rule=lambda state: + state.has("Yellow Eco Switch", player) + or can_free_scout_flies(state, player)) + + frozen_box_cave_crates = JakAndDaxterRegion("Frozen Box Cave Orb Crates", player, multiworld, level_name, 8) + + # Include 6 orbs on the twin elevator ice ramp. + ice_skating_rink = JakAndDaxterRegion("Ice Skating Rink", player, multiworld, level_name, 20) + ice_skating_rink.add_fly_locations([131137], access_rule=lambda state: can_free_scout_flies(state, player)) + + flut_flut_course = JakAndDaxterRegion("Flut Flut Course", player, multiworld, level_name, 15) + flut_flut_course.add_cell_locations([63], access_rule=lambda state: state.has("Flut Flut", player)) + flut_flut_course.add_special_locations([63], access_rule=lambda state: state.has("Flut Flut", player)) + + # Includes the bridge from snowball_canyon, the area beneath that bridge, and the areas around the fort. + fort_exterior = JakAndDaxterRegion("Fort Exterior", player, multiworld, level_name, 20) + fort_exterior.add_fly_locations([65601, 393281], access_rule=lambda state: + can_free_scout_flies(state, player)) + + # Includes the icy island and bridge outside the cave entrance. + bunny_cave_start = JakAndDaxterRegion("Bunny Cave (Start)", player, multiworld, level_name, 10) + + # Includes the cell and 3 orbs at the exit. + bunny_cave_end = JakAndDaxterRegion("Bunny Cave (End)", player, multiworld, level_name, 3) + bunny_cave_end.add_cell_locations([64]) + + switch_cave = JakAndDaxterRegion("Yellow Eco Switch Cave", player, multiworld, level_name, 4) + switch_cave.add_cell_locations([60]) + switch_cave.add_special_locations([60]) + + # Only what can be covered by single jump. + fort_interior = JakAndDaxterRegion("Fort Interior (Main)", player, multiworld, level_name, 19) + + # Reaching the top of the watch tower, getting the fly with the blue eco, and falling down to get the caches. + fort_interior_caches = JakAndDaxterRegion("Fort Interior (Caches)", player, multiworld, level_name, 51) + fort_interior_caches.add_fly_locations([196673]) + fort_interior_caches.add_cache_locations([23348, 23349, 23350]) + + # Need higher jump. + fort_interior_base = JakAndDaxterRegion("Fort Interior (Base)", player, multiworld, level_name, 0) + fort_interior_base.add_fly_locations([262209], access_rule=lambda state: + can_free_scout_flies(state, player)) + + # Need farther jump. + fort_interior_course_end = JakAndDaxterRegion("Fort Interior (Course End)", player, multiworld, level_name, 2) + fort_interior_course_end.add_cell_locations([62]) + + # Wire up the virtual regions first. + main_area.connect(blockers, rule=lambda state: can_jump_blockers(state, player)) + main_area.connect(glacier_lurkers, rule=lambda state: can_fight(state, player)) + + # Yes, the only way into the rest of the level requires advanced movement. + main_area.connect(snowball_canyon, rule=lambda state: + (state.has("Roll", player) and state.has("Roll Jump", player)) + or can_move_fancy(state, player)) + + snowball_canyon.connect(main_area) # But you can just jump down and run up the ramp. + snowball_canyon.connect(bunny_cave_start) # Jump down from the glacier troop cliff. + snowball_canyon.connect(fort_exterior) # Jump down, to the left of frozen box cave. + snowball_canyon.connect(frozen_box_cave, rule=lambda state: # More advanced movement. + can_move_fancy(state, player)) + + frozen_box_cave.connect(snowball_canyon, rule=lambda state: # Same movement to go back. + can_move_fancy(state, player)) + frozen_box_cave.connect(frozen_box_cave_crates, rule=lambda state: # Same movement to get these crates. + state.has("Yellow Eco Switch", player) + and can_move_fancy(state, player)) + frozen_box_cave.connect(ice_skating_rink, rule=lambda state: # Same movement to go forward. + can_move_fancy(state, player)) + + frozen_box_cave_crates.connect(frozen_box_cave) # Semi-virtual region, no moves req'd. + + ice_skating_rink.connect(frozen_box_cave, rule=lambda state: # Same movement to go back. + can_move_fancy(state, player)) + ice_skating_rink.connect(flut_flut_course, rule=lambda state: # Duh. + state.has("Flut Flut", player)) + ice_skating_rink.connect(fort_exterior) # Just slide down the elevator ramp. + + fort_exterior.connect(ice_skating_rink, rule=lambda state: # Twin elevators are tough to reach. + state.has("Double Jump", player) + or state.has("Jump Kick", player)) + fort_exterior.connect(snowball_canyon) # Run across bridge. + fort_exterior.connect(fort_interior, rule=lambda state: # Duh. + state.has("Snowy Fort Gate", player)) + fort_exterior.connect(bunny_cave_start) # Run across bridge. + fort_exterior.connect(switch_cave, rule=lambda state: # Yes, blocker jumps work here. + can_jump_blockers(state, player)) + + fort_interior.connect(fort_interior_caches, rule=lambda state: # Just need a little height. + (state.has("Crouch", player) + and state.has("Crouch Jump", player)) + or state.has("Double Jump", player)) + fort_interior.connect(fort_interior_base, rule=lambda state: # Just need a little height. + (state.has("Crouch", player) + and state.has("Crouch Jump", player)) + or state.has("Double Jump", player)) + fort_interior.connect(fort_interior_course_end, rule=lambda state: # Just need a little distance. + (state.has("Punch", player) + and state.has("Punch Uppercut", player)) + or state.has("Double Jump", player)) + + flut_flut_course.connect(fort_exterior) # Ride the elevator. + + # Must fight way through cave, but there is also a grab-less ledge we must jump over. + bunny_cave_start.connect(bunny_cave_end, rule=lambda state: + can_fight(state, player) + and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) + or state.has("Double Jump", player))) + + # All jump down. + fort_interior_caches.connect(fort_interior) + fort_interior_base.connect(fort_interior) + fort_interior_course_end.connect(fort_interior) + switch_cave.connect(fort_exterior) + bunny_cave_end.connect(fort_exterior) + + # I really hope that is everything. + multiworld.regions.append(main_area) + multiworld.regions.append(glacier_lurkers) + multiworld.regions.append(blockers) + multiworld.regions.append(snowball_canyon) + multiworld.regions.append(frozen_box_cave) + multiworld.regions.append(frozen_box_cave_crates) + multiworld.regions.append(ice_skating_rink) + multiworld.regions.append(flut_flut_course) + multiworld.regions.append(fort_exterior) + multiworld.regions.append(bunny_cave_start) + multiworld.regions.append(bunny_cave_end) + multiworld.regions.append(switch_cave) + multiworld.regions.append(fort_interior) + multiworld.regions.append(fort_interior_caches) + multiworld.regions.append(fort_interior_base) + multiworld.regions.append(fort_interior_course_end) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py new file mode 100644 index 000000000000..3d9e1093e1dd --- /dev/null +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -0,0 +1,110 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_fight + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # A large amount of this area can be covered by single jump, floating platforms, web trampolines, and goggles. + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 63) + main_area.add_cell_locations([78, 84]) + main_area.add_fly_locations([327765, 393301, 196693, 131157]) + + # This is a virtual region describing what you need to DO to get the Dark Crystal power cell, + # rather than describing where each of the crystals ARE, because you can destroy them in any order, + # and you need to destroy ALL of them to get the cell. + dark_crystals = JakAndDaxterRegion("Dark Crystals", player, multiworld, level_name, 0) + + # can_fight = The underwater crystal in dark cave. + # Roll Jump = The underwater crystal across a long dark eco pool. + # The rest of the crystals can be destroyed with yellow eco in main_area. + dark_crystals.add_cell_locations([79], access_rule=lambda state: + can_fight(state, player) + and (state.has("Roll", player) and state.has("Roll Jump", player))) + + dark_cave = JakAndDaxterRegion("Dark Cave", player, multiworld, level_name, 5) + dark_cave.add_cell_locations([80], access_rule=lambda state: + can_fight(state, player) + and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) + or state.has("Double Jump", player))) + dark_cave.add_fly_locations([262229], access_rule=lambda state: + can_fight(state, player) + and can_free_scout_flies(state, player) + and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) + or state.has("Double Jump", player))) + + robot_cave = JakAndDaxterRegion("Robot Cave", player, multiworld, level_name, 0) + + # Need double jump for orbs. + scaffolding_level_zero = JakAndDaxterRegion("Robot Scaffolding Level 0", player, multiworld, level_name, 12) + + scaffolding_level_one = JakAndDaxterRegion("Robot Scaffolding Level 1", player, multiworld, level_name, 53) + scaffolding_level_one.add_fly_locations([85]) # Shootable. + + scaffolding_level_two = JakAndDaxterRegion("Robot Scaffolding Level 2", player, multiworld, level_name, 4) + + scaffolding_level_three = JakAndDaxterRegion("Robot Scaffolding Level 3", player, multiworld, level_name, 29) + scaffolding_level_three.add_cell_locations([81]) + scaffolding_level_three.add_fly_locations([65621], access_rule=lambda state: + can_free_scout_flies(state, player)) # Not shootable. + + pole_course = JakAndDaxterRegion("Pole Course", player, multiworld, level_name, 18) + pole_course.add_cell_locations([82]) + + # You only need combat to fight through the spiders, but to collect the orb crates, + # you will need the yellow eco vent unlocked. + spider_tunnel = JakAndDaxterRegion("Spider Tunnel", player, multiworld, level_name, 4) + spider_tunnel.add_cell_locations([83]) + + spider_tunnel_crates = JakAndDaxterRegion("Spider Tunnel Orb Crates", player, multiworld, level_name, 12) + + main_area.connect(dark_crystals) + main_area.connect(robot_cave) + main_area.connect(dark_cave, rule=lambda state: can_fight(state, player)) + + robot_cave.connect(main_area) + robot_cave.connect(pole_course) # Nothing special required. + robot_cave.connect(scaffolding_level_one) # Ramps lead to level 1. + robot_cave.connect(spider_tunnel) # Web trampolines (bounce twice on each to gain momentum). + + pole_course.connect(robot_cave) # Blue eco platform down. + + scaffolding_level_one.connect(robot_cave) # All scaffolding (level 1+) connects back by jumping down. + + # Elevator, but the orbs need double jump. + scaffolding_level_one.connect(scaffolding_level_zero, rule=lambda state: state.has("Double Jump", player)) + + # Narrow enough that enemies are unavoidable. + scaffolding_level_one.connect(scaffolding_level_two, rule=lambda state: can_fight(state, player)) + + scaffolding_level_zero.connect(scaffolding_level_one) # Elevator. + + scaffolding_level_two.connect(robot_cave) # Jump down. + scaffolding_level_two.connect(scaffolding_level_one) # Elevator. + + # Elevator, but narrow enough that enemies are unavoidable. + scaffolding_level_two.connect(scaffolding_level_three, rule=lambda state: can_fight(state, player)) + + scaffolding_level_three.connect(robot_cave) # Jump down. + scaffolding_level_three.connect(scaffolding_level_two) # Elevator. + + spider_tunnel.connect(robot_cave) # Back to web trampolines. + spider_tunnel.connect(main_area) # Escape with jump pad. + + # Requires yellow eco switch. + spider_tunnel.connect(spider_tunnel_crates, rule=lambda state: state.has("Yellow Eco Switch", player)) + + multiworld.regions.append(main_area) + multiworld.regions.append(dark_crystals) + multiworld.regions.append(dark_cave) + multiworld.regions.append(robot_cave) + multiworld.regions.append(scaffolding_level_zero) + multiworld.regions.append(scaffolding_level_one) + multiworld.regions.append(scaffolding_level_two) + multiworld.regions.append(scaffolding_level_three) + multiworld.regions.append(pole_course) + multiworld.regions.append(spider_tunnel) + multiworld.regions.append(spider_tunnel_crates) + + return [main_area] diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py new file mode 100644 index 000000000000..48241d647cec --- /dev/null +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -0,0 +1,38 @@ +from typing import List +from BaseClasses import CollectionState, MultiWorld +from .RegionBase import JakAndDaxterRegion +from ..Rules import can_free_scout_flies, can_trade +from ..locs import CellLocations as Cells, ScoutLocations as Scouts + + +def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: + + # No area is inaccessible in VC even with only running and jumping. + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) + main_area.add_cell_locations([96], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + main_area.add_cell_locations([97], access_rule=lambda state: + can_trade(state, player, multiworld, 1530, 96)) + main_area.add_cell_locations([98], access_rule=lambda state: + can_trade(state, player, multiworld, 1530, 97)) + main_area.add_cell_locations([99], access_rule=lambda state: + can_trade(state, player, multiworld, 1530, 98)) + main_area.add_cell_locations([100], access_rule=lambda state: + can_trade(state, player, multiworld, 1530)) + main_area.add_cell_locations([101], access_rule=lambda state: + can_trade(state, player, multiworld, 1530, 100)) + + # Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping + # and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode). + main_area.add_cell_locations([74]) + + # No blue eco sources in this area, all boxes must be broken by hand (yellow eco can't be carried far enough). + main_area.add_fly_locations(Scouts.locVC_scoutTable.keys(), access_rule=lambda state: + can_free_scout_flies(state, player)) + + # Approach the gondola to get this check. + main_area.add_special_locations([105]) + + multiworld.regions.append(main_area) + + return [main_area] diff --git a/worlds/jakanddaxter/test/__init__.py b/worlds/jakanddaxter/test/__init__.py new file mode 100644 index 000000000000..a1d7bfb390b3 --- /dev/null +++ b/worlds/jakanddaxter/test/__init__.py @@ -0,0 +1,91 @@ +from .. import JakAndDaxterWorld +from ..GameID import jak1_name +from test.bases import WorldTestBase + + +class JakAndDaxterTestBase(WorldTestBase): + game = jak1_name + world: JakAndDaxterWorld + + level_info = { + "Geyser Rock": { + "cells": 4, + "flies": 7, + "orbs": 50, + }, + "Sandover Village": { + "cells": 6, + "flies": 7, + "orbs": 50, + }, + "Forbidden Jungle": { + "cells": 8, + "flies": 7, + "orbs": 150, + }, + "Sentinel Beach": { + "cells": 8, + "flies": 7, + "orbs": 150, + }, + "Misty Island": { + "cells": 8, + "flies": 7, + "orbs": 150, + }, + "Fire Canyon": { + "cells": 2, + "flies": 7, + "orbs": 50, + }, + "Rock Village": { + "cells": 6, + "flies": 7, + "orbs": 50, + }, + "Precursor Basin": { + "cells": 8, + "flies": 7, + "orbs": 200, + }, + "Lost Precursor City": { + "cells": 8, + "flies": 7, + "orbs": 200, + }, + "Boggy Swamp": { + "cells": 8, + "flies": 7, + "orbs": 200, + }, + "Mountain Pass": { + "cells": 4, + "flies": 7, + "orbs": 50, + }, + "Volcanic Crater": { + "cells": 8, + "flies": 7, + "orbs": 50, + }, + "Spider Cave": { + "cells": 8, + "flies": 7, + "orbs": 200, + }, + "Snowy Mountain": { + "cells": 8, + "flies": 7, + "orbs": 200, + }, + "Lava Tube": { + "cells": 2, + "flies": 7, + "orbs": 50, + }, + "Gol and Maia's Citadel": { + "cells": 5, + "flies": 7, + "orbs": 200, + }, + } diff --git a/worlds/jakanddaxter/test/test_locations.py b/worlds/jakanddaxter/test/test_locations.py new file mode 100644 index 000000000000..4bd144d623f8 --- /dev/null +++ b/worlds/jakanddaxter/test/test_locations.py @@ -0,0 +1,42 @@ +import typing + +from . import JakAndDaxterTestBase +from .. import jak1_id +from ..regs.RegionBase import JakAndDaxterRegion +from ..locs import (OrbLocations as Orbs, + CellLocations as Cells, + ScoutLocations as Scouts, + SpecialLocations as Specials, + OrbCacheLocations as Caches) + + +class LocationsTest(JakAndDaxterTestBase): + + def test_count_cells(self): + regions = [typing.cast(JakAndDaxterRegion, reg) for reg in self.multiworld.get_regions(self.player)] + for level in self.level_info: + cell_count = 0 + sublevels = [reg for reg in regions if reg.level_name == level] + for sl in sublevels: + for loc in sl.locations: + if loc.address in range(jak1_id, jak1_id + Scouts.fly_offset): + cell_count += 1 + self.assertEqual(self.level_info[level]["cells"] - 1, cell_count, level) # Don't count the Free 7 Cells. + + def test_count_flies(self): + regions = [typing.cast(JakAndDaxterRegion, reg) for reg in self.multiworld.get_regions(self.player)] + for level in self.level_info: + fly_count = 0 + sublevels = [reg for reg in regions if reg.level_name == level] + for sl in sublevels: + for loc in sl.locations: + if loc.address in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset): + fly_count += 1 + self.assertEqual(self.level_info[level]["flies"], fly_count, level) + + def test_count_orbs(self): + regions = [typing.cast(JakAndDaxterRegion, reg) for reg in self.multiworld.get_regions(self.player)] + for level in self.level_info: + sublevels = [reg for reg in regions if reg.level_name == level] + orb_count = sum([reg.orb_count for reg in sublevels]) + self.assertEqual(self.level_info[level]["orbs"], orb_count, level) From f8751699fc1bd0b2c9d8b387de37838109499f46 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:25:34 -0400 Subject: [PATCH 36/70] Move rando fixes (#29) * Fix virtual regions in Snowy. Fix some GMC problems. * Fix Deathlink on sunken slides. * Removed unncessary code causing build failure. --- worlds/jakanddaxter/__init__.py | 6 -- worlds/jakanddaxter/client/MemoryReader.py | 2 + .../regs/GolAndMaiasCitadelRegions.py | 25 +++----- .../jakanddaxter/regs/SnowyMountainRegions.py | 61 +++++++++++-------- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 2ba611d76b2e..7660bdd39dfb 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -23,7 +23,6 @@ def launch_client(): components.append(Component("Jak and Daxter Client", - "JakAndDaxterClient", func=launch_client, component_type=Type.CLIENT, icon="egg")) @@ -159,8 +158,3 @@ def create_item(self, name: str) -> Item: def get_filler_item_name(self) -> str: return "Green Eco Pill" - - -def launch_client(): - from .Client import launch - launch_subprocess(launch, name="JakAndDaxterClient") diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index a4468364579f..8fd54b8b3202 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -105,6 +105,8 @@ def autopsy(died: int) -> str: return "Jak got Flut Flut hurt." if died == 18: return "Jak poisoned the whole darn catch." + if died == 19: + return "Jak collided with too many obstacles." return "Jak died." diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index d9439e0be377..da3dabfe7780 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -14,18 +14,9 @@ def can_jump_farther(state: CollectionState, p: int) -> bool: or state.has("Jump Kick", p) or (state.has("Punch", p) and state.has("Punch Uppercut", p))) - def can_uppercut_spin(state: CollectionState, p: int) -> bool: - return (state.has("Punch", p) - and state.has("Punch Uppercut", p) - and state.has("Jump Kick", p)) - def can_triple_jump(state: CollectionState, p: int) -> bool: return state.has("Double Jump", p) and state.has("Jump Kick", p) - # Don't @ me on the name. - def can_move_fancy(state: CollectionState, p: int) -> bool: - return can_uppercut_spin(state, p) or can_triple_jump(state, p) - def can_jump_stairs(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) or (state.has("Crouch", p) and state.has("Crouch Jump", p)) @@ -75,12 +66,14 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: robot_scaffolding.connect(main_area, rule=lambda state: state.has("Jump Dive", player)) robot_scaffolding.connect(blast_furnace, rule=lambda state: state.has("Jump Dive", player) + and can_jump_farther(state, player) and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_uppercut_spin(state, player))) + or can_triple_jump(state, player))) robot_scaffolding.connect(bunny_room, rule=lambda state: - can_fight(state, player) - and (can_move_fancy(state, player) - or (state.has("Roll", player) and state.has("Roll Jump", player)))) + state.has("Jump Dive", player) + and can_jump_farther(state, player) + and ((state.has("Roll", player) and state.has("Roll Jump", player)) + or can_triple_jump(state, player))) jump_pad_room.connect(main_area) jump_pad_room.connect(robot_scaffolding, rule=lambda state: @@ -93,7 +86,7 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: bunny_room.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_triple_jump(state, player))) + or can_jump_farther(state, player))) # Final climb. robot_scaffolding.connect(rotating_tower, rule=lambda state: @@ -104,10 +97,10 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: rotating_tower.connect(main_area) # Take stairs back down. - # You're going to need free-shooting yellow eco to defeat the robot. + # Final elevator. Need to break boxes at summit to get blue eco for platform. rotating_tower.connect(final_boss, rule=lambda state: state.has("Freed The Green Sage", player) - and state.has("Punch", player)) + and can_fight(state, player)) final_boss.connect(rotating_tower) # Take elevator back down. diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index 0c1ffe94284f..dbd92c297c44 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -8,17 +8,16 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: # We need a few helper functions. - def can_uppercut_spin(state: CollectionState, p: int) -> bool: - return (state.has("Punch", p) - and state.has("Punch Uppercut", p) - and state.has("Jump Kick", p)) + def can_cross_main_gap(state: CollectionState, p: int) -> bool: + return ((state.has("Roll", player) + and state.has("Roll Jump", player)) + or (state.has("Double Jump", player) + and state.has("Jump Kick", player))) - def can_triple_jump(state: CollectionState, p: int) -> bool: - return state.has("Double Jump", p) and state.has("Jump Kick", p) - - # Don't @ me on the name. - def can_move_fancy(state: CollectionState, p: int) -> bool: - return can_uppercut_spin(state, p) or can_triple_jump(state, p) + def can_cross_frozen_cave(state: CollectionState, p: int) -> bool: + return (state.has("Jump Kick", p) + and (state.has("Double Jump", p) + or (state.has("Roll", p) and state.has("Roll Jump", p)))) def can_jump_blockers(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) @@ -31,14 +30,29 @@ def can_jump_blockers(state: CollectionState, p: int) -> bool: main_area.add_fly_locations([65], access_rule=lambda state: can_free_scout_flies(state, player)) # We need a few virtual regions like we had for Dark Crystals in Spider Cave. - # First, a virtual region for the glacier lurkers, who all require combat. + # First, a virtual region for the glacier lurkers. glacier_lurkers = JakAndDaxterRegion("Glacier Lurkers", player, multiworld, level_name, 0) - glacier_lurkers.add_cell_locations([61], access_rule=lambda state: can_fight(state, player)) + + # Need to fight all the troops. + # Troop in snowball_canyon: cross main_area. + # Troop in ice_skating_rink: cross main_area and fort_exterior. + # Troop in fort_exterior: cross main_area and fort_exterior. + glacier_lurkers.add_cell_locations([61], access_rule=lambda state: + can_fight(state, player) + and can_cross_main_gap(state, player)) # Second, a virtual region for the precursor blockers. Unlike the others, this contains orbs: # the total number of orbs that sit on top of the blockers. Yes, there are only 8. blockers = JakAndDaxterRegion("Precursor Blockers", player, multiworld, level_name, 8) - blockers.add_cell_locations([66], access_rule=lambda state: can_fight(state, player)) + + # 1 in main_area + # 2 in snowball_canyon + # 4 in ice_skating_rink + # 3 in fort_exterior + # 3 in bunny_cave_start + blockers.add_cell_locations([66], access_rule=lambda state: + can_fight(state, player) + and can_cross_main_gap(state, player)) snowball_canyon = JakAndDaxterRegion("Snowball Canyon", player, multiworld, level_name, 28) @@ -96,35 +110,32 @@ def can_jump_blockers(state: CollectionState, p: int) -> bool: main_area.connect(glacier_lurkers, rule=lambda state: can_fight(state, player)) # Yes, the only way into the rest of the level requires advanced movement. - main_area.connect(snowball_canyon, rule=lambda state: - (state.has("Roll", player) and state.has("Roll Jump", player)) - or can_move_fancy(state, player)) + main_area.connect(snowball_canyon, rule=lambda state: can_cross_main_gap(state, player)) snowball_canyon.connect(main_area) # But you can just jump down and run up the ramp. snowball_canyon.connect(bunny_cave_start) # Jump down from the glacier troop cliff. snowball_canyon.connect(fort_exterior) # Jump down, to the left of frozen box cave. snowball_canyon.connect(frozen_box_cave, rule=lambda state: # More advanced movement. - can_move_fancy(state, player)) + can_cross_frozen_cave(state, player)) frozen_box_cave.connect(snowball_canyon, rule=lambda state: # Same movement to go back. - can_move_fancy(state, player)) + can_cross_frozen_cave(state, player)) frozen_box_cave.connect(frozen_box_cave_crates, rule=lambda state: # Same movement to get these crates. - state.has("Yellow Eco Switch", player) - and can_move_fancy(state, player)) + state.has("Yellow Eco Switch", player) # Plus YES. + and can_cross_frozen_cave(state, player)) frozen_box_cave.connect(ice_skating_rink, rule=lambda state: # Same movement to go forward. - can_move_fancy(state, player)) + can_cross_frozen_cave(state, player)) frozen_box_cave_crates.connect(frozen_box_cave) # Semi-virtual region, no moves req'd. ice_skating_rink.connect(frozen_box_cave, rule=lambda state: # Same movement to go back. - can_move_fancy(state, player)) + can_cross_frozen_cave(state, player)) ice_skating_rink.connect(flut_flut_course, rule=lambda state: # Duh. state.has("Flut Flut", player)) ice_skating_rink.connect(fort_exterior) # Just slide down the elevator ramp. - fort_exterior.connect(ice_skating_rink, rule=lambda state: # Twin elevators are tough to reach. - state.has("Double Jump", player) - or state.has("Jump Kick", player)) + fort_exterior.connect(ice_skating_rink, rule=lambda state: # Twin elevators OR scout fly ledge. + can_cross_main_gap(state, player)) # Both doable with main_gap logic. fort_exterior.connect(snowball_canyon) # Run across bridge. fort_exterior.connect(fort_interior, rule=lambda state: # Duh. state.has("Snowy Fort Gate", player)) From f7b688de388af9554af497798c9c9f4efc3ee2c8 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:42:04 -0400 Subject: [PATCH 37/70] Orbsanity (#32) * My big dumb shortcut: a 2000 item array. * A better idea: bundle orbs as a numerical option and make array variable size. * Have Item/Region generation respect the chosen Orbsanity bundle size. Fix trade logic. * Separate Global/Local Orbsanity options. TODO - re-introduce orb factory for per-level option. * Per-level Orbsanity implemented w/ orb bundle factory. * Implement Orbsanity for client, fix some things up for regions. * Fix location name/id mappings. * Fix client orb collection on connection. * Fix minor Deathlink bug, add Update instructions. --- worlds/jakanddaxter/Client.py | 24 ++- worlds/jakanddaxter/Items.py | 23 ++- worlds/jakanddaxter/JakAndDaxterOptions.py | 73 ++++++-- worlds/jakanddaxter/Locations.py | 4 +- worlds/jakanddaxter/Regions.py | 62 ++++--- worlds/jakanddaxter/Rules.py | 82 ++++++++- worlds/jakanddaxter/__init__.py | 58 ++++-- worlds/jakanddaxter/client/MemoryReader.py | 59 ++++-- worlds/jakanddaxter/client/ReplClient.py | 30 ++- worlds/jakanddaxter/docs/setup_en.md | 18 +- worlds/jakanddaxter/locs/OrbLocations.py | 171 ++++++++++-------- worlds/jakanddaxter/regs/BoggySwampRegions.py | 23 ++- worlds/jakanddaxter/regs/FireCanyonRegions.py | 21 ++- .../regs/ForbiddenJungleRegions.py | 22 ++- worlds/jakanddaxter/regs/GeyserRockRegions.py | 21 ++- .../regs/GolAndMaiasCitadelRegions.py | 22 ++- worlds/jakanddaxter/regs/LavaTubeRegions.py | 21 ++- .../regs/LostPrecursorCityRegions.py | 22 ++- .../jakanddaxter/regs/MistyIslandRegions.py | 22 ++- .../jakanddaxter/regs/MountainPassRegions.py | 21 ++- .../regs/PrecursorBasinRegions.py | 21 ++- worlds/jakanddaxter/regs/RegionBase.py | 33 +++- .../jakanddaxter/regs/RockVillageRegions.py | 35 +++- .../regs/SandoverVillageRegions.py | 30 ++- .../jakanddaxter/regs/SentinelBeachRegions.py | 22 ++- .../jakanddaxter/regs/SnowyMountainRegions.py | 22 ++- worlds/jakanddaxter/regs/SpiderCaveRegions.py | 22 ++- .../regs/VolcanicCraterRegions.py | 34 +++- 28 files changed, 822 insertions(+), 196 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 397f63e7cab7..51b26eb53a65 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -107,6 +107,16 @@ async def server_auth(self, password_requested: bool = False): await self.send_connect() def on_package(self, cmd: str, args: dict): + + if cmd == "Connected": + slot_data = args["slot_data"] + if slot_data["enable_orbsanity"] == 1: + self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["level_orbsanity_bundle_size"]) + elif slot_data["enable_orbsanity"] == 2: + self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["global_orbsanity_bundle_size"]) + else: + self.repl.setup_orbsanity(slot_data["enable_orbsanity"], 1) + if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): logger.debug(f"index: {str(index)}, item: {str(item)}") @@ -137,7 +147,8 @@ def on_finish_check(self): async def ap_inform_deathlink(self): if self.memr.deathlink_enabled: - death_text = self.memr.cause_of_death.replace("Jak", self.player_names[self.slot]) + player = self.player_names[self.slot] if self.slot is not None else "Jak" + death_text = self.memr.cause_of_death.replace("Jak", player) await self.send_death(death_text) logger.info(death_text) @@ -155,6 +166,14 @@ async def ap_inform_deathlink_toggle(self): def on_deathlink_toggle(self): create_task_log_exception(self.ap_inform_deathlink_toggle()) + async def repl_reset_orbsanity(self): + if self.memr.orbsanity_enabled: + self.memr.reset_orbsanity = False + self.repl.reset_orbsanity() + + def on_orbsanity_check(self): + create_task_log_exception(self.repl_reset_orbsanity()) + async def run_repl_loop(self): while True: await self.repl.main_tick() @@ -165,7 +184,8 @@ async def run_memr_loop(self): await self.memr.main_tick(self.on_location_check, self.on_finish_check, self.on_deathlink_check, - self.on_deathlink_toggle) + self.on_deathlink_toggle, + self.on_orbsanity_check) await asyncio.sleep(0.1) diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 4f94e7b85c06..28574b14ca1d 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -39,8 +39,29 @@ class JakAndDaxterItem(Item): } # Orbs are also generic and interchangeable. +# These items are only used by Orbsanity, and only one of these +# items will be used corresponding to the chosen bundle size. orb_item_table = { 1: "Precursor Orb", + 2: "Bundle of 2 Precursor Orbs", + 4: "Bundle of 4 Precursor Orbs", + 5: "Bundle of 5 Precursor Orbs", + 8: "Bundle of 8 Precursor Orbs", + 10: "Bundle of 10 Precursor Orbs", + 16: "Bundle of 16 Precursor Orbs", + 20: "Bundle of 20 Precursor Orbs", + 25: "Bundle of 25 Precursor Orbs", + 40: "Bundle of 40 Precursor Orbs", + 50: "Bundle of 50 Precursor Orbs", + 80: "Bundle of 80 Precursor Orbs", + 100: "Bundle of 100 Precursor Orbs", + 125: "Bundle of 125 Precursor Orbs", + 200: "Bundle of 200 Precursor Orbs", + 250: "Bundle of 250 Precursor Orbs", + 400: "Bundle of 400 Precursor Orbs", + 500: "Bundle of 500 Precursor Orbs", + 1000: "Bundle of 1000 Precursor Orbs", + 2000: "Bundle of 2000 Precursor Orbs", } # These are special items representing unique unlocks in the world. Notice that their Item ID equals their @@ -85,8 +106,8 @@ class JakAndDaxterItem(Item): item_table = { **{Cells.to_ap_id(k): cell_item_table[k] for k in cell_item_table}, **{Scouts.to_ap_id(k): scout_item_table[k] for k in scout_item_table}, - **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table}, **{Specials.to_ap_id(k): special_item_table[k] for k in special_item_table}, **{Caches.to_ap_id(k): move_item_table[k] for k in move_item_table}, + **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table}, jak1_max: "Green Eco Pill" # Filler item. } diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index de5581f4c959..5391b7b094c3 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,23 +1,74 @@ -import os from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions +from Options import Toggle, PerGameCommonOptions, Choice class EnableMoveRandomizer(Toggle): - """Enable to include movement options as items in the randomizer. - Jak is only able to run, swim, and single jump, until you find his other moves. - Adds 11 items to the pool.""" + """Enable to include movement options as items in the randomizer. Jak is only able to run, swim, and single jump, + until you find his other moves. This adds 11 items to the pool.""" display_name = "Enable Move Randomizer" -# class EnableOrbsanity(Toggle): -# """Enable to include Precursor Orbs as an ordered list of progressive checks. -# Each orb you collect triggers the next release in the list. -# Adds 2000 items to the pool.""" -# display_name = "Enable Orbsanity" +class EnableOrbsanity(Choice): + """Enable to include bundles of Precursor Orbs as an ordered list of progressive checks. Every time you collect + the chosen number of orbs, you will trigger the next release in the list. "Per Level" means these lists are + generated and populated for each level in the game (Geyser Rock, Sandover Village, etc.). "Global" means there is + only one list for the entire game. + + This adds a number of Items and Locations to the pool inversely proportional to the size of the bundle. + For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs, + you will add 8 items to the pool.""" + display_name = "Enable Orbsanity" + option_off = 0 + option_per_level = 1 + option_global = 2 + default = 0 + + +class GlobalOrbsanityBundleSize(Choice): + """Set the size of the bundle for Global Orbsanity. + This only applies if "Enable Orbsanity" is set to "Global." + There are 2000 orbs in the game, so your bundle size must be a factor of 2000.""" + display_name = "Global Orbsanity Bundle Size" + option_1_orb = 1 + option_2_orbs = 2 + option_4_orbs = 4 + option_5_orbs = 5 + option_8_orbs = 8 + option_10_orbs = 10 + option_16_orbs = 16 + option_20_orbs = 20 + option_25_orbs = 25 + option_40_orbs = 40 + option_50_orbs = 50 + option_80_orbs = 80 + option_100_orbs = 100 + option_125_orbs = 125 + option_200_orbs = 200 + option_250_orbs = 250 + option_400_orbs = 400 + option_500_orbs = 500 + option_1000_orbs = 1000 + option_2000_orbs = 2000 + default = 1 + + +class PerLevelOrbsanityBundleSize(Choice): + """Set the size of the bundle for Per Level Orbsanity. + This only applies if "Enable Orbsanity" is set to "Per Level." + There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.""" + display_name = "Per Level Orbsanity Bundle Size" + option_1_orb = 1 + option_2_orbs = 2 + option_5_orbs = 5 + option_10_orbs = 10 + option_25_orbs = 25 + option_50_orbs = 50 + default = 1 @dataclass class JakAndDaxterOptions(PerGameCommonOptions): enable_move_randomizer: EnableMoveRandomizer - # enable_orbsanity: EnableOrbsanity + enable_orbsanity: EnableOrbsanity + global_orbsanity_bundle_size: GlobalOrbsanityBundleSize + level_orbsanity_bundle_size: PerLevelOrbsanityBundleSize diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index 3cea4f2aced2..ed0273060e25 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,6 +1,7 @@ from BaseClasses import Location from .GameID import jak1_name -from .locs import (CellLocations as Cells, +from .locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials, OrbCacheLocations as Caches) @@ -48,4 +49,5 @@ class JakAndDaxterLocation(Location): **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable}, **{Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable}, **{Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable}, + **{Orbs.to_ap_id(k): Orbs.loc_orbBundleTable[k] for k in Orbs.loc_orbBundleTable} } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 4e4ca73b3245..168640b9e247 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,8 +1,9 @@ -import typing from BaseClasses import MultiWorld -from .Items import item_table from .JakAndDaxterOptions import JakAndDaxterOptions -from .locs import (CellLocations as Cells, +from .Items import item_table +from .Rules import can_reach_orbs +from .locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Scouts) from .regs.RegionBase import JakAndDaxterRegion from .regs import (GeyserRockRegions as GeyserRock, @@ -30,7 +31,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: multiworld.regions.append(menu) # Build the special "Free 7 Scout Flies" Region. This is a virtual region always accessible to Menu. - # The Power Cells within it are automatically checked when you receive the 7th scout fly for the corresponding cell. + # The Locations within are automatically checked when you receive the 7th scout fly for the corresponding cell. free7 = JakAndDaxterRegion("'Free 7 Scout Flies' Power Cells", player, multiworld) free7.add_cell_locations(Cells.loc7SF_cellTable.keys()) for scout_fly_cell in free7.locations: @@ -39,27 +40,46 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(scout_fly_cell.address)) scout_fly_cell.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7) multiworld.regions.append(free7) + menu.connect(free7) + + # If Global Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Menu. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 2: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld) + + bundle_size = options.global_orbsanity_bundle_size.value + bundle_count = int(2000 / bundle_size) + for bundle_index in range(bundle_count): + + # Unlike Per-Level Orbsanity, Global Orbsanity Locations always have a level_index of 16. + orbs.add_orb_locations(16, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + menu.connect(orbs) # Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules. - [gr] = GeyserRock.build_regions("Geyser Rock", player, multiworld) - [sv] = SandoverVillage.build_regions("Sandover Village", player, multiworld) - [fj] = ForbiddenJungle.build_regions("Forbidden Jungle", player, multiworld) - [sb] = SentinelBeach.build_regions("Sentinel Beach", player, multiworld) - [mi] = MistyIsland.build_regions("Misty Island", player, multiworld) - [fc] = FireCanyon.build_regions("Fire Canyon", player, multiworld) - [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", player, multiworld) - [pb] = PrecursorBasin.build_regions("Precursor Basin", player, multiworld) - [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", player, multiworld) - [bs] = BoggySwamp.build_regions("Boggy Swamp", player, multiworld) - [mp, mpr] = MountainPass.build_regions("Mountain Pass", player, multiworld) - [vc] = VolcanicCrater.build_regions("Volcanic Crater", player, multiworld) - [sc] = SpiderCave.build_regions("Spider Cave", player, multiworld) - [sm] = SnowyMountain.build_regions("Snowy Mountain", player, multiworld) - [lt] = LavaTube.build_regions("Lava Tube", player, multiworld) - [gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", player, multiworld) + [gr] = GeyserRock.build_regions("Geyser Rock", multiworld, options, player) + [sv] = SandoverVillage.build_regions("Sandover Village", multiworld, options, player) + [fj] = ForbiddenJungle.build_regions("Forbidden Jungle", multiworld, options, player) + [sb] = SentinelBeach.build_regions("Sentinel Beach", multiworld, options, player) + [mi] = MistyIsland.build_regions("Misty Island", multiworld, options, player) + [fc] = FireCanyon.build_regions("Fire Canyon", multiworld, options, player) + [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", multiworld, options, player) + [pb] = PrecursorBasin.build_regions("Precursor Basin", multiworld, options, player) + [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", multiworld, options, player) + [bs] = BoggySwamp.build_regions("Boggy Swamp", multiworld, options, player) + [mp, mpr] = MountainPass.build_regions("Mountain Pass", multiworld, options, player) + [vc] = VolcanicCrater.build_regions("Volcanic Crater", multiworld, options, player) + [sc] = SpiderCave.build_regions("Spider Cave", multiworld, options, player) + [sm] = SnowyMountain.build_regions("Snowy Mountain", multiworld, options, player) + [lt] = LavaTube.build_regions("Lava Tube", multiworld, options, player) + [gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", multiworld, options, player) # Define the interconnecting rules. - menu.connect(free7) menu.connect(gr) gr.connect(sv) # Geyser Rock modified to let you leave at any time. sv.connect(fj) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 9ff3d29d3ecc..ad7c6dda1ff7 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,9 +1,51 @@ +import math import typing from BaseClasses import MultiWorld, CollectionState from .JakAndDaxterOptions import JakAndDaxterOptions +from .Items import orb_item_table from .locs import CellLocations as Cells from .Locations import location_table -from .Regions import JakAndDaxterRegion +from .regs.RegionBase import JakAndDaxterRegion + + +def can_reach_orbs(state: CollectionState, + player: int, + multiworld: MultiWorld, + options: JakAndDaxterOptions, + level_name: str = None) -> int: + + # Global Orbsanity and No Orbsanity both treat orbs as completely interchangeable. + # Per Level Orbsanity needs to know if you can reach orbs *in a particular level.* + if options.enable_orbsanity.value in [0, 2]: + return can_reach_orbs_global(state, player, multiworld) + else: + return can_reach_orbs_level(state, player, multiworld, level_name) + + +def can_reach_orbs_global(state: CollectionState, + player: int, + multiworld: MultiWorld) -> int: + + accessible_orbs = 0 + for region in multiworld.get_regions(player): + if state.can_reach(region, "Region", player): + accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count + + return accessible_orbs + + +def can_reach_orbs_level(state: CollectionState, + player: int, + multiworld: MultiWorld, + level_name: str) -> int: + + accessible_orbs = 0 + regions = [typing.cast(JakAndDaxterRegion, reg) for reg in multiworld.get_regions(player)] + for region in regions: + if region.level_name == level_name and state.can_reach(region, "Region", player): + accessible_orbs += region.orb_count + + return accessible_orbs # TODO - Until we come up with a better progressive system for the traders (that avoids hard-locking if you pay the @@ -11,14 +53,28 @@ def can_trade(state: CollectionState, player: int, multiworld: MultiWorld, + options: JakAndDaxterOptions, required_orbs: int, required_previous_trade: int = None) -> bool: - accessible_orbs = 0 - for region in multiworld.get_regions(player): - if state.can_reach(region, "Region", player): - accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count + if options.enable_orbsanity.value == 1: + bundle_size = options.level_orbsanity_bundle_size.value + return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) + elif options.enable_orbsanity.value == 2: + bundle_size = options.global_orbsanity_bundle_size.value + return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) + else: + return can_trade_regular(state, player, multiworld, required_orbs, required_previous_trade) + + +def can_trade_regular(state: CollectionState, + player: int, + multiworld: MultiWorld, + required_orbs: int, + required_previous_trade: int = None) -> bool: + # We know that Orbsanity is off, so count orbs globally. + accessible_orbs = can_reach_orbs_global(state, player, multiworld) if required_previous_trade: name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)] return (accessible_orbs >= required_orbs @@ -27,6 +83,22 @@ def can_trade(state: CollectionState, return accessible_orbs >= required_orbs +def can_trade_orbsanity(state: CollectionState, + player: int, + orb_bundle_size: int, + required_orbs: int, + required_previous_trade: int = None) -> bool: + + required_count = math.ceil(required_orbs / orb_bundle_size) + orb_bundle_name = orb_item_table[orb_bundle_size] + if required_previous_trade: + name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)] + return (state.has(orb_bundle_name, player, required_count) + and state.can_reach(name_of_previous_trade, "Location", player=player)) + else: + return state.has(orb_bundle_name, player, required_count) + + def can_free_scout_flies(state: CollectionState, player: int) -> bool: return (state.has("Jump Dive", player) or (state.has("Crouch", player) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 7660bdd39dfb..ad7555397bda 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,4 +1,4 @@ -import typing +from typing import Dict, Any, ClassVar import settings from Utils import local_path, visualize_regions @@ -66,14 +66,13 @@ class JakAndDaxterWorld(World): required_client_version = (0, 4, 6) # Options - settings: typing.ClassVar[JakAndDaxterSettings] + settings: ClassVar[JakAndDaxterSettings] options_dataclass = JakAndDaxterOptions options: JakAndDaxterOptions # Web world web = JakAndDaxterWebWorld() - # Items and Locations # Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs. # Remember, the game ID and various offsets for each item type have already been calculated. item_name_to_id = {item_table[k]: k for k in item_table} @@ -91,15 +90,22 @@ class JakAndDaxterWorld(World): if k in range(jak1_id + Orbs.orb_offset, jak1_max)}, } - # Regions and Rules # This will also set Locations, Location access rules, Region access rules, etc. - def create_regions(self): + def create_regions(self) -> None: create_regions(self.multiworld, self.options, self.player) # visualize_regions(self.multiworld.get_region("Menu", self.player), "jak.puml") + # Helper function to get the correct orb bundle size. + def get_orb_bundle_size(self) -> int: + if self.options.enable_orbsanity.value == 1: + return self.options.level_orbsanity_bundle_size.value + elif self.options.enable_orbsanity.value == 2: + return self.options.global_orbsanity_bundle_size.value + else: + return 0 + # Helper function to reuse some nasty if/else trees. - @staticmethod - def item_type_helper(item) -> (int, ItemClassification): + def item_type_helper(self, item) -> (int, ItemClassification): # Make 101 Power Cells. if item in range(jak1_id, jak1_id + Scouts.fly_offset): classification = ItemClassification.progression_skip_balancing @@ -120,10 +126,11 @@ def item_type_helper(item) -> (int, ItemClassification): classification = ItemClassification.progression count = 1 - # TODO - Make 2000 Precursor Orbs, ONLY IF Orbsanity is enabled. + # Make N Precursor Orb bundles, where N is 2000 / bundle size. elif item in range(jak1_id + Orbs.orb_offset, jak1_max): classification = ItemClassification.progression_skip_balancing - count = 0 + size = self.get_orb_bundle_size() + count = int(2000 / size) if size > 0 else 0 # Don't divide by zero! # Under normal circumstances, we will create 0 filler items. # We will manually create filler items as needed. @@ -137,19 +144,30 @@ def item_type_helper(item) -> (int, ItemClassification): return count, classification - def create_items(self): - for item_id in item_table: + def create_items(self) -> None: + for item_name in self.item_name_to_id: + item_id = self.item_name_to_id[item_name] # Handle Move Randomizer option. # If it is OFF, put all moves in your starting inventory instead of the item pool, # then fill the item pool with a corresponding amount of filler items. - if not self.options.enable_move_randomizer and item_table[item_id] in self.item_name_groups["Moves"]: - self.multiworld.push_precollected(self.create_item(item_table[item_id])) + if item_name in self.item_name_groups["Moves"] and not self.options.enable_move_randomizer: + self.multiworld.push_precollected(self.create_item(item_name)) self.multiworld.itempool += [self.create_item(self.get_filler_item_name())] - else: - count, classification = self.item_type_helper(item_id) - self.multiworld.itempool += [JakAndDaxterItem(item_table[item_id], classification, item_id, self.player) - for _ in range(count)] + continue + + # Handle Orbsanity option. + # If it is OFF, don't add any orbs to the item pool. + # If it is ON, only add the orb bundle that matches the choice in options. + if (item_name in self.item_name_groups["Precursor Orbs"] + and ((self.options.enable_orbsanity.value == 0 + or Orbs.to_game_id(item_id) != self.get_orb_bundle_size()))): + continue + + # In every other scenario, do this. + count, classification = self.item_type_helper(item_id) + self.multiworld.itempool += [JakAndDaxterItem(item_name, classification, item_id, self.player) + for _ in range(count)] def create_item(self, name: str) -> Item: item_id = self.item_name_to_id[name] @@ -158,3 +176,9 @@ def create_item(self, name: str) -> Item: def get_filler_item_name(self) -> str: return "Green Eco Pill" + + def fill_slot_data(self) -> Dict[str, Any]: + return self.options.as_dict("enable_move_randomizer", + "enable_orbsanity", + "global_orbsanity_bundle_size", + "level_orbsanity_bundle_size") diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 8fd54b8b3202..a0787071bf04 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,5 +1,5 @@ import random -import typing +from typing import ByteString, List, Callable import json import pymem from pymem import pattern @@ -7,7 +7,8 @@ from dataclasses import dataclass from CommonClient import logger -from ..locs import (CellLocations as Cells, +from ..locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials, OrbCacheLocations as Caches) @@ -65,6 +66,12 @@ def define(self, size: int, length: int = 1) -> int: moves_received_offset = offsets.define(sizeof_uint8, 16) moverando_enabled_offset = offsets.define(sizeof_uint8) +# Orbsanity information. +orbsanity_option_offset = offsets.define(sizeof_uint8) +orbsanity_bundle_offset = offsets.define(sizeof_uint32) +collected_bundle_level_offset = offsets.define(sizeof_uint8) +collected_bundle_count_offset = offsets.define(sizeof_uint32) + # The End. end_marker_offset = offsets.define(sizeof_uint8, 4) @@ -111,7 +118,7 @@ def autopsy(died: int) -> str: class JakAndDaxterMemoryReader: - marker: typing.ByteString + marker: ByteString goal_address = None connected: bool = False initiated_connect: bool = False @@ -128,15 +135,20 @@ class JakAndDaxterMemoryReader: send_deathlink: bool = False cause_of_death: str = "" - def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'): + # Orbsanity handling + orbsanity_enabled: bool = False + reset_orbsanity: bool = False + + def __init__(self, marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker self.connect() async def main_tick(self, - location_callback: typing.Callable, - finish_callback: typing.Callable, - deathlink_callback: typing.Callable, - deathlink_toggle: typing.Callable): + location_callback: Callable, + finish_callback: Callable, + deathlink_callback: Callable, + deathlink_toggle: Callable, + orbsanity_callback: Callable): if self.initiated_connect: await self.connect() self.initiated_connect = False @@ -171,6 +183,9 @@ async def main_tick(self, if self.send_deathlink: deathlink_callback() + if self.reset_orbsanity: + orbsanity_callback() + async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel @@ -207,7 +222,7 @@ def print_status(self): logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index]) if self.outbox_index else "None")) - def read_memory(self) -> typing.List[int]: + def read_memory(self) -> List[int]: try: next_cell_index = self.read_goal_address(0, sizeof_uint64) next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64) @@ -262,8 +277,30 @@ def read_memory(self) -> typing.List[int]: logger.debug("Checked orb cache: " + str(next_cache)) # Listen for any changes to this setting. - moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8) - self.moverando_enabled = bool(moverando_flag) + # moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8) + # self.moverando_enabled = bool(moverando_flag) + + orbsanity_option = self.read_goal_address(orbsanity_option_offset, sizeof_uint8) + orbsanity_bundle = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32) + self.orbsanity_enabled = orbsanity_option > 0 + + # Treat these values like the Deathlink flag. They need to be reset once they are checked. + collected_bundle_level = self.read_goal_address(collected_bundle_level_offset, sizeof_uint8) + collected_bundle_count = self.read_goal_address(collected_bundle_count_offset, sizeof_uint32) + + if orbsanity_option > 0 and collected_bundle_count > 0: + # Count up from the first bundle, by bundle size, until you reach the latest collected bundle. + # e.g. {25, 50, 75, 100, 125...} + for k in range(orbsanity_bundle, + orbsanity_bundle + collected_bundle_count, # Range max is non-inclusive. + orbsanity_bundle): + + bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(collected_bundle_level, k, orbsanity_bundle)) + if bundle_ap_id not in self.location_outbox: + self.location_outbox.append(bundle_ap_id) + logger.debug("Checked orb bundle: " + str(bundle_ap_id)) + + # self.reset_orbsanity = True except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index c1fe78d3b221..a646b553116b 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -257,15 +257,15 @@ def receive_move(self, ap_id: int) -> bool: return ok def receive_precursor_orb(self, ap_id: int) -> bool: - orb_id = Orbs.to_game_id(ap_id) + orb_amount = Orbs.to_game_id(ap_id) ok = self.send_form("(send-event " "*target* \'get-archipelago " "(pickup-type money) " - "(the float " + str(orb_id) + "))") + "(the float " + str(orb_amount) + "))") if ok: - logger.debug(f"Received a Precursor Orb!") + logger.debug(f"Received {orb_amount} Precursor Orbs!") else: - logger.error(f"Unable to receive a Precursor Orb!") + logger.error(f"Unable to receive {orb_amount} Precursor Orbs!") return ok # Green eco pills are our filler item. Use the get-pickup event instead to handle being full health. @@ -308,6 +308,28 @@ def reset_deathlink(self) -> bool: logger.error(f"Unable to reset deathlink flag!") return ok + def setup_orbsanity(self, option: int, bundle: int) -> bool: + ok = self.send_form(f"(ap-setup-orbs! (the uint {option}) (the uint {bundle}))") + if ok: + logger.debug(f"Set up orbsanity: Option {option}, Bundle {bundle}!") + else: + logger.error(f"Unable to set up orbsanity: Option {option}, Bundle {bundle}!") + return ok + + def reset_orbsanity(self) -> bool: + ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-level) 0)") + if ok: + logger.debug(f"Reset level ID for collected orbsanity bundle!") + else: + logger.error(f"Unable to reset level ID for collected orbsanity bundle!") + + ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-count) 0)") + if ok: + logger.debug(f"Reset orb count for collected orbsanity bundle!") + else: + logger.error(f"Unable to reset orb count for collected orbsanity bundle!") + return ok + def save_data(self): with open("jakanddaxter_item_inbox.json", "w+") as f: dump = { diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index cdd2bcad7bf9..7199666f6a2c 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -29,11 +29,11 @@ At this time, this method of setup works on Windows only, but Linux support is a - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\archipelagoal\iso_data` should have *all* the same files as - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\_iso_data`, if it doesn't, copy those files over manually. - And then `Recompile` if you needed to copy the files over. -- **DO NOT LAUNCH THE GAME FROM THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). +- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). ***Archipelago Launcher*** -- Copy the `jakanddaxter.apworld` file into your `Archipelago/lib/worlds` directory. +- Copy the `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. - Run the Archipelago Launcher. - From the left-most list, click `Generate Template Options`. @@ -44,6 +44,20 @@ At this time, this method of setup works on Windows only, but Linux support is a - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. - You can sort by Date Modified to make it easy to find. +## Updates and New Releases + +***OpenGOAL Mod Launcher*** + +- Run the Mod Launcher and click `ArchipelaGOAL` in the mod list. +- Click `Launch` to download and install any new updates that have been released. +- You can verify your version once you reach the title screen menu by navigating to `Options > Game Options > Miscellaneous > Speedrunner Mode`. +- Turn on `Speedrunner Mode` and exit the menu. You should see the installed version number in the bottom left corner. Then turn `Speedrunner Mode` back off. +- Once you've verified your version, you can close the game. Remember, this is just for downloading updates. **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** + +***Archipelago Launcher*** + +- Copy the latest `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. + ## Starting a Game ***New Game*** diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index 6e45a300d022..1853730c335c 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + from ..GameID import jak1_id # Precursor Orbs are not necessarily given ID's by the game. @@ -7,14 +9,13 @@ # so like Power Cells these are not ordered, nor contiguous, nor exclusively orbs. # In fact, other ID's in this range belong to actors that spawn orbs when they are activated or when they die, -# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head. +# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head. These orbs that spawn +# from parent actors DON'T have an Actor ID themselves - the parent object keeps track of how many of its orbs +# have been picked up. -# These orbs that spawn from parent actors DON'T have an Actor ID themselves - the parent object keeps -# track of how many of its orbs have been picked up. If you pick up only some of its orbs, it -# will respawn when you leave the area, and only drop the remaining number of orbs when activated/killed. -# Once all the orbs are picked up, the actor will permanently "retire" and never spawn again. -# The maximum number of orbs that any actor can spawn is 30 (the orb caches in citadel). Covering -# these ID-less orbs may need to be a future enhancement. TODO ^^ +# In order to deal with this mess, we're creating a factory class that will generate Orb Locations for us. +# This will be compatible with both Global Orbsanity and Per-Level Orbsanity, allowing us to create any +# number of Locations depending on the bundle size chosen, while also guaranteeing that each has a unique address. # We can use 2^15 to offset them from Orb Caches, because Orb Cache ID's max out at (jak1_id + 17792). orb_offset = 32768 @@ -32,68 +33,96 @@ def to_game_id(ap_id: int) -> int: return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets. -# The ID's you see below correspond directly to that orb's Actor ID in the game. - -# Geyser Rock -locGR_orbTable = { -} - -# Sandover Village -locSV_orbTable = { -} - -# Forbidden Jungle -locFJ_orbTable = { -} - -# Sentinel Beach -locSB_orbTable = { -} - -# Misty Island -locMI_orbTable = { -} - -# Fire Canyon -locFC_orbTable = { -} - -# Rock Village -locRV_orbTable = { -} - -# Precursor Basin -locPB_orbTable = { -} - -# Lost Precursor City -locLPC_orbTable = { -} - -# Boggy Swamp -locBS_orbTable = { -} - -# Mountain Pass -locMP_orbTable = { -} - -# Volcanic Crater -locVC_orbTable = { -} - -# Spider Cave -locSC_orbTable = { -} - -# Snowy Mountain -locSM_orbTable = { -} - -# Lava Tube -locLT_orbTable = { -} - -# Gol and Maias Citadel -locGMC_orbTable = { +# Use this when the Memory Reader learns that you checked a specific bundle. +# Offset each level by 200 orbs (max number in any level), {200, 400, ...} +# then divide orb count by bundle size, {201, 202, ...} +# then subtract 1. {200, 201, ...} +def find_address(level_index: int, orb_count: int, bundle_size: int) -> int: + result = (level_index * 200) + (orb_count // bundle_size) - 1 + return result + + +# Use this when assigning addresses during region generation. +def create_address(level_index: int, bundle_index: int) -> int: + result = (level_index * 200) + bundle_index + return result + + +# What follows is our method of generating all the name/ID pairs for location_name_to_id. +# Remember that not every bundle will be used in the actual seed, we just need this as a static map of strings to ints. +level_info = { + "": { + "level_index": 16, # Global + "orbs": 2000 + }, + "Geyser Rock": { + "level_index": 0, + "orbs": 50 + }, + "Sandover Village": { + "level_index": 1, + "orbs": 50 + }, + "Sentinel Beach": { + "level_index": 2, + "orbs": 150 + }, + "Forbidden Jungle": { + "level_index": 3, + "orbs": 150 + }, + "Misty Island": { + "level_index": 4, + "orbs": 150 + }, + "Fire Canyon": { + "level_index": 5, + "orbs": 50 + }, + "Rock Village": { + "level_index": 6, + "orbs": 50 + }, + "Lost Precursor City": { + "level_index": 7, + "orbs": 200 + }, + "Boggy Swamp": { + "level_index": 8, + "orbs": 200 + }, + "Precursor Basin": { + "level_index": 9, + "orbs": 200 + }, + "Mountain Pass": { + "level_index": 10, + "orbs": 50 + }, + "Volcanic Crater": { + "level_index": 11, + "orbs": 50 + }, + "Snowy Mountain": { + "level_index": 12, + "orbs": 200 + }, + "Spider Cave": { + "level_index": 13, + "orbs": 200 + }, + "Lava Tube": { + "level_index": 14, + "orbs": 50 + }, + "Gol and Maia's Citadel": { + "level_index": 15, + "orbs": 200 + } +} + +loc_orbBundleTable = { + create_address(level_info[name]["level_index"], index): f"{name} Orb Bundle {index + 1}".strip() + for name in level_info + for index in range(level_info[name]["orbs"]) } diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py index 9f5600f5abca..e6b621e38d7d 100644 --- a/worlds/jakanddaxter/regs/BoggySwampRegions.py +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) @@ -20,6 +21,7 @@ def can_jump_higher(state: CollectionState, p: int) -> bool: or (state.has("Punch", p) and state.has("Punch Uppercut", p))) # Orb crates and fly box in this area can be gotten with yellow eco and goggles. + # Start with the first yellow eco cluster near first_bats and work your way backward toward the entrance. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) main_area.add_fly_locations([43]) @@ -151,4 +153,21 @@ def can_jump_higher(state: CollectionState, p: int) -> bool: multiworld.regions.append(last_tar_pit) multiworld.regions.append(fourth_tether) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(8, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/FireCanyonRegions.py b/worlds/jakanddaxter/regs/FireCanyonRegions.py index b77d28b7d626..473248d33c16 100644 --- a/worlds/jakanddaxter/regs/FireCanyonRegions.py +++ b/worlds/jakanddaxter/regs/FireCanyonRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) @@ -14,4 +16,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(5, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py index 33e49a9d04b1..ca48cf2c8b55 100644 --- a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py +++ b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 25) @@ -80,4 +81,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(temple_int_pre_blue) multiworld.regions.append(temple_int_post_blue) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(150 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(3, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py index ed4c4daaf325..938fe51aedd6 100644 --- a/worlds/jakanddaxter/regs/GeyserRockRegions.py +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) main_area.add_cell_locations([92, 93]) @@ -23,4 +25,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) multiworld.regions.append(cliff) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(0, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index da3dabfe7780..14db2b19e6b1 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -1,11 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs # God help me... here we go. -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) @@ -112,4 +113,21 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: multiworld.regions.append(rotating_tower) multiworld.regions.append(final_boss) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(15, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area, final_boss] diff --git a/worlds/jakanddaxter/regs/LavaTubeRegions.py b/worlds/jakanddaxter/regs/LavaTubeRegions.py index d8c8a7ec41d8..04fb16af98ee 100644 --- a/worlds/jakanddaxter/regs/LavaTubeRegions.py +++ b/worlds/jakanddaxter/regs/LavaTubeRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) @@ -14,4 +16,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(14, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py index de5251a78717..102283bf8674 100644 --- a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # Just the starting area. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 4) @@ -127,4 +128,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(second_slide) multiworld.regions.append(helix_room) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(7, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py index 259e9c5c23e9..c40601d794fd 100644 --- a/worlds/jakanddaxter/regs/MistyIslandRegions.py +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 9) @@ -113,4 +114,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(lower_approach) multiworld.regions.append(arena) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(150 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(4, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index b1eaea1019ad..f6e2419689cb 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This is basically just Klaww. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) @@ -30,5 +32,22 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(race) multiworld.regions.append(shortcut) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(10, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + # Return race required for inter-level connections. return [main_area, race] diff --git a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py index 7b1ea8a883fd..0e24c1b774c9 100644 --- a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py +++ b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py @@ -1,10 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion +from .. import JakAndDaxterOptions +from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 200) @@ -14,4 +16,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(9, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/RegionBase.py b/worlds/jakanddaxter/regs/RegionBase.py index 40e4cb92735d..42534e462a8e 100644 --- a/worlds/jakanddaxter/regs/RegionBase.py +++ b/worlds/jakanddaxter/regs/RegionBase.py @@ -3,7 +3,8 @@ from ..GameID import jak1_name from ..JakAndDaxterOptions import JakAndDaxterOptions from ..Locations import JakAndDaxterLocation, location_table -from ..locs import (CellLocations as Cells, +from ..locs import (OrbLocations as Orbs, + CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials, OrbCacheLocations as Caches) @@ -31,7 +32,8 @@ def add_cell_locations(self, locations: List[int], access_rule: Callable = None) Converts Game ID's to AP ID's for you. """ for loc in locations: - self.add_jak_locations(Cells.to_ap_id(loc), access_rule) + ap_id = Cells.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) def add_fly_locations(self, locations: List[int], access_rule: Callable = None): """ @@ -39,7 +41,8 @@ def add_fly_locations(self, locations: List[int], access_rule: Callable = None): Converts Game ID's to AP ID's for you. """ for loc in locations: - self.add_jak_locations(Scouts.to_ap_id(loc), access_rule) + ap_id = Scouts.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) def add_special_locations(self, locations: List[int], access_rule: Callable = None): """ @@ -49,7 +52,8 @@ def add_special_locations(self, locations: List[int], access_rule: Callable = No Power Cell Locations, so you get 2 unlocks for these rather than 1. """ for loc in locations: - self.add_jak_locations(Specials.to_ap_id(loc), access_rule) + ap_id = Specials.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) def add_cache_locations(self, locations: List[int], access_rule: Callable = None): """ @@ -57,13 +61,28 @@ def add_cache_locations(self, locations: List[int], access_rule: Callable = None Converts Game ID's to AP ID's for you. """ for loc in locations: - self.add_jak_locations(Caches.to_ap_id(loc), access_rule) + ap_id = Caches.to_ap_id(loc) + self.add_jak_locations(ap_id, location_table[ap_id], access_rule) - def add_jak_locations(self, ap_id: int, access_rule: Callable = None): + def add_orb_locations(self, level_index: int, bundle_index: int, bundle_size: int, access_rule: Callable = None): + """ + Adds Orb Bundle Locations to this region equal to `bundle_count`. Used only when Per-Level Orbsanity is enabled. + The orb factory class will handle AP ID enumeration. + """ + bundle_address = Orbs.create_address(level_index, bundle_index) + location = JakAndDaxterLocation(self.player, + f"{self.level_name} Orb Bundle {bundle_index + 1}".strip(), + Orbs.to_ap_id(bundle_address), + self) + if access_rule: + location.access_rule = access_rule + self.locations.append(location) + + def add_jak_locations(self, ap_id: int, name: str, access_rule: Callable = None): """ Helper function to add Locations. Not to be used directly. """ - location = JakAndDaxterLocation(self.player, location_table[ap_id], ap_id, self) + location = JakAndDaxterLocation(self.player, name, ap_id, self) if access_rule: location.access_rule = access_rule self.locations.append(location) diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index 09b0858ff3fa..d17ac0cb7802 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -1,23 +1,24 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_trade +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) main_area.add_cell_locations([31], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([32], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([33], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([34], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([35], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 34)) + can_trade(state, player, multiworld, options, 1530, 34)) # These 2 scout fly boxes can be broken by running with nearby blue eco. main_area.add_fly_locations([196684, 262220]) @@ -33,8 +34,9 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ orb_cache.add_cache_locations([10945], access_rule=lambda state: (state.has("Roll", player) and state.has("Roll Jump", player))) + # Fly here can be gotten with Yellow Eco from Boggy, goggles, and no extra movement options (see fly ID 43). pontoon_bridge = JakAndDaxterRegion("Pontoon Bridge", player, multiworld, level_name, 7) - pontoon_bridge.add_fly_locations([393292], access_rule=lambda state: can_free_scout_flies(state, player)) + pontoon_bridge.add_fly_locations([393292]) klaww_cliff = JakAndDaxterRegion("Klaww's Cliff", player, multiworld, level_name, 0) @@ -59,5 +61,22 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(pontoon_bridge) multiworld.regions.append(klaww_cliff) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(6, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + # Return klaww_cliff required for inter-level connections. return [main_area, pontoon_bridge, klaww_cliff] diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index ac4d904a4516..1a0c3ccb00db 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -1,19 +1,20 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_trade +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 26) # Yakows requires no combat. main_area.add_cell_locations([10]) main_area.add_cell_locations([11], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([12], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) # These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach. main_area.add_fly_locations([262219, 327755, 131147, 65611]) @@ -32,9 +33,9 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6) oracle_platforms.add_cell_locations([13], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) oracle_platforms.add_cell_locations([14], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 13)) + can_trade(state, player, multiworld, options, 1530, 13)) oracle_platforms.add_fly_locations([393291], access_rule=lambda state: can_free_scout_flies(state, player)) @@ -68,4 +69,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(yakow_cliff) multiworld.regions.append(oracle_platforms) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(1, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py index 0e85dc0573a7..f573f4ee923f 100644 --- a/worlds/jakanddaxter/regs/SentinelBeachRegions.py +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 128) main_area.add_cell_locations([18, 21, 22]) @@ -82,4 +83,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(blue_ridge) multiworld.regions.append(cannon_tower) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(150 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(2, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index dbd92c297c44..b6fd711171ea 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -1,11 +1,12 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs # God help me... here we go. -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # We need a few helper functions. def can_cross_main_gap(state: CollectionState, p: int) -> bool: @@ -189,4 +190,21 @@ def can_jump_blockers(state: CollectionState, p: int) -> bool: multiworld.regions.append(fort_interior_base) multiworld.regions.append(fort_interior_course_end) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(12, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py index 3d9e1093e1dd..84af260a099a 100644 --- a/worlds/jakanddaxter/regs/SpiderCaveRegions.py +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -1,10 +1,11 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_fight +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # A large amount of this area can be covered by single jump, floating platforms, web trampolines, and goggles. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 63) @@ -107,4 +108,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(spider_tunnel) multiworld.regions.append(spider_tunnel_crates) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(200 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(13, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py index 48241d647cec..4f8bd8a2ebed 100644 --- a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -1,26 +1,27 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from ..Rules import can_free_scout_flies, can_trade +from .. import JakAndDaxterOptions +from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: # No area is inaccessible in VC even with only running and jumping. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) main_area.add_cell_locations([96], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([97], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 96)) + can_trade(state, player, multiworld, options, 1530, 96)) main_area.add_cell_locations([98], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 97)) + can_trade(state, player, multiworld, options, 1530, 97)) main_area.add_cell_locations([99], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 98)) + can_trade(state, player, multiworld, options, 1530, 98)) main_area.add_cell_locations([100], access_rule=lambda state: - can_trade(state, player, multiworld, 1530)) + can_trade(state, player, multiworld, options, 1530)) main_area.add_cell_locations([101], access_rule=lambda state: - can_trade(state, player, multiworld, 1530, 100)) + can_trade(state, player, multiworld, options, 1530, 100)) # Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping # and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode). @@ -35,4 +36,21 @@ def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[ multiworld.regions.append(main_area) + # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always + # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. + if options.enable_orbsanity.value == 1: + orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) + + bundle_size = options.level_orbsanity_bundle_size.value + bundle_count = int(50 / bundle_size) + for bundle_index in range(bundle_count): + orbs.add_orb_locations(11, + bundle_index, + bundle_size, + access_rule=lambda state, bundle=bundle_index: + can_reach_orbs(state, player, multiworld, options, level_name) + >= (bundle_size * (bundle + 1))) + multiworld.regions.append(orbs) + main_area.connect(orbs) + return [main_area] From 35bf07806f557833db9e549ef71bb907e6d60990 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 23 Jul 2024 19:32:15 -0400 Subject: [PATCH 38/70] Finishing Touches (#36) * Set up connector level thresholds, completion goal choices. * Send AP sender/recipient info to game via client. * Slight refactors. * Refactor option checking, add DataStorage handling of traded orbs. * Update instructions to change order of load/connect. * Add Option check to ensure enough Locations exist for Cell Count thresholds. Fix Final Door region. * Need some height move to get LPC sunken chamber cell. --- worlds/jakanddaxter/Client.py | 86 ++++++++++++-- worlds/jakanddaxter/JakAndDaxterOptions.py | 43 ++++++- worlds/jakanddaxter/Regions.py | 109 +++++++++++++++--- worlds/jakanddaxter/Rules.py | 8 +- worlds/jakanddaxter/__init__.py | 18 +-- worlds/jakanddaxter/client/MemoryReader.py | 57 ++++++--- worlds/jakanddaxter/client/ReplClient.py | 65 ++++++++++- .../en_Jak and Daxter The Precursor Legacy.md | 24 ++++ worlds/jakanddaxter/docs/setup_en.md | 11 +- worlds/jakanddaxter/regs/BoggySwampRegions.py | 4 +- worlds/jakanddaxter/regs/FireCanyonRegions.py | 4 +- .../regs/ForbiddenJungleRegions.py | 6 +- worlds/jakanddaxter/regs/GeyserRockRegions.py | 4 +- .../regs/GolAndMaiasCitadelRegions.py | 12 +- worlds/jakanddaxter/regs/LavaTubeRegions.py | 4 +- .../regs/LostPrecursorCityRegions.py | 11 +- .../jakanddaxter/regs/MistyIslandRegions.py | 4 +- .../jakanddaxter/regs/MountainPassRegions.py | 4 +- .../regs/PrecursorBasinRegions.py | 4 +- .../jakanddaxter/regs/RockVillageRegions.py | 4 +- .../regs/SandoverVillageRegions.py | 4 +- .../jakanddaxter/regs/SentinelBeachRegions.py | 4 +- .../jakanddaxter/regs/SnowyMountainRegions.py | 4 +- worlds/jakanddaxter/regs/SpiderCaveRegions.py | 4 +- .../regs/VolcanicCraterRegions.py | 4 +- 25 files changed, 407 insertions(+), 95 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 51b26eb53a65..6968a7a8caa7 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -8,8 +8,9 @@ from pymem.exception import ProcessNotFound, ProcessError import Utils -from NetUtils import ClientStatus +from NetUtils import ClientStatus, NetworkItem from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled +from .JakAndDaxterOptions import EnableOrbsanity from .GameID import jak1_name from .client.ReplClient import JakAndDaxterReplClient @@ -84,8 +85,8 @@ class JakAndDaxterContext(CommonContext): def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: self.repl = JakAndDaxterReplClient() self.memr = JakAndDaxterMemoryReader() - # self.memr.load_data() # self.repl.load_data() + # self.memr.load_data() super().__init__(server_address, password) def run_gui(self): @@ -110,19 +111,71 @@ def on_package(self, cmd: str, args: dict): if cmd == "Connected": slot_data = args["slot_data"] - if slot_data["enable_orbsanity"] == 1: - self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["level_orbsanity_bundle_size"]) - elif slot_data["enable_orbsanity"] == 2: - self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["global_orbsanity_bundle_size"]) + orbsanity_option = slot_data["enable_orbsanity"] + if orbsanity_option == EnableOrbsanity.option_per_level: + orbsanity_bundle = slot_data["level_orbsanity_bundle_size"] + elif orbsanity_option == EnableOrbsanity.option_global: + orbsanity_bundle = slot_data["global_orbsanity_bundle_size"] else: - self.repl.setup_orbsanity(slot_data["enable_orbsanity"], 1) + orbsanity_bundle = 1 + + self.repl.setup_options(orbsanity_option, + orbsanity_bundle, + slot_data["fire_canyon_cell_count"], + slot_data["mountain_pass_cell_count"], + slot_data["lava_tube_cell_count"], + slot_data["completion_condition"]) + + # Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server + # to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost, + # while "ReceivedItems" will tell us the orbs we gained. This will give us the correct balance. + if orbsanity_option in [EnableOrbsanity.option_per_level, EnableOrbsanity.option_global]: + async def get_orb_balance(): + await self.send_msgs([{"cmd": "Get", + "keys": [f"jakanddaxter_{self.auth}_orbs_paid"] + }]) + + create_task_log_exception(get_orb_balance()) + + if cmd == "Retrieved": + if f"jakanddaxter_{self.auth}_orbs_paid" in args["keys"]: + orbs_traded = args["keys"][f"jakanddaxter_{self.auth}_orbs_paid"] + self.repl.subtract_traded_orbs(orbs_traded if orbs_traded is not None else 0) if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): logger.debug(f"index: {str(index)}, item: {str(item)}") self.repl.item_inbox[index] = item - self.memr.save_data() - self.repl.save_data() + + def on_print_json(self, args: dict) -> None: + if "type" in args and args["type"] in {"ItemSend"}: + item = args["item"] + recipient = args["receiving"] + + # Receiving an item from the server. + if self.slot_concerns_self(recipient): + self.repl.my_item_name = self.item_names.lookup_in_game(item.item) + + # Did we find it, or did someone else? + if self.slot_concerns_self(item.player): + self.repl.my_item_finder = "MYSELF" + else: + self.repl.my_item_finder = self.player_names[item.player] + + # Sending an item to the server. + if self.slot_concerns_self(item.player): + self.repl.their_item_name = self.item_names.lookup_in_slot(item.item, recipient) + + # Does it belong to us, or to someone else? + if self.slot_concerns_self(recipient): + self.repl.their_item_owner = "MYSELF" + else: + self.repl.their_item_owner = self.player_names[recipient] + + # Write to game display. + self.repl.write_game_text() + + super(JakAndDaxterContext, self).on_print_json(args) def on_deathlink(self, data: dict): if self.memr.deathlink_enabled: @@ -174,6 +227,18 @@ async def repl_reset_orbsanity(self): def on_orbsanity_check(self): create_task_log_exception(self.repl_reset_orbsanity()) + async def ap_inform_orb_trade(self, orbs_changed: int): + if self.memr.orbsanity_enabled: + await self.send_msgs([{"cmd": "Set", + "key": f"jakanddaxter_{self.auth}_orbs_paid", + "default": 0, + "want_reply": False, + "operations": [{"operation": "add", "value": orbs_changed}] + }]) + + def on_orb_trade(self, orbs_changed: int): + create_task_log_exception(self.ap_inform_orb_trade(orbs_changed)) + async def run_repl_loop(self): while True: await self.repl.main_tick() @@ -185,7 +250,8 @@ async def run_memr_loop(self): self.on_finish_check, self.on_deathlink_check, self.on_deathlink_toggle, - self.on_orbsanity_check) + self.on_orbsanity_check, + self.on_orb_trade) await asyncio.sleep(0.1) diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 5391b7b094c3..4e4ac0d25f0d 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions, Choice +from Options import Toggle, PerGameCommonOptions, Choice, Range class EnableMoveRandomizer(Toggle): @@ -66,9 +66,50 @@ class PerLevelOrbsanityBundleSize(Choice): default = 1 +class FireCanyonCellCount(Range): + """Set the number of orbs you need to cross Fire Canyon.""" + display_name = "Fire Canyon Cell Count" + range_start = 0 + range_end = 100 + default = 20 + + +class MountainPassCellCount(Range): + """Set the number of orbs you need to reach Klaww and cross Mountain Pass.""" + display_name = "Mountain Pass Cell Count" + range_start = 0 + range_end = 100 + default = 45 + + +class LavaTubeCellCount(Range): + """Set the number of orbs you need to cross Lava Tube.""" + display_name = "Lava Tube Cell Count" + range_start = 0 + range_end = 100 + default = 72 + + +class CompletionCondition(Choice): + """Set the goal for completing the game.""" + display_name = "Completion Condition" + option_cross_fire_canyon = 69 + option_cross_mountain_pass = 87 + option_cross_lava_tube = 89 + option_defeat_dark_eco_plant = 6 + option_defeat_klaww = 86 + option_defeat_gol_and_maia = 112 + option_open_100_cell_door = 116 + default = 112 + + @dataclass class JakAndDaxterOptions(PerGameCommonOptions): enable_move_randomizer: EnableMoveRandomizer enable_orbsanity: EnableOrbsanity global_orbsanity_bundle_size: GlobalOrbsanityBundleSize level_orbsanity_bundle_size: PerLevelOrbsanityBundleSize + fire_canyon_cell_count: FireCanyonCellCount + mountain_pass_cell_count: MountainPassCellCount + lava_tube_cell_count: LavaTubeCellCount + completion_condition: CompletionCondition diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 168640b9e247..9089981f358b 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,9 +1,14 @@ -from BaseClasses import MultiWorld -from .JakAndDaxterOptions import JakAndDaxterOptions -from .Items import item_table +from BaseClasses import MultiWorld, CollectionState, ItemClassification +from Options import OptionError +from .JakAndDaxterOptions import (JakAndDaxterOptions, + EnableMoveRandomizer, + EnableOrbsanity, + CompletionCondition) +from .Items import (JakAndDaxterItem, + item_table, + move_item_table) from .Rules import can_reach_orbs -from .locs import (OrbLocations as Orbs, - CellLocations as Cells, +from .locs import (CellLocations as Cells, ScoutLocations as Scouts) from .regs.RegionBase import JakAndDaxterRegion from .regs import (GeyserRockRegions as GeyserRock, @@ -44,7 +49,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: # If Global Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Menu. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 2: + if options.enable_orbsanity == EnableOrbsanity.option_global: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld) bundle_size = options.global_orbsanity_bundle_size.value @@ -64,7 +69,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: # Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules. [gr] = GeyserRock.build_regions("Geyser Rock", multiworld, options, player) [sv] = SandoverVillage.build_regions("Sandover Village", multiworld, options, player) - [fj] = ForbiddenJungle.build_regions("Forbidden Jungle", multiworld, options, player) + [fj, fjp] = ForbiddenJungle.build_regions("Forbidden Jungle", multiworld, options, player) [sb] = SentinelBeach.build_regions("Sentinel Beach", multiworld, options, player) [mi] = MistyIsland.build_regions("Misty Island", multiworld, options, player) [fc] = FireCanyon.build_regions("Fire Canyon", multiworld, options, player) @@ -77,7 +82,12 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: [sc] = SpiderCave.build_regions("Spider Cave", multiworld, options, player) [sm] = SnowyMountain.build_regions("Snowy Mountain", multiworld, options, player) [lt] = LavaTube.build_regions("Lava Tube", multiworld, options, player) - [gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", multiworld, options, player) + [gmc, fb, fd] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", multiworld, options, player) + + # Configurable counts of cells for connector levels. + fc_count = options.fire_canyon_cell_count.value + mp_count = options.mountain_pass_cell_count.value + lt_count = options.lava_tube_cell_count.value # Define the interconnecting rules. menu.connect(gr) @@ -85,17 +95,90 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: sv.connect(fj) sv.connect(sb) sv.connect(mi, rule=lambda state: state.has("Fisherman's Boat", player)) - sv.connect(fc, rule=lambda state: state.has("Power Cell", player, 20)) + sv.connect(fc, rule=lambda state: state.has("Power Cell", player, fc_count)) # Normally 20. fc.connect(rv) rv.connect(pb) rv.connect(lpc) rvp.connect(bs) # rv->rvp/rvc connections defined internally by RockVillageRegions. - rvc.connect(mp, rule=lambda state: state.has("Power Cell", player, 45)) + rvc.connect(mp, rule=lambda state: state.has("Power Cell", player, mp_count)) # Normally 45. mpr.connect(vc) # mp->mpr connection defined internally by MountainPassRegions. vc.connect(sc) vc.connect(sm, rule=lambda state: state.has("Snowy Mountain Gondola", player)) - vc.connect(lt, rule=lambda state: state.has("Power Cell", player, 72)) + vc.connect(lt, rule=lambda state: state.has("Power Cell", player, lt_count)) # Normally 72. lt.connect(gmc) # gmc->fb connection defined internally by GolAndMaiasCitadelRegions. - # Finally, set the completion condition. - multiworld.completion_condition[player] = lambda state: state.can_reach(fb, "Region", player) + # Set the completion condition. + if options.completion_condition == CompletionCondition.option_cross_fire_canyon: + multiworld.completion_condition[player] = lambda state: state.can_reach(rv, "Region", player) + + elif options.completion_condition == CompletionCondition.option_cross_mountain_pass: + multiworld.completion_condition[player] = lambda state: state.can_reach(vc, "Region", player) + + elif options.completion_condition == CompletionCondition.option_cross_lava_tube: + multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player) + + elif options.completion_condition == CompletionCondition.option_defeat_dark_eco_plant: + multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player) + + elif options.completion_condition == CompletionCondition.option_defeat_klaww: + multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player) + + elif options.completion_condition == CompletionCondition.option_defeat_gol_and_maia: + multiworld.completion_condition[player] = lambda state: state.can_reach(fb, "Region", player) + + elif options.completion_condition == CompletionCondition.option_open_100_cell_door: + multiworld.completion_condition[player] = lambda state: state.can_reach(fd, "Region", player) + + # As a final sanity check on these options, verify that we have enough locations to allow us to cross + # the connector levels. E.g. if you set Fire Canyon count to 99, we may not have 99 Locations in hub 1. + verify_connector_level_accessibility(multiworld, options, player) + + +def verify_connector_level_accessibility(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + + # Set up a state where we only have the items we need to progress, exactly when we need them, as well as + # any items we would have/get from our other options. The only variable we're actually testing here is the + # number of power cells we need. + state = CollectionState(multiworld) + if options.enable_move_randomizer == EnableMoveRandomizer.option_false: + for move in move_item_table: + state.collect(JakAndDaxterItem(move_item_table[move], ItemClassification.progression, move, player)) + + thresholds = { + 0: { + "option": options.fire_canyon_cell_count, + "required_items": {}, + }, + 1: { + "option": options.mountain_pass_cell_count, + "required_items": { + 33: "Warrior's Pontoons", + 10945: "Double Jump", + }, + }, + 2: { + "option": options.lava_tube_cell_count, + "required_items": {}, + }, + } + + loc = 0 + for k in thresholds: + option = thresholds[k]["option"] + required_items = thresholds[k]["required_items"] + + # Given our current state (starting with 0 Power Cells), determine if there are enough + # Locations to fill with the number of Power Cells needed for the next threshold. + locations_available = multiworld.get_reachable_locations(state, player) + if len(locations_available) < option.value: + raise OptionError(f"Settings conflict with {option.display_name}: " + f"not enough potential locations ({len(locations_available)}) " + f"for the required number of power cells ({option.value}).") + + # Once we've determined we can pass the current threshold, add what we need to reach the next one. + for _ in range(option.value): + state.collect(JakAndDaxterItem("Power Cell", ItemClassification.progression, loc, player)) + loc += 1 + + for item in required_items: + state.collect(JakAndDaxterItem(required_items[item], ItemClassification.progression, item, player)) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index ad7c6dda1ff7..95d077995142 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,7 +1,7 @@ import math import typing from BaseClasses import MultiWorld, CollectionState -from .JakAndDaxterOptions import JakAndDaxterOptions +from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity from .Items import orb_item_table from .locs import CellLocations as Cells from .Locations import location_table @@ -16,7 +16,7 @@ def can_reach_orbs(state: CollectionState, # Global Orbsanity and No Orbsanity both treat orbs as completely interchangeable. # Per Level Orbsanity needs to know if you can reach orbs *in a particular level.* - if options.enable_orbsanity.value in [0, 2]: + if options.enable_orbsanity != EnableOrbsanity.option_per_level: return can_reach_orbs_global(state, player, multiworld) else: return can_reach_orbs_level(state, player, multiworld, level_name) @@ -57,10 +57,10 @@ def can_trade(state: CollectionState, required_orbs: int, required_previous_trade: int = None) -> bool: - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: bundle_size = options.level_orbsanity_bundle_size.value return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) - elif options.enable_orbsanity.value == 2: + elif options.enable_orbsanity == EnableOrbsanity.option_global: bundle_size = options.global_orbsanity_bundle_size.value return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) else: diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index ad7555397bda..148f7305167f 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -4,7 +4,7 @@ from Utils import local_path, visualize_regions from BaseClasses import Item, ItemClassification, Tutorial from .GameID import jak1_id, jak1_name, jak1_max -from .JakAndDaxterOptions import JakAndDaxterOptions +from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity from .Locations import JakAndDaxterLocation, location_table from .Items import JakAndDaxterItem, item_table from .locs import (CellLocations as Cells, @@ -93,13 +93,13 @@ class JakAndDaxterWorld(World): # This will also set Locations, Location access rules, Region access rules, etc. def create_regions(self) -> None: create_regions(self.multiworld, self.options, self.player) - # visualize_regions(self.multiworld.get_region("Menu", self.player), "jak.puml") + visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml") # Helper function to get the correct orb bundle size. def get_orb_bundle_size(self) -> int: - if self.options.enable_orbsanity.value == 1: + if self.options.enable_orbsanity == EnableOrbsanity.option_per_level: return self.options.level_orbsanity_bundle_size.value - elif self.options.enable_orbsanity.value == 2: + elif self.options.enable_orbsanity == EnableOrbsanity.option_global: return self.options.global_orbsanity_bundle_size.value else: return 0 @@ -158,9 +158,9 @@ def create_items(self) -> None: # Handle Orbsanity option. # If it is OFF, don't add any orbs to the item pool. - # If it is ON, only add the orb bundle that matches the choice in options. + # If it is ON, don't add any orb bundles that don't match the chosen option. if (item_name in self.item_name_groups["Precursor Orbs"] - and ((self.options.enable_orbsanity.value == 0 + and ((self.options.enable_orbsanity == EnableOrbsanity.option_off or Orbs.to_game_id(item_id) != self.get_orb_bundle_size()))): continue @@ -181,4 +181,8 @@ def fill_slot_data(self) -> Dict[str, Any]: return self.options.as_dict("enable_move_randomizer", "enable_orbsanity", "global_orbsanity_bundle_size", - "level_orbsanity_bundle_size") + "level_orbsanity_bundle_size", + "fire_canyon_cell_count", + "mountain_pass_cell_count", + "lava_tube_cell_count", + "completion_condition") diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index a0787071bf04..14cd63c10f14 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -17,6 +17,7 @@ sizeof_uint64 = 8 sizeof_uint32 = 4 sizeof_uint8 = 1 +sizeof_float = 4 # IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to @@ -72,6 +73,19 @@ def define(self, size: int, length: int = 1) -> int: collected_bundle_level_offset = offsets.define(sizeof_uint8) collected_bundle_count_offset = offsets.define(sizeof_uint32) +# Progression and Completion information. +fire_canyon_unlock_offset = offsets.define(sizeof_float) +mountain_pass_unlock_offset = offsets.define(sizeof_float) +lava_tube_unlock_offset = offsets.define(sizeof_float) +completion_goal_offset = offsets.define(sizeof_uint8) +completed_offset = offsets.define(sizeof_uint8) + +# Text to display in the HUD (32 char max per string). +their_item_name_offset = offsets.define(sizeof_uint8, 32) +their_item_owner_offset = offsets.define(sizeof_uint8, 32) +my_item_name_offset = offsets.define(sizeof_uint8, 32) +my_item_finder_offset = offsets.define(sizeof_uint8, 32) + # The End. end_marker_offset = offsets.define(sizeof_uint8, 4) @@ -138,6 +152,7 @@ class JakAndDaxterMemoryReader: # Orbsanity handling orbsanity_enabled: bool = False reset_orbsanity: bool = False + orbs_paid: int = 0 def __init__(self, marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker @@ -148,7 +163,8 @@ async def main_tick(self, finish_callback: Callable, deathlink_callback: Callable, deathlink_toggle: Callable, - orbsanity_callback: Callable): + orbsanity_callback: Callable, + paid_orbs_callback: Callable): if self.initiated_connect: await self.connect() self.initiated_connect = False @@ -171,6 +187,7 @@ async def main_tick(self, # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. if len(self.location_outbox) > self.outbox_index: location_callback(self.location_outbox) + self.save_data() self.outbox_index += 1 if self.finished_game: @@ -186,6 +203,10 @@ async def main_tick(self, if self.reset_orbsanity: orbsanity_callback() + if self.orbs_paid > 0: + paid_orbs_callback(self.orbs_paid) + self.orbs_paid = 0 + async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel @@ -219,7 +240,7 @@ def print_status(self): logger.info("Memory Reader Status:") logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None")) logger.info(" Game state memory address: " + str(self.goal_address)) - logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index]) + logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index - 1]) if self.outbox_index else "None")) def read_memory(self) -> List[int]: @@ -235,6 +256,16 @@ def read_memory(self) -> List[int]: self.location_outbox.append(cell_ap_id) logger.debug("Checked power cell: " + str(next_cell)) + # If orbsanity is ON and next_cell is one of the traders or oracles, then run a callback + # to add their amount to the DataStorage value holding our current orb trade total. + if next_cell in {11, 12, 31, 32, 33, 96, 97, 98, 99}: + self.orbs_paid += 90 + logger.debug("Traded 90 orbs!") + + if next_cell in {13, 14, 34, 35, 100, 101}: + self.orbs_paid += 120 + logger.debug("Traded 120 orbs!") + for k in range(0, next_buzzer_index): next_buzzer = self.read_goal_address(buzzers_checked_offset + (k * sizeof_uint32), sizeof_uint32) buzzer_ap_id = Flies.to_ap_id(next_buzzer) @@ -244,19 +275,10 @@ def read_memory(self) -> List[int]: for k in range(0, next_special_index): next_special = self.read_goal_address(specials_checked_offset + (k * sizeof_uint32), sizeof_uint32) - - # 112 is the game-task ID of `finalboss-movies`, which is written to this array when you grab - # the white eco. This is our victory condition, so we need to catch it and act on it. - if next_special == 112 and not self.finished_game: - self.finished_game = True - logger.info("Congratulations! You finished the game!") - else: - - # All other special checks handled as normal. - special_ap_id = Specials.to_ap_id(next_special) - if special_ap_id not in self.location_outbox: - self.location_outbox.append(special_ap_id) - logger.debug("Checked special: " + str(next_special)) + special_ap_id = Specials.to_ap_id(next_special) + if special_ap_id not in self.location_outbox: + self.location_outbox.append(special_ap_id) + logger.debug("Checked special: " + str(next_special)) died = self.read_goal_address(died_offset, sizeof_uint8) if died > 0: @@ -302,6 +324,11 @@ def read_memory(self) -> List[int]: # self.reset_orbsanity = True + completed = self.read_goal_address(completed_offset, sizeof_uint8) + if completed > 0 and not self.finished_game: + self.finished_game = True + logger.info("Congratulations! You finished the game!") + except (ProcessError, MemoryReadError, WinAPIError): logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") self.connected = False diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index a646b553116b..712c268c6f39 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -1,7 +1,7 @@ import json import time import struct -import typing +from typing import Dict, Callable import random from socket import socket, AF_INET, SOCK_STREAM @@ -33,9 +33,14 @@ class JakAndDaxterReplClient: gk_process: pymem.process = None goalc_process: pymem.process = None - item_inbox: typing.Dict[int, NetworkItem] = {} + item_inbox: Dict[int, NetworkItem] = {} inbox_index = 0 + my_item_name: str = None + my_item_finder: str = None + their_item_name: str = None + their_item_owner: str = None + def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.ip = ip self.port = port @@ -63,6 +68,7 @@ async def main_tick(self): # Receive Items from AP. Handle 1 item per tick. if len(self.item_inbox) > self.inbox_index: self.receive_item() + self.save_data() self.inbox_index += 1 if self.received_deathlink: @@ -189,6 +195,35 @@ def print_status(self): logger.info(" Last item received: " + (str(getattr(self.item_inbox[self.inbox_index], "item")) if self.inbox_index else "None")) + # To properly display in-game text, it must be alphanumeric and uppercase. + # I also only allotted 32 bytes to each string in OpenGOAL, so we must truncate. + @staticmethod + def sanitize_game_text(text: str) -> str: + if text is None: + return "\"NONE\"" + + result = "".join(c for c in text if (c in {"-", " "} or c.isalnum())) + result = result[:32].upper() + return f"\"{result}\"" + + # OpenGOAL can handle both its own string datatype and C-like character pointers (charp). + # So for the game to constantly display this information in the HUD, we have to write it + # to a memory address as a char*. + def write_game_text(self): + logger.debug(f"Sending info to in-game display!") + self.send_form(f"(charp<-string (-> *ap-info-jak1* my-item-name) " + f"{self.sanitize_game_text(self.my_item_name)})", + print_ok=False) + self.send_form(f"(charp<-string (-> *ap-info-jak1* my-item-finder) " + f"{self.sanitize_game_text(self.my_item_finder)})", + print_ok=False) + self.send_form(f"(charp<-string (-> *ap-info-jak1* their-item-name) " + f"{self.sanitize_game_text(self.their_item_name)})", + print_ok=False) + self.send_form(f"(charp<-string (-> *ap-info-jak1* their-item-owner) " + f"{self.sanitize_game_text(self.their_item_owner)})", + print_ok=False) + def receive_item(self): ap_id = getattr(self.item_inbox[self.inbox_index], "item") @@ -308,12 +343,12 @@ def reset_deathlink(self) -> bool: logger.error(f"Unable to reset deathlink flag!") return ok - def setup_orbsanity(self, option: int, bundle: int) -> bool: - ok = self.send_form(f"(ap-setup-orbs! (the uint {option}) (the uint {bundle}))") + def subtract_traded_orbs(self, orb_count: int) -> bool: + ok = self.send_form(f"(-! (-> *game-info* money) (the float {orb_count}))") if ok: - logger.debug(f"Set up orbsanity: Option {option}, Bundle {bundle}!") + logger.debug(f"Subtracting {orb_count} traded orbs!") else: - logger.error(f"Unable to set up orbsanity: Option {option}, Bundle {bundle}!") + logger.error(f"Unable to subtract {orb_count} traded orbs!") return ok def reset_orbsanity(self) -> bool: @@ -330,6 +365,24 @@ def reset_orbsanity(self) -> bool: logger.error(f"Unable to reset orb count for collected orbsanity bundle!") return ok + def setup_options(self, + os_option: int, os_bundle: int, + fc_count: int, mp_count: int, + lt_count: int, goal_id: int) -> bool: + ok = self.send_form(f"(ap-setup-options! " + f"(the uint {os_option}) (the uint {os_bundle}) " + f"(the float {fc_count}) (the float {mp_count}) " + f"(the float {lt_count}) (the uint {goal_id}))") + message = (f"Setting options: \n" + f" Orbsanity Option {os_option}, Orbsanity Bundle {os_bundle}, \n" + f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n" + f" LT Cell Count {lt_count}, Completion GOAL {goal_id}... ") + if ok: + logger.debug(message + "Success!") + else: + logger.error(message + "Failed!") + return ok + def save_data(self): with open("jakanddaxter_item_inbox.json", "w+") as f: dump = { diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index 740afb326240..19597bda31a6 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -128,6 +128,30 @@ This will show you a list of all the moves in the game. - Yellow items indicate you possess that move, but you are missing its prerequisites. - Light blue items indicate you possess that move, as well as its prerequisites. +## What does Orbsanity do? +If you enable Orbsanity, Precursor Orbs will be turned into ordered lists of progressive checks. Every time you collect +a "bundle" of the correct number of orbs, you will trigger the next release in the list. Likewise, these bundles of orbs +will be added to the item pool to be randomized. There are several options to change the difficulty of this challenge. + +- "Per Level" Orbsanity means the lists of orb checks are generated and populated for each level in the game. + - (Geyser Rock, Sandover Village, etc.) +- "Global" Orbsanity means there is only one list of checks for the entire game. + - It does not matter where you pick up the orbs, they all count toward the same list. +- The options with "Bundle Size" in the name indicate how many orbs are in a "bundle." This adds a number of Items + and Locations to the pool inversely proportional to the size of the bundle. + - For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs, + you will add 8 items to the pool. + +### A WARNING ABOUT ORBSANITY OPTIONS + +Unlike other settings, you CANNOT alter Orbsanity options after you generate a seed and start a game. **If you turn +Orbsanity OFF in the middle of an Orbsanity game, you will have NO way of completing the orb checks.** This may cause +you to miss important progression items and prevent you (and others) from completing the run. + +When you connect your text client to the Archipelago Server, the server will tell the game what settings were chosen +for this seed, and the game will apply those settings automatically. You can verify (but DO NOT ALTER) these settings +by navigating to `Options`, then `Archipelago Options`. + ## I got soft-locked and can't leave, how do I get out of here? Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Warp To Home`. Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 7199666f6a2c..f11953d6dd62 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -73,15 +73,17 @@ At this time, this method of setup works on Windows only, but Linux support is a - You should see several messages appear after the compiler has run to 100% completion. If you see `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. - The game should then load in the title screen. - You can *minimize* the 2 powershell windows, **BUT DO NOT CLOSE THEM.** They are required for Archipelago and the game to communicate with each other. +- Use the text client to connect to the Archipelago server while on the title screen. This will communicate your current settings to the game. - Start a new game in the title screen, and play through the cutscenes. -- Once you reach Geyser Rock, you can connect to the Archipelago server. - - Provide your slot/player name and hit Enter, and then start the game! +- Once you reach Geyser Rock, you can start the game! - You can leave Geyser Rock immediately if you so choose - just step on the warp gate button. ***Returning / Async Game*** -- One important note is to connect to the Archipelago server **AFTER** you load your save file. This is to allow AP to give you all the items you had previously. -- Otherwise, the same steps as New Game apply. +- The same steps as New Game apply, with some exceptions: + - Connect to the Archipelago server **BEFORE** you load your save file. This is to allow AP to give the game your current settings and all the items you had previously. + - **THESE SETTINGS AFFECT LOADING AND SAVING OF SAVE FILES, SO IT IS IMPORTANT TO DO THIS FIRST.** + - Then, instead of choosing `New Game` in the title menu, choose `Load Game`, then choose the save file **CORRESPONDING TO YOUR CURRENT ARCHIPELAGO CONNECTION.** ## Troubleshooting @@ -119,3 +121,4 @@ Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. - The game needs to run in debug mode in order to allow the repl to connect to it. We hide the debug text on screen and play the game's introductory cutscenes properly. - The powershell windows cannot be run as background processes due to how the repl works, so the best we can do is minimize them. - The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. +- Orbsanity checks may show up out of order in the text client. diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py index e6b621e38d7d..8b18a5d3f61b 100644 --- a/worlds/jakanddaxter/regs/BoggySwampRegions.py +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_fight, can_reach_orbs @@ -155,7 +155,7 @@ def can_jump_higher(state: CollectionState, p: int) -> bool: # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/FireCanyonRegions.py b/worlds/jakanddaxter/regs/FireCanyonRegions.py index 473248d33c16..2c11278005ad 100644 --- a/worlds/jakanddaxter/regs/FireCanyonRegions.py +++ b/worlds/jakanddaxter/regs/FireCanyonRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts @@ -18,7 +18,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py index ca48cf2c8b55..c2f64b206ca1 100644 --- a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py +++ b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs @@ -83,7 +83,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value @@ -98,4 +98,4 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter multiworld.regions.append(orbs) main_area.connect(orbs) - return [main_area] + return [main_area, temple_int_post_blue] diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py index 938fe51aedd6..0a2646082ff7 100644 --- a/worlds/jakanddaxter/regs/GeyserRockRegions.py +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_reach_orbs from ..locs import ScoutLocations as Scouts @@ -27,7 +27,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index 14db2b19e6b1..59fc7c8a4c16 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs @@ -58,6 +58,8 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0) + final_door = JakAndDaxterRegion("Final Door", player, multiworld, level_name, 0) + # Jump Dive required for a lot of buttons, prepare yourself. main_area.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) @@ -105,6 +107,9 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: final_boss.connect(rotating_tower) # Take elevator back down. + # Final door. Need 100 power cells. + final_boss.connect(final_door, rule=lambda state: state.has("Power Cell", player, 100)) + multiworld.regions.append(main_area) multiworld.regions.append(robot_scaffolding) multiworld.regions.append(jump_pad_room) @@ -112,10 +117,11 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: multiworld.regions.append(bunny_room) multiworld.regions.append(rotating_tower) multiworld.regions.append(final_boss) + multiworld.regions.append(final_door) # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value @@ -130,4 +136,4 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: multiworld.regions.append(orbs) main_area.connect(orbs) - return [main_area, final_boss] + return [main_area, final_boss, final_door] diff --git a/worlds/jakanddaxter/regs/LavaTubeRegions.py b/worlds/jakanddaxter/regs/LavaTubeRegions.py index 04fb16af98ee..45ccdc2937dd 100644 --- a/worlds/jakanddaxter/regs/LavaTubeRegions.py +++ b/worlds/jakanddaxter/regs/LavaTubeRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts @@ -18,7 +18,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py index 102283bf8674..716f0fd46b26 100644 --- a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs @@ -57,7 +57,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter capsule_room = JakAndDaxterRegion("Capsule Chamber", player, multiworld, level_name, 6) # Use jump dive to activate button inside the capsule. Blue eco vent can ready the chamber and get the scout fly. - capsule_room.add_cell_locations([47], access_rule=lambda state: state.has("Jump Dive", player)) + capsule_room.add_cell_locations([47], access_rule=lambda state: + state.has("Jump Dive", player) + and (state.has("Double Jump", player) + or state.has("Jump Kick", player) + or (state.has("Punch", player) + and state.has("Punch Uppercut", player)))) capsule_room.add_fly_locations([327729]) second_slide = JakAndDaxterRegion("Second Slide", player, multiworld, level_name, 31) @@ -130,7 +135,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py index c40601d794fd..1a70095ce57c 100644 --- a/worlds/jakanddaxter/regs/MistyIslandRegions.py +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs @@ -116,7 +116,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index f6e2419689cb..5644fc0add6a 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_reach_orbs from ..locs import ScoutLocations as Scouts @@ -34,7 +34,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py index 0e24c1b774c9..05e3bcf9bc17 100644 --- a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py +++ b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts @@ -18,7 +18,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index d17ac0cb7802..213dd0ae8749 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs @@ -63,7 +63,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index 1a0c3ccb00db..c872d32ebbbe 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs @@ -71,7 +71,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py index f573f4ee923f..e66cb3b88427 100644 --- a/worlds/jakanddaxter/regs/SentinelBeachRegions.py +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs @@ -85,7 +85,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index b6fd711171ea..07bd33ca2419 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs @@ -192,7 +192,7 @@ def can_jump_blockers(state: CollectionState, p: int) -> bool: # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py index 84af260a099a..7124732d0971 100644 --- a/worlds/jakanddaxter/regs/SpiderCaveRegions.py +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs @@ -110,7 +110,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py index 4f8bd8a2ebed..875f1c2cbc8e 100644 --- a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import CollectionState, MultiWorld from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions +from .. import JakAndDaxterOptions, EnableOrbsanity from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs from ..locs import CellLocations as Cells, ScoutLocations as Scouts @@ -38,7 +38,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. - if options.enable_orbsanity.value == 1: + if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) bundle_size = options.level_orbsanity_bundle_size.value From 22b43a8e3d7fc2b72788e7a1c44add0b48c9ad79 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Thu, 1 Aug 2024 00:26:50 +1000 Subject: [PATCH 39/70] Rename completion_condition to jak_completion_condition (#41) --- worlds/jakanddaxter/Client.py | 8 +++++++- worlds/jakanddaxter/JakAndDaxterOptions.py | 2 +- worlds/jakanddaxter/Regions.py | 14 +++++++------- worlds/jakanddaxter/__init__.py | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 6968a7a8caa7..f1464a1ffd11 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -119,12 +119,18 @@ def on_package(self, cmd: str, args: dict): else: orbsanity_bundle = 1 + # Keep compatibility with 0.0.8 at least for now + if "completion_condition" in slot_data: + goal_id = slot_data["completion_condition"] + else: + goal_id = slot_data["jak_completion_condition"] + self.repl.setup_options(orbsanity_option, orbsanity_bundle, slot_data["fire_canyon_cell_count"], slot_data["mountain_pass_cell_count"], slot_data["lava_tube_cell_count"], - slot_data["completion_condition"]) + goal_id) # Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server # to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost, diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 4e4ac0d25f0d..718fe6ff45a4 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -112,4 +112,4 @@ class JakAndDaxterOptions(PerGameCommonOptions): fire_canyon_cell_count: FireCanyonCellCount mountain_pass_cell_count: MountainPassCellCount lava_tube_cell_count: LavaTubeCellCount - completion_condition: CompletionCondition + jak_completion_condition: CompletionCondition diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 9089981f358b..6d016bad2ce3 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -108,25 +108,25 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: lt.connect(gmc) # gmc->fb connection defined internally by GolAndMaiasCitadelRegions. # Set the completion condition. - if options.completion_condition == CompletionCondition.option_cross_fire_canyon: + if options.jak_completion_condition == CompletionCondition.option_cross_fire_canyon: multiworld.completion_condition[player] = lambda state: state.can_reach(rv, "Region", player) - elif options.completion_condition == CompletionCondition.option_cross_mountain_pass: + elif options.jak_completion_condition == CompletionCondition.option_cross_mountain_pass: multiworld.completion_condition[player] = lambda state: state.can_reach(vc, "Region", player) - elif options.completion_condition == CompletionCondition.option_cross_lava_tube: + elif options.jak_completion_condition == CompletionCondition.option_cross_lava_tube: multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player) - elif options.completion_condition == CompletionCondition.option_defeat_dark_eco_plant: + elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant: multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player) - elif options.completion_condition == CompletionCondition.option_defeat_klaww: + elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww: multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player) - elif options.completion_condition == CompletionCondition.option_defeat_gol_and_maia: + elif options.jak_completion_condition == CompletionCondition.option_defeat_gol_and_maia: multiworld.completion_condition[player] = lambda state: state.can_reach(fb, "Region", player) - elif options.completion_condition == CompletionCondition.option_open_100_cell_door: + elif options.jak_completion_condition == CompletionCondition.option_open_100_cell_door: multiworld.completion_condition[player] = lambda state: state.can_reach(fd, "Region", player) # As a final sanity check on these options, verify that we have enough locations to allow us to cross diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 148f7305167f..733539fd9c65 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -185,4 +185,4 @@ def fill_slot_data(self) -> Dict[str, Any]: "fire_canyon_cell_count", "mountain_pass_cell_count", "lava_tube_cell_count", - "completion_condition") + "jak_completion_condition") From ea82846a2b9cef37ba27b1f89a2a7d7de05a3111 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:19:06 -0400 Subject: [PATCH 40/70] The Afterparty (#42) * Fixes to Jak client, rules, options, and more. * Post-rebase fixes. * Remove orbsanity reset code, optimize game text in client. * More game text optimization. --- worlds/jakanddaxter/Client.py | 52 ++-- worlds/jakanddaxter/JakAndDaxterOptions.py | 24 +- worlds/jakanddaxter/Rules.py | 9 +- worlds/jakanddaxter/client/MemoryReader.py | 58 ++-- worlds/jakanddaxter/client/ReplClient.py | 262 +++++++++--------- worlds/jakanddaxter/regs/BoggySwampRegions.py | 15 +- worlds/jakanddaxter/regs/GeyserRockRegions.py | 6 +- .../regs/GolAndMaiasCitadelRegions.py | 49 ++-- .../regs/LostPrecursorCityRegions.py | 20 +- .../jakanddaxter/regs/MistyIslandRegions.py | 13 +- .../jakanddaxter/regs/RockVillageRegions.py | 12 +- .../regs/SandoverVillageRegions.py | 22 +- .../jakanddaxter/regs/SentinelBeachRegions.py | 16 +- .../jakanddaxter/regs/SnowyMountainRegions.py | 35 +-- worlds/jakanddaxter/regs/SpiderCaveRegions.py | 10 +- 15 files changed, 275 insertions(+), 328 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index f1464a1ffd11..23cf1a4cbd3b 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -39,10 +39,9 @@ async def _log_exception(a): class JakAndDaxterClientCommandProcessor(ClientCommandProcessor): ctx: "JakAndDaxterContext" - # The command processor is not async and cannot use async tasks, so long-running operations - # like the /repl connect command (which takes 10-15 seconds to compile the game) have to be requested - # with user-initiated flags. The text client will hang while the operation runs, but at least we can - # inform the user to wait. The flags are checked by the agents every main_tick. + # The command processor is not async so long-running operations like the /repl connect command + # (which takes 10-15 seconds to compile the game) have to be requested with user-initiated flags. + # The flags are checked by the agents every main_tick. def _cmd_repl(self, *arguments: str): """Sends a command to the OpenGOAL REPL. Arguments: - connect : connect the client to the REPL (goalc). @@ -52,7 +51,7 @@ def _cmd_repl(self, *arguments: str): logger.info("This may take a bit... Wait for the success audio cue before continuing!") self.ctx.repl.initiated_connect = True if arguments[0] == "status": - self.ctx.repl.print_status() + create_task_log_exception(self.ctx.repl.print_status()) def _cmd_memr(self, *arguments: str): """Sends a command to the Memory Reader. Arguments: @@ -119,41 +118,41 @@ def on_package(self, cmd: str, args: dict): else: orbsanity_bundle = 1 - # Keep compatibility with 0.0.8 at least for now + # Keep compatibility with 0.0.8 at least for now - TODO: Remove this. if "completion_condition" in slot_data: goal_id = slot_data["completion_condition"] else: goal_id = slot_data["jak_completion_condition"] - self.repl.setup_options(orbsanity_option, - orbsanity_bundle, - slot_data["fire_canyon_cell_count"], - slot_data["mountain_pass_cell_count"], - slot_data["lava_tube_cell_count"], - goal_id) + create_task_log_exception( + self.repl.setup_options(orbsanity_option, + orbsanity_bundle, + slot_data["fire_canyon_cell_count"], + slot_data["mountain_pass_cell_count"], + slot_data["lava_tube_cell_count"], + goal_id)) # Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server # to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost, # while "ReceivedItems" will tell us the orbs we gained. This will give us the correct balance. if orbsanity_option in [EnableOrbsanity.option_per_level, EnableOrbsanity.option_global]: async def get_orb_balance(): - await self.send_msgs([{"cmd": "Get", - "keys": [f"jakanddaxter_{self.auth}_orbs_paid"] - }]) + await self.send_msgs([{"cmd": "Get", "keys": [f"jakanddaxter_{self.auth}_orbs_paid"]}]) create_task_log_exception(get_orb_balance()) if cmd == "Retrieved": if f"jakanddaxter_{self.auth}_orbs_paid" in args["keys"]: orbs_traded = args["keys"][f"jakanddaxter_{self.auth}_orbs_paid"] - self.repl.subtract_traded_orbs(orbs_traded if orbs_traded is not None else 0) + orbs_traded = orbs_traded if orbs_traded is not None else 0 + create_task_log_exception(self.repl.subtract_traded_orbs(orbs_traded)) if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): logger.debug(f"index: {str(index)}, item: {str(item)}") self.repl.item_inbox[index] = item - def on_print_json(self, args: dict) -> None: + async def json_to_game_text(self, args: dict): if "type" in args and args["type"] in {"ItemSend"}: item = args["item"] recipient = args["receiving"] @@ -179,8 +178,14 @@ def on_print_json(self, args: dict) -> None: self.repl.their_item_owner = self.player_names[recipient] # Write to game display. - self.repl.write_game_text() + await self.repl.write_game_text() + + def on_print_json(self, args: dict) -> None: + # Even though N items come in as 1 ReceivedItems packet, there are still N PrintJson packets to process, + # and they all arrive before the ReceivedItems packet does. Defer processing of these packets as + # async tasks to speed up large releases of items. + create_task_log_exception(self.json_to_game_text(args)) super(JakAndDaxterContext, self).on_print_json(args) def on_deathlink(self, data: dict): @@ -214,7 +219,7 @@ async def ap_inform_deathlink(self): # Reset all flags. self.memr.send_deathlink = False self.memr.cause_of_death = "" - self.repl.reset_deathlink() + await self.repl.reset_deathlink() def on_deathlink_check(self): create_task_log_exception(self.ap_inform_deathlink()) @@ -225,14 +230,6 @@ async def ap_inform_deathlink_toggle(self): def on_deathlink_toggle(self): create_task_log_exception(self.ap_inform_deathlink_toggle()) - async def repl_reset_orbsanity(self): - if self.memr.orbsanity_enabled: - self.memr.reset_orbsanity = False - self.repl.reset_orbsanity() - - def on_orbsanity_check(self): - create_task_log_exception(self.repl_reset_orbsanity()) - async def ap_inform_orb_trade(self, orbs_changed: int): if self.memr.orbsanity_enabled: await self.send_msgs([{"cmd": "Set", @@ -256,7 +253,6 @@ async def run_memr_loop(self): self.on_finish_check, self.on_deathlink_check, self.on_deathlink_toggle, - self.on_orbsanity_check, self.on_orb_trade) await asyncio.sleep(0.1) diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 718fe6ff45a4..57f6c1ca7481 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,18 +1,21 @@ from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions, Choice, Range +from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range class EnableMoveRandomizer(Toggle): - """Enable to include movement options as items in the randomizer. Jak is only able to run, swim, and single jump, - until you find his other moves. This adds 11 items to the pool.""" + """Enable to include movement options as items in the randomizer. Jak is only able to run, swim, and single jump + until you find his other moves. + + This adds 11 items to the pool.""" display_name = "Enable Move Randomizer" class EnableOrbsanity(Choice): - """Enable to include bundles of Precursor Orbs as an ordered list of progressive checks. Every time you collect - the chosen number of orbs, you will trigger the next release in the list. "Per Level" means these lists are - generated and populated for each level in the game (Geyser Rock, Sandover Village, etc.). "Global" means there is - only one list for the entire game. + """Enable to include bundles of Precursor Orbs as an ordered list of progressive checks. Every time you collect the + chosen number of orbs, you will trigger the next release in the list. + + "Per Level" means these lists are generated and populated for each level in the game. "Global" means there + is only one list for the entire game. This adds a number of Items and Locations to the pool inversely proportional to the size of the bundle. For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs, @@ -25,8 +28,7 @@ class EnableOrbsanity(Choice): class GlobalOrbsanityBundleSize(Choice): - """Set the size of the bundle for Global Orbsanity. - This only applies if "Enable Orbsanity" is set to "Global." + """Set the orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global." There are 2000 orbs in the game, so your bundle size must be a factor of 2000.""" display_name = "Global Orbsanity Bundle Size" option_1_orb = 1 @@ -53,8 +55,7 @@ class GlobalOrbsanityBundleSize(Choice): class PerLevelOrbsanityBundleSize(Choice): - """Set the size of the bundle for Per Level Orbsanity. - This only applies if "Enable Orbsanity" is set to "Per Level." + """Set the orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level." There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.""" display_name = "Per Level Orbsanity Bundle Size" option_1_orb = 1 @@ -113,3 +114,4 @@ class JakAndDaxterOptions(PerGameCommonOptions): mountain_pass_cell_count: MountainPassCellCount lava_tube_cell_count: LavaTubeCellCount jak_completion_condition: CompletionCondition + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 95d077995142..f165bb406d84 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -100,13 +100,8 @@ def can_trade_orbsanity(state: CollectionState, def can_free_scout_flies(state: CollectionState, player: int) -> bool: - return (state.has("Jump Dive", player) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player))) + return state.has("Jump Dive", player) or state.has_all({"Crouch", "Crouch Uppercut"}, player) def can_fight(state: CollectionState, player: int) -> bool: - return (state.has("Jump Dive", player) - or state.has("Jump Kick", player) - or state.has("Punch", player) - or state.has("Kick", player)) + return state.has_any({"Jump Dive", "Jump Kick", "Punch", "Kick"}, player) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 14cd63c10f14..049fe1eb7caa 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -70,8 +70,7 @@ def define(self, size: int, length: int = 1) -> int: # Orbsanity information. orbsanity_option_offset = offsets.define(sizeof_uint8) orbsanity_bundle_offset = offsets.define(sizeof_uint32) -collected_bundle_level_offset = offsets.define(sizeof_uint8) -collected_bundle_count_offset = offsets.define(sizeof_uint32) +collected_bundle_offset = offsets.define(sizeof_uint32, 17) # Progression and Completion information. fire_canyon_unlock_offset = offsets.define(sizeof_float) @@ -151,7 +150,6 @@ class JakAndDaxterMemoryReader: # Orbsanity handling orbsanity_enabled: bool = False - reset_orbsanity: bool = False orbs_paid: int = 0 def __init__(self, marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'): @@ -163,7 +161,6 @@ async def main_tick(self, finish_callback: Callable, deathlink_callback: Callable, deathlink_toggle: Callable, - orbsanity_callback: Callable, paid_orbs_callback: Callable): if self.initiated_connect: await self.connect() @@ -200,9 +197,6 @@ async def main_tick(self, if self.send_deathlink: deathlink_callback() - if self.reset_orbsanity: - orbsanity_callback() - if self.orbs_paid > 0: paid_orbs_callback(self.orbs_paid) self.orbs_paid = 0 @@ -303,26 +297,40 @@ def read_memory(self) -> List[int]: # self.moverando_enabled = bool(moverando_flag) orbsanity_option = self.read_goal_address(orbsanity_option_offset, sizeof_uint8) - orbsanity_bundle = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32) + bundle_size = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32) self.orbsanity_enabled = orbsanity_option > 0 - # Treat these values like the Deathlink flag. They need to be reset once they are checked. - collected_bundle_level = self.read_goal_address(collected_bundle_level_offset, sizeof_uint8) - collected_bundle_count = self.read_goal_address(collected_bundle_count_offset, sizeof_uint32) - - if orbsanity_option > 0 and collected_bundle_count > 0: - # Count up from the first bundle, by bundle size, until you reach the latest collected bundle. - # e.g. {25, 50, 75, 100, 125...} - for k in range(orbsanity_bundle, - orbsanity_bundle + collected_bundle_count, # Range max is non-inclusive. - orbsanity_bundle): - - bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(collected_bundle_level, k, orbsanity_bundle)) - if bundle_ap_id not in self.location_outbox: - self.location_outbox.append(bundle_ap_id) - logger.debug("Checked orb bundle: " + str(bundle_ap_id)) - - # self.reset_orbsanity = True + # Per Level Orbsanity option. Only need to do this loop if we chose this setting. + if orbsanity_option == 1: + for level in range(0, 16): + collected_bundles = self.read_goal_address(collected_bundle_offset + (level * sizeof_uint32), + sizeof_uint32) + + # Count up from the first bundle, by bundle size, until you reach the latest collected bundle. + # e.g. {25, 50, 75, 100, 125...} + if collected_bundles > 0: + for bundle in range(bundle_size, + bundle_size + collected_bundles, # Range max is non-inclusive. + bundle_size): + + bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(level, bundle, bundle_size)) + if bundle_ap_id not in self.location_outbox: + self.location_outbox.append(bundle_ap_id) + logger.debug("Checked orb bundle: " + str(bundle_ap_id)) + + # Global Orbsanity option. Index 16 refers to all orbs found regardless of level. + if orbsanity_option == 2: + collected_bundles = self.read_goal_address(collected_bundle_offset + (16 * sizeof_uint32), + sizeof_uint32) + if collected_bundles > 0: + for bundle in range(bundle_size, + bundle_size + collected_bundles, # Range max is non-inclusive. + bundle_size): + + bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(16, bundle, bundle_size)) + if bundle_ap_id not in self.location_outbox: + self.location_outbox.append(bundle_ap_id) + logger.debug("Checked orb bundle: " + str(bundle_ap_id)) completed = self.read_goal_address(completed_offset, sizeof_uint8) if completed > 0 and not self.finished_game: diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 712c268c6f39..09b488d17486 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -1,13 +1,15 @@ import json import time import struct -from typing import Dict, Callable import random -from socket import socket, AF_INET, SOCK_STREAM +from typing import Dict, Callable import pymem from pymem.exception import ProcessNotFound, ProcessError +import asyncio +from asyncio import StreamReader, StreamWriter, Lock + from CommonClient import logger from NetUtils import NetworkItem from ..GameID import jak1_id, jak1_max @@ -23,10 +25,13 @@ class JakAndDaxterReplClient: ip: str port: int - sock: socket + reader: StreamReader + writer: StreamWriter + lock: Lock connected: bool = False initiated_connect: bool = False # Signals when user tells us to try reconnecting. received_deathlink: bool = False + balanced_orbs: bool = False # The REPL client needs the REPL/compiler process running, but that process # also needs the game running. Therefore, the REPL client needs both running. @@ -44,6 +49,7 @@ class JakAndDaxterReplClient: def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.ip = ip self.port = port + self.lock = asyncio.Lock() self.connect() async def main_tick(self): @@ -67,33 +73,36 @@ async def main_tick(self): # Receive Items from AP. Handle 1 item per tick. if len(self.item_inbox) > self.inbox_index: - self.receive_item() - self.save_data() + await self.receive_item() + await self.save_data() self.inbox_index += 1 if self.received_deathlink: - self.receive_deathlink() + await self.receive_deathlink() # Reset all flags. # As a precaution, we should reset our own deathlink flag as well. - self.reset_deathlink() + await self.reset_deathlink() self.received_deathlink = False # This helper function formats and sends `form` as a command to the REPL. # ALL commands to the REPL should be sent using this function. - # TODO - this blocks on receiving an acknowledgement from the REPL server. But it doesn't print - # any log info in the meantime. Is that a problem? - def send_form(self, form: str, print_ok: bool = True) -> bool: + async def send_form(self, form: str, print_ok: bool = True) -> bool: header = struct.pack(" str: # OpenGOAL can handle both its own string datatype and C-like character pointers (charp). # So for the game to constantly display this information in the HUD, we have to write it # to a memory address as a char*. - def write_game_text(self): + async def write_game_text(self): logger.debug(f"Sending info to in-game display!") - self.send_form(f"(charp<-string (-> *ap-info-jak1* my-item-name) " - f"{self.sanitize_game_text(self.my_item_name)})", - print_ok=False) - self.send_form(f"(charp<-string (-> *ap-info-jak1* my-item-finder) " - f"{self.sanitize_game_text(self.my_item_finder)})", - print_ok=False) - self.send_form(f"(charp<-string (-> *ap-info-jak1* their-item-name) " - f"{self.sanitize_game_text(self.their_item_name)})", - print_ok=False) - self.send_form(f"(charp<-string (-> *ap-info-jak1* their-item-owner) " - f"{self.sanitize_game_text(self.their_item_owner)})", - print_ok=False) - - def receive_item(self): + await self.send_form(f"(begin " + f" (charp<-string (-> *ap-info-jak1* my-item-name) " + f" {self.sanitize_game_text(self.my_item_name)}) " + f" (charp<-string (-> *ap-info-jak1* my-item-finder) " + f" {self.sanitize_game_text(self.my_item_finder)}) " + f" (charp<-string (-> *ap-info-jak1* their-item-name) " + f" {self.sanitize_game_text(self.their_item_name)}) " + f" (charp<-string (-> *ap-info-jak1* their-item-owner) " + f" {self.sanitize_game_text(self.their_item_owner)}) " + f" (none))", print_ok=False) + + async def receive_item(self): ap_id = getattr(self.item_inbox[self.inbox_index], "item") # Determine the type of item to receive. if ap_id in range(jak1_id, jak1_id + Flies.fly_offset): - self.receive_power_cell(ap_id) + await self.receive_power_cell(ap_id) elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Specials.special_offset): - self.receive_scout_fly(ap_id) + await self.receive_scout_fly(ap_id) elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset): - self.receive_special(ap_id) + await self.receive_special(ap_id) elif ap_id in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset): - self.receive_move(ap_id) + await self.receive_move(ap_id) elif ap_id in range(jak1_id + Orbs.orb_offset, jak1_max): - self.receive_precursor_orb(ap_id) # Ponder the Orbs. + await self.receive_precursor_orb(ap_id) # Ponder the Orbs. elif ap_id == jak1_max: - self.receive_green_eco() # Ponder why I chose to do ID's this way. + await self.receive_green_eco() # Ponder why I chose to do ID's this way. else: raise KeyError(f"Tried to receive item with unknown AP ID {ap_id}.") - def receive_power_cell(self, ap_id: int) -> bool: + async def receive_power_cell(self, ap_id: int) -> bool: cell_id = Cells.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type fuel-cell) " - "(the float " + str(cell_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type fuel-cell) " + "(the float " + str(cell_id) + "))") if ok: logger.debug(f"Received a Power Cell!") else: logger.error(f"Unable to receive a Power Cell!") return ok - def receive_scout_fly(self, ap_id: int) -> bool: + async def receive_scout_fly(self, ap_id: int) -> bool: fly_id = Flies.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type buzzer) " - "(the float " + str(fly_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type buzzer) " + "(the float " + str(fly_id) + "))") if ok: logger.debug(f"Received a {item_table[ap_id]}!") else: logger.error(f"Unable to receive a {item_table[ap_id]}!") return ok - def receive_special(self, ap_id: int) -> bool: + async def receive_special(self, ap_id: int) -> bool: special_id = Specials.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type ap-special) " - "(the float " + str(special_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type ap-special) " + "(the float " + str(special_id) + "))") if ok: logger.debug(f"Received special unlock {item_table[ap_id]}!") else: logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") return ok - def receive_move(self, ap_id: int) -> bool: + async def receive_move(self, ap_id: int) -> bool: move_id = Caches.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type ap-move) " - "(the float " + str(move_id) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type ap-move) " + "(the float " + str(move_id) + "))") if ok: logger.debug(f"Received the ability to {item_table[ap_id]}!") else: logger.error(f"Unable to receive the ability to {item_table[ap_id]}!") return ok - def receive_precursor_orb(self, ap_id: int) -> bool: + async def receive_precursor_orb(self, ap_id: int) -> bool: orb_amount = Orbs.to_game_id(ap_id) - ok = self.send_form("(send-event " - "*target* \'get-archipelago " - "(pickup-type money) " - "(the float " + str(orb_amount) + "))") + ok = await self.send_form("(send-event " + "*target* \'get-archipelago " + "(pickup-type money) " + "(the float " + str(orb_amount) + "))") if ok: logger.debug(f"Received {orb_amount} Precursor Orbs!") else: @@ -304,18 +303,15 @@ def receive_precursor_orb(self, ap_id: int) -> bool: return ok # Green eco pills are our filler item. Use the get-pickup event instead to handle being full health. - def receive_green_eco(self) -> bool: - ok = self.send_form("(send-event " - "*target* \'get-pickup " - "(pickup-type eco-pill) " - "(the float 1))") + async def receive_green_eco(self) -> bool: + ok = await self.send_form("(send-event *target* \'get-pickup (pickup-type eco-pill) (the float 1))") if ok: logger.debug(f"Received a green eco pill!") else: logger.error(f"Unable to receive a green eco pill!") return ok - def receive_deathlink(self) -> bool: + async def receive_deathlink(self) -> bool: # Because it should at least be funny sometimes. death_types = ["\'death", @@ -328,51 +324,43 @@ def receive_deathlink(self) -> bool: "\'dark-eco-pool"] chosen_death = random.choice(death_types) - ok = self.send_form("(ap-deathlink-received! " + chosen_death + ")") + ok = await self.send_form("(ap-deathlink-received! " + chosen_death + ")") if ok: logger.debug(f"Received deathlink signal!") else: logger.error(f"Unable to receive deathlink signal!") return ok - def reset_deathlink(self) -> bool: - ok = self.send_form("(set! (-> *ap-info-jak1* died) 0)") + async def reset_deathlink(self) -> bool: + ok = await self.send_form("(set! (-> *ap-info-jak1* died) 0)") if ok: logger.debug(f"Reset deathlink flag!") else: logger.error(f"Unable to reset deathlink flag!") return ok - def subtract_traded_orbs(self, orb_count: int) -> bool: - ok = self.send_form(f"(-! (-> *game-info* money) (the float {orb_count}))") - if ok: - logger.debug(f"Subtracting {orb_count} traded orbs!") - else: - logger.error(f"Unable to subtract {orb_count} traded orbs!") - return ok + async def subtract_traded_orbs(self, orb_count: int) -> bool: - def reset_orbsanity(self) -> bool: - ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-level) 0)") - if ok: - logger.debug(f"Reset level ID for collected orbsanity bundle!") - else: - logger.error(f"Unable to reset level ID for collected orbsanity bundle!") + # To protect against momentary server disconnects, + # this should only be done once per client session. + if not self.balanced_orbs: + self.balanced_orbs = True - ok = self.send_form(f"(set! (-> *ap-info-jak1* collected-bundle-count) 0)") - if ok: - logger.debug(f"Reset orb count for collected orbsanity bundle!") - else: - logger.error(f"Unable to reset orb count for collected orbsanity bundle!") - return ok - - def setup_options(self, - os_option: int, os_bundle: int, - fc_count: int, mp_count: int, - lt_count: int, goal_id: int) -> bool: - ok = self.send_form(f"(ap-setup-options! " - f"(the uint {os_option}) (the uint {os_bundle}) " - f"(the float {fc_count}) (the float {mp_count}) " - f"(the float {lt_count}) (the uint {goal_id}))") + ok = await self.send_form(f"(-! (-> *game-info* money) (the float {orb_count}))") + if ok: + logger.debug(f"Subtracting {orb_count} traded orbs!") + else: + logger.error(f"Unable to subtract {orb_count} traded orbs!") + return ok + + async def setup_options(self, + os_option: int, os_bundle: int, + fc_count: int, mp_count: int, + lt_count: int, goal_id: int) -> bool: + ok = await self.send_form(f"(ap-setup-options! " + f"(the uint {os_option}) (the uint {os_bundle}) " + f"(the float {fc_count}) (the float {mp_count}) " + f"(the float {lt_count}) (the uint {goal_id}))") message = (f"Setting options: \n" f" Orbsanity Option {os_option}, Orbsanity Bundle {os_bundle}, \n" f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n" @@ -383,7 +371,7 @@ def setup_options(self, logger.error(message + "Failed!") return ok - def save_data(self): + async def save_data(self): with open("jakanddaxter_item_inbox.json", "w+") as f: dump = { "inbox_index": self.inbox_index, diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py index 8b18a5d3f61b..fe5ad5766c98 100644 --- a/worlds/jakanddaxter/regs/BoggySwampRegions.py +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -10,15 +10,13 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) def can_jump_farther(state: CollectionState, p: int) -> bool: - return (state.has("Double Jump", p) - or state.has("Jump Kick", p) - or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + return state.has_any({"Double Jump", "Jump Kick"}, p) or state.has_all({"Punch", "Punch Uppercut"}, p) def can_jump_higher(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) - or (state.has("Crouch", p) and state.has("Crouch Jump", p)) - or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) - or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + or state.has_all({"Crouch", "Crouch Jump"}, p) + or state.has_all({"Crouch", "Crouch Uppercut"}, p) + or state.has_all({"Punch", "Punch Uppercut"}, p)) # Orb crates and fly box in this area can be gotten with yellow eco and goggles. # Start with the first yellow eco cluster near first_bats and work your way backward toward the entrance. @@ -93,9 +91,8 @@ def can_jump_higher(state: CollectionState, p: int) -> bool: first_tether.connect(first_bats) first_tether.connect(first_tether_rat_colony, rule=lambda state: - (state.has("Roll", player) and state.has("Roll Jump", player)) - or (state.has("Double Jump", player) - and state.has("Jump Kick", player))) + (state.has_all({"Roll", "Roll Jump"}, player) + or state.has_all({"Double Jump", "Jump Kick"}, player))) first_tether.connect(second_jump_pad) first_tether.connect(first_pole_course) diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py index 0a2646082ff7..b78ef7588e5f 100644 --- a/worlds/jakanddaxter/regs/GeyserRockRegions.py +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -16,9 +16,9 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter cliff.add_cell_locations([94]) main_area.connect(cliff, rule=lambda state: - ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) and state.has("Crouch Uppercut", player)) - or state.has("Double Jump", player))) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut"}, player)) cliff.connect(main_area) # Jump down or ride blue eco elevator. diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index 59fc7c8a4c16..2104358ad903 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -13,31 +13,29 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter def can_jump_farther(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) or state.has("Jump Kick", p) - or (state.has("Punch", p) and state.has("Punch Uppercut", p))) + or state.has_all({"Punch", "Punch Uppercut"}, p)) def can_triple_jump(state: CollectionState, p: int) -> bool: - return state.has("Double Jump", p) and state.has("Jump Kick", p) + return state.has_all({"Double Jump", "Jump Kick"}, p) def can_jump_stairs(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) - or (state.has("Crouch", p) and state.has("Crouch Jump", p)) - or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) - or state.has("Jump Dive", p)) + or state.has("Jump Dive", p) + or state.has_all({"Crouch", "Crouch Jump"}, p) + or state.has_all({"Crouch", "Crouch Uppercut"}, p)) main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) main_area.add_fly_locations([91], access_rule=lambda state: can_free_scout_flies(state, player)) robot_scaffolding = JakAndDaxterRegion("Scaffolding Around Robot", player, multiworld, level_name, 8) - robot_scaffolding.add_fly_locations([196699], access_rule=lambda state: - can_free_scout_flies(state, player)) + robot_scaffolding.add_fly_locations([196699], access_rule=lambda state: can_free_scout_flies(state, player)) jump_pad_room = JakAndDaxterRegion("Jump Pad Chamber", player, multiworld, level_name, 88) jump_pad_room.add_cell_locations([73], access_rule=lambda state: can_fight(state, player)) jump_pad_room.add_special_locations([73], access_rule=lambda state: can_fight(state, player)) jump_pad_room.add_fly_locations([131163]) # Blue eco vent is right next to it. jump_pad_room.add_fly_locations([65627], access_rule=lambda state: - can_free_scout_flies(state, player) - and can_jump_farther(state, player)) + can_free_scout_flies(state, player) and can_jump_farther(state, player)) blast_furnace = JakAndDaxterRegion("Blast Furnace", player, multiworld, level_name, 39) blast_furnace.add_cell_locations([71], access_rule=lambda state: can_fight(state, player)) @@ -47,14 +45,12 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: bunny_room = JakAndDaxterRegion("Bunny Chamber", player, multiworld, level_name, 45) bunny_room.add_cell_locations([72], access_rule=lambda state: can_fight(state, player)) bunny_room.add_special_locations([72], access_rule=lambda state: can_fight(state, player)) - bunny_room.add_fly_locations([262235], access_rule=lambda state: - can_free_scout_flies(state, player)) + bunny_room.add_fly_locations([262235], access_rule=lambda state: can_free_scout_flies(state, player)) rotating_tower = JakAndDaxterRegion("Rotating Tower", player, multiworld, level_name, 20) rotating_tower.add_cell_locations([70], access_rule=lambda state: can_fight(state, player)) rotating_tower.add_special_locations([70], access_rule=lambda state: can_fight(state, player)) - rotating_tower.add_fly_locations([327771], access_rule=lambda state: - can_free_scout_flies(state, player)) + rotating_tower.add_fly_locations([327771], access_rule=lambda state: can_free_scout_flies(state, player)) final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0) @@ -62,48 +58,43 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: # Jump Dive required for a lot of buttons, prepare yourself. main_area.connect(robot_scaffolding, rule=lambda state: - state.has("Jump Dive", player) - or (state.has("Roll", player) and state.has("Roll Jump", player))) + state.has("Jump Dive", player) or state.has_all({"Roll", "Roll Jump"}, player)) main_area.connect(jump_pad_room) robot_scaffolding.connect(main_area, rule=lambda state: state.has("Jump Dive", player)) robot_scaffolding.connect(blast_furnace, rule=lambda state: state.has("Jump Dive", player) and can_jump_farther(state, player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_triple_jump(state, player))) + and (can_triple_jump(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) robot_scaffolding.connect(bunny_room, rule=lambda state: state.has("Jump Dive", player) and can_jump_farther(state, player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_triple_jump(state, player))) + and (can_triple_jump(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) jump_pad_room.connect(main_area) jump_pad_room.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_triple_jump(state, player))) + and (can_triple_jump(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) blast_furnace.connect(robot_scaffolding) # Blue eco elevator takes you right back. bunny_room.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) - and ((state.has("Roll", player) and state.has("Roll Jump", player)) - or can_jump_farther(state, player))) + and (can_jump_farther(state, player) or state.has_all({"Roll", "Roll Jump"}, player))) # Final climb. robot_scaffolding.connect(rotating_tower, rule=lambda state: - state.has("Freed The Blue Sage", player) - and state.has("Freed The Red Sage", player) - and state.has("Freed The Yellow Sage", player) - and can_jump_stairs(state, player)) + can_jump_stairs(state, player) + and state.has_all({"Freed The Blue Sage", + "Freed The Red Sage", + "Freed The Yellow Sage"}, player)) rotating_tower.connect(main_area) # Take stairs back down. # Final elevator. Need to break boxes at summit to get blue eco for platform. rotating_tower.connect(final_boss, rule=lambda state: - state.has("Freed The Green Sage", player) - and can_fight(state, player)) + can_fight(state, player) + and state.has("Freed The Green Sage", player)) final_boss.connect(rotating_tower) # Take elevator back down. diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py index 716f0fd46b26..2d1f712f57c6 100644 --- a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -19,8 +19,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Need jump dive to activate button, double jump to reach blue eco to unlock cache. first_room_orb_cache.add_cache_locations([14507], access_rule=lambda state: - state.has("Jump Dive", player) - and state.has("Double Jump", player)) + state.has_all({"Jump Dive", "Double Jump"}, player)) first_hallway = JakAndDaxterRegion("First Hallway", player, multiworld, level_name, 10) first_hallway.add_fly_locations([131121], access_rule=lambda state: can_free_scout_flies(state, player)) @@ -59,19 +58,16 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Use jump dive to activate button inside the capsule. Blue eco vent can ready the chamber and get the scout fly. capsule_room.add_cell_locations([47], access_rule=lambda state: state.has("Jump Dive", player) - and (state.has("Double Jump", player) - or state.has("Jump Kick", player) - or (state.has("Punch", player) - and state.has("Punch Uppercut", player)))) + and (state.has_any({"Double Jump", "Jump Kick"}, player) + or state.has_all({"Punch", "Punch Uppercut"}, player))) capsule_room.add_fly_locations([327729]) second_slide = JakAndDaxterRegion("Second Slide", player, multiworld, level_name, 31) helix_room = JakAndDaxterRegion("Helix Chamber", player, multiworld, level_name, 30) helix_room.add_cell_locations([46], access_rule=lambda state: - state.has("Double Jump", player) - or state.has("Jump Kick", player) - or (state.has("Punch", player) and state.has("Punch Uppercut", player))) + state.has_any({"Double Jump", "Jump Kick"}, player) + or state.has_all({"Punch", "Punch Uppercut"}, player)) helix_room.add_cell_locations([50], access_rule=lambda state: state.has("Double Jump", player) or can_fight(state, player)) @@ -86,11 +82,9 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Needs some movement to reach these orbs and orb cache. first_room_lower.connect(first_room_orb_cache, rule=lambda state: - state.has("Jump Dive", player) - and state.has("Double Jump", player)) + state.has_all({"Jump Dive", "Double Jump"}, player)) first_room_orb_cache.connect(first_room_lower, rule=lambda state: - state.has("Jump Dive", player) - and state.has("Double Jump", player)) + state.has_all({"Jump Dive", "Double Jump"}, player)) first_hallway.connect(first_room_upper) # Run and jump down. first_hallway.connect(second_room) # Run and jump (floating platforms). diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py index 1a70095ce57c..16a8790de540 100644 --- a/worlds/jakanddaxter/regs/MistyIslandRegions.py +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -59,9 +59,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter muse_course.connect(main_area) # Run and jump down. # The zoomer pad is low enough that it requires Crouch Jump specifically. - zoomer.connect(main_area, rule=lambda state: - (state.has("Crouch", player) - and state.has("Crouch Jump", player))) + zoomer.connect(main_area, rule=lambda state: state.has_all({"Crouch", "Crouch Jump"}, player)) ship.connect(main_area) # Run and jump down. ship.connect(far_side) # Run and jump down. @@ -72,9 +70,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Only if you can use the seesaw or Crouch Jump from the seesaw's edge. far_side.connect(far_side_cliff, rule=lambda state: - (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or state.has("Jump Dive", player)) + state.has("Jump Dive", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) # Only if you can break the bone bridges to carry blue eco over the mud pit. far_side.connect(far_side_cache, rule=lambda state: can_fight(state, player)) @@ -91,9 +88,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter upper_approach.connect(arena) # Jump down. # One cliff is accessible, but only via Crouch Jump. - lower_approach.connect(upper_approach, rule=lambda state: - (state.has("Crouch", player) - and state.has("Crouch Jump", player))) + lower_approach.connect(upper_approach, rule=lambda state: state.has_all({"Crouch", "Crouch Jump"}, player)) # Requires breaking bone bridges. lower_approach.connect(arena, rule=lambda state: can_fight(state, player)) diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index 213dd0ae8749..f59e63101a09 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -31,8 +31,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter orb_cache = JakAndDaxterRegion("Orb Cache", player, multiworld, level_name, 20) # You need roll jump to be able to reach this before the blue eco runs out. - orb_cache.add_cache_locations([10945], access_rule=lambda state: - (state.has("Roll", player) and state.has("Roll Jump", player))) + orb_cache.add_cache_locations([10945], access_rule=lambda state: state.has_all({"Roll", "Roll Jump"}, player)) # Fly here can be gotten with Yellow Eco from Boggy, goggles, and no extra movement options (see fly ID 43). pontoon_bridge = JakAndDaxterRegion("Pontoon Bridge", player, multiworld, level_name, 7) @@ -40,7 +39,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter klaww_cliff = JakAndDaxterRegion("Klaww's Cliff", player, multiworld, level_name, 0) - main_area.connect(orb_cache, rule=lambda state: (state.has("Roll", player) and state.has("Roll Jump", player))) + main_area.connect(orb_cache, rule=lambda state: state.has_all({"Roll", "Roll Jump"}, player)) main_area.connect(pontoon_bridge, rule=lambda state: state.has("Warrior's Pontoons", player)) orb_cache.connect(main_area) @@ -48,11 +47,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter pontoon_bridge.connect(main_area, rule=lambda state: state.has("Warrior's Pontoons", player)) pontoon_bridge.connect(klaww_cliff, rule=lambda state: state.has("Double Jump", player) - or (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player) - and state.has("Jump Kick", player))) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut", "Jump Kick"}, player)) klaww_cliff.connect(pontoon_bridge) # Just jump back down. diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index c872d32ebbbe..f4ee264a7422 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -21,8 +21,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # The farmer's scout fly. You can either get the Orb Cache Cliff blue eco, or break it normally. main_area.add_fly_locations([196683], access_rule=lambda state: - (state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player) or can_free_scout_flies(state, player)) orb_cache_cliff = JakAndDaxterRegion("Orb Cache Cliff", player, multiworld, level_name, 15) @@ -41,23 +41,17 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter main_area.connect(orb_cache_cliff, rule=lambda state: state.has("Double Jump", player) - or (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player) - and state.has("Jump Kick", player))) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut", "Jump Kick"}, player)) main_area.connect(yakow_cliff, rule=lambda state: state.has("Double Jump", player) - or (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or (state.has("Crouch", player) - and state.has("Crouch Uppercut", player) - and state.has("Jump Kick", player))) + or state.has_all({"Crouch", "Crouch Jump"}, player) + or state.has_all({"Crouch", "Crouch Uppercut", "Jump Kick"}, player)) main_area.connect(oracle_platforms, rule=lambda state: - (state.has("Roll", player) and state.has("Roll Jump", player)) - or (state.has("Double Jump", player) and state.has("Jump Kick", player))) + state.has_all({"Roll", "Roll Jump"}, player) + or state.has_all({"Double Jump", "Jump Kick"}, player)) # All these can go back to main_area immediately. orb_cache_cliff.connect(main_area) diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py index e66cb3b88427..97c7a587724b 100644 --- a/worlds/jakanddaxter/regs/SentinelBeachRegions.py +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -27,13 +27,9 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Only these specific attacks can push the flut flut egg off the cliff. flut_flut_egg = JakAndDaxterRegion("Flut Flut Egg", player, multiworld, level_name, 0) flut_flut_egg.add_cell_locations([17], access_rule=lambda state: - state.has("Punch", player) - or state.has("Kick", player) - or state.has("Jump Kick", player)) + state.has_any({"Punch", "Kick", "Jump Kick"}, player)) flut_flut_egg.add_special_locations([17], access_rule=lambda state: - state.has("Punch", player) - or state.has("Kick", player) - or state.has("Jump Kick", player)) + state.has_any({"Punch", "Kick", "Jump Kick"}, player)) eco_harvesters = JakAndDaxterRegion("Eco Harvesters", player, multiworld, level_name, 0) eco_harvesters.add_cell_locations([15], access_rule=lambda state: can_fight(state, player)) @@ -55,15 +51,15 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # You don't need any kind of uppercut to reach this place, just a high jump from a convenient nearby ledge. main_area.connect(green_ridge, rule=lambda state: - (state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) # Can either uppercut the log and jump from it, or use the blue eco jump pad. main_area.connect(blue_ridge, rule=lambda state: state.has("Blue Eco Switch", player) or (state.has("Double Jump", player) - and ((state.has("Crouch", player) and state.has("Crouch Uppercut", player)) - or (state.has("Punch", player) and state.has("Punch Uppercut", player))))) + and (state.has_all({"Crouch", "Crouch Uppercut"}, player) + or state.has_all({"Punch", "Punch Uppercut"}, player)))) main_area.connect(cannon_tower, rule=lambda state: state.has("Blue Eco Switch", player)) diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index 07bd33ca2419..f6ee58b738ee 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -10,22 +10,20 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # We need a few helper functions. def can_cross_main_gap(state: CollectionState, p: int) -> bool: - return ((state.has("Roll", player) - and state.has("Roll Jump", player)) - or (state.has("Double Jump", player) - and state.has("Jump Kick", player))) + return (state.has_all({"Roll", "Roll Jump"}, p) + or state.has_all({"Double Jump", "Jump Kick"}, p)) def can_cross_frozen_cave(state: CollectionState, p: int) -> bool: return (state.has("Jump Kick", p) and (state.has("Double Jump", p) - or (state.has("Roll", p) and state.has("Roll Jump", p)))) + or state.has_all({"Roll", "Roll Jump"}, p))) def can_jump_blockers(state: CollectionState, p: int) -> bool: return (state.has("Double Jump", p) - or (state.has("Crouch", p) and state.has("Crouch Jump", p)) - or (state.has("Crouch", p) and state.has("Crouch Uppercut", p)) - or (state.has("Punch", p) and state.has("Punch Uppercut", p)) - or state.has("Jump Dive", p)) + or state.has("Jump Dive", p) + or state.has_all({"Crouch", "Crouch Jump"}, p) + or state.has_all({"Crouch", "Crouch Uppercut"}, p) + or state.has_all({"Punch", "Punch Uppercut"}, p)) main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) main_area.add_fly_locations([65], access_rule=lambda state: can_free_scout_flies(state, player)) @@ -145,25 +143,22 @@ def can_jump_blockers(state: CollectionState, p: int) -> bool: can_jump_blockers(state, player)) fort_interior.connect(fort_interior_caches, rule=lambda state: # Just need a little height. - (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) fort_interior.connect(fort_interior_base, rule=lambda state: # Just need a little height. - (state.has("Crouch", player) - and state.has("Crouch Jump", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player)) fort_interior.connect(fort_interior_course_end, rule=lambda state: # Just need a little distance. - (state.has("Punch", player) - and state.has("Punch Uppercut", player)) - or state.has("Double Jump", player)) + state.has("Double Jump", player) + or state.has_all({"Punch", "Punch Uppercut"}, player)) flut_flut_course.connect(fort_exterior) # Ride the elevator. # Must fight way through cave, but there is also a grab-less ledge we must jump over. bunny_cave_start.connect(bunny_cave_end, rule=lambda state: can_fight(state, player) - and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player))) + and (state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player))) # All jump down. fort_interior_caches.connect(fort_interior) diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py index 7124732d0971..c773f8c0644d 100644 --- a/worlds/jakanddaxter/regs/SpiderCaveRegions.py +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -22,18 +22,18 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # The rest of the crystals can be destroyed with yellow eco in main_area. dark_crystals.add_cell_locations([79], access_rule=lambda state: can_fight(state, player) - and (state.has("Roll", player) and state.has("Roll Jump", player))) + and state.has_all({"Roll", "Roll Jump"}, player)) dark_cave = JakAndDaxterRegion("Dark Cave", player, multiworld, level_name, 5) dark_cave.add_cell_locations([80], access_rule=lambda state: can_fight(state, player) - and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player))) + and (state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player))) dark_cave.add_fly_locations([262229], access_rule=lambda state: can_fight(state, player) and can_free_scout_flies(state, player) - and ((state.has("Crouch", player) and state.has("Crouch Jump", player)) - or state.has("Double Jump", player))) + and (state.has("Double Jump", player) + or state.has_all({"Crouch", "Crouch Jump"}, player))) robot_cave = JakAndDaxterRegion("Robot Cave", player, multiworld, level_name, 0) From b3b1ee4e642d4297a28e0734352d1466b5340869 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:50:06 -0400 Subject: [PATCH 41/70] Added more specific troubleshooting/setup instructions. --- worlds/jakanddaxter/docs/setup_en.md | 46 +++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index f11953d6dd62..78e950615e89 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -37,8 +37,21 @@ At this time, this method of setup works on Windows only, but Linux support is a - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. - Run the Archipelago Launcher. - From the left-most list, click `Generate Template Options`. -- Select `Jak and Daxter The Precursor Legacy.yaml`. In the text file that opens, enter the name you want and remember it for later. +- Select `Jak and Daxter The Precursor Legacy.yaml`. +- In the text file that opens, enter the name you want and remember it for later. - Save this file in `Archipelago/players`. You can now close the file. +- Back in the Archipelago Launcher, click `Open host.yaml`. +- In the text file that opens, search for `jakanddaxter_options`. If you do not see it, you will need it add it. + - Change (or add) the `root_directory` entry and provide the path you noted earlier containing `gk.exe` and `goalc.exe`. + - **CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/ `.** + - The result should look like this. You can now save and close the file. + +``` +jakanddaxter_options: + # Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). + root_directory: "C:/Users//AppData/Roaming/OpenGOAL-Mods/archipelagoal" +``` + - Back in the Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. - If you plan to host the game yourself, from the left-most list, click `Host`. - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. @@ -115,6 +128,37 @@ Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. - If the game freezes by replaying the same two frames over and over, but the music still runs in the background, you may have accidentally interacted with the powershell windows in the background - they halt the game if you:scroll up in them, highlight text in them, etc. - To unfreeze the game, scroll to the very bottom of the log output and right click. That will release powershell from your control and allow the game to continue. - It is recommended to keep these windows minimized and out of your way. +- If the client cannot open a REPL connection to the game, you may need to ensure you are not hosting anything on ports 8181 and 8112. + +***Special PAL Instructions*** + +PAL versions of the game seem to require additional troubleshooting/setup in order to work properly. Below are some instructions that may help. + +- If you have `-- Compilation Error! --` after pressing `Recompile` or Launching the ArchipelaGOAL mod. Try this: + - Remove these folders if you have them: + - `%appdata%\OpenGOAL-Mods\iso_data` + - `%appdata%\OpenGOAL-Mods\archipelagoal\iso_data` + - `%appdata%\OpenGOAL-Mods\archipelagoal\data\iso_data` + - Place Jak1 ISO in: `%appdata%\OpenGOAL-Mods\archipelagoal` rename it to `JakAndDaxter.iso` + - Type "CMD" in Windows search, Right click Command Prompt, and pick "Run as Administrator" + - Run: `cd %appdata%\OpenGOAL-Mods\archipelagoal` + - Then run: `extractor.exe --extract --extract-path .\data\iso_data "JakAndDaxter.iso"` + - (Command should end by saying `Uses Decompiler Config Version - ntsc_v1` or `... - pal`) + - Rename: `%appdata%\OpenGOAL-Mods\archipelagoal\data\iso_data\jak1` to `jak1_pal`* + - Run next: `decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "pal" data\iso_data data\decompiler_out`* + - *For NTSCv1 (USA Black Label) keep the folder as `jak1`, and use command: `decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "ntsc_v1" data\iso_data data\decompiler_out` + - Rename: `%appdata%\OpenGOAL-Mods\archipelagoal\data\iso_data\jak1_pal` to `jak1` + - Rename: `%appdata%\OpenGOAL-Mods\archipelagoal\data\decompiler_out\jak1_pal` to `jak1` +- You have to do this last bit in two different terminal **(2 powershell)**. First, from one terminal, launch the compiler: + - `cd %appdata%\OpenGOAL-Mods\archipelagoal` + - `.\goalc.exe --user-auto --game jak1` + - From the compiler (in the same terminal): `(mi)` + - This should compile the game. **Note that the parentheses are important.** + - **Don't close this first terminal, you will need it at the end.** +- Then, **from the second terminal (powershell)**, execute the game: + - `cd %appdata%\OpenGOAL-Mods\archipelagoal` + - `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug` +- Finally, **from the first terminal still in the Goalc compiler**, connect to the game: `(lt)` ### Known Issues From da7de272bc40fbed05345fc5159c485525e623e6 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 3 Aug 2024 15:06:31 -0400 Subject: [PATCH 42/70] Add known issue about large releases taking time. (Dodge 6,666th commit.) --- worlds/jakanddaxter/docs/setup_en.md | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 78e950615e89..6ccc0279d16f 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -166,3 +166,4 @@ PAL versions of the game seem to require additional troubleshooting/setup in ord - The powershell windows cannot be run as background processes due to how the repl works, so the best we can do is minimize them. - The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. - Orbsanity checks may show up out of order in the text client. +- Large item releases may take up to several minutes for the game to process them all. From a675849bebd0768cf8ac4450cb6b63f0781bcb98 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:47:21 -0400 Subject: [PATCH 43/70] Remove "Bundle of", Add location name groups, set better default RootDirectory for new players. --- worlds/jakanddaxter/Items.py | 40 ++++++++++++++-------------- worlds/jakanddaxter/__init__.py | 19 +++++++++++-- worlds/jakanddaxter/docs/setup_en.md | 26 +++++++++--------- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 28574b14ca1d..3a2efba676d1 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -42,26 +42,26 @@ class JakAndDaxterItem(Item): # These items are only used by Orbsanity, and only one of these # items will be used corresponding to the chosen bundle size. orb_item_table = { - 1: "Precursor Orb", - 2: "Bundle of 2 Precursor Orbs", - 4: "Bundle of 4 Precursor Orbs", - 5: "Bundle of 5 Precursor Orbs", - 8: "Bundle of 8 Precursor Orbs", - 10: "Bundle of 10 Precursor Orbs", - 16: "Bundle of 16 Precursor Orbs", - 20: "Bundle of 20 Precursor Orbs", - 25: "Bundle of 25 Precursor Orbs", - 40: "Bundle of 40 Precursor Orbs", - 50: "Bundle of 50 Precursor Orbs", - 80: "Bundle of 80 Precursor Orbs", - 100: "Bundle of 100 Precursor Orbs", - 125: "Bundle of 125 Precursor Orbs", - 200: "Bundle of 200 Precursor Orbs", - 250: "Bundle of 250 Precursor Orbs", - 400: "Bundle of 400 Precursor Orbs", - 500: "Bundle of 500 Precursor Orbs", - 1000: "Bundle of 1000 Precursor Orbs", - 2000: "Bundle of 2000 Precursor Orbs", + 1: "1 Precursor Orb", + 2: "2 Precursor Orbs", + 4: "4 Precursor Orbs", + 5: "5 Precursor Orbs", + 8: "8 Precursor Orbs", + 10: "10 Precursor Orbs", + 16: "16 Precursor Orbs", + 20: "20 Precursor Orbs", + 25: "25 Precursor Orbs", + 40: "40 Precursor Orbs", + 50: "50 Precursor Orbs", + 80: "80 Precursor Orbs", + 100: "100 Precursor Orbs", + 125: "125 Precursor Orbs", + 200: "200 Precursor Orbs", + 250: "250 Precursor Orbs", + 400: "400 Precursor Orbs", + 500: "500 Precursor Orbs", + 1000: "1000 Precursor Orbs", + 2000: "2000 Precursor Orbs", } # These are special items representing unique unlocks in the world. Notice that their Item ID equals their diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 733539fd9c65..7d3339472c5f 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -32,10 +32,11 @@ def launch_client(): class JakAndDaxterSettings(settings.Group): class RootDirectory(settings.UserFolderPath): - """Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe).""" + """Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). + Ensure this path contains forward slashes (/) only.""" description = "ArchipelaGOAL Root Directory" - root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL/out/build/Release/bin") + root_directory: RootDirectory = RootDirectory("%appdata%/OpenGOAL-Mods/archipelagoal") class JakAndDaxterWebWorld(WebWorld): @@ -89,6 +90,20 @@ class JakAndDaxterWorld(World): "Precursor Orbs": {item_table[k]: k for k in item_table if k in range(jak1_id + Orbs.orb_offset, jak1_max)}, } + location_name_groups = { + "Power Cells": {location_table[k]: k for k in location_table + if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, + "Scout Flies": {location_table[k]: k for k in location_table + if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)}, + "Specials": {location_table[k]: k for k in location_table + if k in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset)}, + "Orb Caches": {location_table[k]: k for k in location_table + if k in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset)}, + "Precursor Orbs": {location_table[k]: k for k in location_table + if k in range(jak1_id + Orbs.orb_offset, jak1_max)}, + "Trades": {location_table[k]: k for k in location_table + if k in {Cells.to_ap_id(t) for t in {11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}}}, + } # This will also set Locations, Location access rules, Region access rules, etc. def create_regions(self) -> None: diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 6ccc0279d16f..6933d2b51bd7 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -26,8 +26,8 @@ At this time, this method of setup works on Windows only, but Linux support is a - Click `View Folder`. - In the new file explorer window, take note of the current path. It should contain `gk.exe` and `goalc.exe`. - Verify that the mod launcher copied the extracted ISO files to the mod directory: - - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\archipelagoal\iso_data` should have *all* the same files as - - `C:\Users\\AppData\Roaming\OpenGOAL-Mods\_iso_data`, if it doesn't, copy those files over manually. + - `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` should have *all* the same files as + - `%appdata%/OpenGOAL-Mods/_iso_data`, if it doesn't, copy those files over manually. - And then `Recompile` if you needed to copy the files over. - **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). @@ -41,15 +41,15 @@ At this time, this method of setup works on Windows only, but Linux support is a - In the text file that opens, enter the name you want and remember it for later. - Save this file in `Archipelago/players`. You can now close the file. - Back in the Archipelago Launcher, click `Open host.yaml`. -- In the text file that opens, search for `jakanddaxter_options`. If you do not see it, you will need it add it. - - Change (or add) the `root_directory` entry and provide the path you noted earlier containing `gk.exe` and `goalc.exe`. - - **CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/ `.** - - The result should look like this. You can now save and close the file. +- In the text file that opens, search for `jakanddaxter_options`. + - You should see the block of YAML below. If you do not see it, you will need to add it. + - If the default path does not contain `gk.exe` and `goalc.exe`, you will need to provide the path you noted earlier. **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/ `.** ``` jakanddaxter_options: # Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). - root_directory: "C:/Users//AppData/Roaming/OpenGOAL-Mods/archipelagoal" + # Ensure this path contains forward slashes (/) only. + root_directory: "%appdata%/OpenGOAL-Mods/archipelagoal" ``` - Back in the Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. @@ -108,8 +108,8 @@ jakanddaxter_options: Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. ``` - If this occurs, you may need to copy the extracted data to the mod folder manually. - - From a location like this: `C:\Users\\AppData\Roaming\OpenGOAL-Mods\_iso_data` - - To a location like this: `C:\Users\\AppData\Roaming\OpenGOAL-Mods\archipelagoal\iso_data` + - From a location like this: `%appdata%/OpenGOAL-Mods/_iso_data` + - To a location like this: `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` - Then try clicking `Recompile` in the Mod Launcher (ensure you have selected the right mod first!) ***Game Failure*** @@ -136,10 +136,10 @@ PAL versions of the game seem to require additional troubleshooting/setup in ord - If you have `-- Compilation Error! --` after pressing `Recompile` or Launching the ArchipelaGOAL mod. Try this: - Remove these folders if you have them: - - `%appdata%\OpenGOAL-Mods\iso_data` - - `%appdata%\OpenGOAL-Mods\archipelagoal\iso_data` - - `%appdata%\OpenGOAL-Mods\archipelagoal\data\iso_data` - - Place Jak1 ISO in: `%appdata%\OpenGOAL-Mods\archipelagoal` rename it to `JakAndDaxter.iso` + - `%appdata%/OpenGOAL-Mods/iso_data` + - `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` + - `%appdata%/OpenGOAL-Mods/archipelagoal/data/iso_data` + - Place Jak1 ISO in: `%appdata%/OpenGOAL-Mods/archipelagoal` rename it to `JakAndDaxter.iso` - Type "CMD" in Windows search, Right click Command Prompt, and pick "Run as Administrator" - Run: `cd %appdata%\OpenGOAL-Mods\archipelagoal` - Then run: `extractor.exe --extract --extract-path .\data\iso_data "JakAndDaxter.iso"` From 17b484580e343c2285b10561a267d45f93a24a14 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:15:01 -0400 Subject: [PATCH 44/70] Make orb trade amounts configurable, make orbsanity defaults more reasonable. --- worlds/jakanddaxter/Client.py | 2 + worlds/jakanddaxter/JakAndDaxterOptions.py | 38 +++++++++++++--- worlds/jakanddaxter/Regions.py | 26 +++++++++++ worlds/jakanddaxter/__init__.py | 2 + worlds/jakanddaxter/client/MemoryReader.py | 28 ++++++++---- worlds/jakanddaxter/client/ReplClient.py | 9 ++-- .../en_Jak and Daxter The Precursor Legacy.md | 44 ++++++++++--------- worlds/jakanddaxter/regs/RegionBase.py | 2 +- .../jakanddaxter/regs/RockVillageRegions.py | 12 ++--- .../regs/SandoverVillageRegions.py | 10 +++-- .../regs/VolcanicCraterRegions.py | 14 +++--- 11 files changed, 132 insertions(+), 55 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 23cf1a4cbd3b..ef69fddef3a9 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -130,6 +130,8 @@ def on_package(self, cmd: str, args: dict): slot_data["fire_canyon_cell_count"], slot_data["mountain_pass_cell_count"], slot_data["lava_tube_cell_count"], + slot_data["citizen_orb_trade_amount"], + slot_data["oracle_orb_trade_amount"], goal_id)) # Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 57f6c1ca7481..12b7ebc1f748 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -3,8 +3,8 @@ class EnableMoveRandomizer(Toggle): - """Enable to include movement options as items in the randomizer. Jak is only able to run, swim, and single jump - until you find his other moves. + """Enable to include movement options as items in the randomizer. Until you find his other moves, Jak is limited to + running, swimming, single-jumping, and shooting yellow eco through his goggles. This adds 11 items to the pool.""" display_name = "Enable Move Randomizer" @@ -51,7 +51,7 @@ class GlobalOrbsanityBundleSize(Choice): option_500_orbs = 500 option_1000_orbs = 1000 option_2000_orbs = 2000 - default = 1 + default = 20 class PerLevelOrbsanityBundleSize(Choice): @@ -64,11 +64,11 @@ class PerLevelOrbsanityBundleSize(Choice): option_10_orbs = 10 option_25_orbs = 25 option_50_orbs = 50 - default = 1 + default = 25 class FireCanyonCellCount(Range): - """Set the number of orbs you need to cross Fire Canyon.""" + """Set the number of power cells you need to cross Fire Canyon.""" display_name = "Fire Canyon Cell Count" range_start = 0 range_end = 100 @@ -76,7 +76,7 @@ class FireCanyonCellCount(Range): class MountainPassCellCount(Range): - """Set the number of orbs you need to reach Klaww and cross Mountain Pass.""" + """Set the number of power cells you need to reach Klaww and cross Mountain Pass.""" display_name = "Mountain Pass Cell Count" range_start = 0 range_end = 100 @@ -84,13 +84,35 @@ class MountainPassCellCount(Range): class LavaTubeCellCount(Range): - """Set the number of orbs you need to cross Lava Tube.""" + """Set the number of power cells you need to cross Lava Tube.""" display_name = "Lava Tube Cell Count" range_start = 0 range_end = 100 default = 72 +# 222 is the maximum because there are 9 citizen trades and 2000 orbs to trade (2000/9 = 222). +class CitizenOrbTradeAmount(Range): + """Set the number of orbs you need to trade to ordinary citizens for a power cell (Mayor, Uncle, etc.). + + Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).""" + display_name = "Citizen Orb Trade Amount" + range_start = 0 + range_end = 222 + default = 90 + + +# 333 is the maximum because there are 6 oracle trades and 2000 orbs to trade (2000/6 = 333). +class OracleOrbTradeAmount(Range): + """Set the number of orbs you need to trade to the Oracles for a power cell. + + Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).""" + display_name = "Oracle Orb Trade Amount" + range_start = 0 + range_end = 333 + default = 120 + + class CompletionCondition(Choice): """Set the goal for completing the game.""" display_name = "Completion Condition" @@ -113,5 +135,7 @@ class JakAndDaxterOptions(PerGameCommonOptions): fire_canyon_cell_count: FireCanyonCellCount mountain_pass_cell_count: MountainPassCellCount lava_tube_cell_count: LavaTubeCellCount + citizen_orb_trade_amount: CitizenOrbTradeAmount + oracle_orb_trade_amount: OracleOrbTradeAmount jak_completion_condition: CompletionCondition start_inventory_from_pool: StartInventoryPool diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 6d016bad2ce3..24e7dbccf83e 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -133,6 +133,9 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: # the connector levels. E.g. if you set Fire Canyon count to 99, we may not have 99 Locations in hub 1. verify_connector_level_accessibility(multiworld, options, player) + # Also verify that we didn't overload the trade amounts with more orbs than exist in the world. + verify_orbs_for_trades(multiworld, options, player) + def verify_connector_level_accessibility(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): @@ -182,3 +185,26 @@ def verify_connector_level_accessibility(multiworld: MultiWorld, options: JakAnd for item in required_items: state.collect(JakAndDaxterItem(required_items[item], ItemClassification.progression, item, player)) + + +def verify_orbs_for_trades(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): + + citizen_trade_orbs = 9 * options.citizen_orb_trade_amount + if citizen_trade_orbs > 2000: + raise OptionError(f"Settings conflict with {options.citizen_orb_trade_amount.display_name}: " + f"required number of orbs to trade with citizens ({citizen_trade_orbs}) " + f"is more than all the orbs in the game (2000).") + + oracle_trade_orbs = 6 * options.oracle_orb_trade_amount + if oracle_trade_orbs > 2000: + raise OptionError(f"Settings conflict with {options.oracle_orb_trade_amount.display_name}: " + f"required number of orbs to trade with oracles ({oracle_trade_orbs}) " + f"is more than all the orbs in the game (2000).") + + total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) + if total_trade_orbs > 2000: + raise OptionError(f"Settings conflict with Orb Trade Amounts: " + f"required number of orbs for all trades ({total_trade_orbs}) " + f"is more than all the orbs in the game (2000). " + f"Reduce the value of either {options.citizen_orb_trade_amount.display_name} " + f"or {options.oracle_orb_trade_amount.display_name}.") diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 7d3339472c5f..bbd2f3118e91 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -200,4 +200,6 @@ def fill_slot_data(self) -> Dict[str, Any]: "fire_canyon_cell_count", "mountain_pass_cell_count", "lava_tube_cell_count", + "citizen_orb_trade_amount", + "oracle_orb_trade_amount", "jak_completion_condition") diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 049fe1eb7caa..e9f3bfe182c2 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,4 +1,5 @@ import random +import struct from typing import ByteString, List, Callable import json import pymem @@ -76,6 +77,8 @@ def define(self, size: int, length: int = 1) -> int: fire_canyon_unlock_offset = offsets.define(sizeof_float) mountain_pass_unlock_offset = offsets.define(sizeof_float) lava_tube_unlock_offset = offsets.define(sizeof_float) +citizen_orb_amount_offset = offsets.define(sizeof_float) +oracle_orb_amount_offset = offsets.define(sizeof_float) completion_goal_offset = offsets.define(sizeof_uint8) completed_offset = offsets.define(sizeof_uint8) @@ -89,6 +92,11 @@ def define(self, size: int, length: int = 1) -> int: end_marker_offset = offsets.define(sizeof_uint8, 4) +# Can't believe this is easier to do in GOAL than Python but that's how it be sometimes. +def as_float(value: int) -> int: + return int(struct.unpack('f', value.to_bytes(sizeof_float, "little"))[0]) + + # "Jak" to be replaced by player name in the Client. def autopsy(died: int) -> str: assert died > 0, f"Tried to find Jak's cause of death, but he's still alive!" @@ -239,10 +247,11 @@ def print_status(self): def read_memory(self) -> List[int]: try: - next_cell_index = self.read_goal_address(0, sizeof_uint64) - next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64) - next_special_index = self.read_goal_address(next_special_index_offset, sizeof_uint64) + # Need to grab these first and convert to floats, see below. + citizen_orb_amount = self.read_goal_address(citizen_orb_amount_offset, sizeof_float) + oracle_orb_amount = self.read_goal_address(oracle_orb_amount_offset, sizeof_float) + next_cell_index = self.read_goal_address(next_cell_index_offset, sizeof_uint64) for k in range(0, next_cell_index): next_cell = self.read_goal_address(cells_checked_offset + (k * sizeof_uint32), sizeof_uint32) cell_ap_id = Cells.to_ap_id(next_cell) @@ -253,13 +262,16 @@ def read_memory(self) -> List[int]: # If orbsanity is ON and next_cell is one of the traders or oracles, then run a callback # to add their amount to the DataStorage value holding our current orb trade total. if next_cell in {11, 12, 31, 32, 33, 96, 97, 98, 99}: - self.orbs_paid += 90 - logger.debug("Traded 90 orbs!") + citizen_orb_amount = as_float(citizen_orb_amount) + self.orbs_paid += citizen_orb_amount + logger.debug(f"Traded {citizen_orb_amount} orbs!") if next_cell in {13, 14, 34, 35, 100, 101}: - self.orbs_paid += 120 - logger.debug("Traded 120 orbs!") + oracle_orb_amount = as_float(oracle_orb_amount) + self.orbs_paid += oracle_orb_amount + logger.debug(f"Traded {oracle_orb_amount} orbs!") + next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64) for k in range(0, next_buzzer_index): next_buzzer = self.read_goal_address(buzzers_checked_offset + (k * sizeof_uint32), sizeof_uint32) buzzer_ap_id = Flies.to_ap_id(next_buzzer) @@ -267,6 +279,7 @@ def read_memory(self) -> List[int]: self.location_outbox.append(buzzer_ap_id) logger.debug("Checked scout fly: " + str(next_buzzer)) + next_special_index = self.read_goal_address(next_special_index_offset, sizeof_uint64) for k in range(0, next_special_index): next_special = self.read_goal_address(specials_checked_offset + (k * sizeof_uint32), sizeof_uint32) special_ap_id = Specials.to_ap_id(next_special) @@ -284,7 +297,6 @@ def read_memory(self) -> List[int]: self.deathlink_enabled = bool(deathlink_flag) next_cache_index = self.read_goal_address(next_orb_cache_index_offset, sizeof_uint64) - for k in range(0, next_cache_index): next_cache = self.read_goal_address(orb_caches_checked_offset + (k * sizeof_uint32), sizeof_uint32) cache_ap_id = Caches.to_ap_id(next_cache) diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 09b488d17486..eb551fe83a34 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -356,15 +356,18 @@ async def subtract_traded_orbs(self, orb_count: int) -> bool: async def setup_options(self, os_option: int, os_bundle: int, fc_count: int, mp_count: int, - lt_count: int, goal_id: int) -> bool: + lt_count: int, ct_amount: int, + ot_amount: int, goal_id: int) -> bool: ok = await self.send_form(f"(ap-setup-options! " f"(the uint {os_option}) (the uint {os_bundle}) " f"(the float {fc_count}) (the float {mp_count}) " - f"(the float {lt_count}) (the uint {goal_id}))") + f"(the float {lt_count}) (the float {ct_amount}) " + f"(the float {ot_amount}) (the uint {goal_id}))") message = (f"Setting options: \n" f" Orbsanity Option {os_option}, Orbsanity Bundle {os_bundle}, \n" f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n" - f" LT Cell Count {lt_count}, Completion GOAL {goal_id}... ") + f" LT Cell Count {lt_count}, Citizen Orb Amt {ct_amount}, \n" + f" Oracle Orb Amt {ot_amount}, Completion GOAL {goal_id}... ") if ok: logger.debug(message + "Success!") else: diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index 19597bda31a6..d26e38042744 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -7,9 +7,9 @@ all the options you need to configure and export a config file. At this time, there are several caveats and restrictions: - Power Cells and Scout Flies are **always** randomized. -- Precursor Orbs are **never** randomized. -- **All** of the traders in the game become in-logic checks **if and only if** you have enough Orbs (1530) to pay them all at once. - - This is to prevent hard locks, where an item required for progression is locked behind a trade you can't afford. +- **All** the traders in the game become in-logic checks **if and only if** you have enough Orbs to pay all of them at once. + - This is to prevent hard locks, where an item required for progression is locked behind a trade you can't afford because you spent the orbs elsewhere. + - By default, that total is 1530. ## What does randomization do to this game? The game now contains the following Location checks: @@ -56,10 +56,14 @@ This will show you a list of all the special items in the game, ones not normall Gray items indicate you do not possess that item, light blue items indicate you possess that item. ## What is the goal of the game once randomized? -To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. +By default, to complete the game you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. In order +to reach them, you will need at least 72 Power Cells to cross the Lava Tube, as well as the four special items for +freeing the Red, Blue, Yellow, and Green Sages. -In order to reach them, you will need at least 72 Power Cells to cross the Lava Tube. In addition, -you will need the four special items that free the Red, Blue, Yellow, and Green Sages. +Alternatively, you can choose from a handful of other completion conditions like defeating a particular boss, crossing +a particular connector level, or opening the 100 Power Cell door after defeating the final boss. You can also customize +the thresholds for connector levels and orb trades. These options allow you to tailor the expected length and difficulty +of your run as you see fit. ## What happens when I pick up or receive a power cell? When you pick up a power cell, Jak and Daxter will perform their victory animation. Your power cell count will @@ -85,23 +89,23 @@ scout fly. So in short: - When you _pick up_ your 7th fly, the normal rules apply. - When you _receive_ your 7th fly, 2 things will happen in quick succession. - - First, you will receive that scout fly, as normal. - - Second, you will immediately complete the "Free 7 Scout Flies" check, which will send out another item. + - First, you will receive that scout fly, as normal. + - Second, you will immediately complete the "Free 7 Scout Flies" check, which will send out another item. ## What does Deathlink do? If you enable Deathlink, all the other players in your Multiworld who also have it enabled will be linked on death. That means when Jak dies in your game, the players in your Deathlink group also die. Likewise, if any of the other -players die, Jak will also die in a random fashion. +players die, Jak will also die in a random, possibly spectacular fashion. -You can turn off Deathlink at any time in the game by opening the game's menu, navigate to `Options`, +You can turn off Deathlink at any time in the game by opening the game's menu and navigating to `Options`, then `Archipelago Options`, then `Deathlink`. ## What does Move Randomizer do? If you enable Move Randomizer, most of Jak's movement set will be added to the randomized item pool, and you will need to receive the move in order to use it (i.e. you must find it, or another player must send it to you). Some moves have prerequisite moves that you must also have in order to use them (e.g. Crouch Jump is dependent on Crouch). Jak will only -be able to run, swim (including underwater), and perform single jumps. Note that Flut Flut will have access to her full -movement set at all times. +be able to run, swim (including underwater), perform single jumps, and shoot yellow eco from his goggles ("firing from +the hip" requires Punch). Note that Flut Flut and the Zoomer will have access to their full movement sets at all times. You can turn off Move Rando at any time in the game by opening the game's menu, navigate to `Options`, then `Archipelago Options`, then `Move Randomizer`. This will give you access to the full movement set again. @@ -134,9 +138,9 @@ a "bundle" of the correct number of orbs, you will trigger the next release in t will be added to the item pool to be randomized. There are several options to change the difficulty of this challenge. - "Per Level" Orbsanity means the lists of orb checks are generated and populated for each level in the game. - - (Geyser Rock, Sandover Village, etc.) + - (Geyser Rock, Sandover Village, etc.) - "Global" Orbsanity means there is only one list of checks for the entire game. - - It does not matter where you pick up the orbs, they all count toward the same list. + - It does not matter where you pick up the orbs, they all count toward the same list. - The options with "Bundle Size" in the name indicate how many orbs are in a "bundle." This adds a number of Items and Locations to the pool inversely proportional to the size of the bundle. - For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs, @@ -161,20 +165,18 @@ to the nearest sage's hut to continue your journey. Depending on the nature of the bug, there are a couple of different options. * If you found a logical error in the randomizer, please create a new Issue -[here.](https://github.com/ArchipelaGOAL/Archipelago/issues) - * Use this page if: +[here.](https://github.com/ArchipelaGOAL/Archipelago/issues) Use this page if: * An item required for progression is unreachable. * The randomizer did not respect one of the Options you chose. * You see a mistake, typo, etc. on this webpage. * You see an error or stack trace appear on the text client. - * Please upload your config file and spoiler log file in the Issue, so we can troubleshoot the problem. * If you encountered an error in OpenGOAL, please create a new Issue -[here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues) - * Use this page if: - * You encounter a crash, freeze, reset, etc in the game. +[here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues) Use this page if: + * You encounter a crash, freeze, reset, etc. in the game. * You fail to send Items you find in the game to the Archipelago server. * You fail to receive Items the server sends to you. * Your game disconnects from the server and cannot reconnect. * You go looking for a game item that has already disappeared before you could reach it. - * Please upload any log files that may have been generated. \ No newline at end of file + +* Please upload your config file, spoiler log file, and any other generated logs in the Issue, so we can troubleshoot the problem. \ No newline at end of file diff --git a/worlds/jakanddaxter/regs/RegionBase.py b/worlds/jakanddaxter/regs/RegionBase.py index 42534e462a8e..6629991bc881 100644 --- a/worlds/jakanddaxter/regs/RegionBase.py +++ b/worlds/jakanddaxter/regs/RegionBase.py @@ -14,7 +14,7 @@ class JakAndDaxterRegion(Region): """ Holds region information such as name, level name, number of orbs available, etc. We especially need orb counts to be tracked because we need to know when you can - afford the 90-orb and 120-orb payments for more checks. + afford the Citizen and Oracle orb payments for more checks. """ game: str = jak1_name level_name: str diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index f59e63101a09..353a9ba412d9 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -7,18 +7,20 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: + total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) + # This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) main_area.add_cell_locations([31], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) main_area.add_cell_locations([32], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) main_area.add_cell_locations([33], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) main_area.add_cell_locations([34], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) main_area.add_cell_locations([35], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530, 34)) + can_trade(state, player, multiworld, options, total_trade_orbs, 34)) # These 2 scout fly boxes can be broken by running with nearby blue eco. main_area.add_fly_locations([196684, 262220]) diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index f4ee264a7422..c05321fb4ccd 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -7,14 +7,16 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: + total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) + main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 26) # Yakows requires no combat. main_area.add_cell_locations([10]) main_area.add_cell_locations([11], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) main_area.add_cell_locations([12], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) # These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach. main_area.add_fly_locations([262219, 327755, 131147, 65611]) @@ -33,9 +35,9 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6) oracle_platforms.add_cell_locations([13], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) oracle_platforms.add_cell_locations([14], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530, 13)) + can_trade(state, player, multiworld, options, total_trade_orbs, 13)) oracle_platforms.add_fly_locations([393291], access_rule=lambda state: can_free_scout_flies(state, player)) diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py index 875f1c2cbc8e..e25b6de3ec22 100644 --- a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -8,20 +8,22 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: + total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) + # No area is inaccessible in VC even with only running and jumping. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) main_area.add_cell_locations([96], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) main_area.add_cell_locations([97], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530, 96)) + can_trade(state, player, multiworld, options, total_trade_orbs, 96)) main_area.add_cell_locations([98], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530, 97)) + can_trade(state, player, multiworld, options, total_trade_orbs, 97)) main_area.add_cell_locations([99], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530, 98)) + can_trade(state, player, multiworld, options, total_trade_orbs, 98)) main_area.add_cell_locations([100], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530)) + can_trade(state, player, multiworld, options, total_trade_orbs)) main_area.add_cell_locations([101], access_rule=lambda state: - can_trade(state, player, multiworld, options, 1530, 100)) + can_trade(state, player, multiworld, options, total_trade_orbs, 100)) # Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping # and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode). From b7ca9cbc2f567278fd9abb9ccedd7231e0bddbee Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:04:29 -0400 Subject: [PATCH 45/70] Add HUD info to doc. --- .../en_Jak and Daxter The Precursor Legacy.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index d26e38042744..422276cdfcf5 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -161,6 +161,21 @@ Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back to the nearest sage's hut to continue your journey. +## How does the HUD work? What are the alternate modes? (I didn't know there were alternate modes!) +The game's normal HUD shows you how many power cells, precursor orbs, and scout flies you currently have. But if you +hold `L2 or R2` and press `Up or Down` on the D-Pad, the HUD will show you alternate modes. In all modes, the last +sent/received item and the player who sent/received it will be displayed below the Power Cell icon. This will help you +quickly reference information about which locations you've checked, newly received items and who to thank for them, etc. +Here is how the HUD works: + +| HUD Mode | Button Combo | What You're Seeing | Text Message | +|---------------|------------------------------|-----------------------------------|---------------------------------------| +| Normal | `L2 or R2` + `Left or Right` | Items Received | `GOT {Your Item} FROM {Other Player}` | +| Per-Level | `L2 or R2` + `Down` | Locations Checked (in this level) | `SENT {Other Item} TO {Other Player}` | +| Global | `L2 or R2` + `Up` | Locations Checked (in the game) | `SENT {Other Item} TO {Other Player}` | +| | | | | +| (In Any Mode) | | (If you sent an Item to Yourself) | `FOUND {Your Item}` | + ## I think I found a bug, where should I report it? Depending on the nature of the bug, there are a couple of different options. From 746b281f4810093792191e7be297827eb8f5cd01 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:56:11 -0400 Subject: [PATCH 46/70] Exempt's Code Review Updates (#43) * Round 1 of code review updates, the easy stuff. * Factor options checking away from region/rule creation. * Code review updates round 2, more complex stuff. * Code review updates round 3: the mental health annihilator * Code review updates part 4: redemption. * More code review feedback, simplifying code, etc. --- worlds/jakanddaxter/Client.py | 23 +- worlds/jakanddaxter/JakAndDaxterOptions.py | 13 +- worlds/jakanddaxter/Levels.py | 76 +++++++ worlds/jakanddaxter/Locations.py | 27 ++- worlds/jakanddaxter/Regions.py | 148 +++---------- worlds/jakanddaxter/Rules.py | 197 ++++++++++++------ worlds/jakanddaxter/__init__.py | 150 +++++++++---- worlds/jakanddaxter/client/MemoryReader.py | 5 +- worlds/jakanddaxter/client/ReplClient.py | 12 +- .../en_Jak and Daxter The Precursor Legacy.md | 4 +- worlds/jakanddaxter/docs/setup_en.md | 8 +- worlds/jakanddaxter/locs/CellLocations.py | 6 +- worlds/jakanddaxter/locs/OrbCacheLocations.py | 6 +- worlds/jakanddaxter/locs/OrbLocations.py | 86 +------- worlds/jakanddaxter/locs/ScoutLocations.py | 11 +- worlds/jakanddaxter/locs/SpecialLocations.py | 6 +- worlds/jakanddaxter/regs/BoggySwampRegions.py | 21 +- worlds/jakanddaxter/regs/FireCanyonRegions.py | 20 +- .../regs/ForbiddenJungleRegions.py | 20 +- worlds/jakanddaxter/regs/GeyserRockRegions.py | 20 +- .../regs/GolAndMaiasCitadelRegions.py | 24 +-- worlds/jakanddaxter/regs/LavaTubeRegions.py | 20 +- .../regs/LostPrecursorCityRegions.py | 20 +- .../jakanddaxter/regs/MistyIslandRegions.py | 22 +- .../jakanddaxter/regs/MountainPassRegions.py | 20 +- .../regs/PrecursorBasinRegions.py | 20 +- worlds/jakanddaxter/regs/RegionBase.py | 15 +- .../jakanddaxter/regs/RockVillageRegions.py | 35 ++-- .../regs/SandoverVillageRegions.py | 32 ++- .../jakanddaxter/regs/SentinelBeachRegions.py | 20 +- .../jakanddaxter/regs/SnowyMountainRegions.py | 24 +-- worlds/jakanddaxter/regs/SpiderCaveRegions.py | 20 +- .../regs/VolcanicCraterRegions.py | 40 ++-- worlds/jakanddaxter/test/test_locations.py | 7 +- 34 files changed, 628 insertions(+), 550 deletions(-) create mode 100644 worlds/jakanddaxter/Levels.py diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index ef69fddef3a9..2c8c9b9962ac 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,14 +1,17 @@ -import logging import os import subprocess -import typing -import asyncio import colorama + +import asyncio +from asyncio import Task + +from typing import Set, Awaitable, Optional, List + import pymem -from pymem.exception import ProcessNotFound, ProcessError +from pymem.exception import ProcessNotFound import Utils -from NetUtils import ClientStatus, NetworkItem +from NetUtils import ClientStatus from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled from .JakAndDaxterOptions import EnableOrbsanity @@ -20,10 +23,10 @@ ModuleUpdate.update() -all_tasks = set() +all_tasks: Set[Task] = set() -def create_task_log_exception(awaitable: typing.Awaitable) -> asyncio.Task: +def create_task_log_exception(awaitable: Awaitable) -> asyncio.Task: async def _log_exception(a): try: return await a @@ -81,7 +84,7 @@ class JakAndDaxterContext(CommonContext): repl_task: asyncio.Task memr_task: asyncio.Task - def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: + def __init__(self, server_address: Optional[str], password: Optional[str]) -> None: self.repl = JakAndDaxterReplClient() self.memr = JakAndDaxterMemoryReader() # self.repl.load_data() @@ -195,11 +198,11 @@ def on_deathlink(self, data: dict): self.repl.received_deathlink = True super().on_deathlink(data) - async def ap_inform_location_check(self, location_ids: typing.List[int]): + async def ap_inform_location_check(self, location_ids: List[int]): message = [{"cmd": "LocationChecks", "locations": location_ids}] await self.send_msgs(message) - def on_location_check(self, location_ids: typing.List[int]): + def on_location_check(self, location_ids: List[int]): create_task_log_exception(self.ap_inform_location_check(location_ids)) async def ap_inform_finished_game(self): diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 12b7ebc1f748..b1af9f788f91 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -51,6 +51,7 @@ class GlobalOrbsanityBundleSize(Choice): option_500_orbs = 500 option_1000_orbs = 1000 option_2000_orbs = 2000 + friendly_minimum = 5 default = 20 @@ -64,6 +65,7 @@ class PerLevelOrbsanityBundleSize(Choice): option_10_orbs = 10 option_25_orbs = 25 option_50_orbs = 50 + friendly_minimum = 5 default = 25 @@ -72,6 +74,7 @@ class FireCanyonCellCount(Range): display_name = "Fire Canyon Cell Count" range_start = 0 range_end = 100 + friendly_maximum = 30 default = 20 @@ -80,6 +83,7 @@ class MountainPassCellCount(Range): display_name = "Mountain Pass Cell Count" range_start = 0 range_end = 100 + friendly_maximum = 60 default = 45 @@ -88,6 +92,7 @@ class LavaTubeCellCount(Range): display_name = "Lava Tube Cell Count" range_start = 0 range_end = 100 + friendly_maximum = 90 default = 72 @@ -95,10 +100,12 @@ class LavaTubeCellCount(Range): class CitizenOrbTradeAmount(Range): """Set the number of orbs you need to trade to ordinary citizens for a power cell (Mayor, Uncle, etc.). - Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).""" + Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000). + The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).""" display_name = "Citizen Orb Trade Amount" range_start = 0 range_end = 222 + friendly_maximum = 120 default = 90 @@ -106,10 +113,12 @@ class CitizenOrbTradeAmount(Range): class OracleOrbTradeAmount(Range): """Set the number of orbs you need to trade to the Oracles for a power cell. - Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).""" + Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000). + The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).""" display_name = "Oracle Orb Trade Amount" range_start = 0 range_end = 333 + friendly_maximum = 150 default = 120 diff --git a/worlds/jakanddaxter/Levels.py b/worlds/jakanddaxter/Levels.py new file mode 100644 index 000000000000..2deb2d20879d --- /dev/null +++ b/worlds/jakanddaxter/Levels.py @@ -0,0 +1,76 @@ +# This contains the list of levels in Jak and Daxter. +# Not to be confused with Regions - there can be multiple Regions in every Level. +level_table = { + "Geyser Rock": { + "level_index": 0, + "orbs": 50 + }, + "Sandover Village": { + "level_index": 1, + "orbs": 50 + }, + "Sentinel Beach": { + "level_index": 2, + "orbs": 150 + }, + "Forbidden Jungle": { + "level_index": 3, + "orbs": 150 + }, + "Misty Island": { + "level_index": 4, + "orbs": 150 + }, + "Fire Canyon": { + "level_index": 5, + "orbs": 50 + }, + "Rock Village": { + "level_index": 6, + "orbs": 50 + }, + "Lost Precursor City": { + "level_index": 7, + "orbs": 200 + }, + "Boggy Swamp": { + "level_index": 8, + "orbs": 200 + }, + "Precursor Basin": { + "level_index": 9, + "orbs": 200 + }, + "Mountain Pass": { + "level_index": 10, + "orbs": 50 + }, + "Volcanic Crater": { + "level_index": 11, + "orbs": 50 + }, + "Snowy Mountain": { + "level_index": 12, + "orbs": 200 + }, + "Spider Cave": { + "level_index": 13, + "orbs": 200 + }, + "Lava Tube": { + "level_index": 14, + "orbs": 50 + }, + "Gol and Maia's Citadel": { + "level_index": 15, + "orbs": 200 + } +} + +level_table_with_global = { + **level_table, + "": { + "level_index": 16, # Global + "orbs": 2000 + } +} diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index ed0273060e25..d358ed4213c5 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -11,9 +11,9 @@ class JakAndDaxterLocation(Location): game: str = jak1_name -# All Locations +# Different tables for location groups. # Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed. -location_table = { +cell_location_table = { **{Cells.to_ap_id(k): Cells.loc7SF_cellTable[k] for k in Cells.loc7SF_cellTable}, **{Cells.to_ap_id(k): Cells.locGR_cellTable[k] for k in Cells.locGR_cellTable}, **{Cells.to_ap_id(k): Cells.locSV_cellTable[k] for k in Cells.locSV_cellTable}, @@ -30,7 +30,10 @@ class JakAndDaxterLocation(Location): **{Cells.to_ap_id(k): Cells.locSC_cellTable[k] for k in Cells.locSC_cellTable}, **{Cells.to_ap_id(k): Cells.locSM_cellTable[k] for k in Cells.locSM_cellTable}, **{Cells.to_ap_id(k): Cells.locLT_cellTable[k] for k in Cells.locLT_cellTable}, - **{Cells.to_ap_id(k): Cells.locGMC_cellTable[k] for k in Cells.locGMC_cellTable}, + **{Cells.to_ap_id(k): Cells.locGMC_cellTable[k] for k in Cells.locGMC_cellTable} +} + +scout_location_table = { **{Scouts.to_ap_id(k): Scouts.locGR_scoutTable[k] for k in Scouts.locGR_scoutTable}, **{Scouts.to_ap_id(k): Scouts.locSV_scoutTable[k] for k in Scouts.locSV_scoutTable}, **{Scouts.to_ap_id(k): Scouts.locFJ_scoutTable[k] for k in Scouts.locFJ_scoutTable}, @@ -46,8 +49,18 @@ class JakAndDaxterLocation(Location): **{Scouts.to_ap_id(k): Scouts.locSC_scoutTable[k] for k in Scouts.locSC_scoutTable}, **{Scouts.to_ap_id(k): Scouts.locSM_scoutTable[k] for k in Scouts.locSM_scoutTable}, **{Scouts.to_ap_id(k): Scouts.locLT_scoutTable[k] for k in Scouts.locLT_scoutTable}, - **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable}, - **{Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable}, - **{Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable}, - **{Orbs.to_ap_id(k): Orbs.loc_orbBundleTable[k] for k in Orbs.loc_orbBundleTable} + **{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable} +} + +special_location_table = {Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable} +cache_location_table = {Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable} +orb_location_table = {Orbs.to_ap_id(k): Orbs.loc_orbBundleTable[k] for k in Orbs.loc_orbBundleTable} + +# All Locations +location_table = { + **cell_location_table, + **scout_location_table, + **special_location_table, + **cache_location_table, + **orb_location_table } diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 24e7dbccf83e..a8940e0c9a87 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -1,16 +1,11 @@ -from BaseClasses import MultiWorld, CollectionState, ItemClassification +import typing + from Options import OptionError -from .JakAndDaxterOptions import (JakAndDaxterOptions, - EnableMoveRandomizer, - EnableOrbsanity, - CompletionCondition) -from .Items import (JakAndDaxterItem, - item_table, - move_item_table) -from .Rules import can_reach_orbs -from .locs import (CellLocations as Cells, - ScoutLocations as Scouts) -from .regs.RegionBase import JakAndDaxterRegion +from . import JakAndDaxterWorld +from .Items import item_table +from .JakAndDaxterOptions import EnableOrbsanity, CompletionCondition +from .Rules import can_reach_orbs_global +from .locs import CellLocations as Cells, ScoutLocations as Scouts from .regs import (GeyserRockRegions as GeyserRock, SandoverVillageRegions as SandoverVillage, ForbiddenJungleRegions as ForbiddenJungle, @@ -27,9 +22,13 @@ SnowyMountainRegions as SnowyMountain, LavaTubeRegions as LavaTube, GolAndMaiasCitadelRegions as GolAndMaiasCitadel) +from .regs.RegionBase import JakAndDaxterRegion -def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): +def create_regions(world: JakAndDaxterWorld): + multiworld = world.multiworld + options = world.options + player = world.player # Always start with Menu. menu = JakAndDaxterRegion("Menu", player, multiworld) @@ -42,7 +41,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: for scout_fly_cell in free7.locations: # Translate from Cell AP ID to Scout AP ID using game ID as an intermediary. - scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(scout_fly_cell.address)) + scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(typing.cast(int, scout_fly_cell.address))) scout_fly_cell.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7) multiworld.regions.append(free7) menu.connect(free7) @@ -52,37 +51,34 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: if options.enable_orbsanity == EnableOrbsanity.option_global: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld) - bundle_size = options.global_orbsanity_bundle_size.value - bundle_count = int(2000 / bundle_size) + bundle_count = 2000 // world.orb_bundle_size for bundle_index in range(bundle_count): # Unlike Per-Level Orbsanity, Global Orbsanity Locations always have a level_index of 16. orbs.add_orb_locations(16, bundle_index, - bundle_size, access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options) - >= (bundle_size * (bundle + 1))) + can_reach_orbs_global(state, player, world, bundle)) multiworld.regions.append(orbs) menu.connect(orbs) # Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules. - [gr] = GeyserRock.build_regions("Geyser Rock", multiworld, options, player) - [sv] = SandoverVillage.build_regions("Sandover Village", multiworld, options, player) - [fj, fjp] = ForbiddenJungle.build_regions("Forbidden Jungle", multiworld, options, player) - [sb] = SentinelBeach.build_regions("Sentinel Beach", multiworld, options, player) - [mi] = MistyIsland.build_regions("Misty Island", multiworld, options, player) - [fc] = FireCanyon.build_regions("Fire Canyon", multiworld, options, player) - [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", multiworld, options, player) - [pb] = PrecursorBasin.build_regions("Precursor Basin", multiworld, options, player) - [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", multiworld, options, player) - [bs] = BoggySwamp.build_regions("Boggy Swamp", multiworld, options, player) - [mp, mpr] = MountainPass.build_regions("Mountain Pass", multiworld, options, player) - [vc] = VolcanicCrater.build_regions("Volcanic Crater", multiworld, options, player) - [sc] = SpiderCave.build_regions("Spider Cave", multiworld, options, player) - [sm] = SnowyMountain.build_regions("Snowy Mountain", multiworld, options, player) - [lt] = LavaTube.build_regions("Lava Tube", multiworld, options, player) - [gmc, fb, fd] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", multiworld, options, player) + [gr] = GeyserRock.build_regions("Geyser Rock", world) + [sv] = SandoverVillage.build_regions("Sandover Village", world) + [fj, fjp] = ForbiddenJungle.build_regions("Forbidden Jungle", world) + [sb] = SentinelBeach.build_regions("Sentinel Beach", world) + [mi] = MistyIsland.build_regions("Misty Island", world) + [fc] = FireCanyon.build_regions("Fire Canyon", world) + [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", world) + [pb] = PrecursorBasin.build_regions("Precursor Basin", world) + [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", world) + [bs] = BoggySwamp.build_regions("Boggy Swamp", world) + [mp, mpr] = MountainPass.build_regions("Mountain Pass", world) + [vc] = VolcanicCrater.build_regions("Volcanic Crater", world) + [sc] = SpiderCave.build_regions("Spider Cave", world) + [sm] = SnowyMountain.build_regions("Snowy Mountain", world) + [lt] = LavaTube.build_regions("Lava Tube", world) + [gmc, fb, fd] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", world) # Configurable counts of cells for connector levels. fc_count = options.fire_canyon_cell_count.value @@ -129,82 +125,6 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: elif options.jak_completion_condition == CompletionCondition.option_open_100_cell_door: multiworld.completion_condition[player] = lambda state: state.can_reach(fd, "Region", player) - # As a final sanity check on these options, verify that we have enough locations to allow us to cross - # the connector levels. E.g. if you set Fire Canyon count to 99, we may not have 99 Locations in hub 1. - verify_connector_level_accessibility(multiworld, options, player) - - # Also verify that we didn't overload the trade amounts with more orbs than exist in the world. - verify_orbs_for_trades(multiworld, options, player) - - -def verify_connector_level_accessibility(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - - # Set up a state where we only have the items we need to progress, exactly when we need them, as well as - # any items we would have/get from our other options. The only variable we're actually testing here is the - # number of power cells we need. - state = CollectionState(multiworld) - if options.enable_move_randomizer == EnableMoveRandomizer.option_false: - for move in move_item_table: - state.collect(JakAndDaxterItem(move_item_table[move], ItemClassification.progression, move, player)) - - thresholds = { - 0: { - "option": options.fire_canyon_cell_count, - "required_items": {}, - }, - 1: { - "option": options.mountain_pass_cell_count, - "required_items": { - 33: "Warrior's Pontoons", - 10945: "Double Jump", - }, - }, - 2: { - "option": options.lava_tube_cell_count, - "required_items": {}, - }, - } - - loc = 0 - for k in thresholds: - option = thresholds[k]["option"] - required_items = thresholds[k]["required_items"] - - # Given our current state (starting with 0 Power Cells), determine if there are enough - # Locations to fill with the number of Power Cells needed for the next threshold. - locations_available = multiworld.get_reachable_locations(state, player) - if len(locations_available) < option.value: - raise OptionError(f"Settings conflict with {option.display_name}: " - f"not enough potential locations ({len(locations_available)}) " - f"for the required number of power cells ({option.value}).") - - # Once we've determined we can pass the current threshold, add what we need to reach the next one. - for _ in range(option.value): - state.collect(JakAndDaxterItem("Power Cell", ItemClassification.progression, loc, player)) - loc += 1 - - for item in required_items: - state.collect(JakAndDaxterItem(required_items[item], ItemClassification.progression, item, player)) - - -def verify_orbs_for_trades(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int): - - citizen_trade_orbs = 9 * options.citizen_orb_trade_amount - if citizen_trade_orbs > 2000: - raise OptionError(f"Settings conflict with {options.citizen_orb_trade_amount.display_name}: " - f"required number of orbs to trade with citizens ({citizen_trade_orbs}) " - f"is more than all the orbs in the game (2000).") - - oracle_trade_orbs = 6 * options.oracle_orb_trade_amount - if oracle_trade_orbs > 2000: - raise OptionError(f"Settings conflict with {options.oracle_orb_trade_amount.display_name}: " - f"required number of orbs to trade with oracles ({oracle_trade_orbs}) " - f"is more than all the orbs in the game (2000).") - - total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) - if total_trade_orbs > 2000: - raise OptionError(f"Settings conflict with Orb Trade Amounts: " - f"required number of orbs for all trades ({total_trade_orbs}) " - f"is more than all the orbs in the game (2000). " - f"Reduce the value of either {options.citizen_orb_trade_amount.display_name} " - f"or {options.oracle_orb_trade_amount.display_name}.") + else: + raise OptionError(f"Unknown completion goal ID ({options.jak_completion_condition.value}).") + diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index f165bb406d84..4acd02fdb5db 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -1,102 +1,117 @@ -import math import typing from BaseClasses import MultiWorld, CollectionState -from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity -from .Items import orb_item_table +from Options import OptionError +from . import JakAndDaxterWorld +from .JakAndDaxterOptions import (JakAndDaxterOptions, + EnableOrbsanity, + GlobalOrbsanityBundleSize, + PerLevelOrbsanityBundleSize, + FireCanyonCellCount, + MountainPassCellCount, + LavaTubeCellCount, + CitizenOrbTradeAmount, + OracleOrbTradeAmount) from .locs import CellLocations as Cells from .Locations import location_table +from .Levels import level_table from .regs.RegionBase import JakAndDaxterRegion -def can_reach_orbs(state: CollectionState, - player: int, - multiworld: MultiWorld, - options: JakAndDaxterOptions, - level_name: str = None) -> int: +def set_orb_trade_rule(world: JakAndDaxterWorld): + options = world.options + player = world.player - # Global Orbsanity and No Orbsanity both treat orbs as completely interchangeable. - # Per Level Orbsanity needs to know if you can reach orbs *in a particular level.* - if options.enable_orbsanity != EnableOrbsanity.option_per_level: - return can_reach_orbs_global(state, player, multiworld) + if options.enable_orbsanity == EnableOrbsanity.option_off: + world.can_trade = lambda state, required_orbs, required_previous_trade: ( + can_trade_vanilla(state, player, required_orbs, required_previous_trade)) else: - return can_reach_orbs_level(state, player, multiworld, level_name) + world.can_trade = lambda state, required_orbs, required_previous_trade: ( + can_trade_orbsanity(state, player, required_orbs, required_previous_trade)) -def can_reach_orbs_global(state: CollectionState, - player: int, - multiworld: MultiWorld) -> int: +def recalculate_reachable_orbs(state: CollectionState, player: int) -> None: + + if not state.prog_items[player]["Reachable Orbs Fresh"]: + + # Recalculate every level, every time the cache is stale, because you don't know + # when a specific bundle of orbs in one level may unlock access to another. + for level in level_table: + state.prog_items[player][f"{level} Reachable Orbs".strip()] = ( + count_reachable_orbs_level(state, player, state.multiworld, level)) + + # Also recalculate the global count, still used even when Orbsanity is Off. + state.prog_items[player]["Reachable Orbs"] = count_reachable_orbs_global(state, player, state.multiworld) + state.prog_items[player]["Reachable Orbs Fresh"] = True + + +def count_reachable_orbs_global(state: CollectionState, + player: int, + multiworld: MultiWorld) -> int: accessible_orbs = 0 for region in multiworld.get_regions(player): - if state.can_reach(region, "Region", player): + if region.can_reach(state): + # Only cast the region when we need to. accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count - return accessible_orbs -def can_reach_orbs_level(state: CollectionState, - player: int, - multiworld: MultiWorld, - level_name: str) -> int: +def count_reachable_orbs_level(state: CollectionState, + player: int, + multiworld: MultiWorld, + level_name: str = "") -> int: accessible_orbs = 0 - regions = [typing.cast(JakAndDaxterRegion, reg) for reg in multiworld.get_regions(player)] - for region in regions: - if region.level_name == level_name and state.can_reach(region, "Region", player): + # Need to cast all regions upfront. + for region in typing.cast(typing.List[JakAndDaxterRegion], multiworld.get_regions(player)): + if region.level_name == level_name and region.can_reach(state): accessible_orbs += region.orb_count - return accessible_orbs -# TODO - Until we come up with a better progressive system for the traders (that avoids hard-locking if you pay the -# wrong ones and can't afford the right ones) just make all the traders locked behind the total amount to pay them all. -def can_trade(state: CollectionState, - player: int, - multiworld: MultiWorld, - options: JakAndDaxterOptions, - required_orbs: int, - required_previous_trade: int = None) -> bool: - - if options.enable_orbsanity == EnableOrbsanity.option_per_level: - bundle_size = options.level_orbsanity_bundle_size.value - return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) - elif options.enable_orbsanity == EnableOrbsanity.option_global: - bundle_size = options.global_orbsanity_bundle_size.value - return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade) - else: - return can_trade_regular(state, player, multiworld, required_orbs, required_previous_trade) +def can_reach_orbs_global(state: CollectionState, + player: int, + world: JakAndDaxterWorld, + bundle: int) -> bool: + + recalculate_reachable_orbs(state, player) + return state.has("Reachable Orbs", player, world.orb_bundle_size * (bundle + 1)) + + +def can_reach_orbs_level(state: CollectionState, + player: int, + world: JakAndDaxterWorld, + level_name: str, + bundle: int) -> bool: + + recalculate_reachable_orbs(state, player) + return state.has(f"{level_name} Reachable Orbs", player, world.orb_bundle_size * (bundle + 1)) -def can_trade_regular(state: CollectionState, +def can_trade_vanilla(state: CollectionState, player: int, - multiworld: MultiWorld, required_orbs: int, - required_previous_trade: int = None) -> bool: + required_previous_trade: typing.Optional[int] = None) -> bool: - # We know that Orbsanity is off, so count orbs globally. - accessible_orbs = can_reach_orbs_global(state, player, multiworld) + recalculate_reachable_orbs(state, player) # With Orbsanity Off, Reachable Orbs are in fact Tradeable Orbs. if required_previous_trade: name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)] - return (accessible_orbs >= required_orbs - and state.can_reach(name_of_previous_trade, "Location", player=player)) - else: - return accessible_orbs >= required_orbs + return (state.has("Reachable Orbs", player, required_orbs) + and state.can_reach_location(name_of_previous_trade, player=player)) + return state.has("Reachable Orbs", player, required_orbs) def can_trade_orbsanity(state: CollectionState, player: int, - orb_bundle_size: int, required_orbs: int, - required_previous_trade: int = None) -> bool: + required_previous_trade: typing.Optional[int] = None) -> bool: - required_count = math.ceil(required_orbs / orb_bundle_size) - orb_bundle_name = orb_item_table[orb_bundle_size] + recalculate_reachable_orbs(state, player) # Yes, even Orbsanity trades may unlock access to new Reachable Orbs. if required_previous_trade: name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)] - return (state.has(orb_bundle_name, player, required_count) - and state.can_reach(name_of_previous_trade, "Location", player=player)) - else: - return state.has(orb_bundle_name, player, required_count) + return (state.has("Tradeable Orbs", player, required_orbs) + and state.can_reach_location(name_of_previous_trade, player=player)) + return state.has("Tradeable Orbs", player, required_orbs) def can_free_scout_flies(state: CollectionState, player: int) -> bool: @@ -105,3 +120,65 @@ def can_free_scout_flies(state: CollectionState, player: int) -> bool: def can_fight(state: CollectionState, player: int) -> bool: return state.has_any({"Jump Dive", "Jump Kick", "Punch", "Kick"}, player) + + +def enforce_multiplayer_limits(options: JakAndDaxterOptions): + friendly_message = "" + + if (options.enable_orbsanity == EnableOrbsanity.option_global + and options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum): + friendly_message += (f" " + f"{options.global_orbsanity_bundle_size.display_name} must be no less than " + f"{GlobalOrbsanityBundleSize.friendly_minimum} (currently " + f"{options.global_orbsanity_bundle_size.value}).\n") + + if (options.enable_orbsanity == EnableOrbsanity.option_per_level + and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum): + friendly_message += (f" " + f"{options.level_orbsanity_bundle_size.display_name} must be no less than " + f"{PerLevelOrbsanityBundleSize.friendly_minimum} (currently " + f"{options.level_orbsanity_bundle_size.value}).\n") + + if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum: + friendly_message += (f" " + f"{options.fire_canyon_cell_count.display_name} must be no greater than " + f"{FireCanyonCellCount.friendly_maximum} (currently " + f"{options.fire_canyon_cell_count.value}).\n") + + if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum: + friendly_message += (f" " + f"{options.mountain_pass_cell_count.display_name} must be no greater than " + f"{MountainPassCellCount.friendly_maximum} (currently " + f"{options.mountain_pass_cell_count.value}).\n") + + if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum: + friendly_message += (f" " + f"{options.lava_tube_cell_count.display_name} must be no greater than " + f"{LavaTubeCellCount.friendly_maximum} (currently " + f"{options.lava_tube_cell_count.value}).\n") + + if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum: + friendly_message += (f" " + f"{options.citizen_orb_trade_amount.display_name} must be no greater than " + f"{CitizenOrbTradeAmount.friendly_maximum} (currently " + f"{options.citizen_orb_trade_amount.value}).\n") + + if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum: + friendly_message += (f" " + f"{options.oracle_orb_trade_amount.display_name} must be no greater than " + f"{OracleOrbTradeAmount.friendly_maximum} (currently " + f"{options.oracle_orb_trade_amount.value}).\n") + + if friendly_message != "": + raise OptionError(f"Please adjust the following Options for a multiplayer game.\n" + f"{friendly_message}") + + +def verify_orb_trade_amounts(options: JakAndDaxterOptions): + + total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) + if total_trade_orbs > 2000: + raise OptionError(f"Required number of orbs for all trades ({total_trade_orbs}) " + f"is more than all the orbs in the game (2000). " + f"Reduce the value of either {options.citizen_orb_trade_amount.display_name} " + f"or {options.oracle_orb_trade_amount.display_name}.") diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index bbd2f3118e91..dd04599b7d41 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,18 +1,30 @@ -from typing import Dict, Any, ClassVar +from typing import Dict, Any, ClassVar, Tuple, Callable, Optional import settings -from Utils import local_path, visualize_regions -from BaseClasses import Item, ItemClassification, Tutorial +from Utils import local_path +from BaseClasses import Item, ItemClassification, Tutorial, CollectionState from .GameID import jak1_id, jak1_name, jak1_max from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity -from .Locations import JakAndDaxterLocation, location_table -from .Items import JakAndDaxterItem, item_table +from .Locations import (JakAndDaxterLocation, + location_table, + cell_location_table, + scout_location_table, + special_location_table, + cache_location_table, + orb_location_table) +from .Items import (JakAndDaxterItem, + item_table, + cell_item_table, + scout_item_table, + special_item_table, + move_item_table, + orb_item_table) +from .Levels import level_table, level_table_with_global from .locs import (CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials, OrbCacheLocations as Caches, OrbLocations as Orbs) -from .Regions import create_regions from worlds.AutoWorld import World, WebWorld from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths @@ -79,48 +91,62 @@ class JakAndDaxterWorld(World): item_name_to_id = {item_table[k]: k for k in item_table} location_name_to_id = {location_table[k]: k for k in location_table} item_name_groups = { - "Power Cells": {item_table[k]: k for k in item_table - if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, - "Scout Flies": {item_table[k]: k for k in item_table - if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)}, - "Specials": {item_table[k]: k for k in item_table - if k in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset)}, - "Moves": {item_table[k]: k for k in item_table - if k in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset)}, - "Precursor Orbs": {item_table[k]: k for k in item_table - if k in range(jak1_id + Orbs.orb_offset, jak1_max)}, + "Power Cells": set(cell_item_table.values()), + "Scout Flies": set(scout_item_table.values()), + "Specials": set(special_item_table.values()), + "Moves": set(move_item_table.values()), + "Precursor Orbs": set(orb_item_table.values()), } location_name_groups = { - "Power Cells": {location_table[k]: k for k in location_table - if k in range(jak1_id, jak1_id + Scouts.fly_offset)}, - "Scout Flies": {location_table[k]: k for k in location_table - if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)}, - "Specials": {location_table[k]: k for k in location_table - if k in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset)}, - "Orb Caches": {location_table[k]: k for k in location_table - if k in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset)}, - "Precursor Orbs": {location_table[k]: k for k in location_table - if k in range(jak1_id + Orbs.orb_offset, jak1_max)}, - "Trades": {location_table[k]: k for k in location_table - if k in {Cells.to_ap_id(t) for t in {11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}}}, + "Power Cells": set(cell_location_table.values()), + "Scout Flies": set(scout_location_table.values()), + "Specials": set(special_location_table.values()), + "Orb Caches": set(cache_location_table.values()), + "Precursor Orbs": set(orb_location_table.values()), + "Trades": {location_table[Cells.to_ap_id(k)] for k in + {11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}}, } + # Functions and Variables that are Options-driven, keep them as instance variables here so that we don't clog up + # the seed generation routines with options checking. So we set these once, and then just use them as needed. + can_trade: Callable[[CollectionState, int, Optional[int]], bool] + orb_bundle_size: int = 0 + orb_bundle_item_name: str = "" + + def generate_early(self) -> None: + # For the fairness of other players in a multiworld game, enforce some friendly limitations on our options, + # so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen. + if self.multiworld.players > 1: + from .Rules import enforce_multiplayer_limits + enforce_multiplayer_limits(self.options) + + # Verify that we didn't overload the trade amounts with more orbs than exist in the world. + # This is easy to do by accident even in a single-player world. + from .Rules import verify_orb_trade_amounts + verify_orb_trade_amounts(self.options) + + # Cache the orb bundle size and item name for quicker reference. + if self.options.enable_orbsanity == EnableOrbsanity.option_per_level: + self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value + self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size] + elif self.options.enable_orbsanity == EnableOrbsanity.option_global: + self.orb_bundle_size = self.options.global_orbsanity_bundle_size.value + self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size] + + # Options drive which trade rules to use, so they need to be setup before we create_regions. + from .Rules import set_orb_trade_rule + set_orb_trade_rule(self) + # This will also set Locations, Location access rules, Region access rules, etc. def create_regions(self) -> None: - create_regions(self.multiworld, self.options, self.player) - visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml") + from .Regions import create_regions + create_regions(self) - # Helper function to get the correct orb bundle size. - def get_orb_bundle_size(self) -> int: - if self.options.enable_orbsanity == EnableOrbsanity.option_per_level: - return self.options.level_orbsanity_bundle_size.value - elif self.options.enable_orbsanity == EnableOrbsanity.option_global: - return self.options.global_orbsanity_bundle_size.value - else: - return 0 + # from Utils import visualize_regions + # visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml") # Helper function to reuse some nasty if/else trees. - def item_type_helper(self, item) -> (int, ItemClassification): + def item_type_helper(self, item) -> Tuple[int, ItemClassification]: # Make 101 Power Cells. if item in range(jak1_id, jak1_id + Scouts.fly_offset): classification = ItemClassification.progression_skip_balancing @@ -144,8 +170,7 @@ def item_type_helper(self, item) -> (int, ItemClassification): # Make N Precursor Orb bundles, where N is 2000 / bundle size. elif item in range(jak1_id + Orbs.orb_offset, jak1_max): classification = ItemClassification.progression_skip_balancing - size = self.get_orb_bundle_size() - count = int(2000 / size) if size > 0 else 0 # Don't divide by zero! + count = 2000 // self.orb_bundle_size if self.orb_bundle_size > 0 else 0 # Don't divide by zero! # Under normal circumstances, we will create 0 filler items. # We will manually create filler items as needed. @@ -168,15 +193,15 @@ def create_items(self) -> None: # then fill the item pool with a corresponding amount of filler items. if item_name in self.item_name_groups["Moves"] and not self.options.enable_move_randomizer: self.multiworld.push_precollected(self.create_item(item_name)) - self.multiworld.itempool += [self.create_item(self.get_filler_item_name())] + self.multiworld.itempool += [self.create_filler()] continue # Handle Orbsanity option. - # If it is OFF, don't add any orbs to the item pool. + # If it is OFF, don't add any orb bundles to the item pool, period. # If it is ON, don't add any orb bundles that don't match the chosen option. if (item_name in self.item_name_groups["Precursor Orbs"] - and ((self.options.enable_orbsanity == EnableOrbsanity.option_off - or Orbs.to_game_id(item_id) != self.get_orb_bundle_size()))): + and (self.options.enable_orbsanity == EnableOrbsanity.option_off + or item_name != self.orb_bundle_item_name)): continue # In every other scenario, do this. @@ -192,6 +217,41 @@ def create_item(self, name: str) -> Item: def get_filler_item_name(self) -> str: return "Green Eco Pill" + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change: + # No matter the option, no matter the item, set the caches to stale. + state.prog_items[self.player]["Reachable Orbs Fresh"] = False + + # Matching the item name implies Orbsanity is ON, so we don't need to check the option. + # When Orbsanity is OFF, there won't even be any orb bundle items to collect. + # Give the player the appropriate number of Tradeable Orbs based on bundle size. + if item.name == self.orb_bundle_item_name: + state.prog_items[self.player]["Tradeable Orbs"] += self.orb_bundle_size + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change: + # No matter the option, no matter the item, set the caches to stale. + state.prog_items[self.player]["Reachable Orbs Fresh"] = False + + # The opposite of what we did in collect: Take away from the player + # the appropriate number of Tradeable Orbs based on bundle size. + if item.name == self.orb_bundle_item_name: + state.prog_items[self.player]["Tradeable Orbs"] -= self.orb_bundle_size + + # TODO - 3.8 compatibility, remove this block when no longer required. + if state.prog_items[self.player]["Tradeable Orbs"] < 1: + del state.prog_items[self.player]["Tradeable Orbs"] + if state.prog_items[self.player]["Reachable Orbs"] < 1: + del state.prog_items[self.player]["Reachable Orbs"] + for level in level_table: + if state.prog_items[self.player][f"{level} Reachable Orbs".strip()] < 1: + del state.prog_items[self.player][f"{level} Reachable Orbs".strip()] + + return change + def fill_slot_data(self) -> Dict[str, Any]: return self.options.as_dict("enable_move_randomizer", "enable_orbsanity", diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index e9f3bfe182c2..e3d8900fe8a2 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -99,7 +99,6 @@ def as_float(value: int) -> int: # "Jak" to be replaced by player name in the Client. def autopsy(died: int) -> str: - assert died > 0, f"Tried to find Jak's cause of death, but he's still alive!" if died in [1, 2, 3, 4]: return random.choice(["Jak said goodnight.", "Jak stepped into the light.", @@ -147,7 +146,7 @@ class JakAndDaxterMemoryReader: # The memory reader just needs the game running. gk_process: pymem.process = None - location_outbox = [] + location_outbox: List[int] = [] outbox_index: int = 0 finished_game: bool = False @@ -224,7 +223,7 @@ async def connect(self): if marker_address: # At this address is another address that contains the struct we're looking for: the game's state. # From here we need to add the length in bytes for the marker and 4 bytes of padding, - # and the struct address is 8 bytes long (it's a uint64). + # and the struct address is 8 bytes long (it's an uint64). goal_pointer = marker_address + len(self.marker) + 4 self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, sizeof_uint64), byteorder="little", diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index eb551fe83a34..b6cd5206e797 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -2,7 +2,7 @@ import time import struct import random -from typing import Dict, Callable +from typing import Dict, Optional import pymem from pymem.exception import ProcessNotFound, ProcessError @@ -41,10 +41,10 @@ class JakAndDaxterReplClient: item_inbox: Dict[int, NetworkItem] = {} inbox_index = 0 - my_item_name: str = None - my_item_finder: str = None - their_item_name: str = None - their_item_owner: str = None + my_item_name: Optional[str] = None + my_item_finder: Optional[str] = None + their_item_name: Optional[str] = None + their_item_owner: Optional[str] = None def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.ip = ip @@ -353,6 +353,8 @@ async def subtract_traded_orbs(self, orb_count: int) -> bool: logger.error(f"Unable to subtract {orb_count} traded orbs!") return ok + return True + async def setup_options(self, os_option: int, os_bundle: int, fc_count: int, mp_count: int, diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index 422276cdfcf5..8e72cef88167 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -180,14 +180,14 @@ Here is how the HUD works: Depending on the nature of the bug, there are a couple of different options. * If you found a logical error in the randomizer, please create a new Issue -[here.](https://github.com/ArchipelaGOAL/Archipelago/issues) Use this page if: +[here](https://github.com/ArchipelaGOAL/Archipelago/issues). Use this page if: * An item required for progression is unreachable. * The randomizer did not respect one of the Options you chose. * You see a mistake, typo, etc. on this webpage. * You see an error or stack trace appear on the text client. * If you encountered an error in OpenGOAL, please create a new Issue -[here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues) Use this page if: +[here](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues). Use this page if: * You encounter a crash, freeze, reset, etc. in the game. * You fail to send Items you find in the game to the Archipelago server. * You fail to receive Items the server sends to you. diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 6933d2b51bd7..9058031ae8ff 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -26,8 +26,7 @@ At this time, this method of setup works on Windows only, but Linux support is a - Click `View Folder`. - In the new file explorer window, take note of the current path. It should contain `gk.exe` and `goalc.exe`. - Verify that the mod launcher copied the extracted ISO files to the mod directory: - - `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` should have *all* the same files as - - `%appdata%/OpenGOAL-Mods/_iso_data`, if it doesn't, copy those files over manually. + - `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` and `%appdata%/OpenGOAL-Mods/_iso_data` should have *all* the same files; if they don't, copy those files over manually. - And then `Recompile` if you needed to copy the files over. - **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). @@ -43,7 +42,7 @@ At this time, this method of setup works on Windows only, but Linux support is a - Back in the Archipelago Launcher, click `Open host.yaml`. - In the text file that opens, search for `jakanddaxter_options`. - You should see the block of YAML below. If you do not see it, you will need to add it. - - If the default path does not contain `gk.exe` and `goalc.exe`, you will need to provide the path you noted earlier. **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/ `.** + - If the default path does not contain `gk.exe` and `goalc.exe`, you will need to provide the path you noted earlier. **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** ``` jakanddaxter_options: @@ -162,8 +161,7 @@ PAL versions of the game seem to require additional troubleshooting/setup in ord ### Known Issues -- The game needs to run in debug mode in order to allow the repl to connect to it. We hide the debug text on screen and play the game's introductory cutscenes properly. +- The game needs to boot in debug mode in order to allow the repl to connect to it. We disable debug mode once we connect to the AP server. - The powershell windows cannot be run as background processes due to how the repl works, so the best we can do is minimize them. -- The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server. - Orbsanity checks may show up out of order in the text client. - Large item releases may take up to several minutes for the game to process them all. diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py index 109e18de2208..3904c7c7fcfc 100644 --- a/worlds/jakanddaxter/locs/CellLocations.py +++ b/worlds/jakanddaxter/locs/CellLocations.py @@ -11,12 +11,14 @@ # These helper functions do all the math required to get information about each # power cell and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: - assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + if game_id >= jak1_id: + raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.") return jak1_id + game_id def to_game_id(ap_id: int) -> int: - assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + if ap_id < jak1_id: + raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.") return ap_id - jak1_id diff --git a/worlds/jakanddaxter/locs/OrbCacheLocations.py b/worlds/jakanddaxter/locs/OrbCacheLocations.py index 5d237c797b55..5c0fb200a449 100644 --- a/worlds/jakanddaxter/locs/OrbCacheLocations.py +++ b/worlds/jakanddaxter/locs/OrbCacheLocations.py @@ -19,13 +19,15 @@ # special check and translate its ID between AP and OpenGOAL. Similar to Scout Flies, these large numbers are not # necessary, and we can flatten out the range in which these numbers lie. def to_ap_id(game_id: int) -> int: - assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + if game_id >= jak1_id: + raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.") uncompressed_id = jak1_id + orb_cache_offset + game_id # Add the offsets and the orb cache Actor ID. return uncompressed_id - 10344 # Subtract the smallest Actor ID. def to_game_id(ap_id: int) -> int: - assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + if ap_id < jak1_id: + raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.") uncompressed_id = ap_id + 10344 # Reverse process, add back the smallest Actor ID. return uncompressed_id - jak1_id - orb_cache_offset # Subtract the offsets. diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index 1853730c335c..e042165bbe3a 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -1,6 +1,5 @@ -from dataclasses import dataclass - from ..GameID import jak1_id +from ..Levels import level_table_with_global # Precursor Orbs are not necessarily given ID's by the game. @@ -24,12 +23,14 @@ # These helper functions do all the math required to get information about each # precursor orb and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: - assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + if game_id >= jak1_id: + raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.") return jak1_id + orb_offset + game_id # Add the offsets and the orb Actor ID. def to_game_id(ap_id: int) -> int: - assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + if ap_id < jak1_id: + raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.") return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets. @@ -50,79 +51,8 @@ def create_address(level_index: int, bundle_index: int) -> int: # What follows is our method of generating all the name/ID pairs for location_name_to_id. # Remember that not every bundle will be used in the actual seed, we just need this as a static map of strings to ints. -level_info = { - "": { - "level_index": 16, # Global - "orbs": 2000 - }, - "Geyser Rock": { - "level_index": 0, - "orbs": 50 - }, - "Sandover Village": { - "level_index": 1, - "orbs": 50 - }, - "Sentinel Beach": { - "level_index": 2, - "orbs": 150 - }, - "Forbidden Jungle": { - "level_index": 3, - "orbs": 150 - }, - "Misty Island": { - "level_index": 4, - "orbs": 150 - }, - "Fire Canyon": { - "level_index": 5, - "orbs": 50 - }, - "Rock Village": { - "level_index": 6, - "orbs": 50 - }, - "Lost Precursor City": { - "level_index": 7, - "orbs": 200 - }, - "Boggy Swamp": { - "level_index": 8, - "orbs": 200 - }, - "Precursor Basin": { - "level_index": 9, - "orbs": 200 - }, - "Mountain Pass": { - "level_index": 10, - "orbs": 50 - }, - "Volcanic Crater": { - "level_index": 11, - "orbs": 50 - }, - "Snowy Mountain": { - "level_index": 12, - "orbs": 200 - }, - "Spider Cave": { - "level_index": 13, - "orbs": 200 - }, - "Lava Tube": { - "level_index": 14, - "orbs": 50 - }, - "Gol and Maia's Citadel": { - "level_index": 15, - "orbs": 200 - } -} - loc_orbBundleTable = { - create_address(level_info[name]["level_index"], index): f"{name} Orb Bundle {index + 1}".strip() - for name in level_info - for index in range(level_info[name]["orbs"]) + create_address(level_table_with_global[name]["level_index"], index): f"{name} Orb Bundle {index + 1}".strip() + for name in level_table_with_global + for index in range(level_table_with_global[name]["orbs"]) } diff --git a/worlds/jakanddaxter/locs/ScoutLocations.py b/worlds/jakanddaxter/locs/ScoutLocations.py index c1349d0d6367..884de0ad2393 100644 --- a/worlds/jakanddaxter/locs/ScoutLocations.py +++ b/worlds/jakanddaxter/locs/ScoutLocations.py @@ -23,7 +23,8 @@ # These helper functions do all the math required to get information about each # scout fly and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: - assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + if game_id >= jak1_id: + raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.") cell_id = get_cell_id(game_id) # Get the power cell ID from the lowest 7 bits. buzzer_index = (game_id - cell_id) >> 9 # Get the index, bit shift it down 9 places. compressed_id = fly_offset + buzzer_index + cell_id # Add the offset, the bit-shifted index, and the cell ID. @@ -31,7 +32,8 @@ def to_ap_id(game_id: int) -> int: def to_game_id(ap_id: int) -> int: - assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + if ap_id < jak1_id: + raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.") compressed_id = ap_id - jak1_id # Reverse process. First thing: subtract the game's ID. cell_id = get_cell_id(compressed_id) # Get the power cell ID from the lowest 7 bits. buzzer_index = compressed_id - fly_offset - cell_id # Get the bit-shifted index. @@ -42,7 +44,8 @@ def to_game_id(ap_id: int) -> int: # Make sure to use this function ONLY when the input argument does NOT include jak1_id, # because that number may flip some of the bottom 7 bits, and that will throw off this bit mask. def get_cell_id(buzzer_id: int) -> int: - assert buzzer_id < jak1_id, f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}." + if buzzer_id >= jak1_id: + raise ValueError(f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}.") return buzzer_id & 0b1111111 @@ -184,7 +187,7 @@ def get_cell_id(buzzer_id: int) -> int: # Spider Cave locSC_scoutTable = { - 327765: "SC: Scout Fly Near Dark Dave Entrance", + 327765: "SC: Scout Fly Near Dark Cave Entrance", 262229: "SC: Scout Fly In Dark Cave", 393301: "SC: Scout Fly Main Cave, Overlooking Entrance", 196693: "SC: Scout Fly Main Cave, Near Dark Crystal", diff --git a/worlds/jakanddaxter/locs/SpecialLocations.py b/worlds/jakanddaxter/locs/SpecialLocations.py index e9ebecbfd9e9..6ad8c66e2806 100644 --- a/worlds/jakanddaxter/locs/SpecialLocations.py +++ b/worlds/jakanddaxter/locs/SpecialLocations.py @@ -21,12 +21,14 @@ # These helper functions do all the math required to get information about each # special check and translate its ID between AP and OpenGOAL. def to_ap_id(game_id: int) -> int: - assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one." + if game_id >= jak1_id: + raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.") return jak1_id + special_offset + game_id # Add the offsets and the orb Actor ID. def to_game_id(ap_id: int) -> int: - assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one." + if ap_id < jak1_id: + raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.") return ap_id - jak1_id - special_offset # Reverse process, subtract the offsets. diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py index fe5ad5766c98..0f55bf15ec2a 100644 --- a/worlds/jakanddaxter/regs/BoggySwampRegions.py +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -1,11 +1,15 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + +from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_fight, can_reach_orbs_level -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) @@ -155,15 +159,12 @@ def can_jump_higher(state: CollectionState, p: int) -> bool: if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(200 / bundle_size) + bundle_count = 200 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(8, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/FireCanyonRegions.py b/worlds/jakanddaxter/regs/FireCanyonRegions.py index 2c11278005ad..70a6e258099e 100644 --- a/worlds/jakanddaxter/regs/FireCanyonRegions.py +++ b/worlds/jakanddaxter/regs/FireCanyonRegions.py @@ -1,12 +1,15 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_reach_orbs_level from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) @@ -21,15 +24,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(50 / bundle_size) + bundle_count = 50 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(5, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py index c2f64b206ca1..07e7d9157c1d 100644 --- a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py +++ b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py @@ -1,11 +1,14 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 25) @@ -86,15 +89,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(150 / bundle_size) + bundle_count = 150 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(3, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py index b78ef7588e5f..8376e89ce8f4 100644 --- a/worlds/jakanddaxter/regs/GeyserRockRegions.py +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -1,12 +1,15 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_reach_orbs_level from ..locs import ScoutLocations as Scouts -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) main_area.add_cell_locations([92, 93]) @@ -30,15 +33,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(50 / bundle_size) + bundle_count = 50 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(0, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index 2104358ad903..e85d93fc6f96 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -1,18 +1,21 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + +from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level # God help me... here we go. -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player # This level is full of short-medium gaps that cannot be crossed by single jump alone. # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...) def can_jump_farther(state: CollectionState, p: int) -> bool: - return (state.has("Double Jump", p) - or state.has("Jump Kick", p) + return (state.has_any({"Double Jump", "Jump Kick"}, p) or state.has_all({"Punch", "Punch Uppercut"}, p)) def can_triple_jump(state: CollectionState, p: int) -> bool: @@ -115,15 +118,12 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(200 / bundle_size) + bundle_count = 200 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(15, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/LavaTubeRegions.py b/worlds/jakanddaxter/regs/LavaTubeRegions.py index 45ccdc2937dd..383ea524d541 100644 --- a/worlds/jakanddaxter/regs/LavaTubeRegions.py +++ b/worlds/jakanddaxter/regs/LavaTubeRegions.py @@ -1,12 +1,15 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_reach_orbs_level from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) @@ -21,15 +24,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(50 / bundle_size) + bundle_count = 50 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(14, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py index 2d1f712f57c6..4d3a5e68d010 100644 --- a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -1,11 +1,14 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player # Just the starting area. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 4) @@ -132,15 +135,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(200 / bundle_size) + bundle_count = 200 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(7, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py index 16a8790de540..5c7ba031d5af 100644 --- a/worlds/jakanddaxter/regs/MistyIslandRegions.py +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -1,12 +1,15 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level + +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: - main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 9) muse_course = JakAndDaxterRegion("Muse Course", player, multiworld, level_name, 21) @@ -114,15 +117,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(150 / bundle_size) + bundle_count = 150 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(4, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index 5644fc0add6a..019b717c4175 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -1,12 +1,15 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_reach_orbs_level from ..locs import ScoutLocations as Scouts -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player # This is basically just Klaww. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) @@ -37,15 +40,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(50 / bundle_size) + bundle_count = 50 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(10, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py index 05e3bcf9bc17..6fbdcee3d0ae 100644 --- a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py +++ b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py @@ -1,12 +1,15 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_reach_orbs_level from ..locs import CellLocations as Cells, ScoutLocations as Scouts -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 200) @@ -21,15 +24,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(200 / bundle_size) + bundle_count = 200 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(9, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/RegionBase.py b/worlds/jakanddaxter/regs/RegionBase.py index 6629991bc881..e8c7da161036 100644 --- a/worlds/jakanddaxter/regs/RegionBase.py +++ b/worlds/jakanddaxter/regs/RegionBase.py @@ -1,7 +1,6 @@ -from typing import List, Callable +from typing import Iterable, Callable, Optional from BaseClasses import MultiWorld, Region from ..GameID import jak1_name -from ..JakAndDaxterOptions import JakAndDaxterOptions from ..Locations import JakAndDaxterLocation, location_table from ..locs import (OrbLocations as Orbs, CellLocations as Cells, @@ -26,7 +25,7 @@ def __init__(self, name: str, player: int, multiworld: MultiWorld, level_name: s self.level_name = level_name self.orb_count = orb_count - def add_cell_locations(self, locations: List[int], access_rule: Callable = None): + def add_cell_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None): """ Adds a Power Cell Location to this region with the given access rule. Converts Game ID's to AP ID's for you. @@ -35,7 +34,7 @@ def add_cell_locations(self, locations: List[int], access_rule: Callable = None) ap_id = Cells.to_ap_id(loc) self.add_jak_locations(ap_id, location_table[ap_id], access_rule) - def add_fly_locations(self, locations: List[int], access_rule: Callable = None): + def add_fly_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None): """ Adds a Scout Fly Location to this region with the given access rule. Converts Game ID's to AP ID's for you. @@ -44,7 +43,7 @@ def add_fly_locations(self, locations: List[int], access_rule: Callable = None): ap_id = Scouts.to_ap_id(loc) self.add_jak_locations(ap_id, location_table[ap_id], access_rule) - def add_special_locations(self, locations: List[int], access_rule: Callable = None): + def add_special_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None): """ Adds a Special Location to this region with the given access rule. Converts Game ID's to AP ID's for you. @@ -55,7 +54,7 @@ def add_special_locations(self, locations: List[int], access_rule: Callable = No ap_id = Specials.to_ap_id(loc) self.add_jak_locations(ap_id, location_table[ap_id], access_rule) - def add_cache_locations(self, locations: List[int], access_rule: Callable = None): + def add_cache_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None): """ Adds an Orb Cache Location to this region with the given access rule. Converts Game ID's to AP ID's for you. @@ -64,7 +63,7 @@ def add_cache_locations(self, locations: List[int], access_rule: Callable = None ap_id = Caches.to_ap_id(loc) self.add_jak_locations(ap_id, location_table[ap_id], access_rule) - def add_orb_locations(self, level_index: int, bundle_index: int, bundle_size: int, access_rule: Callable = None): + def add_orb_locations(self, level_index: int, bundle_index: int, access_rule: Optional[Callable] = None): """ Adds Orb Bundle Locations to this region equal to `bundle_count`. Used only when Per-Level Orbsanity is enabled. The orb factory class will handle AP ID enumeration. @@ -78,7 +77,7 @@ def add_orb_locations(self, level_index: int, bundle_index: int, bundle_size: in location.access_rule = access_rule self.locations.append(location) - def add_jak_locations(self, ap_id: int, name: str, access_rule: Callable = None): + def add_jak_locations(self, ap_id: int, name: str, access_rule: Optional[Callable] = None): """ Helper function to add Locations. Not to be used directly. """ diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index 353a9ba412d9..113fbb79f787 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -1,26 +1,24 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_reach_orbs_level -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) # This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) - main_area.add_cell_locations([31], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - main_area.add_cell_locations([32], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - main_area.add_cell_locations([33], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - main_area.add_cell_locations([34], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - main_area.add_cell_locations([35], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs, 34)) + main_area.add_cell_locations([31], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([32], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([33], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([34], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([35], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 34)) # These 2 scout fly boxes can be broken by running with nearby blue eco. main_area.add_fly_locations([196684, 262220]) @@ -64,15 +62,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(50 / bundle_size) + bundle_count = 50 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(6, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index c05321fb4ccd..72ee7bc1d5b6 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -1,11 +1,14 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_reach_orbs_level -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) @@ -13,10 +16,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter # Yakows requires no combat. main_area.add_cell_locations([10]) - main_area.add_cell_locations([11], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - main_area.add_cell_locations([12], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) + main_area.add_cell_locations([11], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([12], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) # These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach. main_area.add_fly_locations([262219, 327755, 131147, 65611]) @@ -34,10 +35,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter yakow_cliff.add_fly_locations([75], access_rule=lambda state: can_free_scout_flies(state, player)) oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6) - oracle_platforms.add_cell_locations([13], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - oracle_platforms.add_cell_locations([14], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs, 13)) + oracle_platforms.add_cell_locations([13], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + oracle_platforms.add_cell_locations([14], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 13)) oracle_platforms.add_fly_locations([393291], access_rule=lambda state: can_free_scout_flies(state, player)) @@ -70,15 +69,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(50 / bundle_size) + bundle_count = 50 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(1, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py index 97c7a587724b..7d43fe18fbe6 100644 --- a/worlds/jakanddaxter/regs/SentinelBeachRegions.py +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -1,11 +1,14 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 128) main_area.add_cell_locations([18, 21, 22]) @@ -84,15 +87,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(150 / bundle_size) + bundle_count = 150 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(2, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index f6ee58b738ee..d0bc64292b83 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -1,12 +1,16 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + +from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level # God help me... here we go. -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player # We need a few helper functions. def can_cross_main_gap(state: CollectionState, p: int) -> bool: @@ -19,8 +23,7 @@ def can_cross_frozen_cave(state: CollectionState, p: int) -> bool: or state.has_all({"Roll", "Roll Jump"}, p))) def can_jump_blockers(state: CollectionState, p: int) -> bool: - return (state.has("Double Jump", p) - or state.has("Jump Dive", p) + return (state.has_any({"Double Jump", "Jump Dive"}, p) or state.has_all({"Crouch", "Crouch Jump"}, p) or state.has_all({"Crouch", "Crouch Uppercut"}, p) or state.has_all({"Punch", "Punch Uppercut"}, p)) @@ -190,15 +193,12 @@ def can_jump_blockers(state: CollectionState, p: int) -> bool: if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(200 / bundle_size) + bundle_count = 200 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(12, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py index c773f8c0644d..fe641c2cbce4 100644 --- a/worlds/jakanddaxter/regs/SpiderCaveRegions.py +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -1,11 +1,14 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player # A large amount of this area can be covered by single jump, floating platforms, web trampolines, and goggles. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 63) @@ -113,15 +116,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(200 / bundle_size) + bundle_count = 200 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(13, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py index e25b6de3ec22..f772f09c6323 100644 --- a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -1,29 +1,26 @@ from typing import List -from BaseClasses import CollectionState, MultiWorld + from .RegionBase import JakAndDaxterRegion -from .. import JakAndDaxterOptions, EnableOrbsanity -from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs -from ..locs import CellLocations as Cells, ScoutLocations as Scouts +from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Rules import can_free_scout_flies, can_reach_orbs_level +from ..locs import ScoutLocations as Scouts -def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]: +def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: + multiworld = world.multiworld + options = world.options + player = world.player total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) # No area is inaccessible in VC even with only running and jumping. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) - main_area.add_cell_locations([96], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - main_area.add_cell_locations([97], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs, 96)) - main_area.add_cell_locations([98], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs, 97)) - main_area.add_cell_locations([99], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs, 98)) - main_area.add_cell_locations([100], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs)) - main_area.add_cell_locations([101], access_rule=lambda state: - can_trade(state, player, multiworld, options, total_trade_orbs, 100)) + main_area.add_cell_locations([96], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([97], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 96)) + main_area.add_cell_locations([98], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 97)) + main_area.add_cell_locations([99], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 98)) + main_area.add_cell_locations([100], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([101], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 100)) # Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping # and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode). @@ -43,15 +40,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter if options.enable_orbsanity == EnableOrbsanity.option_per_level: orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name) - bundle_size = options.level_orbsanity_bundle_size.value - bundle_count = int(50 / bundle_size) + bundle_count = 50 // world.orb_bundle_size for bundle_index in range(bundle_count): orbs.add_orb_locations(11, bundle_index, - bundle_size, - access_rule=lambda state, bundle=bundle_index: - can_reach_orbs(state, player, multiworld, options, level_name) - >= (bundle_size * (bundle + 1))) + access_rule=lambda state, level=level_name, bundle=bundle_index: + can_reach_orbs_level(state, player, world, level, bundle)) multiworld.regions.append(orbs) main_area.connect(orbs) diff --git a/worlds/jakanddaxter/test/test_locations.py b/worlds/jakanddaxter/test/test_locations.py index 4bd144d623f8..1a8f754b563b 100644 --- a/worlds/jakanddaxter/test/test_locations.py +++ b/worlds/jakanddaxter/test/test_locations.py @@ -3,11 +3,8 @@ from . import JakAndDaxterTestBase from .. import jak1_id from ..regs.RegionBase import JakAndDaxterRegion -from ..locs import (OrbLocations as Orbs, - CellLocations as Cells, - ScoutLocations as Scouts, - SpecialLocations as Specials, - OrbCacheLocations as Caches) +from ..locs import (ScoutLocations as Scouts, + SpecialLocations as Specials) class LocationsTest(JakAndDaxterTestBase): From 3eadf54bf3b63e7d073ae7d6a8f28ca92178a6cb Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:23:28 -0400 Subject: [PATCH 47/70] Added a host.yaml option to override friendly limits, plus a couple of code review updates. --- worlds/jakanddaxter/Client.py | 1 + worlds/jakanddaxter/Rules.py | 11 ++--- worlds/jakanddaxter/__init__.py | 46 +++++++++++++------ .../jakanddaxter/regs/RockVillageRegions.py | 12 ++--- .../regs/SandoverVillageRegions.py | 10 ++-- .../regs/VolcanicCraterRegions.py | 14 +++--- 6 files changed, 53 insertions(+), 41 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 2c8c9b9962ac..e15e188fe4f8 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -107,6 +107,7 @@ async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(JakAndDaxterContext, self).server_auth(password_requested) await self.get_username() + self.tags = set() await self.send_connect() def on_package(self, cmd: str, args: dict): diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 4acd02fdb5db..4acff951459c 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -174,11 +174,10 @@ def enforce_multiplayer_limits(options: JakAndDaxterOptions): f"{friendly_message}") -def verify_orb_trade_amounts(options: JakAndDaxterOptions): +def verify_orb_trade_amounts(world: JakAndDaxterWorld): - total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) - if total_trade_orbs > 2000: - raise OptionError(f"Required number of orbs for all trades ({total_trade_orbs}) " + if world.total_trade_orbs > 2000: + raise OptionError(f"Required number of orbs for all trades ({world.total_trade_orbs}) " f"is more than all the orbs in the game (2000). " - f"Reduce the value of either {options.citizen_orb_trade_amount.display_name} " - f"or {options.oracle_orb_trade_amount.display_name}.") + f"Reduce the value of either {world.options.citizen_orb_trade_amount.display_name} " + f"or {world.options.oracle_orb_trade_amount.display_name}.") diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index dd04599b7d41..1983a276f379 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,8 +1,13 @@ -from typing import Dict, Any, ClassVar, Tuple, Callable, Optional +from typing import Dict, Any, ClassVar, Tuple, Callable, Optional, Union + +import Utils import settings from Utils import local_path -from BaseClasses import Item, ItemClassification, Tutorial, CollectionState +from BaseClasses import (Item, + ItemClassification as ItemClass, + Tutorial, + CollectionState) from .GameID import jak1_id, jak1_name, jak1_max from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity from .Locations import (JakAndDaxterLocation, @@ -48,7 +53,13 @@ class RootDirectory(settings.UserFolderPath): Ensure this path contains forward slashes (/) only.""" description = "ArchipelaGOAL Root Directory" + class EnforceFriendlyOptions(settings.Bool): + """Enforce friendly player options to be used in a multiplayer seed. + Disabling this allows for more disruptive and challenging options, but may impact seed generation.""" + description = "ArchipelaGOAL Enforce Friendly Options" + root_directory: RootDirectory = RootDirectory("%appdata%/OpenGOAL-Mods/archipelagoal") + enforce_friendly_options: Union[EnforceFriendlyOptions, bool] = True class JakAndDaxterWebWorld(WebWorld): @@ -107,23 +118,27 @@ class JakAndDaxterWorld(World): {11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}}, } - # Functions and Variables that are Options-driven, keep them as instance variables here so that we don't clog up + # These functions and variables are Options-driven, keep them as instance variables here so that we don't clog up # the seed generation routines with options checking. So we set these once, and then just use them as needed. can_trade: Callable[[CollectionState, int, Optional[int]], bool] - orb_bundle_size: int = 0 orb_bundle_item_name: str = "" + orb_bundle_size: int = 0 + total_trade_orbs: int = 0 + # Handles various options validation, rules enforcement, and caching of important information. def generate_early(self) -> None: # For the fairness of other players in a multiworld game, enforce some friendly limitations on our options, # so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen. - if self.multiworld.players > 1: + enforce_friendly_options = Utils.get_settings()["jakanddaxter_options"]["enforce_friendly_options"] + if self.multiworld.players > 1 and enforce_friendly_options: from .Rules import enforce_multiplayer_limits enforce_multiplayer_limits(self.options) # Verify that we didn't overload the trade amounts with more orbs than exist in the world. # This is easy to do by accident even in a single-player world. + self.total_trade_orbs = (9 * self.options.citizen_orb_trade_amount) + (6 * self.options.oracle_orb_trade_amount) from .Rules import verify_orb_trade_amounts - verify_orb_trade_amounts(self.options) + verify_orb_trade_amounts(self) # Cache the orb bundle size and item name for quicker reference. if self.options.enable_orbsanity == EnableOrbsanity.option_per_level: @@ -146,36 +161,39 @@ def create_regions(self) -> None: # visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml") # Helper function to reuse some nasty if/else trees. - def item_type_helper(self, item) -> Tuple[int, ItemClassification]: + def item_type_helper(self, item) -> Tuple[int, ItemClass]: # Make 101 Power Cells. if item in range(jak1_id, jak1_id + Scouts.fly_offset): - classification = ItemClassification.progression_skip_balancing + classification = ItemClass.progression_skip_balancing count = 101 # Make 7 Scout Flies per level. elif item in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset): - classification = ItemClassification.progression_skip_balancing + classification = ItemClass.progression_skip_balancing count = 7 # Make only 1 of each Special Item. elif item in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset): - classification = ItemClassification.progression + classification = ItemClass.progression | ItemClass.useful count = 1 # Make only 1 of each Move Item. elif item in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset): - classification = ItemClassification.progression + classification = ItemClass.progression | ItemClass.useful count = 1 # Make N Precursor Orb bundles, where N is 2000 / bundle size. elif item in range(jak1_id + Orbs.orb_offset, jak1_max): - classification = ItemClassification.progression_skip_balancing + if self.total_trade_orbs == 0: + classification = ItemClass.filler # If you don't need orbs to do trades, they are useless. + else: + classification = ItemClass.progression_skip_balancing count = 2000 // self.orb_bundle_size if self.orb_bundle_size > 0 else 0 # Don't divide by zero! # Under normal circumstances, we will create 0 filler items. # We will manually create filler items as needed. elif item == jak1_max: - classification = ItemClassification.filler + classification = ItemClass.filler count = 0 # If we try to make items with ID's higher than we've defined, something has gone wrong. @@ -193,7 +211,7 @@ def create_items(self) -> None: # then fill the item pool with a corresponding amount of filler items. if item_name in self.item_name_groups["Moves"] and not self.options.enable_move_randomizer: self.multiworld.push_precollected(self.create_item(item_name)) - self.multiworld.itempool += [self.create_filler()] + self.multiworld.itempool.append(self.create_filler()) continue # Handle Orbsanity option. diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index 113fbb79f787..706c44bc1624 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -10,15 +10,13 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte options = world.options player = world.player - total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) - # This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23) - main_area.add_cell_locations([31], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - main_area.add_cell_locations([32], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - main_area.add_cell_locations([33], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - main_area.add_cell_locations([34], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - main_area.add_cell_locations([35], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 34)) + main_area.add_cell_locations([31], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + main_area.add_cell_locations([32], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + main_area.add_cell_locations([33], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + main_area.add_cell_locations([34], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + main_area.add_cell_locations([35], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 34)) # These 2 scout fly boxes can be broken by running with nearby blue eco. main_area.add_fly_locations([196684, 262220]) diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index 72ee7bc1d5b6..a42e3a0b2b64 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -10,14 +10,12 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte options = world.options player = world.player - total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) - main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 26) # Yakows requires no combat. main_area.add_cell_locations([10]) - main_area.add_cell_locations([11], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - main_area.add_cell_locations([12], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) + main_area.add_cell_locations([11], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + main_area.add_cell_locations([12], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) # These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach. main_area.add_fly_locations([262219, 327755, 131147, 65611]) @@ -35,8 +33,8 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte yakow_cliff.add_fly_locations([75], access_rule=lambda state: can_free_scout_flies(state, player)) oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6) - oracle_platforms.add_cell_locations([13], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - oracle_platforms.add_cell_locations([14], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 13)) + oracle_platforms.add_cell_locations([13], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + oracle_platforms.add_cell_locations([14], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 13)) oracle_platforms.add_fly_locations([393291], access_rule=lambda state: can_free_scout_flies(state, player)) diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py index f772f09c6323..11934aa703ef 100644 --- a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -11,16 +11,14 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte options = world.options player = world.player - total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount) - # No area is inaccessible in VC even with only running and jumping. main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50) - main_area.add_cell_locations([96], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - main_area.add_cell_locations([97], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 96)) - main_area.add_cell_locations([98], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 97)) - main_area.add_cell_locations([99], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 98)) - main_area.add_cell_locations([100], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None)) - main_area.add_cell_locations([101], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 100)) + main_area.add_cell_locations([96], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + main_area.add_cell_locations([97], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 96)) + main_area.add_cell_locations([98], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 97)) + main_area.add_cell_locations([99], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 98)) + main_area.add_cell_locations([100], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None)) + main_area.add_cell_locations([101], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 100)) # Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping # and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode). From 30f5d84ab35d9d86d5bb4bcc10975f0e536d2f37 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:00:36 -0400 Subject: [PATCH 48/70] Added singleplayer limits, player names to enforcement rules. --- worlds/jakanddaxter/JakAndDaxterOptions.py | 18 +++-- worlds/jakanddaxter/Regions.py | 4 +- worlds/jakanddaxter/Rules.py | 70 ++++++++++++++----- worlds/jakanddaxter/__init__.py | 12 ++-- worlds/jakanddaxter/locs/OrbCacheLocations.py | 4 +- 5 files changed, 74 insertions(+), 34 deletions(-) diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index b1af9f788f91..cc4178723e92 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -51,7 +51,8 @@ class GlobalOrbsanityBundleSize(Choice): option_500_orbs = 500 option_1000_orbs = 1000 option_2000_orbs = 2000 - friendly_minimum = 5 + multiplayer_minimum = 5 + multiplayer_maximum = 400 default = 20 @@ -65,7 +66,7 @@ class PerLevelOrbsanityBundleSize(Choice): option_10_orbs = 10 option_25_orbs = 25 option_50_orbs = 50 - friendly_minimum = 5 + multiplayer_minimum = 5 default = 25 @@ -74,7 +75,8 @@ class FireCanyonCellCount(Range): display_name = "Fire Canyon Cell Count" range_start = 0 range_end = 100 - friendly_maximum = 30 + multiplayer_maximum = 30 + singleplayer_maximum = 34 default = 20 @@ -83,7 +85,8 @@ class MountainPassCellCount(Range): display_name = "Mountain Pass Cell Count" range_start = 0 range_end = 100 - friendly_maximum = 60 + multiplayer_maximum = 60 + singleplayer_maximum = 63 default = 45 @@ -92,7 +95,8 @@ class LavaTubeCellCount(Range): display_name = "Lava Tube Cell Count" range_start = 0 range_end = 100 - friendly_maximum = 90 + multiplayer_maximum = 90 + singleplayer_maximum = 99 default = 72 @@ -105,7 +109,7 @@ class CitizenOrbTradeAmount(Range): display_name = "Citizen Orb Trade Amount" range_start = 0 range_end = 222 - friendly_maximum = 120 + multiplayer_maximum = 120 default = 90 @@ -118,7 +122,7 @@ class OracleOrbTradeAmount(Range): display_name = "Oracle Orb Trade Amount" range_start = 0 range_end = 333 - friendly_maximum = 150 + multiplayer_maximum = 150 default = 120 diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index a8940e0c9a87..6ddcf9f4cbbf 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -126,5 +126,5 @@ def create_regions(world: JakAndDaxterWorld): multiworld.completion_condition[player] = lambda state: state.can_reach(fd, "Region", player) else: - raise OptionError(f"Unknown completion goal ID ({options.jak_completion_condition.value}).") - + raise OptionError(f"{world.player_name}: Unknown completion goal ID " + f"({options.jak_completion_condition.value}).") diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 4acff951459c..0e3e4b8412cf 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -122,62 +122,94 @@ def can_fight(state: CollectionState, player: int) -> bool: return state.has_any({"Jump Dive", "Jump Kick", "Punch", "Kick"}, player) -def enforce_multiplayer_limits(options: JakAndDaxterOptions): +def enforce_multiplayer_limits(world: JakAndDaxterWorld): + options = world.options friendly_message = "" if (options.enable_orbsanity == EnableOrbsanity.option_global - and options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum): + and (options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.multiplayer_minimum + or options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.multiplayer_maximum)): friendly_message += (f" " f"{options.global_orbsanity_bundle_size.display_name} must be no less than " - f"{GlobalOrbsanityBundleSize.friendly_minimum} (currently " + f"{GlobalOrbsanityBundleSize.multiplayer_minimum} and no greater than" + f"{GlobalOrbsanityBundleSize.multiplayer_maximum} (currently " f"{options.global_orbsanity_bundle_size.value}).\n") if (options.enable_orbsanity == EnableOrbsanity.option_per_level - and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum): + and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.multiplayer_minimum): friendly_message += (f" " f"{options.level_orbsanity_bundle_size.display_name} must be no less than " - f"{PerLevelOrbsanityBundleSize.friendly_minimum} (currently " + f"{PerLevelOrbsanityBundleSize.multiplayer_minimum} (currently " f"{options.level_orbsanity_bundle_size.value}).\n") - if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum: + if options.fire_canyon_cell_count.value > FireCanyonCellCount.multiplayer_maximum: friendly_message += (f" " f"{options.fire_canyon_cell_count.display_name} must be no greater than " - f"{FireCanyonCellCount.friendly_maximum} (currently " + f"{FireCanyonCellCount.multiplayer_maximum} (currently " f"{options.fire_canyon_cell_count.value}).\n") - if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum: + if options.mountain_pass_cell_count.value > MountainPassCellCount.multiplayer_maximum: friendly_message += (f" " f"{options.mountain_pass_cell_count.display_name} must be no greater than " - f"{MountainPassCellCount.friendly_maximum} (currently " + f"{MountainPassCellCount.multiplayer_maximum} (currently " f"{options.mountain_pass_cell_count.value}).\n") - if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum: + if options.lava_tube_cell_count.value > LavaTubeCellCount.multiplayer_maximum: friendly_message += (f" " f"{options.lava_tube_cell_count.display_name} must be no greater than " - f"{LavaTubeCellCount.friendly_maximum} (currently " + f"{LavaTubeCellCount.multiplayer_maximum} (currently " f"{options.lava_tube_cell_count.value}).\n") - if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum: + if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.multiplayer_maximum: friendly_message += (f" " f"{options.citizen_orb_trade_amount.display_name} must be no greater than " - f"{CitizenOrbTradeAmount.friendly_maximum} (currently " + f"{CitizenOrbTradeAmount.multiplayer_maximum} (currently " f"{options.citizen_orb_trade_amount.value}).\n") - if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum: + if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.multiplayer_maximum: friendly_message += (f" " f"{options.oracle_orb_trade_amount.display_name} must be no greater than " - f"{OracleOrbTradeAmount.friendly_maximum} (currently " + f"{OracleOrbTradeAmount.multiplayer_maximum} (currently " f"{options.oracle_orb_trade_amount.value}).\n") if friendly_message != "": - raise OptionError(f"Please adjust the following Options for a multiplayer game.\n" + raise OptionError(f"{world.player_name}: Please adjust the following Options for a multiplayer game.\n" f"{friendly_message}") +def enforce_singleplayer_limits(world: JakAndDaxterWorld): + options = world.options + friendly_message = "" + + if options.fire_canyon_cell_count.value > FireCanyonCellCount.singleplayer_maximum: + friendly_message += (f" " + f"{options.fire_canyon_cell_count.display_name} must be no greater than " + f"{FireCanyonCellCount.singleplayer_maximum} (currently " + f"{options.fire_canyon_cell_count.value}).\n") + + if options.mountain_pass_cell_count.value > MountainPassCellCount.singleplayer_maximum: + friendly_message += (f" " + f"{options.mountain_pass_cell_count.display_name} must be no greater than " + f"{MountainPassCellCount.singleplayer_maximum} (currently " + f"{options.mountain_pass_cell_count.value}).\n") + + if options.lava_tube_cell_count.value > LavaTubeCellCount.singleplayer_maximum: + friendly_message += (f" " + f"{options.lava_tube_cell_count.display_name} must be no greater than " + f"{LavaTubeCellCount.singleplayer_maximum} (currently " + f"{options.lava_tube_cell_count.value}).\n") + + if friendly_message != "": + raise OptionError(f"The options you have chosen may result in seed generation failures." + f"Please adjust the following Options for a singleplayer game.\n" + f"{friendly_message} " + f"Or set 'enforce_friendly_options' in host.yaml to false. (Use at your own risk!)") + + def verify_orb_trade_amounts(world: JakAndDaxterWorld): if world.total_trade_orbs > 2000: - raise OptionError(f"Required number of orbs for all trades ({world.total_trade_orbs}) " - f"is more than all the orbs in the game (2000). " - f"Reduce the value of either {world.options.citizen_orb_trade_amount.display_name} " + raise OptionError(f"{world.player_name}: Required number of orbs for all trades ({world.total_trade_orbs}) " + f"is more than all the orbs in the game (2000). Reduce the value of either " + f"{world.options.citizen_orb_trade_amount.display_name} " f"or {world.options.oracle_orb_trade_amount.display_name}.") diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 1983a276f379..483484b15c56 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -130,12 +130,16 @@ def generate_early(self) -> None: # For the fairness of other players in a multiworld game, enforce some friendly limitations on our options, # so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen. enforce_friendly_options = Utils.get_settings()["jakanddaxter_options"]["enforce_friendly_options"] - if self.multiworld.players > 1 and enforce_friendly_options: - from .Rules import enforce_multiplayer_limits - enforce_multiplayer_limits(self.options) + if enforce_friendly_options: + if self.multiworld.players > 1: + from .Rules import enforce_multiplayer_limits + enforce_multiplayer_limits(self) + else: + from .Rules import enforce_singleplayer_limits + enforce_singleplayer_limits(self) # Verify that we didn't overload the trade amounts with more orbs than exist in the world. - # This is easy to do by accident even in a single-player world. + # This is easy to do by accident even in a singleplayer world. self.total_trade_orbs = (9 * self.options.citizen_orb_trade_amount) + (6 * self.options.oracle_orb_trade_amount) from .Rules import verify_orb_trade_amounts verify_orb_trade_amounts(self) diff --git a/worlds/jakanddaxter/locs/OrbCacheLocations.py b/worlds/jakanddaxter/locs/OrbCacheLocations.py index 5c0fb200a449..984d8e7c2809 100644 --- a/worlds/jakanddaxter/locs/OrbCacheLocations.py +++ b/worlds/jakanddaxter/locs/OrbCacheLocations.py @@ -46,7 +46,7 @@ def to_game_id(ap_id: int) -> int: 23348: "Orb Cache in Snowy Fort (1)", 23349: "Orb Cache in Snowy Fort (2)", 23350: "Orb Cache in Snowy Fort (3)", - # 24038: "Orb Cache at End of Blast Furnace", # TODO - IDK, we didn't need all of the orb caches for move rando. - # 24039: "Orb Cache at End of Launch Pad Room", + # 24038: "Orb Cache at End of Blast Furnace", # TODO - We didn't need all of the orb caches for move rando. + # 24039: "Orb Cache at End of Launch Pad Room", # In future, could add/fill these with filler items? # 24040: "Orb Cache at Start of Launch Pad Room", } From b63ed86955d2cb7aafad0417db3c19fd6881d2e4 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:02:21 -0400 Subject: [PATCH 49/70] Updated friendly limits to be more strict, optimized recalculate logic. --- worlds/jakanddaxter/JakAndDaxterOptions.py | 6 +++--- worlds/jakanddaxter/Locations.py | 9 ++++++++- worlds/jakanddaxter/Rules.py | 14 ++++++++------ worlds/jakanddaxter/__init__.py | 4 ++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index cc4178723e92..21fb2725b0b2 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -51,8 +51,8 @@ class GlobalOrbsanityBundleSize(Choice): option_500_orbs = 500 option_1000_orbs = 1000 option_2000_orbs = 2000 - multiplayer_minimum = 5 - multiplayer_maximum = 400 + multiplayer_minimum = 10 + multiplayer_maximum = 200 default = 20 @@ -66,7 +66,7 @@ class PerLevelOrbsanityBundleSize(Choice): option_10_orbs = 10 option_25_orbs = 25 option_50_orbs = 50 - multiplayer_minimum = 5 + multiplayer_minimum = 10 default = 25 diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py index d358ed4213c5..98517aa44955 100644 --- a/worlds/jakanddaxter/Locations.py +++ b/worlds/jakanddaxter/Locations.py @@ -1,4 +1,4 @@ -from BaseClasses import Location +from BaseClasses import Location, CollectionState from .GameID import jak1_name from .locs import (OrbLocations as Orbs, CellLocations as Cells, @@ -10,6 +10,13 @@ class JakAndDaxterLocation(Location): game: str = jak1_name + # In AP 0.5.0, the base Location.can_reach function had its two boolean conditions swapped for a faster + # short-circuit for better performance. However, Jak seeds actually generate faster using the older method, + # which has been re-implemented below. + def can_reach(self, state: CollectionState) -> bool: + assert self.parent_region, "Can't reach location without region" + return self.parent_region.can_reach(state) and self.access_rule(state) + # Different tables for location groups. # Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed. diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 0e3e4b8412cf..6403c60c3bdd 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -49,10 +49,11 @@ def count_reachable_orbs_global(state: CollectionState, multiworld: MultiWorld) -> int: accessible_orbs = 0 - for region in multiworld.get_regions(player): - if region.can_reach(state): - # Only cast the region when we need to. - accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count + # Cast all regions upfront to access their unique attributes. + for region in typing.cast(typing.List[JakAndDaxterRegion], multiworld.get_regions(player)): + # Rely on short-circuiting to skip region.can_reach whenever possible. + if region.orb_count > 0 and region.can_reach(state): + accessible_orbs += region.orb_count return accessible_orbs @@ -62,9 +63,10 @@ def count_reachable_orbs_level(state: CollectionState, level_name: str = "") -> int: accessible_orbs = 0 - # Need to cast all regions upfront. + # Cast all regions upfront to access their unique attributes. for region in typing.cast(typing.List[JakAndDaxterRegion], multiworld.get_regions(player)): - if region.level_name == level_name and region.can_reach(state): + # Rely on short-circuiting to skip region.can_reach whenever possible. + if region.level_name == level_name and region.orb_count > 0 and region.can_reach(state): accessible_orbs += region.orb_count return accessible_orbs diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 483484b15c56..f58fc8a37632 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -54,8 +54,8 @@ class RootDirectory(settings.UserFolderPath): description = "ArchipelaGOAL Root Directory" class EnforceFriendlyOptions(settings.Bool): - """Enforce friendly player options to be used in a multiplayer seed. - Disabling this allows for more disruptive and challenging options, but may impact seed generation.""" + """Enforce friendly player options in both single and multiplayer seeds. Disabling this allows for + more disruptive and challenging options, but may impact seed generation. Use at your own risk!""" description = "ArchipelaGOAL Enforce Friendly Options" root_directory: RootDirectory = RootDirectory("%appdata%/OpenGOAL-Mods/archipelagoal") From 0e1a3dfbe327817b10b41a367bf54ec6329c4c90 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 1 Sep 2024 13:26:01 -0400 Subject: [PATCH 50/70] Today's the big day Jak: updates docs for mod support in OpenGOAL Launcher --- worlds/jakanddaxter/__init__.py | 29 +-- worlds/jakanddaxter/docs/setup_en.md | 213 ++++++++++++----------- worlds/jakanddaxter/locs/OrbLocations.py | 4 +- 3 files changed, 126 insertions(+), 120 deletions(-) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index f58fc8a37632..f5765503f516 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -58,7 +58,7 @@ class EnforceFriendlyOptions(settings.Bool): more disruptive and challenging options, but may impact seed generation. Use at your own risk!""" description = "ArchipelaGOAL Enforce Friendly Options" - root_directory: RootDirectory = RootDirectory("%appdata%/OpenGOAL-Mods/archipelagoal") + root_directory: RootDirectory = RootDirectory("%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal") enforce_friendly_options: Union[EnforceFriendlyOptions, bool] = True @@ -116,6 +116,7 @@ class JakAndDaxterWorld(World): "Precursor Orbs": set(orb_location_table.values()), "Trades": {location_table[Cells.to_ap_id(k)] for k in {11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}}, + "'Free 7 Scout Flies' Power Cells": set(Cells.loc7SF_cellTable.values()), } # These functions and variables are Options-driven, keep them as instance variables here so that we don't clog up @@ -242,26 +243,30 @@ def get_filler_item_name(self) -> str: def collect(self, state: CollectionState, item: Item) -> bool: change = super().collect(state, item) if change: - # No matter the option, no matter the item, set the caches to stale. - state.prog_items[self.player]["Reachable Orbs Fresh"] = False + # Orbsanity as an option is no-factor to these conditions. Matching the item name implies Orbsanity is ON, + # so we don't need to check the option. When Orbsanity is OFF, there won't even be any orb bundle items + # to collect. - # Matching the item name implies Orbsanity is ON, so we don't need to check the option. - # When Orbsanity is OFF, there won't even be any orb bundle items to collect. - # Give the player the appropriate number of Tradeable Orbs based on bundle size. + # Orb items do not intrinsically unlock anything that contains more Reachable Orbs, so they do not need to + # set the cache to stale. They just change how many orbs you have to trade with. if item.name == self.orb_bundle_item_name: - state.prog_items[self.player]["Tradeable Orbs"] += self.orb_bundle_size + state.prog_items[self.player]["Tradeable Orbs"] += self.orb_bundle_size # Give a bundle of Trade Orbs + + # However, every other item that changes the CollectionState should set the cache to stale, because they + # likely made it possible to reach more orb locations (level unlocks, region unlocks, etc.). + else: + state.prog_items[self.player]["Reachable Orbs Fresh"] = False return change def remove(self, state: CollectionState, item: Item) -> bool: change = super().remove(state, item) if change: - # No matter the option, no matter the item, set the caches to stale. - state.prog_items[self.player]["Reachable Orbs Fresh"] = False - # The opposite of what we did in collect: Take away from the player - # the appropriate number of Tradeable Orbs based on bundle size. + # Do the same thing we did in collect, except subtract trade orbs instead of add. if item.name == self.orb_bundle_item_name: - state.prog_items[self.player]["Tradeable Orbs"] -= self.orb_bundle_size + state.prog_items[self.player]["Tradeable Orbs"] -= self.orb_bundle_size # Take a bundle of Trade Orbs + else: + state.prog_items[self.player]["Reachable Orbs Fresh"] = False # TODO - 3.8 compatibility, remove this block when no longer required. if state.prog_items[self.player]["Tradeable Orbs"] < 1: diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 9058031ae8ff..6e27ac723152 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -3,165 +3,166 @@ ## Required Software - A legally purchased copy of *Jak And Daxter: The Precursor Legacy.* -- [The OpenGOAL Mod Launcher](https://jakmods.dev/) +- [The OpenGOAL Launcher](https://opengoal.dev/) - [The Jak and Daxter .APWORLD package](https://github.com/ArchipelaGOAL/Archipelago/releases) -At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future. -(OpenGOAL itself supports Linux, and the mod launcher is runnable with Python.) +At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux. -## Preparations +## Installation -- Dump your copy of the game as an ISO file to your PC. -- Install the Mod Launcher. -- If you are prompted by the Mod Launcher at any time during setup, provide the path to your ISO file. +### OpenGOAL Launcher -## Installation +- Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation). + - You must set up a vanilla installation of Jak and Daxter before you can install mods for it. +- Follow the setup process for adding mods to the OpenGOAL Launcher. See [here](https://jakmods.dev/). +- Install the `ArchipelaGOAL` mod from the `Available Mods` list, then click on `ArchipelaGOAL` from the `Installed Mods` list. +- **If you installed the OpenGOAL Launcher to a non-default directory, you must follow the steps below.** + - In the bottom right corner of `ArchipelaGOAL`, click `Advanced`, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. + - In the File Explorer, go to the parent directory for `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Take note of this directory. + - Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. + - Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the directory you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. + **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** -***OpenGOAL Mod Launcher*** +``` +jakanddaxter_options: + # Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). + # Ensure this path contains forward slashes (/) only. + root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal" +``` -- Run the Mod Launcher and click `ArchipelaGOAL` in the mod list. -- Click `Install` and wait for it to complete. - - If you have yet to be prompted for the ISO, click `Re-Extract` and provide the path to your ISO file. -- Click `Recompile`. This may take between 30-60 seconds. It should run to 100% completion. If it does not, see the Troubleshooting section. -- Click `View Folder`. - - In the new file explorer window, take note of the current path. It should contain `gk.exe` and `goalc.exe`. -- Verify that the mod launcher copied the extracted ISO files to the mod directory: - - `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` and `%appdata%/OpenGOAL-Mods/_iso_data` should have *all* the same files; if they don't, copy those files over manually. - - And then `Recompile` if you needed to copy the files over. -- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below). + - Save the file and close it. +- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you (see below). -***Archipelago Launcher*** +### Archipelago Launcher - Copy the `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. - - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. + - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. - Run the Archipelago Launcher. - From the left-most list, click `Generate Template Options`. - Select `Jak and Daxter The Precursor Legacy.yaml`. - In the text file that opens, enter the name you want and remember it for later. - Save this file in `Archipelago/players`. You can now close the file. -- Back in the Archipelago Launcher, click `Open host.yaml`. -- In the text file that opens, search for `jakanddaxter_options`. - - You should see the block of YAML below. If you do not see it, you will need to add it. - - If the default path does not contain `gk.exe` and `goalc.exe`, you will need to provide the path you noted earlier. **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** - -``` -jakanddaxter_options: - # Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). - # Ensure this path contains forward slashes (/) only. - root_directory: "%appdata%/OpenGOAL-Mods/archipelagoal" -``` - -- Back in the Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. +- Back in the Archipelago Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. - If you plan to host the game yourself, from the left-most list, click `Host`. - - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. - - You can sort by Date Modified to make it easy to find. + - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. + - You can sort by Date Modified to make it easy to find. ## Updates and New Releases -***OpenGOAL Mod Launcher*** +### OpenGOAL Launcher -- Run the Mod Launcher and click `ArchipelaGOAL` in the mod list. -- Click `Launch` to download and install any new updates that have been released. -- You can verify your version once you reach the title screen menu by navigating to `Options > Game Options > Miscellaneous > Speedrunner Mode`. -- Turn on `Speedrunner Mode` and exit the menu. You should see the installed version number in the bottom left corner. Then turn `Speedrunner Mode` back off. -- Once you've verified your version, you can close the game. Remember, this is just for downloading updates. **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** +If you are in the middle of an async game, and you do not want to update the mod, you do not need to do this step. The mod will only update when you tell it to. + +- Run the OpenGOAL Launcher, then click `Features`, then click `Mods`, then click on `ArchipelaGOAL` from the `Installed Mods` list. +- Click `Update` to download and install any new updates that have been released. +- You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it. -***Archipelago Launcher*** +### Archipelago Launcher - Copy the latest `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. ## Starting a Game -***New Game*** +### New Game - Run the Archipelago Launcher. - From the right-most list, find and click `Jak and Daxter Client`. - 4 new windows should appear: - - A powershell window will open to run the OpenGOAL compiler. It should take about 30 seconds to compile the game. - - As before, it should run to 100% completion, and you should hear a musical cue to indicate it is done. If it does not run to 100%, or you do not hear the musical cue, see the Troubleshooting section. - - Another powershell window will open to run the game. - - The game window itself will launch, and Jak will be standing outside Samos's Hut. + - Two powershell windows will open to run the OpenGOAL compiler and the game. They should take about 30 seconds to compile. + - You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section. + - The game window itself will launch, and Jak will be standing outside Samos's Hut. + - Once compilation is complete, the title intro sequence will start. - Finally, the Archipelago text client will open. - - You should see several messages appear after the compiler has run to 100% completion. If you see `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. - - The game should then load in the title screen. + - If you see `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. - You can *minimize* the 2 powershell windows, **BUT DO NOT CLOSE THEM.** They are required for Archipelago and the game to communicate with each other. - Use the text client to connect to the Archipelago server while on the title screen. This will communicate your current settings to the game. - Start a new game in the title screen, and play through the cutscenes. - Once you reach Geyser Rock, you can start the game! - - You can leave Geyser Rock immediately if you so choose - just step on the warp gate button. + - You can leave Geyser Rock immediately if you so choose - just step on the warp gate button. -***Returning / Async Game*** +### Returning / Async Game - The same steps as New Game apply, with some exceptions: - - Connect to the Archipelago server **BEFORE** you load your save file. This is to allow AP to give the game your current settings and all the items you had previously. - - **THESE SETTINGS AFFECT LOADING AND SAVING OF SAVE FILES, SO IT IS IMPORTANT TO DO THIS FIRST.** - - Then, instead of choosing `New Game` in the title menu, choose `Load Game`, then choose the save file **CORRESPONDING TO YOUR CURRENT ARCHIPELAGO CONNECTION.** + - Connect to the Archipelago server **BEFORE** you load your save file. This is to allow AP to give the game your current settings and all the items you had previously. + - **THESE SETTINGS AFFECT LOADING AND SAVING OF SAVE FILES, SO IT IS IMPORTANT TO DO THIS FIRST.** + - Then, instead of choosing `New Game` in the title menu, choose `Load Game`, then choose the save file **CORRESPONDING TO YOUR CURRENT ARCHIPELAGO CONNECTION.** ## Troubleshooting -***Installation Failure*** +### The Game Fails To Load The Title Screen + +You may start the game via the Text Client, but it never loads in the title screen. Check the Compiler window and you may see an error like this. -- If you encounter errors during extraction or compilation of the game when using the Mod Launcher, you may see errors like this: ``` -- Compilation Error! -- Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. ``` - - If this occurs, you may need to copy the extracted data to the mod folder manually. - - From a location like this: `%appdata%/OpenGOAL-Mods/_iso_data` - - To a location like this: `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` - - Then try clicking `Recompile` in the Mod Launcher (ensure you have selected the right mod first!) - -***Game Failure*** - -- If at any point the text client says `The process has died`, you will need to restart the appropriate - application: - - Open a new powershell window. - - Navigating to the directory containing `gk.exe` and `goalc.exe` via `cd`. - - Run the command corresponding to the dead process: - - `.\gk.exe --game jak1 -- -v -boot -fakeiso -debug` - - `.\goalc.exe --game jak1` - - Then enter the following commands into the text client to reconnect everything to the game. + +If this happens, run the OpenGOAL Launcher. + +- On the **vanilla** Jak and Daxter page, click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this location. +- Back in the OpenGOAL Launcher, click `Features`, then click `Mods`, then click `ArchipelaGOAL`, then click `Advanced`, then click `Open Game Data Folder` again. +- Paste the `iso_data` folder you copied here. +- Back on the ArchipelaGOAL page in the OpenGOAL Launcher, click `Advanced`, then click `Compile`. + +### The Text Client Says "The process has died" + +If at any point the text client says `The process has died`, you will need to restart the appropriate application. + +- Run the OpenGOAL Launcher, then click `Features`, then click `Mods`, then click `ArchipelaGOAL`. +- If the gk process died, click `Advanced`, then click `Play in Debug Mode`. +- If the goalc process died, click `Advanced`, then click `Open REPL`. +- Then enter the following commands into the text client to reconnect everything to the game. - `/repl connect` - `/memr connect` - - Once these are done, you can enter `/repl status` and `/memr status` to verify. -- If the game freezes by replaying the same two frames over and over, but the music still runs in the background, you may have accidentally interacted with the powershell windows in the background - they halt the game if you:scroll up in them, highlight text in them, etc. - - To unfreeze the game, scroll to the very bottom of the log output and right click. That will release powershell from your control and allow the game to continue. - - It is recommended to keep these windows minimized and out of your way. -- If the client cannot open a REPL connection to the game, you may need to ensure you are not hosting anything on ports 8181 and 8112. +- Once these are done, you can enter `/repl status` and `/memr status` to verify. -***Special PAL Instructions*** +### The Game Freezes On The Same Two Frames, But The Music Is Still Playing -PAL versions of the game seem to require additional troubleshooting/setup in order to work properly. Below are some instructions that may help. +If the game freezes by replaying the same two frames over and over, but the music still runs in the background, you may have accidentally interacted with the powershell windows in the background. They halt the game if you scroll up in them, highlight text in them, etc. + +- To unfreeze the game, scroll to the very bottom of the powershell window and right click. That will release powershell from your control and allow the game to continue. +- It is recommended to keep these windows minimized and out of your way. + +### The Client Cannot Open A REPL Connection -- If you have `-- Compilation Error! --` after pressing `Recompile` or Launching the ArchipelaGOAL mod. Try this: - - Remove these folders if you have them: - - `%appdata%/OpenGOAL-Mods/iso_data` - - `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` - - `%appdata%/OpenGOAL-Mods/archipelagoal/data/iso_data` - - Place Jak1 ISO in: `%appdata%/OpenGOAL-Mods/archipelagoal` rename it to `JakAndDaxter.iso` - - Type "CMD" in Windows search, Right click Command Prompt, and pick "Run as Administrator" - - Run: `cd %appdata%\OpenGOAL-Mods\archipelagoal` - - Then run: `extractor.exe --extract --extract-path .\data\iso_data "JakAndDaxter.iso"` - - (Command should end by saying `Uses Decompiler Config Version - ntsc_v1` or `... - pal`) - - Rename: `%appdata%\OpenGOAL-Mods\archipelagoal\data\iso_data\jak1` to `jak1_pal`* - - Run next: `decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "pal" data\iso_data data\decompiler_out`* - - *For NTSCv1 (USA Black Label) keep the folder as `jak1`, and use command: `decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "ntsc_v1" data\iso_data data\decompiler_out` - - Rename: `%appdata%\OpenGOAL-Mods\archipelagoal\data\iso_data\jak1_pal` to `jak1` - - Rename: `%appdata%\OpenGOAL-Mods\archipelagoal\data\decompiler_out\jak1_pal` to `jak1` -- You have to do this last bit in two different terminal **(2 powershell)**. First, from one terminal, launch the compiler: - - `cd %appdata%\OpenGOAL-Mods\archipelagoal` - - `.\goalc.exe --user-auto --game jak1` - - From the compiler (in the same terminal): `(mi)` - - This should compile the game. **Note that the parentheses are important.** +If the client cannot open a REPL connection to the game, you may need to ensure you are not hosting anything on ports `8181` and `8112`. + +### Special PAL Instructions + +PAL versions of the game seem to require additional troubleshooting/setup in order to work properly. Below are some instructions that may help. +If you see `-- Compilation Error! --` after pressing `Compile` or Launching the ArchipelaGOAL mod, try these steps. + +- Remove these folders if you have them: + - `/iso_data` + - `/iso_data` + - `/data/iso_data` +- Place your Jak1 ISO in `` and rename it to `JakAndDaxter.iso` +- Type `cmd` in Windows search, right click `Command Prompt`, and pick `Run as Administrator` +- Run `cd ` +- Then run `.\extractor.exe --extract --extract-path .\data\iso_data "JakAndDaxter.iso"` + - This command should end by saying `Uses Decompiler Config Version - ntsc_v1` or `... - pal`. Take note of this message. +- If you saw `ntsc_v1`: + - In cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "ntsc_v1" data\iso_data data\decompiler_out` +- If you saw `pal`: + - Rename `\data\iso_data\jak1` to `jak1_pal` + - Back in cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "pal" data\iso_data data\decompiler_out` + - Rename `\data\iso_data\jak1_pal` back to `jak1` + - Rename `\data\decompiler_out\jak1_pal` back to `jak1` +- Open a **brand new** Powershell window and launch the compiler: + - `cd ` + - `.\goalc.exe --user-auto --game jak1` + - From the compiler (in the same window): `(mi)`. This should compile the game. **Note that the parentheses are important.** - **Don't close this first terminal, you will need it at the end.** -- Then, **from the second terminal (powershell)**, execute the game: - - `cd %appdata%\OpenGOAL-Mods\archipelagoal` - - `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug` -- Finally, **from the first terminal still in the Goalc compiler**, connect to the game: `(lt)` +- Then, open **another brand new** Powershell window and execute the game: + - `cd ` + - `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug` +- Finally, **from the first Powershell still in the Goalc compiler**, connect to the game: `(lt)`. -### Known Issues +## Known Issues -- The game needs to boot in debug mode in order to allow the repl to connect to it. We disable debug mode once we connect to the AP server. -- The powershell windows cannot be run as background processes due to how the repl works, so the best we can do is minimize them. +- The game needs to boot in debug mode in order to allow the REPL to connect to it. We disable debug mode once we connect to the AP server. +- The REPL Powershell window is orphaned once you close the game - you will have to kill it manually when you stop playing. +- The powershell windows cannot be run as background processes due to how the REPL works, so the best we can do is minimize them. - Orbsanity checks may show up out of order in the text client. - Large item releases may take up to several minutes for the game to process them all. diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py index e042165bbe3a..d39a2c31d90a 100644 --- a/worlds/jakanddaxter/locs/OrbLocations.py +++ b/worlds/jakanddaxter/locs/OrbLocations.py @@ -12,8 +12,8 @@ # from parent actors DON'T have an Actor ID themselves - the parent object keeps track of how many of its orbs # have been picked up. -# In order to deal with this mess, we're creating a factory class that will generate Orb Locations for us. -# This will be compatible with both Global Orbsanity and Per-Level Orbsanity, allowing us to create any +# In order to deal with this mess, we're creating 2 extra functions that will create and identify Orb Locations for us. +# These will be compatible with both Global Orbsanity and Per-Level Orbsanity, allowing us to create any # number of Locations depending on the bundle size chosen, while also guaranteeing that each has a unique address. # We can use 2^15 to offset them from Orb Caches, because Orb Cache ID's max out at (jak1_id + 17792). From 9317486328e98b0d8e8d04068e9428525c84f55c Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:12:50 -0400 Subject: [PATCH 51/70] Rearranged and clarified some instructions, ADDED PATH-SPACE FIX TO CLIENT. --- worlds/jakanddaxter/Client.py | 6 ++-- worlds/jakanddaxter/docs/setup_en.md | 45 +++++++++++++++------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index e15e188fe4f8..66fc60b1e285 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -292,8 +292,9 @@ async def run_game(ctx: JakAndDaxterContext): return if gk_path: + # Prefixing ampersand and wrapping gk_path in quotes is necessary for paths with spaces in them. gk_process = subprocess.Popen( - ["powershell.exe", gk_path, "--game jak1", "--", "-v", "-boot", "-fakeiso", "-debug"], + ["powershell.exe", f"& \"{gk_path}\"", "--game jak1", "--", "-v", "-boot", "-fakeiso", "-debug"], creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. if not goalc_running: @@ -306,8 +307,9 @@ async def run_game(ctx: JakAndDaxterContext): return if goalc_path: + # Prefixing ampersand and wrapping goalc_path in quotes is necessary for paths with spaces in them. goalc_process = subprocess.Popen( - ["powershell.exe", goalc_path, "--game jak1"], + ["powershell.exe", f"& \"{goalc_path}\"", "--game jak1"], creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. # Auto connect the repl and memr agents. Sleep 5 because goalc takes just a little bit of time to load, diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 6e27ac723152..36ca5bc9abe2 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -10,14 +10,31 @@ At this time, this method of setup works on Windows only, but Linux support is a ## Installation +### Archipelago Launcher + +- Copy the `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. + - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. +- Run the Archipelago Launcher. +- From the left-most list, click `Generate Template Options`. +- Select `Jak and Daxter The Precursor Legacy.yaml`. +- In the text file that opens, enter the name you want and remember it for later. +- Save this file in `Archipelago/players`. You can now close the file. +- Back in the Archipelago Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. +- If you plan to host the game yourself, from the left-most list, click `Host`. + - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. + - You can sort by Date Modified to make it easy to find. + ### OpenGOAL Launcher - Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation). - You must set up a vanilla installation of Jak and Daxter before you can install mods for it. - Follow the setup process for adding mods to the OpenGOAL Launcher. See [here](https://jakmods.dev/). - Install the `ArchipelaGOAL` mod from the `Available Mods` list, then click on `ArchipelaGOAL` from the `Installed Mods` list. -- **If you installed the OpenGOAL Launcher to a non-default directory, you must follow the steps below.** - - In the bottom right corner of `ArchipelaGOAL`, click `Advanced`, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. +- **As a temporary measure, you may need to copy the extracted ISO data to the mod directory so the compiler will work properly.** + - If you have the NTSC version of the game, follow the `The Game Fails To Load The Title Screen` instructions below. + - If you have the PAL version of the game, follow the `Special PAL Instructions` instructions **instead.** +- **If you installed the OpenGOAL Launcher to a non-default directory, you must follow these steps.** + - While on the ArchipelaGOAL page, click `Advanced`, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. - In the File Explorer, go to the parent directory for `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Take note of this directory. - Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. - Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the directory you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. @@ -33,21 +50,11 @@ jakanddaxter_options: - Save the file and close it. - **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you (see below). +## Updates and New Releases + ### Archipelago Launcher -- Copy the `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. - - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. -- Run the Archipelago Launcher. -- From the left-most list, click `Generate Template Options`. -- Select `Jak and Daxter The Precursor Legacy.yaml`. -- In the text file that opens, enter the name you want and remember it for later. -- Save this file in `Archipelago/players`. You can now close the file. -- Back in the Archipelago Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. -- If you plan to host the game yourself, from the left-most list, click `Host`. - - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. - - You can sort by Date Modified to make it easy to find. - -## Updates and New Releases +- Copy the latest `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. ### OpenGOAL Launcher @@ -56,10 +63,6 @@ If you are in the middle of an async game, and you do not want to update the mod - Run the OpenGOAL Launcher, then click `Features`, then click `Mods`, then click on `ArchipelaGOAL` from the `Installed Mods` list. - Click `Update` to download and install any new updates that have been released. - You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it. - -### Archipelago Launcher - -- Copy the latest `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. ## Starting a Game @@ -98,7 +101,7 @@ You may start the game via the Text Client, but it never loads in the title scre Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. ``` -If this happens, run the OpenGOAL Launcher. +If this happens, run the OpenGOAL Launcher. If you are using a PAL version of the game, you should skip these instructions and follow `Special PAL Instructions` below. - On the **vanilla** Jak and Daxter page, click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this location. - Back in the OpenGOAL Launcher, click `Features`, then click `Mods`, then click `ArchipelaGOAL`, then click `Advanced`, then click `Open Game Data Folder` again. @@ -157,7 +160,7 @@ If you see `-- Compilation Error! --` after pressing `Compile` or Launching the - Then, open **another brand new** Powershell window and execute the game: - `cd ` - `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug` -- Finally, **from the first Powershell still in the Goalc compiler**, connect to the game: `(lt)`. +- Finally, **from the first Powershell still in the GOALC compiler**, connect to the game: `(lt)`. ## Known Issues From e6b58aa2be7c0fd8e393f962c69809f0a1228854 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 7 Sep 2024 15:30:23 -0400 Subject: [PATCH 52/70] Fix deathlink reset stalls on a busy client. (#47) --- worlds/jakanddaxter/Client.py | 3 +- worlds/jakanddaxter/client/MemoryReader.py | 46 ++++++++++++---------- worlds/jakanddaxter/client/ReplClient.py | 12 ------ 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 66fc60b1e285..f71e94ef7550 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -222,10 +222,9 @@ async def ap_inform_deathlink(self): await self.send_death(death_text) logger.info(death_text) - # Reset all flags. + # Reset all flags, but leave the death count alone. self.memr.send_deathlink = False self.memr.cause_of_death = "" - await self.repl.reset_deathlink() def on_deathlink_check(self): create_task_log_exception(self.ap_inform_deathlink()) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index e3d8900fe8a2..2b33af349e8b 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -59,7 +59,8 @@ def define(self, size: int, length: int = 1) -> int: specials_received_offset = offsets.define(sizeof_uint8, 32) # Deathlink information. -died_offset = offsets.define(sizeof_uint8) +death_count_offset = offsets.define(sizeof_uint32) +death_cause_offset = offsets.define(sizeof_uint8) deathlink_enabled_offset = offsets.define(sizeof_uint8) # Move Rando information. @@ -98,41 +99,41 @@ def as_float(value: int) -> int: # "Jak" to be replaced by player name in the Client. -def autopsy(died: int) -> str: - if died in [1, 2, 3, 4]: +def autopsy(cause: int) -> str: + if cause in [1, 2, 3, 4]: return random.choice(["Jak said goodnight.", "Jak stepped into the light.", "Jak gave Daxter his insect collection.", "Jak did not follow Step 1."]) - if died == 5: + if cause == 5: return "Jak fell into an endless pit." - if died == 6: + if cause == 6: return "Jak drowned in the spicy water." - if died == 7: + if cause == 7: return "Jak tried to tackle a Lurker Shark." - if died == 8: + if cause == 8: return "Jak hit 500 degrees." - if died == 9: + if cause == 9: return "Jak took a bath in a pool of dark eco." - if died == 10: + if cause == 10: return "Jak got bombarded with flaming 30-ton boulders." - if died == 11: + if cause == 11: return "Jak hit 800 degrees." - if died == 12: + if cause == 12: return "Jak ceased to be." - if died == 13: + if cause == 13: return "Jak got eaten by the dark eco plant." - if died == 14: + if cause == 14: return "Jak burned up." - if died == 15: + if cause == 15: return "Jak hit the ground hard." - if died == 16: + if cause == 16: return "Jak crashed the zoomer." - if died == 17: + if cause == 17: return "Jak got Flut Flut hurt." - if died == 18: + if cause == 18: return "Jak poisoned the whole darn catch." - if died == 19: + if cause == 19: return "Jak collided with too many obstacles." return "Jak died." @@ -154,6 +155,7 @@ class JakAndDaxterMemoryReader: deathlink_enabled: bool = False send_deathlink: bool = False cause_of_death: str = "" + death_count: int = 0 # Orbsanity handling orbsanity_enabled: bool = False @@ -286,10 +288,12 @@ def read_memory(self) -> List[int]: self.location_outbox.append(special_ap_id) logger.debug("Checked special: " + str(next_special)) - died = self.read_goal_address(died_offset, sizeof_uint8) - if died > 0: + death_count = self.read_goal_address(death_count_offset, sizeof_uint32) + death_cause = self.read_goal_address(death_cause_offset, sizeof_uint8) + if death_count > self.death_count: + self.cause_of_death = autopsy(death_cause) # The way he names his variables? Wack! self.send_deathlink = True - self.cause_of_death = autopsy(died) + self.death_count += 1 # Listen for any changes to this setting. deathlink_flag = self.read_goal_address(deathlink_enabled_offset, sizeof_uint8) diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index b6cd5206e797..ed0dd7084f19 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -79,10 +79,6 @@ async def main_tick(self): if self.received_deathlink: await self.receive_deathlink() - - # Reset all flags. - # As a precaution, we should reset our own deathlink flag as well. - await self.reset_deathlink() self.received_deathlink = False # This helper function formats and sends `form` as a command to the REPL. @@ -331,14 +327,6 @@ async def receive_deathlink(self) -> bool: logger.error(f"Unable to receive deathlink signal!") return ok - async def reset_deathlink(self) -> bool: - ok = await self.send_form("(set! (-> *ap-info-jak1* died) 0)") - if ok: - logger.debug(f"Reset deathlink flag!") - else: - logger.error(f"Unable to reset deathlink flag!") - return ok - async def subtract_traded_orbs(self, orb_count: int) -> bool: # To protect against momentary server disconnects, From 8922b1a5c93a0098b7e251d3df0ab6fc4c665973 Mon Sep 17 00:00:00 2001 From: Romain BERNARD <30secondstodraw@gmail.com> Date: Tue, 10 Sep 2024 02:55:00 +0200 Subject: [PATCH 53/70] Jak & Daxter Client : queue game text messages to get items faster during release (#48) * queue game text messages to write them during the main_tick function and empty the message queue faster during release * wrap comment for code style character limit Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> * remove useless blank line Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> * whitespace code style Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> * Move JsonMessageData dataclass outside of ReplClient class for code clarity --------- Co-authored-by: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> --- worlds/jakanddaxter/Client.py | 18 ++++++----- worlds/jakanddaxter/client/ReplClient.py | 39 ++++++++++++++++++------ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index f71e94ef7550..77db95350b0c 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -160,31 +160,35 @@ async def get_orb_balance(): async def json_to_game_text(self, args: dict): if "type" in args and args["type"] in {"ItemSend"}: + my_item_name = Optional[str] + my_item_finder = Optional[str] + their_item_name = Optional[str] + their_item_owner = Optional[str] item = args["item"] recipient = args["receiving"] # Receiving an item from the server. if self.slot_concerns_self(recipient): - self.repl.my_item_name = self.item_names.lookup_in_game(item.item) + my_item_name = self.item_names.lookup_in_game(item.item) # Did we find it, or did someone else? if self.slot_concerns_self(item.player): - self.repl.my_item_finder = "MYSELF" + my_item_finder = "MYSELF" else: - self.repl.my_item_finder = self.player_names[item.player] + my_item_finder = self.player_names[item.player] # Sending an item to the server. if self.slot_concerns_self(item.player): - self.repl.their_item_name = self.item_names.lookup_in_slot(item.item, recipient) + their_item_name = self.item_names.lookup_in_slot(item.item, recipient) # Does it belong to us, or to someone else? if self.slot_concerns_self(recipient): - self.repl.their_item_owner = "MYSELF" + their_item_owner = "MYSELF" else: - self.repl.their_item_owner = self.player_names[recipient] + their_item_owner = self.player_names[recipient] # Write to game display. - await self.repl.write_game_text() + self.repl.queue_game_text(my_item_name, my_item_finder, their_item_name, their_item_owner) def on_print_json(self, args: dict) -> None: diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index ed0dd7084f19..73009f0ad40d 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -1,7 +1,10 @@ import json +import queue import time import struct import random +from dataclasses import dataclass +from queue import Queue from typing import Dict, Optional import pymem @@ -22,6 +25,14 @@ OrbCacheLocations as Caches) +@dataclass +class JsonMessageData: + my_item_name: Optional[str] = None + my_item_finder: Optional[str] = None + their_item_name: Optional[str] = None + their_item_owner: Optional[str] = None + + class JakAndDaxterReplClient: ip: str port: int @@ -40,11 +51,7 @@ class JakAndDaxterReplClient: item_inbox: Dict[int, NetworkItem] = {} inbox_index = 0 - - my_item_name: Optional[str] = None - my_item_finder: Optional[str] = None - their_item_name: Optional[str] = None - their_item_owner: Optional[str] = None + json_message_queue: Queue[JsonMessageData] = queue.Queue() def __init__(self, ip: str = "127.0.0.1", port: int = 8181): self.ip = ip @@ -81,6 +88,13 @@ async def main_tick(self): await self.receive_deathlink() self.received_deathlink = False + # Progressively empty the queue during each tick + # if text messages happen to be too slow we could pool dequeuing here, + # but it'd slow down the ItemReceived message during release + if not self.json_message_queue.empty(): + json_txt_data = self.json_message_queue.get_nowait() + await self.write_game_text(json_txt_data) + # This helper function formats and sends `form` as a command to the REPL. # ALL commands to the REPL should be sent using this function. async def send_form(self, form: str, print_ok: bool = True) -> bool: @@ -203,20 +217,25 @@ def sanitize_game_text(text: str) -> str: result = result[:32].upper() return f"\"{result}\"" + # Pushes a JsonMessageData object to the json message queue to be processed during the repl main_tick + def queue_game_text(self, my_item_name, my_item_finder, their_item_name, their_item_owner): + self.json_message_queue.put(self.JsonMessageData(my_item_name, my_item_finder, + their_item_name, their_item_owner)) + # OpenGOAL can handle both its own string datatype and C-like character pointers (charp). # So for the game to constantly display this information in the HUD, we have to write it # to a memory address as a char*. - async def write_game_text(self): + async def write_game_text(self, data: JsonMessageData): logger.debug(f"Sending info to in-game display!") await self.send_form(f"(begin " f" (charp<-string (-> *ap-info-jak1* my-item-name) " - f" {self.sanitize_game_text(self.my_item_name)}) " + f" {self.sanitize_game_text(data.my_item_name)}) " f" (charp<-string (-> *ap-info-jak1* my-item-finder) " - f" {self.sanitize_game_text(self.my_item_finder)}) " + f" {self.sanitize_game_text(data.my_item_finder)}) " f" (charp<-string (-> *ap-info-jak1* their-item-name) " - f" {self.sanitize_game_text(self.their_item_name)}) " + f" {self.sanitize_game_text(data.their_item_name)}) " f" (charp<-string (-> *ap-info-jak1* their-item-owner) " - f" {self.sanitize_game_text(self.their_item_owner)}) " + f" {self.sanitize_game_text(data.their_item_owner)}) " f" (none))", print_ok=False) async def receive_item(self): From d15de80d57f6e01df7079b818b982aa4ace5dd56 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:29:59 -0400 Subject: [PATCH 54/70] Item Classifications (and REPL fixes) (#49) * Changes to item classifications * Bugfixes to power cell thresholds. * Fix bugs in item_type_helper. * Refactor 100 cell door to pass unit tests. * Quick fix to ReplClient. * Not so quick fix to ReplClient. * Display friendly limits in options tooltips. --- worlds/jakanddaxter/Client.py | 9 +- worlds/jakanddaxter/JakAndDaxterOptions.py | 36 ++++-- worlds/jakanddaxter/__init__.py | 114 +++++++++++++----- worlds/jakanddaxter/client/ReplClient.py | 30 ++--- .../regs/GolAndMaiasCitadelRegions.py | 19 +-- 5 files changed, 143 insertions(+), 65 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 77db95350b0c..3aff8b22222c 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -160,10 +160,11 @@ async def get_orb_balance(): async def json_to_game_text(self, args: dict): if "type" in args and args["type"] in {"ItemSend"}: - my_item_name = Optional[str] - my_item_finder = Optional[str] - their_item_name = Optional[str] - their_item_owner = Optional[str] + my_item_name: Optional[str] = None + my_item_finder: Optional[str] = None + their_item_name: Optional[str] = None + their_item_owner: Optional[str] = None + item = args["item"] recipient = args["receiving"] diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 21fb2725b0b2..55bfea49467f 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -29,7 +29,10 @@ class EnableOrbsanity(Choice): class GlobalOrbsanityBundleSize(Choice): """Set the orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global." - There are 2000 orbs in the game, so your bundle size must be a factor of 2000.""" + There are 2000 orbs in the game, so your bundle size must be a factor of 2000. + + Multiplayer Minimum: 10 + Multiplayer Maximum: 200""" display_name = "Global Orbsanity Bundle Size" option_1_orb = 1 option_2_orbs = 2 @@ -58,7 +61,9 @@ class GlobalOrbsanityBundleSize(Choice): class PerLevelOrbsanityBundleSize(Choice): """Set the orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level." - There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.""" + There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50. + + Multiplayer Minimum: 10""" display_name = "Per Level Orbsanity Bundle Size" option_1_orb = 1 option_2_orbs = 2 @@ -71,7 +76,10 @@ class PerLevelOrbsanityBundleSize(Choice): class FireCanyonCellCount(Range): - """Set the number of power cells you need to cross Fire Canyon.""" + """Set the number of power cells you need to cross Fire Canyon. + + Multiplayer Maximum: 30 + Singleplayer Maximum: 34""" display_name = "Fire Canyon Cell Count" range_start = 0 range_end = 100 @@ -81,7 +89,10 @@ class FireCanyonCellCount(Range): class MountainPassCellCount(Range): - """Set the number of power cells you need to reach Klaww and cross Mountain Pass.""" + """Set the number of power cells you need to reach Klaww and cross Mountain Pass. + + Multiplayer Maximum: 60 + Singleplayer Maximum: 63""" display_name = "Mountain Pass Cell Count" range_start = 0 range_end = 100 @@ -91,7 +102,10 @@ class MountainPassCellCount(Range): class LavaTubeCellCount(Range): - """Set the number of power cells you need to cross Lava Tube.""" + """Set the number of power cells you need to cross Lava Tube. + + Multiplayer Maximum: 90 + Singleplayer Maximum: 99""" display_name = "Lava Tube Cell Count" range_start = 0 range_end = 100 @@ -100,12 +114,14 @@ class LavaTubeCellCount(Range): default = 72 -# 222 is the maximum because there are 9 citizen trades and 2000 orbs to trade (2000/9 = 222). +# 222 is the absolute maximum because there are 9 citizen trades and 2000 orbs to trade (2000/9 = 222). class CitizenOrbTradeAmount(Range): """Set the number of orbs you need to trade to ordinary citizens for a power cell (Mayor, Uncle, etc.). Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000). - The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).""" + The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades). + + Multiplayer Maximum: 120""" display_name = "Citizen Orb Trade Amount" range_start = 0 range_end = 222 @@ -113,12 +129,14 @@ class CitizenOrbTradeAmount(Range): default = 90 -# 333 is the maximum because there are 6 oracle trades and 2000 orbs to trade (2000/6 = 333). +# 333 is the absolute maximum because there are 6 oracle trades and 2000 orbs to trade (2000/6 = 333). class OracleOrbTradeAmount(Range): """Set the number of orbs you need to trade to the Oracles for a power cell. Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000). - The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).""" + The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades). + + Multiplayer Maximum: 150""" display_name = "Oracle Orb Trade Amount" range_start = 0 range_end = 333 diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index f5765503f516..91608b7649fc 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, ClassVar, Tuple, Callable, Optional, Union +from typing import Dict, Any, ClassVar, Tuple, Callable, Optional, Union, List import Utils import settings @@ -9,7 +9,7 @@ Tutorial, CollectionState) from .GameID import jak1_id, jak1_name, jak1_max -from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity +from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity, CompletionCondition from .Locations import (JakAndDaxterLocation, location_table, cell_location_table, @@ -125,6 +125,7 @@ class JakAndDaxterWorld(World): orb_bundle_item_name: str = "" orb_bundle_size: int = 0 total_trade_orbs: int = 0 + power_cell_thresholds: List[int] = [] # Handles various options validation, rules enforcement, and caching of important information. def generate_early(self) -> None: @@ -152,6 +153,16 @@ def generate_early(self) -> None: elif self.options.enable_orbsanity == EnableOrbsanity.option_global: self.orb_bundle_size = self.options.global_orbsanity_bundle_size.value self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size] + else: + self.orb_bundle_size = 0 + self.orb_bundle_item_name = "" + + # Cache the power cell threshold values for quicker reference. + self.power_cell_thresholds = [] + self.power_cell_thresholds.append(self.options.fire_canyon_cell_count.value) + self.power_cell_thresholds.append(self.options.mountain_pass_cell_count.value) + self.power_cell_thresholds.append(self.options.lava_tube_cell_count.value) + self.power_cell_thresholds.append(100) # The 100 Power Cell Door. # Options drive which trade rules to use, so they need to be setup before we create_regions. from .Rules import set_orb_trade_rule @@ -165,47 +176,68 @@ def create_regions(self) -> None: # from Utils import visualize_regions # visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml") - # Helper function to reuse some nasty if/else trees. - def item_type_helper(self, item) -> Tuple[int, ItemClass]: - # Make 101 Power Cells. + # Helper function to reuse some nasty if/else trees. This outputs a list of pairs of item count and classification. + # For instance, not all 101 power cells need to be marked progression if you only need 72 to beat the game. So we + # will have 72 Progression Power Cells, and 29 Filler Power Cells. + def item_type_helper(self, item) -> List[Tuple[int, ItemClass]]: + counts_and_classes: List[Tuple[int, ItemClass]] = [] + + # Make 101 Power Cells. Not all of them will be Progression, some will be Filler. We only want AP's Progression + # Fill routine to handle the amount of cells we need to reach the furthest possible region. Even for early + # completion goals, all areas in the game must be reachable or generation will fail. TODO - Enormous refactor. if item in range(jak1_id, jak1_id + Scouts.fly_offset): - classification = ItemClass.progression_skip_balancing - count = 101 + + # If for some unholy reason we don't have the list of power cell thresholds, have a fallback plan. + if self.power_cell_thresholds: + prog_count = max(self.power_cell_thresholds[:3]) + non_prog_count = 101 - prog_count + + if self.options.jak_completion_condition == CompletionCondition.option_open_100_cell_door: + counts_and_classes.append((100, ItemClass.progression_skip_balancing)) + counts_and_classes.append((1, ItemClass.filler)) + else: + counts_and_classes.append((prog_count, ItemClass.progression_skip_balancing)) + counts_and_classes.append((non_prog_count, ItemClass.filler)) + else: + counts_and_classes.append((101, ItemClass.progression_skip_balancing)) # Make 7 Scout Flies per level. elif item in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset): - classification = ItemClass.progression_skip_balancing - count = 7 + counts_and_classes.append((7, ItemClass.progression_skip_balancing)) # Make only 1 of each Special Item. elif item in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset): - classification = ItemClass.progression | ItemClass.useful - count = 1 + counts_and_classes.append((1, ItemClass.progression | ItemClass.useful)) # Make only 1 of each Move Item. elif item in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset): - classification = ItemClass.progression | ItemClass.useful - count = 1 + counts_and_classes.append((1, ItemClass.progression | ItemClass.useful)) - # Make N Precursor Orb bundles, where N is 2000 / bundle size. + # Make N Precursor Orb bundles, where N is 2000 // bundle size. Like Power Cells, only a fraction of these will + # be marked as Progression with the remainder as Filler, but they are still entirely fungible. elif item in range(jak1_id + Orbs.orb_offset, jak1_max): - if self.total_trade_orbs == 0: - classification = ItemClass.filler # If you don't need orbs to do trades, they are useless. + + # Don't divide by zero! + if self.orb_bundle_size > 0: + item_count = 2000 // self.orb_bundle_size + + prog_count = -(-self.total_trade_orbs // self.orb_bundle_size) # Lazy ceil using integer division. + non_prog_count = item_count - prog_count + + counts_and_classes.append((prog_count, ItemClass.progression_skip_balancing)) + counts_and_classes.append((non_prog_count, ItemClass.filler)) else: - classification = ItemClass.progression_skip_balancing - count = 2000 // self.orb_bundle_size if self.orb_bundle_size > 0 else 0 # Don't divide by zero! + counts_and_classes.append((0, ItemClass.filler)) # No orbs in a bundle means no bundles. - # Under normal circumstances, we will create 0 filler items. - # We will manually create filler items as needed. + # Under normal circumstances, we create 0 green eco fillers. We will manually create filler items as needed. elif item == jak1_max: - classification = ItemClass.filler - count = 0 + counts_and_classes.append((0, ItemClass.filler)) # If we try to make items with ID's higher than we've defined, something has gone wrong. else: raise KeyError(f"Tried to fill item pool with unknown ID {item}.") - return count, classification + return counts_and_classes def create_items(self) -> None: for item_name in self.item_name_to_id: @@ -227,14 +259,15 @@ def create_items(self) -> None: or item_name != self.orb_bundle_item_name)): continue - # In every other scenario, do this. - count, classification = self.item_type_helper(item_id) - self.multiworld.itempool += [JakAndDaxterItem(item_name, classification, item_id, self.player) - for _ in range(count)] + # In every other scenario, do this. Not all items with the same name will have the same classification. + counts_and_classes = self.item_type_helper(item_id) + for (count, classification) in counts_and_classes: + self.multiworld.itempool += [JakAndDaxterItem(item_name, classification, item_id, self.player) + for _ in range(count)] def create_item(self, name: str) -> Item: item_id = self.item_name_to_id[name] - _, classification = self.item_type_helper(item_id) + _, classification = self.item_type_helper(item_id)[0] # Use first tuple (will likely be the most important). return JakAndDaxterItem(name, classification, item_id, self.player) def get_filler_item_name(self) -> str: @@ -252,6 +285,17 @@ def collect(self, state: CollectionState, item: Item) -> bool: if item.name == self.orb_bundle_item_name: state.prog_items[self.player]["Tradeable Orbs"] += self.orb_bundle_size # Give a bundle of Trade Orbs + # Scout Flies ALSO do not unlock anything that contains more Reachable Orbs, NOR do they give you more + # tradeable orbs. So let's just pass on them. + elif item.name in self.item_name_groups["Scout Flies"]: + pass + + # Power Cells DO unlock new regions that contain more Reachable Orbs - the connector levels and new + # hub levels - BUT they only do that when you have a number of them equal to one of the threshold values. + elif (item.name == "Power Cell" + and state.count("Power Cell", self.player) not in self.power_cell_thresholds): + pass + # However, every other item that changes the CollectionState should set the cache to stale, because they # likely made it possible to reach more orb locations (level unlocks, region unlocks, etc.). else: @@ -265,10 +309,22 @@ def remove(self, state: CollectionState, item: Item) -> bool: # Do the same thing we did in collect, except subtract trade orbs instead of add. if item.name == self.orb_bundle_item_name: state.prog_items[self.player]["Tradeable Orbs"] -= self.orb_bundle_size # Take a bundle of Trade Orbs + + # Ditto Scout Flies. + elif item.name in self.item_name_groups["Scout Flies"]: + pass + + # Ditto Power Cells, but check count + 1, because we potentially crossed the threshold in the opposite + # direction. E.g. we've removed the 20th power cell, our count is now 19, so we should stale the cache. + elif (item.name == "Power Cell" + and state.count("Power Cell", self.player) + 1 not in self.power_cell_thresholds): + pass + + # Ditto everything else. else: state.prog_items[self.player]["Reachable Orbs Fresh"] = False - # TODO - 3.8 compatibility, remove this block when no longer required. + # TODO - Python 3.8 compatibility, remove this block when no longer required. if state.prog_items[self.player]["Tradeable Orbs"] < 1: del state.prog_items[self.player]["Tradeable Orbs"] if state.prog_items[self.player]["Reachable Orbs"] < 1: diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 73009f0ad40d..06c254de9ca9 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -210,33 +210,33 @@ async def print_status(self): # I also only allotted 32 bytes to each string in OpenGOAL, so we must truncate. @staticmethod def sanitize_game_text(text: str) -> str: - if text is None: - return "\"NONE\"" - result = "".join(c for c in text if (c in {"-", " "} or c.isalnum())) result = result[:32].upper() return f"\"{result}\"" # Pushes a JsonMessageData object to the json message queue to be processed during the repl main_tick def queue_game_text(self, my_item_name, my_item_finder, their_item_name, their_item_owner): - self.json_message_queue.put(self.JsonMessageData(my_item_name, my_item_finder, - their_item_name, their_item_owner)) + self.json_message_queue.put(JsonMessageData(my_item_name, my_item_finder, their_item_name, their_item_owner)) # OpenGOAL can handle both its own string datatype and C-like character pointers (charp). # So for the game to constantly display this information in the HUD, we have to write it # to a memory address as a char*. async def write_game_text(self, data: JsonMessageData): logger.debug(f"Sending info to in-game display!") - await self.send_form(f"(begin " - f" (charp<-string (-> *ap-info-jak1* my-item-name) " - f" {self.sanitize_game_text(data.my_item_name)}) " - f" (charp<-string (-> *ap-info-jak1* my-item-finder) " - f" {self.sanitize_game_text(data.my_item_finder)}) " - f" (charp<-string (-> *ap-info-jak1* their-item-name) " - f" {self.sanitize_game_text(data.their_item_name)}) " - f" (charp<-string (-> *ap-info-jak1* their-item-owner) " - f" {self.sanitize_game_text(data.their_item_owner)}) " - f" (none))", print_ok=False) + body = "" + if data.my_item_name: + body += (f" (charp<-string (-> *ap-info-jak1* my-item-name)" + f" {self.sanitize_game_text(data.my_item_name)})") + if data.my_item_finder: + body += (f" (charp<-string (-> *ap-info-jak1* my-item-finder)" + f" {self.sanitize_game_text(data.my_item_finder)})") + if data.their_item_name: + body += (f" (charp<-string (-> *ap-info-jak1* their-item-name)" + f" {self.sanitize_game_text(data.their_item_name)})") + if data.their_item_owner: + body += (f" (charp<-string (-> *ap-info-jak1* their-item-owner)" + f" {self.sanitize_game_text(data.their_item_owner)})") + await self.send_form(f"(begin {body} (none))", print_ok=False) async def receive_item(self): ap_id = getattr(self.item_inbox[self.inbox_index], "item") diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index e85d93fc6f96..bfcbce606c9e 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -2,7 +2,7 @@ from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from .. import EnableOrbsanity, JakAndDaxterWorld, CompletionCondition from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level @@ -57,8 +57,6 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0) - final_door = JakAndDaxterRegion("Final Door", player, multiworld, level_name, 0) - # Jump Dive required for a lot of buttons, prepare yourself. main_area.connect(robot_scaffolding, rule=lambda state: state.has("Jump Dive", player) or state.has_all({"Roll", "Roll Jump"}, player)) @@ -101,9 +99,6 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: final_boss.connect(rotating_tower) # Take elevator back down. - # Final door. Need 100 power cells. - final_boss.connect(final_door, rule=lambda state: state.has("Power Cell", player, 100)) - multiworld.regions.append(main_area) multiworld.regions.append(robot_scaffolding) multiworld.regions.append(jump_pad_room) @@ -111,7 +106,6 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: multiworld.regions.append(bunny_room) multiworld.regions.append(rotating_tower) multiworld.regions.append(final_boss) - multiworld.regions.append(final_door) # If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always # accessible to Main Area. The Locations within are automatically checked when you collect enough orbs. @@ -127,4 +121,13 @@ def can_jump_stairs(state: CollectionState, p: int) -> bool: multiworld.regions.append(orbs) main_area.connect(orbs) - return [main_area, final_boss, final_door] + # Final door. Need 100 power cells. + if options.jak_completion_condition == CompletionCondition.option_open_100_cell_door: + final_door = JakAndDaxterRegion("Final Door", player, multiworld, level_name, 0) + final_boss.connect(final_door, rule=lambda state: state.has("Power Cell", player, 100)) + + multiworld.regions.append(final_door) + + return [main_area, final_boss, final_door] + else: + return [main_area, final_boss, None] From 8af8dd7a61425b1ec96758c6f7a62585b0c1606c Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:46:42 -0400 Subject: [PATCH 55/70] Use math.ceil like a normal person. --- worlds/jakanddaxter/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 91608b7649fc..aa8f465925d1 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -1,5 +1,5 @@ from typing import Dict, Any, ClassVar, Tuple, Callable, Optional, Union, List - +from math import ceil import Utils import settings @@ -221,7 +221,7 @@ def item_type_helper(self, item) -> List[Tuple[int, ItemClass]]: if self.orb_bundle_size > 0: item_count = 2000 // self.orb_bundle_size - prog_count = -(-self.total_trade_orbs // self.orb_bundle_size) # Lazy ceil using integer division. + prog_count = ceil(self.total_trade_orbs // self.orb_bundle_size) non_prog_count = item_count - prog_count counts_and_classes.append((prog_count, ItemClass.progression_skip_balancing)) From 2431ba03c9a2fa82094c0e7679db824b7a790246 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:26:08 -0400 Subject: [PATCH 56/70] Missed a space. --- worlds/jakanddaxter/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 6403c60c3bdd..9b8852d85069 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -133,7 +133,7 @@ def enforce_multiplayer_limits(world: JakAndDaxterWorld): or options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.multiplayer_maximum)): friendly_message += (f" " f"{options.global_orbsanity_bundle_size.display_name} must be no less than " - f"{GlobalOrbsanityBundleSize.multiplayer_minimum} and no greater than" + f"{GlobalOrbsanityBundleSize.multiplayer_minimum} and no greater than " f"{GlobalOrbsanityBundleSize.multiplayer_maximum} (currently " f"{options.global_orbsanity_bundle_size.value}).\n") From fd47d02b485b95fa4259aa08a614e113ba14a80e Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:29:58 -0400 Subject: [PATCH 57/70] Fix non-accessibility due to bad orb calculation. --- worlds/jakanddaxter/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index aa8f465925d1..08b7f9b998d7 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -219,9 +219,10 @@ def item_type_helper(self, item) -> List[Tuple[int, ItemClass]]: # Don't divide by zero! if self.orb_bundle_size > 0: - item_count = 2000 // self.orb_bundle_size + item_count = 2000 // self.orb_bundle_size # Integer division here, bundle size is a factor of 2000. - prog_count = ceil(self.total_trade_orbs // self.orb_bundle_size) + # Have enough bundles to do all trades. The rest can be filler. + prog_count = ceil(self.total_trade_orbs / self.orb_bundle_size) non_prog_count = item_count - prog_count counts_and_classes.append((prog_count, ItemClass.progression_skip_balancing)) From 39f0955db91684d10c686dd2fa4e5417bc48cbed Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sat, 14 Sep 2024 10:43:41 -0400 Subject: [PATCH 58/70] Updated documentation. --- worlds/jakanddaxter/docs/setup_en.md | 43 +++++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 36ca5bc9abe2..c01de29afc70 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -29,16 +29,23 @@ At this time, this method of setup works on Windows only, but Linux support is a - Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation). - You must set up a vanilla installation of Jak and Daxter before you can install mods for it. - Follow the setup process for adding mods to the OpenGOAL Launcher. See [here](https://jakmods.dev/). -- Install the `ArchipelaGOAL` mod from the `Available Mods` list, then click on `ArchipelaGOAL` from the `Installed Mods` list. -- **As a temporary measure, you may need to copy the extracted ISO data to the mod directory so the compiler will work properly.** +- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). +- Click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`. +- Under `Available Mods`, click `ArchipelaGOAL`. The mod should begin installing. When it is done, click `Continue` in the bottom right corner. +- Once you are back in the mod menu, click on `ArchipelaGOAL` from the `Installed Mods` list. +- **As a temporary measure, you need to copy the extracted ISO data to the mod directory so the compiler will work properly.** - If you have the NTSC version of the game, follow the `The Game Fails To Load The Title Screen` instructions below. - If you have the PAL version of the game, follow the `Special PAL Instructions` instructions **instead.** -- **If you installed the OpenGOAL Launcher to a non-default directory, you must follow these steps.** - - While on the ArchipelaGOAL page, click `Advanced`, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. - - In the File Explorer, go to the parent directory for `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Take note of this directory. +- **If you installed the OpenGOAL Launcher to a non-default directory, you must now follow these steps.** + - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). + - Click the Jak and Daxter logo on the left sidebar. + - Click `Features` in the bottom right corner, then click `Mods`. + - Under `Installed Mods`, then click `ArchipelaGOAL`, then click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. + - In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Take note of this directory. - Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. - Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the directory you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. - **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** + - **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** ``` jakanddaxter_options: @@ -60,9 +67,13 @@ jakanddaxter_options: If you are in the middle of an async game, and you do not want to update the mod, you do not need to do this step. The mod will only update when you tell it to. -- Run the OpenGOAL Launcher, then click `Features`, then click `Mods`, then click on `ArchipelaGOAL` from the `Installed Mods` list. +- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). +- Click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`. +- Under `Available Mods`, click `ArchipelaGOAL`. - Click `Update` to download and install any new updates that have been released. - You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it. +- **After the update is installed, you must click `Advanced`, then click `Compile` to make the update take effect.** ## Starting a Game @@ -94,19 +105,23 @@ If you are in the middle of an async game, and you do not want to update the mod ### The Game Fails To Load The Title Screen -You may start the game via the Text Client, but it never loads in the title screen. Check the Compiler window and you may see an error like this. +You may start the game via the Text Client, but it never loads in the title screen. Check the Compiler window and you may see red and yellow errors like this. ``` --- Compilation Error! -- -Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist. +-- Compilation Error! -- ``` If this happens, run the OpenGOAL Launcher. If you are using a PAL version of the game, you should skip these instructions and follow `Special PAL Instructions` below. -- On the **vanilla** Jak and Daxter page, click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this location. -- Back in the OpenGOAL Launcher, click `Features`, then click `Mods`, then click `ArchipelaGOAL`, then click `Advanced`, then click `Open Game Data Folder` again. -- Paste the `iso_data` folder you copied here. -- Back on the ArchipelaGOAL page in the OpenGOAL Launcher, click `Advanced`, then click `Compile`. +- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). +- Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory. +- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Available Mods`, click `ArchipelaGOAL`. +- In the bottom right corner, click `Advanced`, then click `Open Game Data Folder`. +- Paste the `iso_data` folder you copied earlier. +- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Available Mods`, click `ArchipelaGOAL`. +- In the bottom right corner, click `Advanced`, then click `Compile`. ### The Text Client Says "The process has died" From e0410eabdd57f3071fd789f8bd43f9188071bbb2 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:03:30 -0400 Subject: [PATCH 59/70] More Options, More Docs, More Tests (#51) * Reorder cell counts, require punch for Klaww. * Friendlier friendly friendlies. * Removed custom_worlds references from docs/setup guide, focused OpenGOAL Launcher language. * Increased breadth of unit tests. --- worlds/jakanddaxter/Client.py | 8 +-- worlds/jakanddaxter/JakAndDaxterOptions.py | 19 +++++- worlds/jakanddaxter/Rules.py | 16 +++-- worlds/jakanddaxter/__init__.py | 32 +++++++--- worlds/jakanddaxter/docs/setup_en.md | 38 +++--------- .../jakanddaxter/regs/MountainPassRegions.py | 6 ++ worlds/jakanddaxter/test/test_moverando.py | 23 +++++++ worlds/jakanddaxter/test/test_orbsanity.py | 61 +++++++++++++++++++ .../test/test_orderedcellcounts.py | 35 +++++++++++ worlds/jakanddaxter/test/test_trades.py | 45 ++++++++++++++ 10 files changed, 229 insertions(+), 54 deletions(-) create mode 100644 worlds/jakanddaxter/test/test_moverando.py create mode 100644 worlds/jakanddaxter/test/test_orbsanity.py create mode 100644 worlds/jakanddaxter/test/test_orderedcellcounts.py create mode 100644 worlds/jakanddaxter/test/test_trades.py diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 3aff8b22222c..23ead2baf708 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -122,12 +122,6 @@ def on_package(self, cmd: str, args: dict): else: orbsanity_bundle = 1 - # Keep compatibility with 0.0.8 at least for now - TODO: Remove this. - if "completion_condition" in slot_data: - goal_id = slot_data["completion_condition"] - else: - goal_id = slot_data["jak_completion_condition"] - create_task_log_exception( self.repl.setup_options(orbsanity_option, orbsanity_bundle, @@ -136,7 +130,7 @@ def on_package(self, cmd: str, args: dict): slot_data["lava_tube_cell_count"], slot_data["citizen_orb_trade_amount"], slot_data["oracle_orb_trade_amount"], - goal_id)) + slot_data["jak_completion_condition"])) # Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server # to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost, diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py index 55bfea49467f..35303c29386f 100644 --- a/worlds/jakanddaxter/JakAndDaxterOptions.py +++ b/worlds/jakanddaxter/JakAndDaxterOptions.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range +from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle class EnableMoveRandomizer(Toggle): @@ -114,6 +114,21 @@ class LavaTubeCellCount(Range): default = 72 +class EnableOrderedCellCounts(DefaultOnToggle): + """Enable to reorder the Cell Count options in ascending order. This is useful if you choose to randomize + those options. + + For example, if Fire Canyon Cell Count, Mountain Pass Cell Count, and Lava Tube Cell Count are 60, 30, and 40 + respectively, they will be reordered to 30, 40, and 60 respectively.""" + display_name = "Enable Ordered Cell Counts" + + +class RequirePunchForKlaww(DefaultOnToggle): + """Enable to force the Punch move to come before Klaww. Disabling this setting may require Jak to fight Klaww + and Gol and Maia by shooting yellow eco through his goggles. This only applies if "Enable Move Randomizer" is ON.""" + display_name = "Require Punch For Klaww" + + # 222 is the absolute maximum because there are 9 citizen trades and 2000 orbs to trade (2000/9 = 222). class CitizenOrbTradeAmount(Range): """Set the number of orbs you need to trade to ordinary citizens for a power cell (Mayor, Uncle, etc.). @@ -166,6 +181,8 @@ class JakAndDaxterOptions(PerGameCommonOptions): fire_canyon_cell_count: FireCanyonCellCount mountain_pass_cell_count: MountainPassCellCount lava_tube_cell_count: LavaTubeCellCount + enable_ordered_cell_counts: EnableOrderedCellCounts + require_punch_for_klaww: RequirePunchForKlaww citizen_orb_trade_amount: CitizenOrbTradeAmount oracle_orb_trade_amount: OracleOrbTradeAmount jak_completion_condition: CompletionCondition diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index 9b8852d85069..c9b36cae5052 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -175,8 +175,11 @@ def enforce_multiplayer_limits(world: JakAndDaxterWorld): f"{options.oracle_orb_trade_amount.value}).\n") if friendly_message != "": - raise OptionError(f"{world.player_name}: Please adjust the following Options for a multiplayer game.\n" - f"{friendly_message}") + raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n" + f"Please adjust the following Options for a multiplayer game. \n" + f"{friendly_message}" + f"Or set 'enforce_friendly_options' in the seed generator's host.yaml to false. " + f"(Use at your own risk!)") def enforce_singleplayer_limits(world: JakAndDaxterWorld): @@ -202,10 +205,11 @@ def enforce_singleplayer_limits(world: JakAndDaxterWorld): f"{options.lava_tube_cell_count.value}).\n") if friendly_message != "": - raise OptionError(f"The options you have chosen may result in seed generation failures." - f"Please adjust the following Options for a singleplayer game.\n" - f"{friendly_message} " - f"Or set 'enforce_friendly_options' in host.yaml to false. (Use at your own risk!)") + raise OptionError(f"The options you have chosen may result in seed generation failures. \n" + f"Please adjust the following Options for a singleplayer game. \n" + f"{friendly_message}" + f"Or set 'enforce_friendly_options' in your host.yaml to false. " + f"(Use at your own risk!)") def verify_orb_trade_amounts(world: JakAndDaxterWorld): diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 08b7f9b998d7..86b4b8ceb571 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -129,6 +129,25 @@ class JakAndDaxterWorld(World): # Handles various options validation, rules enforcement, and caching of important information. def generate_early(self) -> None: + + # Cache the power cell threshold values for quicker reference. + self.power_cell_thresholds = [] + self.power_cell_thresholds.append(self.options.fire_canyon_cell_count.value) + self.power_cell_thresholds.append(self.options.mountain_pass_cell_count.value) + self.power_cell_thresholds.append(self.options.lava_tube_cell_count.value) + self.power_cell_thresholds.append(100) # The 100 Power Cell Door. + + # Order the thresholds ascending and set the options values to the new order. + # TODO - How does this affect region access rules and other things? + try: + if self.options.enable_ordered_cell_counts: + self.power_cell_thresholds.sort() + self.options.fire_canyon_cell_count.value = self.power_cell_thresholds[0] + self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1] + self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2] + except IndexError: + pass # Skip if not possible. + # For the fairness of other players in a multiworld game, enforce some friendly limitations on our options, # so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen. enforce_friendly_options = Utils.get_settings()["jakanddaxter_options"]["enforce_friendly_options"] @@ -157,13 +176,6 @@ def generate_early(self) -> None: self.orb_bundle_size = 0 self.orb_bundle_item_name = "" - # Cache the power cell threshold values for quicker reference. - self.power_cell_thresholds = [] - self.power_cell_thresholds.append(self.options.fire_canyon_cell_count.value) - self.power_cell_thresholds.append(self.options.mountain_pass_cell_count.value) - self.power_cell_thresholds.append(self.options.lava_tube_cell_count.value) - self.power_cell_thresholds.append(100) # The 100 Power Cell Door. - # Options drive which trade rules to use, so they need to be setup before we create_regions. from .Rules import set_orb_trade_rule set_orb_trade_rule(self) @@ -182,9 +194,9 @@ def create_regions(self) -> None: def item_type_helper(self, item) -> List[Tuple[int, ItemClass]]: counts_and_classes: List[Tuple[int, ItemClass]] = [] - # Make 101 Power Cells. Not all of them will be Progression, some will be Filler. We only want AP's Progression - # Fill routine to handle the amount of cells we need to reach the furthest possible region. Even for early - # completion goals, all areas in the game must be reachable or generation will fail. TODO - Enormous refactor. + # Make 101 Power Cells. We only want AP's Progression Fill routine to handle the amount of cells we need + # to reach the furthest possible region. Even for early completion goals, all areas in the game must be + # reachable or generation will fail. TODO - Option-driven region creation would be an enormous refactor. if item in range(jak1_id, jak1_id + Scouts.fly_offset): # If for some unholy reason we don't have the list of power cell thresholds, have a fallback plan. diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index c01de29afc70..f74b85cd0f56 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -8,26 +8,10 @@ At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux. -## Installation - -### Archipelago Launcher - -- Copy the `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. - - Reminder: the default installation location for Archipelago is `C:\ProgramData\Archipelago`. -- Run the Archipelago Launcher. -- From the left-most list, click `Generate Template Options`. -- Select `Jak and Daxter The Precursor Legacy.yaml`. -- In the text file that opens, enter the name you want and remember it for later. -- Save this file in `Archipelago/players`. You can now close the file. -- Back in the Archipelago Launcher, from the left-most list, click `Generate`. A window will appear to generate your seed and close itself. -- If you plan to host the game yourself, from the left-most list, click `Host`. - - When asked to select your multiworld seed, navigate to `Archipelago/output` and select the zip file containing the seed you just generated. - - You can sort by Date Modified to make it easy to find. - -### OpenGOAL Launcher +## Installation via OpenGOAL Launcher - Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation). - - You must set up a vanilla installation of Jak and Daxter before you can install mods for it. + - **You must set up a vanilla installation of Jak and Daxter before you can install mods for it.** - Follow the setup process for adding mods to the OpenGOAL Launcher. See [here](https://jakmods.dev/). - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar. @@ -57,20 +41,14 @@ jakanddaxter_options: - Save the file and close it. - **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you (see below). -## Updates and New Releases - -### Archipelago Launcher - -- Copy the latest `jakanddaxter.apworld` file into your `Archipelago/custom_worlds` directory. - -### OpenGOAL Launcher +## Updates and New Releases via OpenGOAL Launcher If you are in the middle of an async game, and you do not want to update the mod, you do not need to do this step. The mod will only update when you tell it to. - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar. - Click `Features` in the bottom right corner, then click `Mods`. -- Under `Available Mods`, click `ArchipelaGOAL`. +- Under `Installed Mods`, click `ArchipelaGOAL`. - Click `Update` to download and install any new updates that have been released. - You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it. - **After the update is installed, you must click `Advanced`, then click `Compile` to make the update take effect.** @@ -111,16 +89,16 @@ You may start the game via the Text Client, but it never loads in the title scre -- Compilation Error! -- ``` -If this happens, run the OpenGOAL Launcher. If you are using a PAL version of the game, you should skip these instructions and follow `Special PAL Instructions` below. +If this happens, follow these instructions. If you are using a PAL version of the game, you should skip these instructions and follow `Special PAL Instructions` below. - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory. - Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. -- Click `Features` in the bottom right corner, then click `Mods`, then under `Available Mods`, click `ArchipelaGOAL`. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - In the bottom right corner, click `Advanced`, then click `Open Game Data Folder`. - Paste the `iso_data` folder you copied earlier. - Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. -- Click `Features` in the bottom right corner, then click `Mods`, then under `Available Mods`, click `ArchipelaGOAL`. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - In the bottom right corner, click `Advanced`, then click `Compile`. ### The Text Client Says "The process has died" @@ -133,7 +111,7 @@ If at any point the text client says `The process has died`, you will - Then enter the following commands into the text client to reconnect everything to the game. - `/repl connect` - `/memr connect` -- Once these are done, you can enter `/repl status` and `/memr status` to verify. +- Once these are done, you can enter `/repl status` and `/memr status` in the text client to verify. ### The Game Freezes On The Same Two Frames, But The Music Is Still Playing diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index 019b717c4175..23314a0dc41c 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -4,6 +4,7 @@ from .. import EnableOrbsanity, JakAndDaxterWorld from ..Rules import can_reach_orbs_level from ..locs import ScoutLocations as Scouts +from worlds.generic.Rules import add_rule def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]: @@ -15,6 +16,11 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0) main_area.add_cell_locations([86]) + # Some folks prefer firing Yellow Eco from the hip, so optionally put this rule before Klaww. Klaww is the only + # location in main_area, so he's at index 0. + if world.options.require_punch_for_klaww: + add_rule(main_area.locations[0], lambda state: state.has("Punch", player)) + race = JakAndDaxterRegion("Race", player, multiworld, level_name, 50) race.add_cell_locations([87]) diff --git a/worlds/jakanddaxter/test/test_moverando.py b/worlds/jakanddaxter/test/test_moverando.py new file mode 100644 index 000000000000..dddc9f420195 --- /dev/null +++ b/worlds/jakanddaxter/test/test_moverando.py @@ -0,0 +1,23 @@ +import typing + +from BaseClasses import CollectionState +from . import JakAndDaxterTestBase +from ..GameID import jak1_id +from ..Items import move_item_table +from ..regs.RegionBase import JakAndDaxterRegion + + +class MoveRandoTest(JakAndDaxterTestBase): + options = { + "enable_move_randomizer": True + } + + def test_move_items_in_pool(self): + for move in move_item_table: + self.assertIn(move_item_table[move], {item.name for item in self.multiworld.itempool}) + + def test_cannot_reach_without_move(self): + self.assertAccessDependency( + ["GR: Climb Up The Cliff"], + [["Double Jump"], ["Crouch"]], + only_check_listed=True) diff --git a/worlds/jakanddaxter/test/test_orbsanity.py b/worlds/jakanddaxter/test/test_orbsanity.py new file mode 100644 index 000000000000..4f84256be104 --- /dev/null +++ b/worlds/jakanddaxter/test/test_orbsanity.py @@ -0,0 +1,61 @@ +from . import JakAndDaxterTestBase +from ..Items import orb_item_table + + +class NoOrbsanityTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 0, # Off + "level_orbsanity_bundle_size": 25, + "global_orbsanity_bundle_size": 16 + } + + def test_orb_bundles_not_exist_in_pool(self): + for bundle in orb_item_table: + self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + + def test_orb_bundle_count(self): + bundle_name = orb_item_table[self.options["level_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(0, count) + + bundle_name = orb_item_table[self.options["global_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(0, count) + + +class PerLevelOrbsanityTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 1, # Per Level + "level_orbsanity_bundle_size": 25 + } + + def test_orb_bundles_exist_in_pool(self): + for bundle in orb_item_table: + if bundle == self.options["level_orbsanity_bundle_size"]: + self.assertIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + else: + self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + + def test_orb_bundle_count(self): + bundle_name = orb_item_table[self.options["level_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(80, count) + + +class GlobalOrbsanityTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 2, # Global + "global_orbsanity_bundle_size": 16 + } + + def test_orb_bundles_exist_in_pool(self): + for bundle in orb_item_table: + if bundle == self.options["global_orbsanity_bundle_size"]: + self.assertIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + else: + self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool}) + + def test_orb_bundle_count(self): + bundle_name = orb_item_table[self.options["global_orbsanity_bundle_size"]] + count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name]) + self.assertEqual(125, count) diff --git a/worlds/jakanddaxter/test/test_orderedcellcounts.py b/worlds/jakanddaxter/test/test_orderedcellcounts.py new file mode 100644 index 000000000000..a457beaa4a20 --- /dev/null +++ b/worlds/jakanddaxter/test/test_orderedcellcounts.py @@ -0,0 +1,35 @@ +import typing + +from BaseClasses import CollectionState +from . import JakAndDaxterTestBase +from ..GameID import jak1_id +from ..Items import move_item_table +from ..regs.RegionBase import JakAndDaxterRegion + + +class ReorderedCellCountsTest(JakAndDaxterTestBase): + options = { + "enable_ordered_cell_counts": True, + "fire_canyon_cell_count": 20, + "mountain_pass_cell_count": 15, + "lava_tube_cell_count": 10, + } + + def test_reordered_cell_counts(self): + self.world.generate_early() + self.assertLessEqual(self.world.options.fire_canyon_cell_count, self.world.options.mountain_pass_cell_count) + self.assertLessEqual(self.world.options.mountain_pass_cell_count, self.world.options.lava_tube_cell_count) + + +class UnorderedCellCountsTest(JakAndDaxterTestBase): + options = { + "enable_ordered_cell_counts": False, + "fire_canyon_cell_count": 20, + "mountain_pass_cell_count": 15, + "lava_tube_cell_count": 10, + } + + def test_unordered_cell_counts(self): + self.world.generate_early() + self.assertGreaterEqual(self.world.options.fire_canyon_cell_count, self.world.options.mountain_pass_cell_count) + self.assertGreaterEqual(self.world.options.mountain_pass_cell_count, self.world.options.lava_tube_cell_count) diff --git a/worlds/jakanddaxter/test/test_trades.py b/worlds/jakanddaxter/test/test_trades.py new file mode 100644 index 000000000000..4a9331307fdd --- /dev/null +++ b/worlds/jakanddaxter/test/test_trades.py @@ -0,0 +1,45 @@ +import typing + +from BaseClasses import CollectionState +from . import JakAndDaxterTestBase +from ..GameID import jak1_id +from ..Items import move_item_table +from ..regs.RegionBase import JakAndDaxterRegion + + +class TradesCostNothingTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 2, + "global_orbsanity_bundle_size": 5, + "citizen_orb_trade_amount": 0, + "oracle_orb_trade_amount": 0 + } + + def test_orb_items_are_filler(self): + self.collect_all_but("") + self.assertNotIn("5 Precursor Orbs", self.multiworld.state.prog_items) + + def test_trades_are_accessible(self): + self.assertTrue(self.multiworld + .get_location("SV: Bring 90 Orbs To The Mayor", self.player) + .can_reach(self.multiworld.state)) + + +class TradesCostEverythingTest(JakAndDaxterTestBase): + options = { + "enable_orbsanity": 2, + "global_orbsanity_bundle_size": 5, + "citizen_orb_trade_amount": 222, + "oracle_orb_trade_amount": 0 + } + + def test_orb_items_are_progression(self): + self.collect_all_but("") + self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player]) + self.assertEqual(400, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"]) + + def test_trades_are_accessible(self): + self.collect_all_but("") + self.assertTrue(self.multiworld + .get_location("SV: Bring 90 Orbs To The Mayor", self.player) + .can_reach(self.multiworld.state)) \ No newline at end of file From 8f0a2bc137e097bcb3e110a04d8be0f3994dd0ce Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:29:05 -0400 Subject: [PATCH 60/70] Clean imports of unit tests. --- worlds/jakanddaxter/test/__init__.py | 2 +- worlds/jakanddaxter/test/test_locations.py | 4 ++-- worlds/jakanddaxter/test/test_moverando.py | 7 +------ worlds/jakanddaxter/test/test_orbsanity.py | 2 +- worlds/jakanddaxter/test/test_orderedcellcounts.py | 8 +------- worlds/jakanddaxter/test/test_trades.py | 8 +------- 6 files changed, 7 insertions(+), 24 deletions(-) diff --git a/worlds/jakanddaxter/test/__init__.py b/worlds/jakanddaxter/test/__init__.py index a1d7bfb390b3..7c666a97b494 100644 --- a/worlds/jakanddaxter/test/__init__.py +++ b/worlds/jakanddaxter/test/__init__.py @@ -1,4 +1,4 @@ -from .. import JakAndDaxterWorld +from worlds.jakanddaxter import JakAndDaxterWorld from ..GameID import jak1_name from test.bases import WorldTestBase diff --git a/worlds/jakanddaxter/test/test_locations.py b/worlds/jakanddaxter/test/test_locations.py index 1a8f754b563b..46c5aa5887c8 100644 --- a/worlds/jakanddaxter/test/test_locations.py +++ b/worlds/jakanddaxter/test/test_locations.py @@ -1,7 +1,7 @@ import typing -from . import JakAndDaxterTestBase -from .. import jak1_id +from ..test import JakAndDaxterTestBase +from ..GameID import jak1_id from ..regs.RegionBase import JakAndDaxterRegion from ..locs import (ScoutLocations as Scouts, SpecialLocations as Specials) diff --git a/worlds/jakanddaxter/test/test_moverando.py b/worlds/jakanddaxter/test/test_moverando.py index dddc9f420195..b0dcd1363662 100644 --- a/worlds/jakanddaxter/test/test_moverando.py +++ b/worlds/jakanddaxter/test/test_moverando.py @@ -1,10 +1,5 @@ -import typing - -from BaseClasses import CollectionState -from . import JakAndDaxterTestBase -from ..GameID import jak1_id +from ..test import JakAndDaxterTestBase from ..Items import move_item_table -from ..regs.RegionBase import JakAndDaxterRegion class MoveRandoTest(JakAndDaxterTestBase): diff --git a/worlds/jakanddaxter/test/test_orbsanity.py b/worlds/jakanddaxter/test/test_orbsanity.py index 4f84256be104..2883570f8e83 100644 --- a/worlds/jakanddaxter/test/test_orbsanity.py +++ b/worlds/jakanddaxter/test/test_orbsanity.py @@ -1,4 +1,4 @@ -from . import JakAndDaxterTestBase +from ..test import JakAndDaxterTestBase from ..Items import orb_item_table diff --git a/worlds/jakanddaxter/test/test_orderedcellcounts.py b/worlds/jakanddaxter/test/test_orderedcellcounts.py index a457beaa4a20..b8cc7eac557b 100644 --- a/worlds/jakanddaxter/test/test_orderedcellcounts.py +++ b/worlds/jakanddaxter/test/test_orderedcellcounts.py @@ -1,10 +1,4 @@ -import typing - -from BaseClasses import CollectionState -from . import JakAndDaxterTestBase -from ..GameID import jak1_id -from ..Items import move_item_table -from ..regs.RegionBase import JakAndDaxterRegion +from ..test import JakAndDaxterTestBase class ReorderedCellCountsTest(JakAndDaxterTestBase): diff --git a/worlds/jakanddaxter/test/test_trades.py b/worlds/jakanddaxter/test/test_trades.py index 4a9331307fdd..6468f347b053 100644 --- a/worlds/jakanddaxter/test/test_trades.py +++ b/worlds/jakanddaxter/test/test_trades.py @@ -1,10 +1,4 @@ -import typing - -from BaseClasses import CollectionState -from . import JakAndDaxterTestBase -from ..GameID import jak1_id -from ..Items import move_item_table -from ..regs.RegionBase import JakAndDaxterRegion +from ..test import JakAndDaxterTestBase class TradesCostNothingTest(JakAndDaxterTestBase): From 4e4a59dd9a6c0f37f4dd0b8de635a32f09e8a48c Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:24:10 -0400 Subject: [PATCH 61/70] Create OptionGroups. --- worlds/jakanddaxter/Client.py | 2 +- .../{JakAndDaxterOptions.py => Options.py} | 0 worlds/jakanddaxter/Regions.py | 2 +- worlds/jakanddaxter/Rules.py | 18 +++++----- worlds/jakanddaxter/__init__.py | 33 +++++++++++++++---- worlds/jakanddaxter/regs/BoggySwampRegions.py | 3 +- worlds/jakanddaxter/regs/FireCanyonRegions.py | 3 +- .../regs/ForbiddenJungleRegions.py | 3 +- worlds/jakanddaxter/regs/GeyserRockRegions.py | 3 +- .../regs/GolAndMaiasCitadelRegions.py | 3 +- worlds/jakanddaxter/regs/LavaTubeRegions.py | 3 +- .../regs/LostPrecursorCityRegions.py | 3 +- .../jakanddaxter/regs/MistyIslandRegions.py | 3 +- .../jakanddaxter/regs/MountainPassRegions.py | 3 +- .../regs/PrecursorBasinRegions.py | 3 +- .../jakanddaxter/regs/RockVillageRegions.py | 3 +- .../regs/SandoverVillageRegions.py | 3 +- .../jakanddaxter/regs/SentinelBeachRegions.py | 3 +- .../jakanddaxter/regs/SnowyMountainRegions.py | 3 +- worlds/jakanddaxter/regs/SpiderCaveRegions.py | 3 +- .../regs/VolcanicCraterRegions.py | 3 +- 21 files changed, 69 insertions(+), 34 deletions(-) rename worlds/jakanddaxter/{JakAndDaxterOptions.py => Options.py} (100%) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 23ead2baf708..3b770327dca1 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -13,7 +13,7 @@ import Utils from NetUtils import ClientStatus from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled -from .JakAndDaxterOptions import EnableOrbsanity +from .Options import EnableOrbsanity from .GameID import jak1_name from .client.ReplClient import JakAndDaxterReplClient diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/Options.py similarity index 100% rename from worlds/jakanddaxter/JakAndDaxterOptions.py rename to worlds/jakanddaxter/Options.py diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py index 6ddcf9f4cbbf..c1a72b1ef8e4 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -3,7 +3,7 @@ from Options import OptionError from . import JakAndDaxterWorld from .Items import item_table -from .JakAndDaxterOptions import EnableOrbsanity, CompletionCondition +from .Options import EnableOrbsanity, CompletionCondition from .Rules import can_reach_orbs_global from .locs import CellLocations as Cells, ScoutLocations as Scouts from .regs import (GeyserRockRegions as GeyserRock, diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py index c9b36cae5052..6dbeda537fec 100644 --- a/worlds/jakanddaxter/Rules.py +++ b/worlds/jakanddaxter/Rules.py @@ -2,15 +2,15 @@ from BaseClasses import MultiWorld, CollectionState from Options import OptionError from . import JakAndDaxterWorld -from .JakAndDaxterOptions import (JakAndDaxterOptions, - EnableOrbsanity, - GlobalOrbsanityBundleSize, - PerLevelOrbsanityBundleSize, - FireCanyonCellCount, - MountainPassCellCount, - LavaTubeCellCount, - CitizenOrbTradeAmount, - OracleOrbTradeAmount) +from .Options import (JakAndDaxterOptions, + EnableOrbsanity, + GlobalOrbsanityBundleSize, + PerLevelOrbsanityBundleSize, + FireCanyonCellCount, + MountainPassCellCount, + LavaTubeCellCount, + CitizenOrbTradeAmount, + OracleOrbTradeAmount) from .locs import CellLocations as Cells from .Locations import location_table from .Levels import level_table diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 86b4b8ceb571..7ebf10e75a20 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -2,6 +2,7 @@ from math import ceil import Utils import settings +from Options import OptionGroup from Utils import local_path from BaseClasses import (Item, @@ -9,7 +10,7 @@ Tutorial, CollectionState) from .GameID import jak1_id, jak1_name, jak1_max -from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity, CompletionCondition +from . import Options from .Locations import (JakAndDaxterLocation, location_table, cell_location_table, @@ -74,6 +75,24 @@ class JakAndDaxterWebWorld(WebWorld): tutorials = [setup_en] + option_groups = [ + OptionGroup("Orbsanity", [ + Options.EnableOrbsanity, + Options.GlobalOrbsanityBundleSize, + Options.PerLevelOrbsanityBundleSize, + ]), + OptionGroup("Power Cell Counts", [ + Options.EnableOrderedCellCounts, + Options.FireCanyonCellCount, + Options.MountainPassCellCount, + Options.LavaTubeCellCount, + ]), + OptionGroup("Orb Trade Counts", [ + Options.CitizenOrbTradeAmount, + Options.OracleOrbTradeAmount, + ]), + ] + class JakAndDaxterWorld(World): """ @@ -91,8 +110,8 @@ class JakAndDaxterWorld(World): # Options settings: ClassVar[JakAndDaxterSettings] - options_dataclass = JakAndDaxterOptions - options: JakAndDaxterOptions + options_dataclass = Options.JakAndDaxterOptions + options: Options.JakAndDaxterOptions # Web world web = JakAndDaxterWebWorld() @@ -166,10 +185,10 @@ def generate_early(self) -> None: verify_orb_trade_amounts(self) # Cache the orb bundle size and item name for quicker reference. - if self.options.enable_orbsanity == EnableOrbsanity.option_per_level: + if self.options.enable_orbsanity == Options.EnableOrbsanity.option_per_level: self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size] - elif self.options.enable_orbsanity == EnableOrbsanity.option_global: + elif self.options.enable_orbsanity == Options.EnableOrbsanity.option_global: self.orb_bundle_size = self.options.global_orbsanity_bundle_size.value self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size] else: @@ -204,7 +223,7 @@ def item_type_helper(self, item) -> List[Tuple[int, ItemClass]]: prog_count = max(self.power_cell_thresholds[:3]) non_prog_count = 101 - prog_count - if self.options.jak_completion_condition == CompletionCondition.option_open_100_cell_door: + if self.options.jak_completion_condition == Options.CompletionCondition.option_open_100_cell_door: counts_and_classes.append((100, ItemClass.progression_skip_balancing)) counts_and_classes.append((1, ItemClass.filler)) else: @@ -268,7 +287,7 @@ def create_items(self) -> None: # If it is OFF, don't add any orb bundles to the item pool, period. # If it is ON, don't add any orb bundles that don't match the chosen option. if (item_name in self.item_name_groups["Precursor Orbs"] - and (self.options.enable_orbsanity == EnableOrbsanity.option_off + and (self.options.enable_orbsanity == Options.EnableOrbsanity.option_off or item_name != self.orb_bundle_item_name)): continue diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py index 0f55bf15ec2a..85e9d722f612 100644 --- a/worlds/jakanddaxter/regs/BoggySwampRegions.py +++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py @@ -2,7 +2,8 @@ from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/FireCanyonRegions.py b/worlds/jakanddaxter/regs/FireCanyonRegions.py index 70a6e258099e..21872385fe98 100644 --- a/worlds/jakanddaxter/regs/FireCanyonRegions.py +++ b/worlds/jakanddaxter/regs/FireCanyonRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_reach_orbs_level from ..locs import CellLocations as Cells, ScoutLocations as Scouts diff --git a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py index 07e7d9157c1d..9210dcbbf996 100644 --- a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py +++ b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py index 8376e89ce8f4..adeba39b08de 100644 --- a/worlds/jakanddaxter/regs/GeyserRockRegions.py +++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_reach_orbs_level from ..locs import ScoutLocations as Scouts diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py index bfcbce606c9e..eb5ac00aaea5 100644 --- a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py +++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py @@ -2,7 +2,8 @@ from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld, CompletionCondition +from ..Options import EnableOrbsanity, CompletionCondition +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/LavaTubeRegions.py b/worlds/jakanddaxter/regs/LavaTubeRegions.py index 383ea524d541..e26cb8eb4ebc 100644 --- a/worlds/jakanddaxter/regs/LavaTubeRegions.py +++ b/worlds/jakanddaxter/regs/LavaTubeRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_reach_orbs_level from ..locs import CellLocations as Cells, ScoutLocations as Scouts diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py index 4d3a5e68d010..e02b07733c55 100644 --- a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py +++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py index 5c7ba031d5af..512649c6b890 100644 --- a/worlds/jakanddaxter/regs/MistyIslandRegions.py +++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index 23314a0dc41c..0e45bf7451f4 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_reach_orbs_level from ..locs import ScoutLocations as Scouts from worlds.generic.Rules import add_rule diff --git a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py index 6fbdcee3d0ae..0cb5483badb5 100644 --- a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py +++ b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_reach_orbs_level from ..locs import CellLocations as Cells, ScoutLocations as Scouts diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py index 706c44bc1624..1fc7a83d1c42 100644 --- a/worlds/jakanddaxter/regs/RockVillageRegions.py +++ b/worlds/jakanddaxter/regs/RockVillageRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py index a42e3a0b2b64..0c7697eed6fc 100644 --- a/worlds/jakanddaxter/regs/SandoverVillageRegions.py +++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py index 7d43fe18fbe6..61fb2bdc6e19 100644 --- a/worlds/jakanddaxter/regs/SentinelBeachRegions.py +++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py index d0bc64292b83..0f72c8b8bd66 100644 --- a/worlds/jakanddaxter/regs/SnowyMountainRegions.py +++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py @@ -2,7 +2,8 @@ from BaseClasses import CollectionState from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py index fe641c2cbce4..589703facba0 100644 --- a/worlds/jakanddaxter/regs/SpiderCaveRegions.py +++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py index 11934aa703ef..63ef898d1579 100644 --- a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py +++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py @@ -1,7 +1,8 @@ from typing import List from .RegionBase import JakAndDaxterRegion -from .. import EnableOrbsanity, JakAndDaxterWorld +from ..Options import EnableOrbsanity +from .. import JakAndDaxterWorld from ..Rules import can_free_scout_flies, can_reach_orbs_level from ..locs import ScoutLocations as Scouts From 95ec860330699139a8da1494626879549d9dc75a Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:30:50 -0400 Subject: [PATCH 62/70] Fix region rule bug with Punch for Klaww. --- worlds/jakanddaxter/regs/MountainPassRegions.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py index 0e45bf7451f4..1f919629bd33 100644 --- a/worlds/jakanddaxter/regs/MountainPassRegions.py +++ b/worlds/jakanddaxter/regs/MountainPassRegions.py @@ -31,9 +31,15 @@ def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxte shortcut = JakAndDaxterRegion("Shortcut", player, multiworld, level_name, 0) shortcut.add_cell_locations([110]) - main_area.connect(race) + # Of course, in order to make it to the race region, you must defeat Klaww. He's not optional. + # So we need to set up this inter-region rule as well (or make it free if the setting is off). + if world.options.require_punch_for_klaww: + main_area.connect(race, rule=lambda state: state.has("Punch", player)) + else: + main_area.connect(race) - # You cannot go backwards from Klaww. + # You actually can go backwards from the race back to Klaww's area. + race.connect(main_area) race.connect(shortcut, rule=lambda state: state.has("Yellow Eco Switch", player)) shortcut.connect(race) From 15145f956999f196fe23fd613c23923ee9373436 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:23:44 -0400 Subject: [PATCH 63/70] Include Punch For Klaww in slot data. --- worlds/jakanddaxter/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 7ebf10e75a20..574e78843a0e 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -377,4 +377,5 @@ def fill_slot_data(self) -> Dict[str, Any]: "lava_tube_cell_count", "citizen_orb_trade_amount", "oracle_orb_trade_amount", - "jak_completion_condition") + "jak_completion_condition", + "require_punch_for_klaww") From 5a2da8ea4db34c7fbb71cb7d88f91e1f914f4e68 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:22:13 -0400 Subject: [PATCH 64/70] Update worlds/jakanddaxter/__init__.py Co-authored-by: Scipio Wright --- worlds/jakanddaxter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 574e78843a0e..4a818c5d0377 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -378,4 +378,4 @@ def fill_slot_data(self) -> Dict[str, Any]: "citizen_orb_trade_amount", "oracle_orb_trade_amount", "jak_completion_condition", - "require_punch_for_klaww") + "require_punch_for_klaww",) From 9bec9377fd2006d92f1d562d80d17e4393a18fc2 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:50:30 -0400 Subject: [PATCH 65/70] Temper and Harden Text Client (#52) * Provide config path so OpenGOAL can use mod-specific saves and settings. * Add versioning to MemoryReader. Harden the client against user errors. * Updated comments. * Add Deathlink as a "statement of intent" to the YAML. Small updates to client. * Revert deathlink changes. * Update error message. * Added color markup to log messages printed in text client. * Separate loggers by agent, write markup to GUI and non-markup to disk simultaneously. * Refactor MemoryReader callbacks from main_tick to constructor. * Make callback names more... informative. * Give users explicit instructions in error messages. --- worlds/jakanddaxter/Client.py | 175 ++++++++++++++++----- worlds/jakanddaxter/Items.py | 34 ++-- worlds/jakanddaxter/client/MemoryReader.py | 163 +++++++++++++------ worlds/jakanddaxter/client/ReplClient.py | 112 ++++++++----- 4 files changed, 348 insertions(+), 136 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 3b770327dca1..c00d9fa18656 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,5 +1,8 @@ +import logging import os import subprocess +from logging import Logger + import colorama import asyncio @@ -12,7 +15,7 @@ import Utils from NetUtils import ClientStatus -from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled +from CommonClient import ClientCommandProcessor, CommonContext, server_loop, gui_enabled from .Options import EnableOrbsanity from .GameID import jak1_name @@ -23,6 +26,7 @@ ModuleUpdate.update() +logger = logging.getLogger("JakClient") all_tasks: Set[Task] = set() @@ -51,7 +55,7 @@ def _cmd_repl(self, *arguments: str): - status : check internal status of the REPL.""" if arguments: if arguments[0] == "connect": - logger.info("This may take a bit... Wait for the success audio cue before continuing!") + self.ctx.on_log_info(logger, "This may take a bit... Wait for the success audio cue before continuing!") self.ctx.repl.initiated_connect = True if arguments[0] == "status": create_task_log_exception(self.ctx.repl.print_status()) @@ -64,7 +68,7 @@ def _cmd_memr(self, *arguments: str): if arguments[0] == "connect": self.ctx.memr.initiated_connect = True if arguments[0] == "status": - self.ctx.memr.print_status() + create_task_log_exception(self.ctx.memr.print_status()) class JakAndDaxterContext(CommonContext): @@ -85,8 +89,19 @@ class JakAndDaxterContext(CommonContext): memr_task: asyncio.Task def __init__(self, server_address: Optional[str], password: Optional[str]) -> None: - self.repl = JakAndDaxterReplClient() - self.memr = JakAndDaxterMemoryReader() + self.repl = JakAndDaxterReplClient(self.on_log_error, + self.on_log_warn, + self.on_log_success, + self.on_log_info) + self.memr = JakAndDaxterMemoryReader(self.on_location_check, + self.on_finish_check, + self.on_deathlink_check, + self.on_deathlink_toggle, + self.on_orb_trade, + self.on_log_error, + self.on_log_warn, + self.on_log_success, + self.on_log_info) # self.repl.load_data() # self.memr.load_data() super().__init__(server_address, password) @@ -219,7 +234,7 @@ async def ap_inform_deathlink(self): player = self.player_names[self.slot] if self.slot is not None else "Jak" death_text = self.memr.cause_of_death.replace("Jak", player) await self.send_death(death_text) - logger.info(death_text) + self.on_log_warn(logger, death_text) # Reset all flags, but leave the death count alone. self.memr.send_deathlink = False @@ -246,6 +261,33 @@ async def ap_inform_orb_trade(self, orbs_changed: int): def on_orb_trade(self, orbs_changed: int): create_task_log_exception(self.ap_inform_orb_trade(orbs_changed)) + def on_log_error(self, lg: Logger, message: str): + lg.error(message) + if self.ui: + color = self.jsontotextparser.color_codes["red"] + self.ui.log_panels["Archipelago"].on_message_markup(f"[color={color}]{message}[/color]") + self.ui.log_panels["All"].on_message_markup(f"[color={color}]{message}[/color]") + + def on_log_warn(self, lg: Logger, message: str): + lg.warning(message) + if self.ui: + color = self.jsontotextparser.color_codes["orange"] + self.ui.log_panels["Archipelago"].on_message_markup(f"[color={color}]{message}[/color]") + self.ui.log_panels["All"].on_message_markup(f"[color={color}]{message}[/color]") + + def on_log_success(self, lg: Logger, message: str): + lg.info(message) + if self.ui: + color = self.jsontotextparser.color_codes["green"] + self.ui.log_panels["Archipelago"].on_message_markup(f"[color={color}]{message}[/color]") + self.ui.log_panels["All"].on_message_markup(f"[color={color}]{message}[/color]") + + def on_log_info(self, lg: Logger, message: str): + lg.info(message) + if self.ui: + self.ui.log_panels["Archipelago"].on_message_markup(f"{message}") + self.ui.log_panels["All"].on_message_markup(f"{message}") + async def run_repl_loop(self): while True: await self.repl.main_tick() @@ -253,66 +295,125 @@ async def run_repl_loop(self): async def run_memr_loop(self): while True: - await self.memr.main_tick(self.on_location_check, - self.on_finish_check, - self.on_deathlink_check, - self.on_deathlink_toggle, - self.on_orb_trade) + await self.memr.main_tick() await asyncio.sleep(0.1) async def run_game(ctx: JakAndDaxterContext): # These may already be running. If they are not running, try to start them. + # TODO - Support other OS's. cmd for some reason does not work with goalc. Pymem is Windows-only. gk_running = False try: pymem.Pymem("gk.exe") # The GOAL Kernel gk_running = True except ProcessNotFound: - logger.info("Game not running, attempting to start.") + ctx.on_log_warn(logger, "Game not running, attempting to start.") goalc_running = False try: pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL goalc_running = True except ProcessNotFound: - logger.info("Compiler not running, attempting to start.") + ctx.on_log_warn(logger, "Compiler not running, attempting to start.") - # Don't mind all the arguments, they are exactly what you get when you run "task boot-game" or "task repl". - # TODO - Support other OS's. cmd for some reason does not work with goalc. Pymem is Windows-only. - if not gk_running: - try: - gk_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] - gk_path = os.path.normpath(gk_path) - gk_path = os.path.join(gk_path, "gk.exe") - except AttributeError as e: - logger.error(f"Hosts.yaml does not contain {e.args[0]}, unable to locate game executables.") + try: + # Validate folder and file structures of the ArchipelaGOAL root directory. + root_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] + + # Always trust your instincts. + if "/" not in root_path: + msg = (f"The ArchipelaGOAL root directory contains no path. (Are you missing forward slashes?)\n" + f"Please check that the value of 'jakanddaxter_options > root_directory' in your host.yaml file " + f"is a valid existing path, and all backslashes have been replaced with forward slashes.") + ctx.on_log_error(logger, msg) return - if gk_path: - # Prefixing ampersand and wrapping gk_path in quotes is necessary for paths with spaces in them. - gk_process = subprocess.Popen( - ["powershell.exe", f"& \"{gk_path}\"", "--game jak1", "--", "-v", "-boot", "-fakeiso", "-debug"], - creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. + # Start by checking the existence of the root directory provided in the host.yaml file. + root_path = os.path.normpath(root_path) + if not os.path.exists(root_path): + msg = (f"The ArchipelaGOAL root directory does not exist, unable to locate the Game and Compiler.\n" + f"Please check that the value of 'jakanddaxter_options > root_directory' in your host.yaml file " + f"is a valid existing path, and all backslashes have been replaced with forward slashes.") + ctx.on_log_error(logger, msg) + return - if not goalc_running: - try: - goalc_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] - goalc_path = os.path.normpath(goalc_path) - goalc_path = os.path.join(goalc_path, "goalc.exe") - except AttributeError as e: - logger.error(f"Hosts.yaml does not contain {e.args[0]}, unable to locate game executables.") + # Now double-check the existence of the two executables we need. + gk_path = os.path.join(root_path, "gk.exe") + goalc_path = os.path.join(root_path, "goalc.exe") + if not os.path.exists(gk_path) or not os.path.exists(goalc_path): + msg = (f"The Game and Compiler could not be found in the ArchipelaGOAL root directory.\n" + f"Please check the value of 'jakanddaxter_options > root_directory' in your host.yaml file, " + f"and ensure that path contains gk.exe, goalc.exe, and a data folder.") + ctx.on_log_error(logger, msg) return - if goalc_path: + # IMPORTANT: Before we check the existence of the next piece, we must ask "Are you a developer?" + # The OpenGOAL Compiler checks the existence of the "data" folder to determine if you're running from source + # or from a built package. As a developer, your repository folder itself IS the data folder and the Compiler + # knows this. You would have created your "iso_data" folder here as well, so we can skip the "iso_data" check. + # HOWEVER, for everyone who is NOT a developer, we must ensure that they copied the "iso_data" folder INTO + # the "data" folder per the setup instructions. + data_path = os.path.join(root_path, "data") + if os.path.exists(data_path): + + # NOW double-check the existence of the iso_data folder under /data. This is necessary + # for the compiler to compile the game correctly. + # TODO - If the GOALC compiler is updated to take the iso_data folder as a runtime argument, + # we may be able to remove this step. + iso_data_path = os.path.join(root_path, "data", "iso_data") + if not os.path.exists(iso_data_path): + msg = (f"The iso_data folder could not be found in the ArchipelaGOAL data directory.\n" + f"Please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Advanced > Open Game Data Folder.\n" + f" Copy the iso_data folder from this location.\n" + f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL > Advanced > Open Game Data Folder.\n" + f" Paste the iso_data folder in this location.\n" + f" Click Advanced > Compile. When this is done, click Continue.\n" + f" Close all launchers, games, clients, and Powershell windows, then restart Archipelago.\n" + f"(See Setup Guide for more details.)") + ctx.on_log_error(logger, msg) + return + + # Now we can FINALLY attempt to start the programs. + if not gk_running: + # Per-mod saves and settings are stored in a spot that is a little unusual to get to. We have to .. out of + # ArchipelaGOAL root folder, then traverse down to _settings/archipelagoal. Then we normalize this path + # and pass it in as an argument to gk. This folder will be created if it does not exist. + config_relative_path = "../_settings/archipelagoal" + config_path = os.path.normpath( + os.path.join( + os.path.normpath(root_path), + os.path.normpath(config_relative_path))) + + # Prefixing ampersand and wrapping in quotes is necessary for paths with spaces in them. + gk_process = subprocess.Popen( + ["powershell.exe", + f"& \"{gk_path}\"", + f"--config-path \"{config_path}\"", + "--game jak1", + "--", "-v", "-boot", "-fakeiso", "-debug"], + creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. + + if not goalc_running: # Prefixing ampersand and wrapping goalc_path in quotes is necessary for paths with spaces in them. goalc_process = subprocess.Popen( ["powershell.exe", f"& \"{goalc_path}\"", "--game jak1"], creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. + except AttributeError as e: + ctx.on_log_error(logger, f"Host.yaml does not contain {e.args[0]}, unable to locate game executables.") + return + except FileNotFoundError as e: + msg = (f"The ArchipelaGOAL root directory path is invalid.\n" + f"Please check that the value of 'jakanddaxter_options > root_directory' in your host.yaml file " + f"is a valid existing path, and all backslashes have been replaced with forward slashes.") + ctx.on_log_error(logger, msg) + return + # Auto connect the repl and memr agents. Sleep 5 because goalc takes just a little bit of time to load, # and it's not something we can await. - logger.info("This may take a bit... Wait for the success audio cue before continuing!") + ctx.on_log_info(logger, "This may take a bit... Wait for the game's title sequence before continuing!") await asyncio.sleep(5) ctx.repl.initiated_connect = True ctx.memr.initiated_connect = True @@ -331,7 +432,7 @@ async def main(): ctx.run_cli() # Find and run the game (gk) and compiler/repl (goalc). - await run_game(ctx) + create_task_log_exception(run_game(ctx)) await ctx.exit_event.wait() await ctx.shutdown() diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 3a2efba676d1..1d5022664c8c 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -16,26 +16,26 @@ class JakAndDaxterItem(Item): 0: "Power Cell", } -# Scout flies are interchangeable within their respective sets of 7. Notice the abbreviated level name after each item. +# Scout flies are interchangeable within their respective sets of 7. Notice the level name after each item. # Also, notice that their Item ID equals their respective Power Cell's Location ID. This is necessary for # game<->archipelago communication. scout_item_table = { - 95: "Scout Fly - GR", - 75: "Scout Fly - SV", - 7: "Scout Fly - FJ", - 20: "Scout Fly - SB", - 28: "Scout Fly - MI", - 68: "Scout Fly - FC", - 76: "Scout Fly - RV", - 57: "Scout Fly - PB", - 49: "Scout Fly - LPC", - 43: "Scout Fly - BS", - 88: "Scout Fly - MP", - 77: "Scout Fly - VC", - 85: "Scout Fly - SC", - 65: "Scout Fly - SM", - 90: "Scout Fly - LT", - 91: "Scout Fly - GMC", + 95: "Scout Fly - Geyser Rock", + 75: "Scout Fly - Sandover Village", + 7: "Scout Fly - Sentinel Beach", + 20: "Scout Fly - Forbidden Jungle", + 28: "Scout Fly - Misty Island", + 68: "Scout Fly - Fire Canyon", + 76: "Scout Fly - Rock Village", + 57: "Scout Fly - Lost Precursor City", + 49: "Scout Fly - Boggy Swamp", + 43: "Scout Fly - Precursor Basin", + 88: "Scout Fly - Mountain Pass", + 77: "Scout Fly - Volcanic Crater", + 85: "Scout Fly - Snowy Mountain", + 65: "Scout Fly - Spider Cave", + 90: "Scout Fly - Lava Tube", + 91: "Scout Fly - Citadel", # Had to shorten, it was >32 characters. } # Orbs are also generic and interchangeable. diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 2b33af349e8b..edd4c6f805bd 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -1,19 +1,23 @@ +import logging import random import struct -from typing import ByteString, List, Callable +from typing import ByteString, List, Callable, Optional import json import pymem from pymem import pattern from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError from dataclasses import dataclass -from CommonClient import logger from ..locs import (OrbLocations as Orbs, CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials, OrbCacheLocations as Caches) + +logger = logging.getLogger("MemoryReader") + + # Some helpful constants. sizeof_uint64 = 8 sizeof_uint32 = 4 @@ -21,6 +25,12 @@ sizeof_float = 4 +# ***************************************************************************** +# **** This number must match (-> *ap-info-jak1* version) in ap-struct.gc! **** +# ***************************************************************************** +expected_memory_version = 2 + + # IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to # their size in bits. The address for an N-bit field must be divisible by N. Use this class to define the memory offsets # of important values in the struct. It will also do the byte alignment properly for you. @@ -89,6 +99,12 @@ def define(self, size: int, length: int = 1) -> int: my_item_name_offset = offsets.define(sizeof_uint8, 32) my_item_finder_offset = offsets.define(sizeof_uint8, 32) +# Version of the memory struct, to cut down on mod/apworld version mismatches. +memory_version_offset = offsets.define(sizeof_uint32) + +# Connection status to AP server (not the game!) +server_connection_offset = offsets.define(sizeof_uint8) + # The End. end_marker_offset = offsets.define(sizeof_uint8, 4) @@ -161,61 +177,94 @@ class JakAndDaxterMemoryReader: orbsanity_enabled: bool = False orbs_paid: int = 0 - def __init__(self, marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'): + # Game-related callbacks (inform the AP server of changes to game state) + inform_checked_location: Callable + inform_finished_game: Callable + inform_died: Callable + inform_toggled_deathlink: Callable + inform_traded_orbs: Callable + + # Logging callbacks + # These will write to the provided logger, as well as the Client GUI with color markup. + log_error: Callable # Red + log_warn: Callable # Orange + log_success: Callable # Green + log_info: Callable # White (default) + + def __init__(self, + location_check_callback: Callable, + finish_game_callback: Callable, + send_deathlink_callback: Callable, + toggle_deathlink_callback: Callable, + orb_trade_callback: Callable, + log_error_callback: Callable, + log_warn_callback: Callable, + log_success_callback: Callable, + log_info_callback: Callable, + marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'): self.marker = marker - self.connect() - - async def main_tick(self, - location_callback: Callable, - finish_callback: Callable, - deathlink_callback: Callable, - deathlink_toggle: Callable, - paid_orbs_callback: Callable): + + self.inform_checked_location = location_check_callback + self.inform_finished_game = finish_game_callback + self.inform_died = send_deathlink_callback + self.inform_toggled_deathlink = toggle_deathlink_callback + self.inform_traded_orbs = orb_trade_callback + + self.log_error = log_error_callback + self.log_warn = log_warn_callback + self.log_success = log_success_callback + self.log_info = log_info_callback + + async def main_tick(self): if self.initiated_connect: await self.connect() + await self.verify_memory_version() self.initiated_connect = False if self.connected: try: self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. except (ProcessError, MemoryReadError, WinAPIError): - logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") + self.log_error(logger, "The gk process has died. Restart the game and run \"/memr connect\" again.") self.connected = False else: return - # Save some state variables temporarily. - old_deathlink_enabled = self.deathlink_enabled + # TODO - How drastic of a change is this, to wrap all of main_tick in a self.connected check? + if self.connected: + + # Save some state variables temporarily. + old_deathlink_enabled = self.deathlink_enabled - # Read the memory address to check the state of the game. - self.read_memory() + # Read the memory address to check the state of the game. + self.read_memory() - # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. - if len(self.location_outbox) > self.outbox_index: - location_callback(self.location_outbox) - self.save_data() - self.outbox_index += 1 + # Checked Locations in game. Handle the entire outbox every tick until we're up to speed. + if len(self.location_outbox) > self.outbox_index: + self.inform_checked_location(self.location_outbox) + self.save_data() + self.outbox_index += 1 - if self.finished_game: - finish_callback() + if self.finished_game: + self.inform_finished_game() - if old_deathlink_enabled != self.deathlink_enabled: - deathlink_toggle() - logger.debug("Toggled DeathLink " + ("ON" if self.deathlink_enabled else "OFF")) + if old_deathlink_enabled != self.deathlink_enabled: + self.inform_toggled_deathlink() + logger.debug("Toggled DeathLink " + ("ON" if self.deathlink_enabled else "OFF")) - if self.send_deathlink: - deathlink_callback() + if self.send_deathlink: + self.inform_died() - if self.orbs_paid > 0: - paid_orbs_callback(self.orbs_paid) - self.orbs_paid = 0 + if self.orbs_paid > 0: + self.inform_traded_orbs(self.orbs_paid) + self.orbs_paid = 0 async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel - logger.info("Found the gk process: " + str(self.gk_process.process_id)) + logger.debug("Found the gk process: " + str(self.gk_process.process_id)) except ProcessNotFound: - logger.error("Could not find the gk process.") + self.log_error(logger, "Could not find the gk process.") self.connected = False return @@ -230,21 +279,45 @@ async def connect(self): self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, sizeof_uint64), byteorder="little", signed=False) - logger.info("Found the archipelago memory address: " + str(self.goal_address)) + logger.debug("Found the archipelago memory address: " + str(self.goal_address)) self.connected = True else: - logger.error("Could not find the archipelago memory address.") + self.log_error(logger, "Could not find the archipelago memory address!") self.connected = False - if self.connected: - logger.info("The Memory Reader is ready!") + async def verify_memory_version(self): + if not self.connected: + self.log_error(logger, "The Memory Reader is not connected!") + + memory_version: Optional[int] = None + try: + memory_version = self.read_goal_address(memory_version_offset, sizeof_uint32) + if memory_version == expected_memory_version: + self.log_success(logger, "The Memory Reader is ready!") + else: + raise MemoryReadError(memory_version_offset, sizeof_uint32) + except (ProcessError, MemoryReadError, WinAPIError): + msg = (f"The OpenGOAL memory structure is incompatible with the current Archipelago client!\n" + f" Expected Version: {str(expected_memory_version)}\n" + f" Found Version: {str(memory_version)}\n" + f"Please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Click Update (if one is available).\n" + f" Click Advanced > Compile. When this is done, click Continue.\n" + f" Click Versions and verify the latest version is marked 'Active'.\n" + f" Close all launchers, games, clients, and Powershell windows, then restart Archipelago.") + self.log_error(logger, msg) + self.connected = False - def print_status(self): - logger.info("Memory Reader Status:") - logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None")) - logger.info(" Game state memory address: " + str(self.goal_address)) - logger.info(" Last location checked: " + (str(self.location_outbox[self.outbox_index - 1]) - if self.outbox_index else "None")) + async def print_status(self): + proc_id = str(self.gk_process.process_id) if self.gk_process else "None" + last_loc = str(self.location_outbox[self.outbox_index - 1] if self.outbox_index else "None") + msg = (f"Memory Reader Status:\n" + f" Game process ID: {proc_id}\n" + f" Game state memory address: {str(self.goal_address)}\n" + f" Last location checked: {last_loc}") + await self.verify_memory_version() + self.log_info(logger, msg) def read_memory(self) -> List[int]: try: @@ -350,10 +423,10 @@ def read_memory(self) -> List[int]: completed = self.read_goal_address(completed_offset, sizeof_uint8) if completed > 0 and not self.finished_game: self.finished_game = True - logger.info("Congratulations! You finished the game!") + self.log_success(logger, "Congratulations! You finished the game!") except (ProcessError, MemoryReadError, WinAPIError): - logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.") + self.log_error(logger, "The gk process has died. Restart the game and run \"/memr connect\" again.") self.connected = False return self.location_outbox diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index 06c254de9ca9..f97a8f473d60 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -1,11 +1,12 @@ import json +import logging import queue import time import struct import random from dataclasses import dataclass from queue import Queue -from typing import Dict, Optional +from typing import Dict, Optional, Callable import pymem from pymem.exception import ProcessNotFound, ProcessError @@ -13,7 +14,6 @@ import asyncio from asyncio import StreamReader, StreamWriter, Lock -from CommonClient import logger from NetUtils import NetworkItem from ..GameID import jak1_id, jak1_max from ..Items import item_table @@ -25,6 +25,9 @@ OrbCacheLocations as Caches) +logger = logging.getLogger("ReplClient") + + @dataclass class JsonMessageData: my_item_name: Optional[str] = None @@ -53,11 +56,27 @@ class JakAndDaxterReplClient: inbox_index = 0 json_message_queue: Queue[JsonMessageData] = queue.Queue() - def __init__(self, ip: str = "127.0.0.1", port: int = 8181): + # Logging callbacks + # These will write to the provided logger, as well as the Client GUI with color markup. + log_error: Callable # Red + log_warn: Callable # Orange + log_success: Callable # Green + log_info: Callable # White (default) + + def __init__(self, + log_error_callback: Callable, + log_warn_callback: Callable, + log_success_callback: Callable, + log_info_callback: Callable, + ip: str = "127.0.0.1", + port: int = 8181): self.ip = ip self.port = port self.lock = asyncio.Lock() - self.connect() + self.log_error = log_error_callback + self.log_warn = log_warn_callback + self.log_success = log_success_callback + self.log_info = log_info_callback async def main_tick(self): if self.initiated_connect: @@ -68,12 +87,13 @@ async def main_tick(self): try: self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. except ProcessError: - logger.error("The gk process has died. Restart the game and run \"/repl connect\" again.") + self.log_error(logger, "The gk process has died. Restart the game and run \"/repl connect\" again.") self.connected = False try: self.goalc_process.read_bool(self.goalc_process.base_address) # Ping to see if it's alive. except ProcessError: - logger.error("The goalc process has died. Restart the compiler and run \"/repl connect\" again.") + self.log_error(logger, + "The goalc process has died. Restart the compiler and run \"/repl connect\" again.") self.connected = False else: return @@ -111,22 +131,22 @@ async def send_form(self, form: str, print_ok: bool = True) -> bool: logger.debug(response) return True else: - logger.error(f"Unexpected response from REPL: {response}") + self.log_error(logger, f"Unexpected response from REPL: {response}") return False async def connect(self): try: self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel - logger.info("Found the gk process: " + str(self.gk_process.process_id)) + logger.debug("Found the gk process: " + str(self.gk_process.process_id)) except ProcessNotFound: - logger.error("Could not find the gk process.") + self.log_error(logger, "Could not find the gk process.") return try: self.goalc_process = pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL - logger.info("Found the goalc process: " + str(self.goalc_process.process_id)) + logger.debug("Found the goalc process: " + str(self.goalc_process.process_id)) except ProcessNotFound: - logger.error("Could not find the goalc process.") + self.log_error(logger, "Could not find the goalc process.") return try: @@ -139,9 +159,10 @@ async def connect(self): if "Connected to OpenGOAL" and "nREPL!" in welcome_message: logger.debug(welcome_message) else: - logger.error(f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"") + self.log_error(logger, + f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"") except ConnectionRefusedError as e: - logger.error(f"Unable to connect to REPL websocket: {e.strerror}") + self.log_error(logger, f"Unable to connect to REPL websocket: {e.strerror}") return ok_count = 0 @@ -175,7 +196,7 @@ async def connect(self): if await self.send_form("(set! *cheat-mode* #f)", print_ok=False): ok_count += 1 - # Run the retail game start sequence (while still in debug). + # Run the retail game start sequence (while still connected with REPL). if await self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"): ok_count += 1 @@ -186,25 +207,29 @@ async def connect(self): self.connected = False if self.connected: - logger.info("The REPL is ready!") + self.log_success(logger, "The REPL is ready!") async def print_status(self): - logger.info("REPL Status:") - logger.info(" REPL process ID: " + (str(self.goalc_process.process_id) if self.goalc_process else "None")) - logger.info(" Game process ID: " + (str(self.gk_process.process_id) if self.gk_process else "None")) + gc_proc_id = str(self.goalc_process.process_id) if self.goalc_process else "None" + gk_proc_id = str(self.gk_process.process_id) if self.gk_process else "None" + msg = (f"REPL Status:\n" + f" REPL process ID: {gc_proc_id}\n" + f" Game process ID: {gk_proc_id}\n") try: if self.reader and self.writer: addr = self.writer.get_extra_info("peername") - logger.info(" Game websocket: " + (str(addr) if addr else "None")) + addr = str(addr) if addr else "None" + msg += f" Game websocket: {addr}\n" await self.send_form("(dotimes (i 1) " "(sound-play-by-name " "(static-sound-name \"menu-close\") " "(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False) except ConnectionResetError: - logger.warn(" Connection to the game was lost or reset!") - logger.info(" Did you hear the success audio cue?") - logger.info(" Last item received: " + (str(getattr(self.item_inbox[self.inbox_index], "item")) - if self.inbox_index else "None")) + msg += f" Connection to the game was lost or reset!" + last_item = str(getattr(self.item_inbox[self.inbox_index], "item")) if self.inbox_index else "None" + msg += f" Last item received: {last_item}\n" + msg += f" Did you hear the success audio cue?" + self.log_info(logger, msg) # To properly display in-game text, it must be alphanumeric and uppercase. # I also only allotted 32 bytes to each string in OpenGOAL, so we must truncate. @@ -255,7 +280,7 @@ async def receive_item(self): elif ap_id == jak1_max: await self.receive_green_eco() # Ponder why I chose to do ID's this way. else: - raise KeyError(f"Tried to receive item with unknown AP ID {ap_id}.") + self.log_error(logger, f"Tried to receive item with unknown AP ID {ap_id}!") async def receive_power_cell(self, ap_id: int) -> bool: cell_id = Cells.to_game_id(ap_id) @@ -266,7 +291,7 @@ async def receive_power_cell(self, ap_id: int) -> bool: if ok: logger.debug(f"Received a Power Cell!") else: - logger.error(f"Unable to receive a Power Cell!") + self.log_error(logger, f"Unable to receive a Power Cell!") return ok async def receive_scout_fly(self, ap_id: int) -> bool: @@ -278,7 +303,7 @@ async def receive_scout_fly(self, ap_id: int) -> bool: if ok: logger.debug(f"Received a {item_table[ap_id]}!") else: - logger.error(f"Unable to receive a {item_table[ap_id]}!") + self.log_error(logger, f"Unable to receive a {item_table[ap_id]}!") return ok async def receive_special(self, ap_id: int) -> bool: @@ -290,7 +315,7 @@ async def receive_special(self, ap_id: int) -> bool: if ok: logger.debug(f"Received special unlock {item_table[ap_id]}!") else: - logger.error(f"Unable to receive special unlock {item_table[ap_id]}!") + self.log_error(logger, f"Unable to receive special unlock {item_table[ap_id]}!") return ok async def receive_move(self, ap_id: int) -> bool: @@ -302,7 +327,7 @@ async def receive_move(self, ap_id: int) -> bool: if ok: logger.debug(f"Received the ability to {item_table[ap_id]}!") else: - logger.error(f"Unable to receive the ability to {item_table[ap_id]}!") + self.log_error(logger, f"Unable to receive the ability to {item_table[ap_id]}!") return ok async def receive_precursor_orb(self, ap_id: int) -> bool: @@ -314,7 +339,7 @@ async def receive_precursor_orb(self, ap_id: int) -> bool: if ok: logger.debug(f"Received {orb_amount} Precursor Orbs!") else: - logger.error(f"Unable to receive {orb_amount} Precursor Orbs!") + self.log_error(logger, f"Unable to receive {orb_amount} Precursor Orbs!") return ok # Green eco pills are our filler item. Use the get-pickup event instead to handle being full health. @@ -323,7 +348,7 @@ async def receive_green_eco(self) -> bool: if ok: logger.debug(f"Received a green eco pill!") else: - logger.error(f"Unable to receive a green eco pill!") + self.log_error(logger, f"Unable to receive a green eco pill!") return ok async def receive_deathlink(self) -> bool: @@ -343,7 +368,7 @@ async def receive_deathlink(self) -> bool: if ok: logger.debug(f"Received deathlink signal!") else: - logger.error(f"Unable to receive deathlink signal!") + self.log_error(logger, f"Unable to receive deathlink signal!") return ok async def subtract_traded_orbs(self, orb_count: int) -> bool: @@ -357,11 +382,15 @@ async def subtract_traded_orbs(self, orb_count: int) -> bool: if ok: logger.debug(f"Subtracting {orb_count} traded orbs!") else: - logger.error(f"Unable to subtract {orb_count} traded orbs!") + self.log_error(logger, f"Unable to subtract {orb_count} traded orbs!") return ok return True + # OpenGOAL has a limit of 8 parameters per function. We've already hit this limit. We may have to split these + # options into two groups, both of which required to be sent successfully, in the future. + # TODO - Alternatively, define a new datatype in OpenGOAL that holds all these options, instantiate the type here, + # and rewrite the ap-setup-options! function to take that instance as input. async def setup_options(self, os_option: int, os_bundle: int, fc_count: int, mp_count: int, @@ -373,14 +402,23 @@ async def setup_options(self, f"(the float {lt_count}) (the float {ct_amount}) " f"(the float {ot_amount}) (the uint {goal_id}))") message = (f"Setting options: \n" - f" Orbsanity Option {os_option}, Orbsanity Bundle {os_bundle}, \n" - f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n" - f" LT Cell Count {lt_count}, Citizen Orb Amt {ct_amount}, \n" - f" Oracle Orb Amt {ot_amount}, Completion GOAL {goal_id}... ") + f" Orbsanity Option {os_option}, Orbsanity Bundle {os_bundle}, \n" + f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n" + f" LT Cell Count {lt_count}, Citizen Orb Amt {ct_amount}, \n" + f" Oracle Orb Amt {ot_amount}, Completion GOAL {goal_id}... ") if ok: logger.debug(message + "Success!") + status = 1 + else: + self.log_error(logger, message + "Failed!") + status = 2 + + ok = await self.send_form(f"(ap-set-connection-status! (the uint {status}))") + if ok: + logger.debug(f"Connection Status {status} set!") else: - logger.error(message + "Failed!") + self.log_error(logger, f"Connection Status {status} failed to set!") + return ok async def save_data(self): From 86460b4c5e7800dd5107d8a615516e9e1d641b9a Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:24:09 -0400 Subject: [PATCH 66/70] Stellar Messaging (#54) * Use new ap-messenger functions for text writing. * Remove Powershell requirement, bump memory version to 3. * Error message update w/ instructions for game crash. * Create no console window for gk. --- worlds/jakanddaxter/Client.py | 30 +++-- worlds/jakanddaxter/client/MemoryReader.py | 30 ++++- worlds/jakanddaxter/client/ReplClient.py | 45 ++++--- worlds/jakanddaxter/docs/setup_en.md | 144 +++++++++++---------- 4 files changed, 141 insertions(+), 108 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index c00d9fa18656..7885a9dbc1e6 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -302,7 +302,7 @@ async def run_memr_loop(self): async def run_game(ctx: JakAndDaxterContext): # These may already be running. If they are not running, try to start them. - # TODO - Support other OS's. cmd for some reason does not work with goalc. Pymem is Windows-only. + # TODO - Support other OS's. Pymem is Windows-only. gk_running = False try: pymem.Pymem("gk.exe") # The GOAL Kernel @@ -370,7 +370,7 @@ async def run_game(ctx: JakAndDaxterContext): f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL > Advanced > Open Game Data Folder.\n" f" Paste the iso_data folder in this location.\n" f" Click Advanced > Compile. When this is done, click Continue.\n" - f" Close all launchers, games, clients, and Powershell windows, then restart Archipelago.\n" + f" Close all launchers, games, clients, and console windows, then restart Archipelago.\n" f"(See Setup Guide for more details.)") ctx.on_log_error(logger, msg) return @@ -386,20 +386,24 @@ async def run_game(ctx: JakAndDaxterContext): os.path.normpath(root_path), os.path.normpath(config_relative_path))) - # Prefixing ampersand and wrapping in quotes is necessary for paths with spaces in them. - gk_process = subprocess.Popen( - ["powershell.exe", - f"& \"{gk_path}\"", - f"--config-path \"{config_path}\"", - "--game jak1", - "--", "-v", "-boot", "-fakeiso", "-debug"], - creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. + # The game freezes if text is inadvertently selected in the stdout/stderr data streams. Let's pipe those + # streams to a file, and let's not clutter the screen with another console window. + log_path = os.path.join(Utils.user_path("logs"), "JakAndDaxterGame.txt") + log_path = os.path.normpath(log_path) + with open(log_path, "w") as log_file: + gk_process = subprocess.Popen( + [gk_path, "--game", "jak1", + "--config-path", config_path, + "--", "-v", "-boot", "-fakeiso", "-debug"], + stdout=log_file, + stderr=log_file, + creationflags=subprocess.CREATE_NO_WINDOW) if not goalc_running: - # Prefixing ampersand and wrapping goalc_path in quotes is necessary for paths with spaces in them. + # This needs to be a new console. The REPL console cannot share a window with any other process. goalc_process = subprocess.Popen( - ["powershell.exe", f"& \"{goalc_path}\"", "--game jak1"], - creationflags=subprocess.CREATE_NEW_CONSOLE) # These need to be new consoles for stability. + [goalc_path, "--game", "jak1"], + creationflags=subprocess.CREATE_NEW_CONSOLE) except AttributeError as e: ctx.on_log_error(logger, f"Host.yaml does not contain {e.args[0]}, unable to locate game executables.") diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index edd4c6f805bd..65469006d7da 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -28,7 +28,7 @@ # ***************************************************************************** # **** This number must match (-> *ap-info-jak1* version) in ap-struct.gc! **** # ***************************************************************************** -expected_memory_version = 2 +expected_memory_version = 3 # IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to @@ -225,7 +225,15 @@ async def main_tick(self): try: self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. except (ProcessError, MemoryReadError, WinAPIError): - self.log_error(logger, "The gk process has died. Restart the game and run \"/memr connect\" again.") + msg = (f"Error reading game memory! (Did the game crash?)\n" + f"Please close all open windows and reopen the Jak and Daxter Client " + f"from the Archipelago Launcher.\n" + f"If the game and compiler do not restart automatically, please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Then click Advanced > Play in Debug Mode.\n" + f" Then click Advanced > Open REPL.\n" + f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.") + self.log_error(logger, msg) self.connected = False else: return @@ -264,7 +272,7 @@ async def connect(self): self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel logger.debug("Found the gk process: " + str(self.gk_process.process_id)) except ProcessNotFound: - self.log_error(logger, "Could not find the gk process.") + self.log_error(logger, "Could not find the game process.") self.connected = False return @@ -305,7 +313,7 @@ async def verify_memory_version(self): f" Click Update (if one is available).\n" f" Click Advanced > Compile. When this is done, click Continue.\n" f" Click Versions and verify the latest version is marked 'Active'.\n" - f" Close all launchers, games, clients, and Powershell windows, then restart Archipelago.") + f" Close all launchers, games, clients, and console windows, then restart Archipelago.") self.log_error(logger, msg) self.connected = False @@ -404,7 +412,7 @@ def read_memory(self) -> List[int]: bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(level, bundle, bundle_size)) if bundle_ap_id not in self.location_outbox: self.location_outbox.append(bundle_ap_id) - logger.debug("Checked orb bundle: " + str(bundle_ap_id)) + logger.debug(f"Checked orb bundle: L{level} {bundle}") # Global Orbsanity option. Index 16 refers to all orbs found regardless of level. if orbsanity_option == 2: @@ -418,7 +426,7 @@ def read_memory(self) -> List[int]: bundle_ap_id = Orbs.to_ap_id(Orbs.find_address(16, bundle, bundle_size)) if bundle_ap_id not in self.location_outbox: self.location_outbox.append(bundle_ap_id) - logger.debug("Checked orb bundle: " + str(bundle_ap_id)) + logger.debug(f"Checked orb bundle: G {bundle}") completed = self.read_goal_address(completed_offset, sizeof_uint8) if completed > 0 and not self.finished_game: @@ -426,7 +434,15 @@ def read_memory(self) -> List[int]: self.log_success(logger, "Congratulations! You finished the game!") except (ProcessError, MemoryReadError, WinAPIError): - self.log_error(logger, "The gk process has died. Restart the game and run \"/memr connect\" again.") + msg = (f"Error reading game memory! (Did the game crash?)\n" + f"Please close all open windows and reopen the Jak and Daxter Client " + f"from the Archipelago Launcher.\n" + f"If the game and compiler do not restart automatically, please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Then click Advanced > Play in Debug Mode.\n" + f" Then click Advanced > Open REPL.\n" + f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.") + self.log_error(logger, msg) self.connected = False return self.location_outbox diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py index f97a8f473d60..f811c883dbdb 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -87,13 +87,28 @@ async def main_tick(self): try: self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive. except ProcessError: - self.log_error(logger, "The gk process has died. Restart the game and run \"/repl connect\" again.") + msg = (f"Error reading game memory! (Did the game crash?)\n" + f"Please close all open windows and reopen the Jak and Daxter Client " + f"from the Archipelago Launcher.\n" + f"If the game and compiler do not restart automatically, please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Then click Advanced > Play in Debug Mode.\n" + f" Then click Advanced > Open REPL.\n" + f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.") + self.log_error(logger, msg) self.connected = False try: self.goalc_process.read_bool(self.goalc_process.base_address) # Ping to see if it's alive. except ProcessError: - self.log_error(logger, - "The goalc process has died. Restart the compiler and run \"/repl connect\" again.") + msg = (f"Error sending data to compiler! (Did the compiler crash?)\n" + f"Please close all open windows and reopen the Jak and Daxter Client " + f"from the Archipelago Launcher.\n" + f"If the game and compiler do not restart automatically, please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Then click Advanced > Play in Debug Mode.\n" + f" Then click Advanced > Open REPL.\n" + f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.") + self.log_error(logger, msg) self.connected = False else: return @@ -139,14 +154,14 @@ async def connect(self): self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel logger.debug("Found the gk process: " + str(self.gk_process.process_id)) except ProcessNotFound: - self.log_error(logger, "Could not find the gk process.") + self.log_error(logger, "Could not find the game process.") return try: self.goalc_process = pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL logger.debug("Found the goalc process: " + str(self.goalc_process.process_id)) except ProcessNotFound: - self.log_error(logger, "Could not find the goalc process.") + self.log_error(logger, "Could not find the compiler process.") return try: @@ -244,22 +259,16 @@ def queue_game_text(self, my_item_name, my_item_finder, their_item_name, their_i self.json_message_queue.put(JsonMessageData(my_item_name, my_item_finder, their_item_name, their_item_owner)) # OpenGOAL can handle both its own string datatype and C-like character pointers (charp). - # So for the game to constantly display this information in the HUD, we have to write it - # to a memory address as a char*. async def write_game_text(self, data: JsonMessageData): - logger.debug(f"Sending info to in-game display!") + logger.debug(f"Sending info to the in-game messenger!") body = "" - if data.my_item_name: - body += (f" (charp<-string (-> *ap-info-jak1* my-item-name)" - f" {self.sanitize_game_text(data.my_item_name)})") - if data.my_item_finder: - body += (f" (charp<-string (-> *ap-info-jak1* my-item-finder)" + if data.my_item_name and data.my_item_finder: + body += (f" (append-messages (-> *ap-messenger* 0) \'recv " + f" {self.sanitize_game_text(data.my_item_name)} " f" {self.sanitize_game_text(data.my_item_finder)})") - if data.their_item_name: - body += (f" (charp<-string (-> *ap-info-jak1* their-item-name)" - f" {self.sanitize_game_text(data.their_item_name)})") - if data.their_item_owner: - body += (f" (charp<-string (-> *ap-info-jak1* their-item-owner)" + if data.their_item_name and data.their_item_owner: + body += (f" (append-messages (-> *ap-messenger* 0) \'sent " + f" {self.sanitize_game_text(data.their_item_name)} " f" {self.sanitize_game_text(data.their_item_owner)})") await self.send_form(f"(begin {body} (none))", print_ok=False) diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index f74b85cd0f56..ec0f5cfeecd6 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -10,26 +10,68 @@ At this time, this method of setup works on Windows only, but Linux support is a ## Installation via OpenGOAL Launcher -- Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation). - - **You must set up a vanilla installation of Jak and Daxter before you can install mods for it.** +**You must set up a vanilla installation of Jak and Daxter before you can install mods for it.** + +- Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation). - Follow the setup process for adding mods to the OpenGOAL Launcher. See [here](https://jakmods.dev/). - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar. - Click `Features` in the bottom right corner, then click `Mods`. - Under `Available Mods`, click `ArchipelaGOAL`. The mod should begin installing. When it is done, click `Continue` in the bottom right corner. -- Once you are back in the mod menu, click on `ArchipelaGOAL` from the `Installed Mods` list. -- **As a temporary measure, you need to copy the extracted ISO data to the mod directory so the compiler will work properly.** - - If you have the NTSC version of the game, follow the `The Game Fails To Load The Title Screen` instructions below. - - If you have the PAL version of the game, follow the `Special PAL Instructions` instructions **instead.** -- **If you installed the OpenGOAL Launcher to a non-default directory, you must now follow these steps.** - - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - - Click the Jak and Daxter logo on the left sidebar. - - Click `Features` in the bottom right corner, then click `Mods`. - - Under `Installed Mods`, then click `ArchipelaGOAL`, then click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. - - In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Take note of this directory. - - Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. - - Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the directory you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. - - **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** + +### For NTSC versions of the game, follow these steps. + +- Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory. +- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. +- In the bottom right corner, click `Advanced`, then click `Open Game Data Folder`. +- Paste the `iso_data` folder you copied earlier. +- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. +- In the bottom right corner, click `Advanced`, then click `Compile`. + +### For PAL versions of the game, follow these steps. + +PAL versions of the game seem to require additional troubleshooting/setup in order to work properly. +Below are some instructions that may help. +If you see `-- Compilation Error! --` after pressing `Compile` or Launching the ArchipelaGOAL mod, try these steps. + +- Remove these folders if you have them: + - `/iso_data` + - `/iso_data` + - `/data/iso_data` +- Place your Jak1 ISO in `` and rename it to `JakAndDaxter.iso` +- Type `cmd` in Windows search, right click `Command Prompt`, and pick `Run as Administrator` +- Run `cd ` +- Then run `.\extractor.exe --extract --extract-path .\data\iso_data "JakAndDaxter.iso"` + - This command should end by saying `Uses Decompiler Config Version - ntsc_v1` or `... - pal`. Take note of this message. +- If you saw `ntsc_v1`: + - In cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "ntsc_v1" data\iso_data data\decompiler_out` +- If you saw `pal`: + - Rename `\data\iso_data\jak1` to `jak1_pal` + - Back in cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "pal" data\iso_data data\decompiler_out` + - Rename `\data\iso_data\jak1_pal` back to `jak1` + - Rename `\data\decompiler_out\jak1_pal` back to `jak1` +- Open a **brand new** console window and launch the compiler: + - `cd ` + - `.\goalc.exe --user-auto --game jak1` + - From the compiler (in the same window): `(mi)`. This should compile the game. **Note that the parentheses are important.** + - **Don't close this first terminal, you will need it at the end.** +- Then, open **another brand new** console window and execute the game: + - `cd ` + - `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug` +- Finally, **from the first console still in the GOALC compiler**, connect to the game: `(lt)`. + +### For OpenGOAL Launchers installed to a non-default directory, follow these steps. + +- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). +- Click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`. +- Under `Installed Mods`, then click `ArchipelaGOAL`, then click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. +- In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Take note of this directory. +- Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. +- Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the directory you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. +- **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** ``` jakanddaxter_options: @@ -38,7 +80,7 @@ jakanddaxter_options: root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal" ``` - - Save the file and close it. +- Save the file and close it. - **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you (see below). ## Updates and New Releases via OpenGOAL Launcher @@ -51,7 +93,8 @@ If you are in the middle of an async game, and you do not want to update the mod - Under `Installed Mods`, click `ArchipelaGOAL`. - Click `Update` to download and install any new updates that have been released. - You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it. -- **After the update is installed, you must click `Advanced`, then click `Compile` to make the update take effect.** +- **After the update is installed, you will need to copy your `iso_data` folder to the mod's data directory, as you did during install.** +- **Then you must click `Advanced`, then click `Compile` to make the update take effect.** ## Starting a Game @@ -59,14 +102,14 @@ If you are in the middle of an async game, and you do not want to update the mod - Run the Archipelago Launcher. - From the right-most list, find and click `Jak and Daxter Client`. -- 4 new windows should appear: - - Two powershell windows will open to run the OpenGOAL compiler and the game. They should take about 30 seconds to compile. +- 3 new windows should appear: + - The OpenGOAL compiler will launch and compile the game. They should take about 30 seconds to compile. - You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section. - The game window itself will launch, and Jak will be standing outside Samos's Hut. - Once compilation is complete, the title intro sequence will start. - Finally, the Archipelago text client will open. - If you see `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. -- You can *minimize* the 2 powershell windows, **BUT DO NOT CLOSE THEM.** They are required for Archipelago and the game to communicate with each other. +- You can *minimize* the Compiler window, **BUT DO NOT CLOSE IT.** It is required for Archipelago and the game to communicate with each other. - Use the text client to connect to the Archipelago server while on the title screen. This will communicate your current settings to the game. - Start a new game in the title screen, and play through the cutscenes. - Once you reach Geyser Rock, you can start the game! @@ -83,13 +126,13 @@ If you are in the middle of an async game, and you do not want to update the mod ### The Game Fails To Load The Title Screen -You may start the game via the Text Client, but it never loads in the title screen. Check the Compiler window and you may see red and yellow errors like this. +You may start the game via the Text Client, but it never loads in the title screen. Check the Compiler window: you may see red and yellow errors like this. ``` -- Compilation Error! -- ``` -If this happens, follow these instructions. If you are using a PAL version of the game, you should skip these instructions and follow `Special PAL Instructions` below. +If this happens, follow these instructions. If you are using a PAL version of the game, you should skip these instructions and follow the `Special PAL Instructions` above. - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory. @@ -101,64 +144,25 @@ If this happens, follow these instructions. If you are using a PAL version of th - Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - In the bottom right corner, click `Advanced`, then click `Compile`. -### The Text Client Says "The process has died" +### The Text Client Says "Error reading game memory!" or "Error sending data to compiler" -If at any point the text client says `The process has died`, you will need to restart the appropriate application. +If at any point the text client says this, you will need to restart the **all** of these applications. +- Close all open windows: the client, the compiler, and the game. - Run the OpenGOAL Launcher, then click `Features`, then click `Mods`, then click `ArchipelaGOAL`. -- If the gk process died, click `Advanced`, then click `Play in Debug Mode`. -- If the goalc process died, click `Advanced`, then click `Open REPL`. -- Then enter the following commands into the text client to reconnect everything to the game. - - `/repl connect` - - `/memr connect` +- Click `Advanced`, then click `Play in Debug Mode`. +- Click `Advanced`, then click `Open REPL`. +- Then close and reopen the Jak and Daxter Client from the Archipelago Launcher. - Once these are done, you can enter `/repl status` and `/memr status` in the text client to verify. -### The Game Freezes On The Same Two Frames, But The Music Is Still Playing - -If the game freezes by replaying the same two frames over and over, but the music still runs in the background, you may have accidentally interacted with the powershell windows in the background. They halt the game if you scroll up in them, highlight text in them, etc. - -- To unfreeze the game, scroll to the very bottom of the powershell window and right click. That will release powershell from your control and allow the game to continue. -- It is recommended to keep these windows minimized and out of your way. - ### The Client Cannot Open A REPL Connection If the client cannot open a REPL connection to the game, you may need to ensure you are not hosting anything on ports `8181` and `8112`. -### Special PAL Instructions - -PAL versions of the game seem to require additional troubleshooting/setup in order to work properly. Below are some instructions that may help. -If you see `-- Compilation Error! --` after pressing `Compile` or Launching the ArchipelaGOAL mod, try these steps. - -- Remove these folders if you have them: - - `/iso_data` - - `/iso_data` - - `/data/iso_data` -- Place your Jak1 ISO in `` and rename it to `JakAndDaxter.iso` -- Type `cmd` in Windows search, right click `Command Prompt`, and pick `Run as Administrator` -- Run `cd ` -- Then run `.\extractor.exe --extract --extract-path .\data\iso_data "JakAndDaxter.iso"` - - This command should end by saying `Uses Decompiler Config Version - ntsc_v1` or `... - pal`. Take note of this message. -- If you saw `ntsc_v1`: - - In cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "ntsc_v1" data\iso_data data\decompiler_out` -- If you saw `pal`: - - Rename `\data\iso_data\jak1` to `jak1_pal` - - Back in cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "pal" data\iso_data data\decompiler_out` - - Rename `\data\iso_data\jak1_pal` back to `jak1` - - Rename `\data\decompiler_out\jak1_pal` back to `jak1` -- Open a **brand new** Powershell window and launch the compiler: - - `cd ` - - `.\goalc.exe --user-auto --game jak1` - - From the compiler (in the same window): `(mi)`. This should compile the game. **Note that the parentheses are important.** - - **Don't close this first terminal, you will need it at the end.** -- Then, open **another brand new** Powershell window and execute the game: - - `cd ` - - `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug` -- Finally, **from the first Powershell still in the GOALC compiler**, connect to the game: `(lt)`. - ## Known Issues -- The game needs to boot in debug mode in order to allow the REPL to connect to it. We disable debug mode once we connect to the AP server. -- The REPL Powershell window is orphaned once you close the game - you will have to kill it manually when you stop playing. -- The powershell windows cannot be run as background processes due to how the REPL works, so the best we can do is minimize them. +- The game needs to boot in debug mode in order to allow the compiler to connect to it. **Clicking "Play" on the mod page in the OpenGOAL Launcher will not work.** +- The Compiler console window is orphaned once you close the game - you will have to kill it manually when you stop playing. +- The console windows cannot be run as background processes due to how the REPL works, so the best we can do is minimize them. - Orbsanity checks may show up out of order in the text client. - Large item releases may take up to several minutes for the game to process them all. From 077179678bdde9db7ba5f8a70ce92fcfd7c5545f Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 29 Oct 2024 00:12:32 -0400 Subject: [PATCH 67/70] ISO Data Enhancement (#58) * Add iso-path as argument to GOAL compiler. # Conflicts: # worlds/jakanddaxter/Client.py * More resilient handling of iso_path. * Fixed scout fly ID mismatches. * Corrected iso_data subpath. --- worlds/jakanddaxter/Client.py | 85 +++++++++++++++++++++-------------- worlds/jakanddaxter/Items.py | 14 +++--- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index 7885a9dbc1e6..bddf1bdd8c5f 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -348,42 +348,15 @@ async def run_game(ctx: JakAndDaxterContext): ctx.on_log_error(logger, msg) return - # IMPORTANT: Before we check the existence of the next piece, we must ask "Are you a developer?" - # The OpenGOAL Compiler checks the existence of the "data" folder to determine if you're running from source - # or from a built package. As a developer, your repository folder itself IS the data folder and the Compiler - # knows this. You would have created your "iso_data" folder here as well, so we can skip the "iso_data" check. - # HOWEVER, for everyone who is NOT a developer, we must ensure that they copied the "iso_data" folder INTO - # the "data" folder per the setup instructions. - data_path = os.path.join(root_path, "data") - if os.path.exists(data_path): - - # NOW double-check the existence of the iso_data folder under /data. This is necessary - # for the compiler to compile the game correctly. - # TODO - If the GOALC compiler is updated to take the iso_data folder as a runtime argument, - # we may be able to remove this step. - iso_data_path = os.path.join(root_path, "data", "iso_data") - if not os.path.exists(iso_data_path): - msg = (f"The iso_data folder could not be found in the ArchipelaGOAL data directory.\n" - f"Please follow these steps:\n" - f" Run the OpenGOAL Launcher, click Jak and Daxter > Advanced > Open Game Data Folder.\n" - f" Copy the iso_data folder from this location.\n" - f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL > Advanced > Open Game Data Folder.\n" - f" Paste the iso_data folder in this location.\n" - f" Click Advanced > Compile. When this is done, click Continue.\n" - f" Close all launchers, games, clients, and console windows, then restart Archipelago.\n" - f"(See Setup Guide for more details.)") - ctx.on_log_error(logger, msg) - return - # Now we can FINALLY attempt to start the programs. if not gk_running: - # Per-mod saves and settings are stored in a spot that is a little unusual to get to. We have to .. out of - # ArchipelaGOAL root folder, then traverse down to _settings/archipelagoal. Then we normalize this path - # and pass it in as an argument to gk. This folder will be created if it does not exist. + # Per-mod saves and settings are stored outside the ArchipelaGOAL root folder, so we have to traverse + # a relative path, normalize it, and pass it in as an argument to gk. This folder will be created if + # it does not exist. config_relative_path = "../_settings/archipelagoal" config_path = os.path.normpath( os.path.join( - os.path.normpath(root_path), + root_path, os.path.normpath(config_relative_path))) # The game freezes if text is inadvertently selected in the stdout/stderr data streams. Let's pipe those @@ -400,10 +373,54 @@ async def run_game(ctx: JakAndDaxterContext): creationflags=subprocess.CREATE_NO_WINDOW) if not goalc_running: + # For the OpenGOAL Compiler, the existence of the "data" subfolder indicates you are running it from + # a built package. This subfolder is treated as its proj_path. + proj_path = os.path.join(root_path, "data") + if os.path.exists(proj_path): + + # Look for "iso_data" path to automate away an oft-forgotten manual step of mod updates. + # All relative paths should start from root_path and end with "jak1". + goalc_args = [] + possible_relative_paths = { + "../../../../../active/jak1/data/iso_data/jak1", + "./data/iso_data/jak1", + } + + for iso_relative_path in possible_relative_paths: + iso_path = os.path.normpath( + os.path.join( + root_path, + os.path.normpath(iso_relative_path))) + + if os.path.exists(iso_path): + goalc_args = [goalc_path, "--game", "jak1", "--proj-path", proj_path, "--iso-path", iso_path] + logger.debug(f"iso_data folder found: {iso_path}") + break + else: + logger.debug(f"iso_data folder not found, continuing: {iso_path}") + + if not goalc_args: + msg = (f"The iso_data folder could not be found.\n" + f"Please follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Advanced > Open Game Data Folder.\n" + f" Copy the iso_data folder from this location.\n" + f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL > Advanced > " + f"Open Game Data Folder.\n" + f" Paste the iso_data folder in this location.\n" + f" Click Advanced > Compile. When this is done, click Continue.\n" + f" Close all launchers, games, clients, and console windows, then restart Archipelago.\n" + f"(See Setup Guide for more details.)") + ctx.on_log_error(logger, msg) + return + + # The non-existence of the "data" subfolder indicates you are running it from source, as a developer. + # The compiler will traverse upward to find the project path on its own. It will also assume your + # "iso_data" folder is at the root of your repository. Therefore, we don't need any of those arguments. + else: + goalc_args = [goalc_path, "--game", "jak1"] + # This needs to be a new console. The REPL console cannot share a window with any other process. - goalc_process = subprocess.Popen( - [goalc_path, "--game", "jak1"], - creationflags=subprocess.CREATE_NEW_CONSOLE) + goalc_process = subprocess.Popen(goalc_args, creationflags=subprocess.CREATE_NEW_CONSOLE) except AttributeError as e: ctx.on_log_error(logger, f"Host.yaml does not contain {e.args[0]}, unable to locate game executables.") diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py index 1d5022664c8c..00d6cb98097c 100644 --- a/worlds/jakanddaxter/Items.py +++ b/worlds/jakanddaxter/Items.py @@ -22,18 +22,18 @@ class JakAndDaxterItem(Item): scout_item_table = { 95: "Scout Fly - Geyser Rock", 75: "Scout Fly - Sandover Village", - 7: "Scout Fly - Sentinel Beach", - 20: "Scout Fly - Forbidden Jungle", + 7: "Scout Fly - Forbidden Jungle", + 20: "Scout Fly - Sentinel Beach", 28: "Scout Fly - Misty Island", 68: "Scout Fly - Fire Canyon", 76: "Scout Fly - Rock Village", - 57: "Scout Fly - Lost Precursor City", - 49: "Scout Fly - Boggy Swamp", - 43: "Scout Fly - Precursor Basin", + 57: "Scout Fly - Precursor Basin", + 49: "Scout Fly - Lost Precursor City", + 43: "Scout Fly - Boggy Swamp", 88: "Scout Fly - Mountain Pass", 77: "Scout Fly - Volcanic Crater", - 85: "Scout Fly - Snowy Mountain", - 65: "Scout Fly - Spider Cave", + 85: "Scout Fly - Spider Cave", + 65: "Scout Fly - Snowy Mountain", 90: "Scout Fly - Lava Tube", 91: "Scout Fly - Citadel", # Had to shorten, it was >32 characters. } From ff5165e8b99f404028d0429bad3df3f11865522a Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:27:36 -0400 Subject: [PATCH 68/70] Update memory version to 4. --- worlds/jakanddaxter/client/MemoryReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py index 65469006d7da..de675c4efa28 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -28,7 +28,7 @@ # ***************************************************************************** # **** This number must match (-> *ap-info-jak1* version) in ap-struct.gc! **** # ***************************************************************************** -expected_memory_version = 3 +expected_memory_version = 4 # IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to From d0d609a930cb9dcf7a17faeb539f5e944ca3d5aa Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:34:37 -0500 Subject: [PATCH 69/70] Docs update for iso_data. --- .../en_Jak and Daxter The Precursor Legacy.md | 4 +- worlds/jakanddaxter/docs/setup_en.md | 50 ++++++++----------- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md index 8e72cef88167..f9bd239915af 100644 --- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md +++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md @@ -154,10 +154,10 @@ you to miss important progression items and prevent you (and others) from comple When you connect your text client to the Archipelago Server, the server will tell the game what settings were chosen for this seed, and the game will apply those settings automatically. You can verify (but DO NOT ALTER) these settings -by navigating to `Options`, then `Archipelago Options`. +by navigating to `Options`, then `Archipelago Options`, then `Seed Options`. ## I got soft-locked and can't leave, how do I get out of here? -Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Warp To Home`. +Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Warp To Home`. Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back to the nearest sage's hut to continue your journey. diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index ec0f5cfeecd6..847ebbbb1ff0 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -18,15 +18,26 @@ At this time, this method of setup works on Windows only, but Linux support is a - Click the Jak and Daxter logo on the left sidebar. - Click `Features` in the bottom right corner, then click `Mods`. - Under `Available Mods`, click `ArchipelaGOAL`. The mod should begin installing. When it is done, click `Continue` in the bottom right corner. +- Click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. +- In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Copy this path. +- Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. +- Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the path you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. +- **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** + +``` +jakanddaxter_options: + # Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). + # Ensure this path contains forward slashes (/) only. + root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal" +``` + +- Save the file and close it. +- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you. ### For NTSC versions of the game, follow these steps. -- Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory. -- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. -- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. -- In the bottom right corner, click `Advanced`, then click `Open Game Data Folder`. -- Paste the `iso_data` folder you copied earlier. -- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar. +- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). +- Click the Jak and Daxter logo on the left sidebar. - Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - In the bottom right corner, click `Advanced`, then click `Compile`. @@ -62,38 +73,15 @@ If you see `-- Compilation Error! --` after pressing `Compile` or Launching the - `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug` - Finally, **from the first console still in the GOALC compiler**, connect to the game: `(lt)`. -### For OpenGOAL Launchers installed to a non-default directory, follow these steps. - -- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). -- Click the Jak and Daxter logo on the left sidebar. -- Click `Features` in the bottom right corner, then click `Mods`. -- Under `Installed Mods`, then click `ArchipelaGOAL`, then click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. -- In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Take note of this directory. -- Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. -- Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the directory you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. -- **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** - -``` -jakanddaxter_options: - # Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). - # Ensure this path contains forward slashes (/) only. - root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal" -``` - -- Save the file and close it. -- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you (see below). - ## Updates and New Releases via OpenGOAL Launcher If you are in the middle of an async game, and you do not want to update the mod, you do not need to do this step. The mod will only update when you tell it to. - Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). - Click the Jak and Daxter logo on the left sidebar. -- Click `Features` in the bottom right corner, then click `Mods`. -- Under `Installed Mods`, click `ArchipelaGOAL`. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. - Click `Update` to download and install any new updates that have been released. - You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it. -- **After the update is installed, you will need to copy your `iso_data` folder to the mod's data directory, as you did during install.** - **Then you must click `Advanced`, then click `Compile` to make the update take effect.** ## Starting a Game @@ -111,6 +99,7 @@ If you are in the middle of an async game, and you do not want to update the mod - If you see `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. - You can *minimize* the Compiler window, **BUT DO NOT CLOSE IT.** It is required for Archipelago and the game to communicate with each other. - Use the text client to connect to the Archipelago server while on the title screen. This will communicate your current settings to the game. + - Once you see `AP CONNECTED!` on the title screen, you should be ready. - Start a new game in the title screen, and play through the cutscenes. - Once you reach Geyser Rock, you can start the game! - You can leave Geyser Rock immediately if you so choose - just step on the warp gate button. @@ -120,6 +109,7 @@ If you are in the middle of an async game, and you do not want to update the mod - The same steps as New Game apply, with some exceptions: - Connect to the Archipelago server **BEFORE** you load your save file. This is to allow AP to give the game your current settings and all the items you had previously. - **THESE SETTINGS AFFECT LOADING AND SAVING OF SAVE FILES, SO IT IS IMPORTANT TO DO THIS FIRST.** + - Once you see `AP CONNECTED!` on the title screen, you should be ready. - Then, instead of choosing `New Game` in the title menu, choose `Load Game`, then choose the save file **CORRESPONDING TO YOUR CURRENT ARCHIPELAGO CONNECTION.** ## Troubleshooting From 0ae5faa862a439996f8953ad5e36ce07b9a6e67e Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:36:42 -0500 Subject: [PATCH 70/70] Auto Detect OpenGOAL Install (#63) * Auto detect OpenGOAL install path. Also fix Deathlink on server connection. * Updated docs, add instructions to error messages. * Slight tweak to error text. --- worlds/jakanddaxter/Client.py | 140 +++++++++++++++++++++++---- worlds/jakanddaxter/__init__.py | 12 ++- worlds/jakanddaxter/docs/setup_en.md | 44 ++++++--- 3 files changed, 159 insertions(+), 37 deletions(-) diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index bddf1bdd8c5f..e737cbb67f7b 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -1,5 +1,7 @@ import logging import os +import sys +import json import subprocess from logging import Logger @@ -156,6 +158,10 @@ async def get_orb_balance(): create_task_log_exception(get_orb_balance()) + # Tell the server if Deathlink is enabled or disabled in the in-game options. + # This allows us to "remember" the user's choice. + self.on_deathlink_toggle() + if cmd == "Retrieved": if f"jakanddaxter_{self.auth}_orbs_paid" in args["keys"]: orbs_traded = args["keys"][f"jakanddaxter_{self.auth}_orbs_paid"] @@ -299,10 +305,90 @@ async def run_memr_loop(self): await asyncio.sleep(0.1) +def find_root_directory(ctx: JakAndDaxterContext): + + # The path to this file is platform-dependent. + if sys.platform == "win32": + appdata = os.getenv("APPDATA") + settings_path = os.path.normpath(f"{appdata}/OpenGOAL-Launcher/settings.json") + elif sys.platform == "linux": + home = os.path.expanduser("~") + settings_path = os.path.normpath(f"{home}/.config/OpenGOAL-Launcher/settings.json") + elif sys.platform == "darwin": + home = os.path.expanduser("~") # MacOS + settings_path = os.path.normpath(f"{home}/Library/Application Support/OpenGOAL-Launcher/settings.json") + else: + ctx.on_log_error(logger, f"Unknown operating system: {sys.platform}!") + return + + # Boilerplate message that all error messages in this function should add at the end. + alt_instructions = (f"Please verify that OpenGOAL and ArchipelaGOAL are installed properly. " + f"If the problem persists, follow these steps:\n" + f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n" + f" Then click Advanced > Open Game Data Folder.\n" + f" Go up one folder, then copy this path.\n" + f" Run the Archipelago Launcher, click Open host.yaml.\n" + f" Set the value of 'jakanddaxter_options > root_directory' to this path.\n" + f" Replace all backslashes in the path with forward slashes.\n" + f" Set the value of 'jakanddaxter_options > auto_detect_root_directory' to false, " + f"then save and close the host.yaml file.\n" + f" Close all launchers, games, clients, and console windows, then restart Archipelago.") + + if not os.path.exists(settings_path): + msg = (f"Unable to locate the ArchipelaGOAL install directory: the OpenGOAL settings file does not exist.\n" + f"{alt_instructions}") + ctx.on_log_error(logger, msg) + return + + with open(settings_path, "r") as f: + load = json.load(f) + + jak1_installed = load["games"]["Jak 1"]["isInstalled"] + if not jak1_installed: + msg = (f"Unable to locate the ArchipelaGOAL install directory: " + f"The OpenGOAL Launcher is missing a normal install of Jak 1!\n" + f"{alt_instructions}") + ctx.on_log_error(logger, msg) + return + + mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"] + if mod_sources is None: + msg = (f"Unable to locate the ArchipelaGOAL install directory: " + f"No mod sources have been configured in the OpenGOAL Launcher!\n" + f"{alt_instructions}") + ctx.on_log_error(logger, msg) + return + + # Mods can come from multiple user-defined sources. + # Make no assumptions about where ArchipelaGOAL comes from, we should find it ourselves. + archipelagoal_source = None + for src in mod_sources: + for mod in mod_sources[src].keys(): + if mod == "archipelagoal": + archipelagoal_source = src + # TODO - We could verify the right version is installed. Do we need to? + if archipelagoal_source is None: + msg = (f"Unable to locate the ArchipelaGOAL install directory: " + f"The ArchipelaGOAL mod is not installed in the OpenGOAL Launcher!\n" + f"{alt_instructions}") + ctx.on_log_error(logger, msg) + return + + # This is just the base OpenGOAL directory, we need to go deeper. + base_path = load["installationDir"] + mod_relative_path = f"features/jak1/mods/{archipelagoal_source}/archipelagoal" + mod_path = os.path.normpath( + os.path.join( + os.path.normpath(base_path), + os.path.normpath(mod_relative_path))) + + return mod_path + + async def run_game(ctx: JakAndDaxterContext): # These may already be running. If they are not running, try to start them. - # TODO - Support other OS's. Pymem is Windows-only. + # TODO - Support other OS's. 1: Pymem is Windows-only. 2: on Linux, there's no ".exe." gk_running = False try: pymem.Pymem("gk.exe") # The GOAL Kernel @@ -318,23 +404,31 @@ async def run_game(ctx: JakAndDaxterContext): ctx.on_log_warn(logger, "Compiler not running, attempting to start.") try: - # Validate folder and file structures of the ArchipelaGOAL root directory. - root_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] - - # Always trust your instincts. - if "/" not in root_path: - msg = (f"The ArchipelaGOAL root directory contains no path. (Are you missing forward slashes?)\n" - f"Please check that the value of 'jakanddaxter_options > root_directory' in your host.yaml file " - f"is a valid existing path, and all backslashes have been replaced with forward slashes.") - ctx.on_log_error(logger, msg) - return - - # Start by checking the existence of the root directory provided in the host.yaml file. + auto_detect_root_directory = Utils.get_settings()["jakanddaxter_options"]["auto_detect_root_directory"] + if auto_detect_root_directory: + root_path = find_root_directory(ctx) + else: + root_path = Utils.get_settings()["jakanddaxter_options"]["root_directory"] + + # Always trust your instincts... the user may not have entered their root_directory properly. + # We don't have to do this check if the root directory was auto-detected. + if "/" not in root_path: + msg = (f"The ArchipelaGOAL root directory contains no path. (Are you missing forward slashes?)\n" + f"Please check your host.yaml file.\n" + f"Verify the value of 'jakanddaxter_options > root_directory' is a valid existing path, " + f"and all backslashes have been replaced with forward slashes.") + ctx.on_log_error(logger, msg) + return + + # Start by checking the existence of the root directory provided in the host.yaml file (or found automatically). root_path = os.path.normpath(root_path) if not os.path.exists(root_path): msg = (f"The ArchipelaGOAL root directory does not exist, unable to locate the Game and Compiler.\n" - f"Please check that the value of 'jakanddaxter_options > root_directory' in your host.yaml file " - f"is a valid existing path, and all backslashes have been replaced with forward slashes.") + f"Please check your host.yaml file.\n" + f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL " + f"is installed properly.\n" + f"If it is false, check the value of 'jakanddaxter_options > root_directory'. " + f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.") ctx.on_log_error(logger, msg) return @@ -343,8 +437,11 @@ async def run_game(ctx: JakAndDaxterContext): goalc_path = os.path.join(root_path, "goalc.exe") if not os.path.exists(gk_path) or not os.path.exists(goalc_path): msg = (f"The Game and Compiler could not be found in the ArchipelaGOAL root directory.\n" - f"Please check the value of 'jakanddaxter_options > root_directory' in your host.yaml file, " - f"and ensure that path contains gk.exe, goalc.exe, and a data folder.") + f"Please check your host.yaml file.\n" + f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL " + f"is installed properly.\n" + f"If it is false, check the value of 'jakanddaxter_options > root_directory'. " + f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.") ctx.on_log_error(logger, msg) return @@ -426,9 +523,12 @@ async def run_game(ctx: JakAndDaxterContext): ctx.on_log_error(logger, f"Host.yaml does not contain {e.args[0]}, unable to locate game executables.") return except FileNotFoundError as e: - msg = (f"The ArchipelaGOAL root directory path is invalid.\n" - f"Please check that the value of 'jakanddaxter_options > root_directory' in your host.yaml file " - f"is a valid existing path, and all backslashes have been replaced with forward slashes.") + msg = (f"The following path could not be found: {e.filename}\n" + f"Please check your host.yaml file.\n" + f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL " + f"is installed properly.\n" + f"If it is false, check the value of 'jakanddaxter_options > root_directory'." + f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.") ctx.on_log_error(logger, msg) return diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py index 4a818c5d0377..8fba65d1981d 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -51,15 +51,23 @@ def launch_client(): class JakAndDaxterSettings(settings.Group): class RootDirectory(settings.UserFolderPath): """Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). - Ensure this path contains forward slashes (/) only.""" + Ensure this path contains forward slashes (/) only. This setting only applies if + Auto Detect Root Directory is set to false.""" description = "ArchipelaGOAL Root Directory" + class AutoDetectRootDirectory(settings.Bool): + """Attempt to find the OpenGOAL installation and the mod executables (gk.exe and goalc.exe) + automatically. If set to true, the ArchipelaGOAL Root Directory setting is ignored.""" + description = "ArchipelaGOAL Auto Detect Root Directory" + class EnforceFriendlyOptions(settings.Bool): """Enforce friendly player options in both single and multiplayer seeds. Disabling this allows for more disruptive and challenging options, but may impact seed generation. Use at your own risk!""" description = "ArchipelaGOAL Enforce Friendly Options" - root_directory: RootDirectory = RootDirectory("%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal") + root_directory: RootDirectory = RootDirectory( + "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal") + auto_detect_root_directory: Union[AutoDetectRootDirectory, bool] = True enforce_friendly_options: Union[EnforceFriendlyOptions, bool] = True diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md index 847ebbbb1ff0..7d39e8deb529 100644 --- a/worlds/jakanddaxter/docs/setup_en.md +++ b/worlds/jakanddaxter/docs/setup_en.md @@ -18,21 +18,7 @@ At this time, this method of setup works on Windows only, but Linux support is a - Click the Jak and Daxter logo on the left sidebar. - Click `Features` in the bottom right corner, then click `Mods`. - Under `Available Mods`, click `ArchipelaGOAL`. The mod should begin installing. When it is done, click `Continue` in the bottom right corner. -- Click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. -- In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Copy this path. -- Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. -- Search for `jakanddaxter_options`, then find the `root_directory` entry underneath it. Paste the path you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. -- **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** - -``` -jakanddaxter_options: - # Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe). - # Ensure this path contains forward slashes (/) only. - root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal" -``` - -- Save the file and close it. -- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Jak and Daxter Client should handle everything for you. +- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Archipelago Client should handle everything for you. ### For NTSC versions of the game, follow these steps. @@ -114,6 +100,34 @@ If you are in the middle of an async game, and you do not want to update the mod ## Troubleshooting +### The Text Client Says "Unable to locate the OpenGOAL install directory" + +Normally, the Archipelago client should be able to find your OpenGOAL installation automatically. + +If it cannot, you may have to tell it yourself. Follow these instructions. + +- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it). +- Click the Jak and Daxter logo on the left sidebar. +- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`. +- Click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory. +- In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Copy this path. +- Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file. +- Search for `jakanddaxter_options`, and you will need to make 2 changes here. +- First, find the `root_directory` entry. Paste the path you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes. +- **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.** + +```yaml + root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal" +``` + +- Second, find the `root_directory` entry. Change this to `false`. You do not need to use double quotes. + +```yaml + auto_detect_root_directory: true +``` + +- Save the file and close it. + ### The Game Fails To Load The Title Screen You may start the game via the Text Client, but it never loads in the title screen. Check the Compiler window: you may see red and yellow errors like this.