diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py index b6e982a9e3..51a38bb3f5 100644 --- a/worlds/jakanddaxter/Client.py +++ b/worlds/jakanddaxter/Client.py @@ -6,6 +6,7 @@ import asyncio 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 @@ class JakAndDaxterContext(CommonContext): 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 @@ class JakAndDaxterContext(CommonContext): 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 d051f15869..2fa0654726 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 ef56137cf1..ac18ce84c8 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 @@ location_table = { **{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 fa4a6aa880..16bab791ff 100644 --- a/worlds/jakanddaxter/Regions.py +++ b/worlds/jakanddaxter/Regions.py @@ -4,10 +4,27 @@ 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 +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 bb355e0e2c..f0c4a7e694 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 75a87c9dbf..8bdfffc5ba 100644 --- a/worlds/jakanddaxter/__init__.py +++ b/worlds/jakanddaxter/__init__.py @@ -5,12 +5,13 @@ 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 .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 @@ class JakAndDaxterWorld(World): 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 4373e9676e..64a37352bc 100644 --- a/worlds/jakanddaxter/client/MemoryReader.py +++ b/worlds/jakanddaxter/client/MemoryReader.py @@ -4,20 +4,25 @@ 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 +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 @@ class JakAndDaxterMemoryReader: 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 @@ class JakAndDaxterMemoryReader: 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 @@ class JakAndDaxterMemoryReader: 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 @@ class JakAndDaxterMemoryReader: 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 @@ class JakAndDaxterMemoryReader: 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 c70d633b00..e5c8030c3e 100644 --- a/worlds/jakanddaxter/client/ReplClient.py +++ b/worlds/jakanddaxter/client/ReplClient.py @@ -6,8 +6,13 @@ 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 +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 @@ class JakAndDaxterReplClient: # 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 @@ class JakAndDaxterReplClient: "(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 @@ class JakAndDaxterReplClient: "(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 scout fly {fly_id}!") + 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 special unlock {item_table[ap_id]}!") return ok diff --git a/worlds/jakanddaxter/locs/CellLocations.py b/worlds/jakanddaxter/locs/CellLocations.py index 304810af80..109e18de22 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 @@ locSV_cellTable = { 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 @@ locFJ_cellTable = { 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 @@ locSB_cellTable = { 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 @@ locMI_cellTable = { 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 @@ locRV_cellTable = { 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 @@ locPB_cellTable = { 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 @@ locLPC_cellTable = { 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 @@ locBS_cellTable = { 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 @@ 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 @@ -145,7 +154,6 @@ locVC_cellTable = { 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 @@ locSC_cellTable = { 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 @@ locSM_cellTable = { 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 @@ locGMC_cellTable = { 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 0000000000..4ce5b63812 --- /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", +}