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.
This commit is contained in:
massimilianodelliubaldini
2024-07-11 18:42:04 -04:00
committed by GitHub
parent f8751699fc
commit f7b688de38
28 changed files with 820 additions and 194 deletions

View File

@@ -107,6 +107,16 @@ class JakAndDaxterContext(CommonContext):
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 @@ class JakAndDaxterContext(CommonContext):
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 @@ class JakAndDaxterContext(CommonContext):
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 @@ class JakAndDaxterContext(CommonContext):
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)

View File

@@ -39,8 +39,29 @@ scout_item_table = {
}
# 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 @@ move_item_table = {
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.
}

View File

@@ -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

View File

@@ -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 @@ location_table = {
**{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}
}

View File

@@ -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)

View File

@@ -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 @@ from .Regions import JakAndDaxterRegion
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)

View File

@@ -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 @@ class JakAndDaxterWorld(World):
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 @@ class JakAndDaxterWorld(World):
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 @@ class JakAndDaxterWorld(World):
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")

View File

@@ -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 pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinA
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 @@ orb_caches_checked_offset = offsets.define(sizeof_uint32, 16)
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 @@ class JakAndDaxterMemoryReader:
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 @@ class JakAndDaxterMemoryReader:
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 @@ class JakAndDaxterMemoryReader:
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.")

View File

@@ -257,15 +257,15 @@ class JakAndDaxterReplClient:
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 @@ class JakAndDaxterReplClient:
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 = {

View File

@@ -29,11 +29,11 @@ At this time, this method of setup works on Windows only, but Linux support is a
- `C:\Users\<YourName>\AppData\Roaming\OpenGOAL-Mods\archipelagoal\iso_data` should have *all* the same files as
- `C:\Users\<YourName>\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***

View File

@@ -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 @@ from ..GameID import jak1_id
# 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.
# 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
# Geyser Rock
locGR_orbTable = {
# 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
}
}
# 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 = {
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"])
}

View File

@@ -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 build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[
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 build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[
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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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 build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[
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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -3,7 +3,8 @@ 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,
from ..locs import (OrbLocations as Orbs,
CellLocations as Cells,
ScoutLocations as Scouts,
SpecialLocations as Specials,
OrbCacheLocations as Caches)
@@ -31,7 +32,8 @@ class JakAndDaxterRegion(Region):
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 @@ class JakAndDaxterRegion(Region):
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 @@ class JakAndDaxterRegion(Region):
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 @@ class JakAndDaxterRegion(Region):
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):
"""
Helper function to add Locations. Not to be used directly.
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.
"""
location = JakAndDaxterLocation(self.player, location_table[ap_id], ap_id, self)
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, name, ap_id, self)
if access_rule:
location.access_rule = access_rule
self.locations.append(location)

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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 build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[
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]

View File

@@ -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]

View File

@@ -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]