Exempt's Code Review Updates (#43)

* Round 1 of code review updates, the easy stuff.

* Factor options checking away from region/rule creation.

* Code review updates round 2, more complex stuff.

* Code review updates round 3: the mental health annihilator

* Code review updates part 4: redemption.

* More code review feedback, simplifying code, etc.
This commit is contained in:
massimilianodelliubaldini
2024-08-27 09:56:11 -04:00
committed by GitHub
parent b7ca9cbc2f
commit 746b281f48
34 changed files with 632 additions and 554 deletions

View File

@@ -1,14 +1,17 @@
import logging
import os
import subprocess
import typing
import asyncio
import colorama
import asyncio
from asyncio import Task
from typing import Set, Awaitable, Optional, List
import pymem
from pymem.exception import ProcessNotFound, ProcessError
from pymem.exception import ProcessNotFound
import Utils
from NetUtils import ClientStatus, NetworkItem
from NetUtils import ClientStatus
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled
from .JakAndDaxterOptions import EnableOrbsanity
@@ -20,10 +23,10 @@ import ModuleUpdate
ModuleUpdate.update()
all_tasks = set()
all_tasks: Set[Task] = set()
def create_task_log_exception(awaitable: typing.Awaitable) -> asyncio.Task:
def create_task_log_exception(awaitable: Awaitable) -> asyncio.Task:
async def _log_exception(a):
try:
return await a
@@ -81,7 +84,7 @@ class JakAndDaxterContext(CommonContext):
repl_task: asyncio.Task
memr_task: asyncio.Task
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
def __init__(self, server_address: Optional[str], password: Optional[str]) -> None:
self.repl = JakAndDaxterReplClient()
self.memr = JakAndDaxterMemoryReader()
# self.repl.load_data()
@@ -195,11 +198,11 @@ class JakAndDaxterContext(CommonContext):
self.repl.received_deathlink = True
super().on_deathlink(data)
async def ap_inform_location_check(self, location_ids: typing.List[int]):
async def ap_inform_location_check(self, location_ids: List[int]):
message = [{"cmd": "LocationChecks", "locations": location_ids}]
await self.send_msgs(message)
def on_location_check(self, location_ids: typing.List[int]):
def on_location_check(self, location_ids: List[int]):
create_task_log_exception(self.ap_inform_location_check(location_ids))
async def ap_inform_finished_game(self):

View File

@@ -51,6 +51,7 @@ class GlobalOrbsanityBundleSize(Choice):
option_500_orbs = 500
option_1000_orbs = 1000
option_2000_orbs = 2000
friendly_minimum = 5
default = 20
@@ -64,6 +65,7 @@ class PerLevelOrbsanityBundleSize(Choice):
option_10_orbs = 10
option_25_orbs = 25
option_50_orbs = 50
friendly_minimum = 5
default = 25
@@ -72,6 +74,7 @@ class FireCanyonCellCount(Range):
display_name = "Fire Canyon Cell Count"
range_start = 0
range_end = 100
friendly_maximum = 30
default = 20
@@ -80,6 +83,7 @@ class MountainPassCellCount(Range):
display_name = "Mountain Pass Cell Count"
range_start = 0
range_end = 100
friendly_maximum = 60
default = 45
@@ -88,6 +92,7 @@ class LavaTubeCellCount(Range):
display_name = "Lava Tube Cell Count"
range_start = 0
range_end = 100
friendly_maximum = 90
default = 72
@@ -95,10 +100,12 @@ class LavaTubeCellCount(Range):
class CitizenOrbTradeAmount(Range):
"""Set the number of orbs you need to trade to ordinary citizens for a power cell (Mayor, Uncle, etc.).
Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000)."""
Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).
The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades)."""
display_name = "Citizen Orb Trade Amount"
range_start = 0
range_end = 222
friendly_maximum = 120
default = 90
@@ -106,10 +113,12 @@ class CitizenOrbTradeAmount(Range):
class OracleOrbTradeAmount(Range):
"""Set the number of orbs you need to trade to the Oracles for a power cell.
Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000)."""
Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).
The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades)."""
display_name = "Oracle Orb Trade Amount"
range_start = 0
range_end = 333
friendly_maximum = 150
default = 120

View File

@@ -0,0 +1,76 @@
# This contains the list of levels in Jak and Daxter.
# Not to be confused with Regions - there can be multiple Regions in every Level.
level_table = {
"Geyser Rock": {
"level_index": 0,
"orbs": 50
},
"Sandover Village": {
"level_index": 1,
"orbs": 50
},
"Sentinel Beach": {
"level_index": 2,
"orbs": 150
},
"Forbidden Jungle": {
"level_index": 3,
"orbs": 150
},
"Misty Island": {
"level_index": 4,
"orbs": 150
},
"Fire Canyon": {
"level_index": 5,
"orbs": 50
},
"Rock Village": {
"level_index": 6,
"orbs": 50
},
"Lost Precursor City": {
"level_index": 7,
"orbs": 200
},
"Boggy Swamp": {
"level_index": 8,
"orbs": 200
},
"Precursor Basin": {
"level_index": 9,
"orbs": 200
},
"Mountain Pass": {
"level_index": 10,
"orbs": 50
},
"Volcanic Crater": {
"level_index": 11,
"orbs": 50
},
"Snowy Mountain": {
"level_index": 12,
"orbs": 200
},
"Spider Cave": {
"level_index": 13,
"orbs": 200
},
"Lava Tube": {
"level_index": 14,
"orbs": 50
},
"Gol and Maia's Citadel": {
"level_index": 15,
"orbs": 200
}
}
level_table_with_global = {
**level_table,
"": {
"level_index": 16, # Global
"orbs": 2000
}
}

View File

@@ -11,9 +11,9 @@ class JakAndDaxterLocation(Location):
game: str = jak1_name
# All Locations
# Different tables for location groups.
# Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed.
location_table = {
cell_location_table = {
**{Cells.to_ap_id(k): Cells.loc7SF_cellTable[k] for k in Cells.loc7SF_cellTable},
**{Cells.to_ap_id(k): Cells.locGR_cellTable[k] for k in Cells.locGR_cellTable},
**{Cells.to_ap_id(k): Cells.locSV_cellTable[k] for k in Cells.locSV_cellTable},
@@ -30,7 +30,10 @@ location_table = {
**{Cells.to_ap_id(k): Cells.locSC_cellTable[k] for k in Cells.locSC_cellTable},
**{Cells.to_ap_id(k): Cells.locSM_cellTable[k] for k in Cells.locSM_cellTable},
**{Cells.to_ap_id(k): Cells.locLT_cellTable[k] for k in Cells.locLT_cellTable},
**{Cells.to_ap_id(k): Cells.locGMC_cellTable[k] for k in Cells.locGMC_cellTable},
**{Cells.to_ap_id(k): Cells.locGMC_cellTable[k] for k in Cells.locGMC_cellTable}
}
scout_location_table = {
**{Scouts.to_ap_id(k): Scouts.locGR_scoutTable[k] for k in Scouts.locGR_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locSV_scoutTable[k] for k in Scouts.locSV_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locFJ_scoutTable[k] for k in Scouts.locFJ_scoutTable},
@@ -46,8 +49,18 @@ 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},
**{Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable},
**{Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable},
**{Orbs.to_ap_id(k): Orbs.loc_orbBundleTable[k] for k in Orbs.loc_orbBundleTable}
**{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable}
}
special_location_table = {Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable}
cache_location_table = {Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable}
orb_location_table = {Orbs.to_ap_id(k): Orbs.loc_orbBundleTable[k] for k in Orbs.loc_orbBundleTable}
# All Locations
location_table = {
**cell_location_table,
**scout_location_table,
**special_location_table,
**cache_location_table,
**orb_location_table
}

View File

@@ -1,16 +1,11 @@
from BaseClasses import MultiWorld, CollectionState, ItemClassification
import typing
from Options import OptionError
from .JakAndDaxterOptions import (JakAndDaxterOptions,
EnableMoveRandomizer,
EnableOrbsanity,
CompletionCondition)
from .Items import (JakAndDaxterItem,
item_table,
move_item_table)
from .Rules import can_reach_orbs
from .locs import (CellLocations as Cells,
ScoutLocations as Scouts)
from .regs.RegionBase import JakAndDaxterRegion
from . import JakAndDaxterWorld
from .Items import item_table
from .JakAndDaxterOptions import EnableOrbsanity, CompletionCondition
from .Rules import can_reach_orbs_global
from .locs import CellLocations as Cells, ScoutLocations as Scouts
from .regs import (GeyserRockRegions as GeyserRock,
SandoverVillageRegions as SandoverVillage,
ForbiddenJungleRegions as ForbiddenJungle,
@@ -27,9 +22,13 @@ from .regs import (GeyserRockRegions as GeyserRock,
SnowyMountainRegions as SnowyMountain,
LavaTubeRegions as LavaTube,
GolAndMaiasCitadelRegions as GolAndMaiasCitadel)
from .regs.RegionBase import JakAndDaxterRegion
def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int):
def create_regions(world: JakAndDaxterWorld):
multiworld = world.multiworld
options = world.options
player = world.player
# Always start with Menu.
menu = JakAndDaxterRegion("Menu", player, multiworld)
@@ -42,7 +41,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player:
for scout_fly_cell in free7.locations:
# Translate from Cell AP ID to Scout AP ID using game ID as an intermediary.
scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(scout_fly_cell.address))
scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(typing.cast(int, scout_fly_cell.address)))
scout_fly_cell.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7)
multiworld.regions.append(free7)
menu.connect(free7)
@@ -52,37 +51,34 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player:
if options.enable_orbsanity == EnableOrbsanity.option_global:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld)
bundle_size = options.global_orbsanity_bundle_size.value
bundle_count = int(2000 / bundle_size)
bundle_count = 2000 // world.orb_bundle_size
for bundle_index in range(bundle_count):
# Unlike Per-Level Orbsanity, Global Orbsanity Locations always have a level_index of 16.
orbs.add_orb_locations(16,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options)
>= (bundle_size * (bundle + 1)))
can_reach_orbs_global(state, player, world, bundle))
multiworld.regions.append(orbs)
menu.connect(orbs)
# Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules.
[gr] = GeyserRock.build_regions("Geyser Rock", multiworld, options, player)
[sv] = SandoverVillage.build_regions("Sandover Village", multiworld, options, player)
[fj, fjp] = ForbiddenJungle.build_regions("Forbidden Jungle", multiworld, options, player)
[sb] = SentinelBeach.build_regions("Sentinel Beach", multiworld, options, player)
[mi] = MistyIsland.build_regions("Misty Island", multiworld, options, player)
[fc] = FireCanyon.build_regions("Fire Canyon", multiworld, options, player)
[rv, rvp, rvc] = RockVillage.build_regions("Rock Village", multiworld, options, player)
[pb] = PrecursorBasin.build_regions("Precursor Basin", multiworld, options, player)
[lpc] = LostPrecursorCity.build_regions("Lost Precursor City", multiworld, options, player)
[bs] = BoggySwamp.build_regions("Boggy Swamp", multiworld, options, player)
[mp, mpr] = MountainPass.build_regions("Mountain Pass", multiworld, options, player)
[vc] = VolcanicCrater.build_regions("Volcanic Crater", multiworld, options, player)
[sc] = SpiderCave.build_regions("Spider Cave", multiworld, options, player)
[sm] = SnowyMountain.build_regions("Snowy Mountain", multiworld, options, player)
[lt] = LavaTube.build_regions("Lava Tube", multiworld, options, player)
[gmc, fb, fd] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", multiworld, options, player)
[gr] = GeyserRock.build_regions("Geyser Rock", world)
[sv] = SandoverVillage.build_regions("Sandover Village", world)
[fj, fjp] = ForbiddenJungle.build_regions("Forbidden Jungle", world)
[sb] = SentinelBeach.build_regions("Sentinel Beach", world)
[mi] = MistyIsland.build_regions("Misty Island", world)
[fc] = FireCanyon.build_regions("Fire Canyon", world)
[rv, rvp, rvc] = RockVillage.build_regions("Rock Village", world)
[pb] = PrecursorBasin.build_regions("Precursor Basin", world)
[lpc] = LostPrecursorCity.build_regions("Lost Precursor City", world)
[bs] = BoggySwamp.build_regions("Boggy Swamp", world)
[mp, mpr] = MountainPass.build_regions("Mountain Pass", world)
[vc] = VolcanicCrater.build_regions("Volcanic Crater", world)
[sc] = SpiderCave.build_regions("Spider Cave", world)
[sm] = SnowyMountain.build_regions("Snowy Mountain", world)
[lt] = LavaTube.build_regions("Lava Tube", world)
[gmc, fb, fd] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", world)
# Configurable counts of cells for connector levels.
fc_count = options.fire_canyon_cell_count.value
@@ -129,82 +125,6 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player:
elif options.jak_completion_condition == CompletionCondition.option_open_100_cell_door:
multiworld.completion_condition[player] = lambda state: state.can_reach(fd, "Region", player)
# As a final sanity check on these options, verify that we have enough locations to allow us to cross
# the connector levels. E.g. if you set Fire Canyon count to 99, we may not have 99 Locations in hub 1.
verify_connector_level_accessibility(multiworld, options, player)
else:
raise OptionError(f"Unknown completion goal ID ({options.jak_completion_condition.value}).")
# Also verify that we didn't overload the trade amounts with more orbs than exist in the world.
verify_orbs_for_trades(multiworld, options, player)
def verify_connector_level_accessibility(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int):
# Set up a state where we only have the items we need to progress, exactly when we need them, as well as
# any items we would have/get from our other options. The only variable we're actually testing here is the
# number of power cells we need.
state = CollectionState(multiworld)
if options.enable_move_randomizer == EnableMoveRandomizer.option_false:
for move in move_item_table:
state.collect(JakAndDaxterItem(move_item_table[move], ItemClassification.progression, move, player))
thresholds = {
0: {
"option": options.fire_canyon_cell_count,
"required_items": {},
},
1: {
"option": options.mountain_pass_cell_count,
"required_items": {
33: "Warrior's Pontoons",
10945: "Double Jump",
},
},
2: {
"option": options.lava_tube_cell_count,
"required_items": {},
},
}
loc = 0
for k in thresholds:
option = thresholds[k]["option"]
required_items = thresholds[k]["required_items"]
# Given our current state (starting with 0 Power Cells), determine if there are enough
# Locations to fill with the number of Power Cells needed for the next threshold.
locations_available = multiworld.get_reachable_locations(state, player)
if len(locations_available) < option.value:
raise OptionError(f"Settings conflict with {option.display_name}: "
f"not enough potential locations ({len(locations_available)}) "
f"for the required number of power cells ({option.value}).")
# Once we've determined we can pass the current threshold, add what we need to reach the next one.
for _ in range(option.value):
state.collect(JakAndDaxterItem("Power Cell", ItemClassification.progression, loc, player))
loc += 1
for item in required_items:
state.collect(JakAndDaxterItem(required_items[item], ItemClassification.progression, item, player))
def verify_orbs_for_trades(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int):
citizen_trade_orbs = 9 * options.citizen_orb_trade_amount
if citizen_trade_orbs > 2000:
raise OptionError(f"Settings conflict with {options.citizen_orb_trade_amount.display_name}: "
f"required number of orbs to trade with citizens ({citizen_trade_orbs}) "
f"is more than all the orbs in the game (2000).")
oracle_trade_orbs = 6 * options.oracle_orb_trade_amount
if oracle_trade_orbs > 2000:
raise OptionError(f"Settings conflict with {options.oracle_orb_trade_amount.display_name}: "
f"required number of orbs to trade with oracles ({oracle_trade_orbs}) "
f"is more than all the orbs in the game (2000).")
total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
if total_trade_orbs > 2000:
raise OptionError(f"Settings conflict with Orb Trade Amounts: "
f"required number of orbs for all trades ({total_trade_orbs}) "
f"is more than all the orbs in the game (2000). "
f"Reduce the value of either {options.citizen_orb_trade_amount.display_name} "
f"or {options.oracle_orb_trade_amount.display_name}.")

View File

@@ -1,102 +1,117 @@
import math
import typing
from BaseClasses import MultiWorld, CollectionState
from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity
from .Items import orb_item_table
from Options import OptionError
from . import JakAndDaxterWorld
from .JakAndDaxterOptions import (JakAndDaxterOptions,
EnableOrbsanity,
GlobalOrbsanityBundleSize,
PerLevelOrbsanityBundleSize,
FireCanyonCellCount,
MountainPassCellCount,
LavaTubeCellCount,
CitizenOrbTradeAmount,
OracleOrbTradeAmount)
from .locs import CellLocations as Cells
from .Locations import location_table
from .Levels import level_table
from .regs.RegionBase import JakAndDaxterRegion
def can_reach_orbs(state: CollectionState,
player: int,
multiworld: MultiWorld,
options: JakAndDaxterOptions,
level_name: str = None) -> int:
def set_orb_trade_rule(world: JakAndDaxterWorld):
options = world.options
player = world.player
# Global Orbsanity and No Orbsanity both treat orbs as completely interchangeable.
# Per Level Orbsanity needs to know if you can reach orbs *in a particular level.*
if options.enable_orbsanity != EnableOrbsanity.option_per_level:
return can_reach_orbs_global(state, player, multiworld)
if options.enable_orbsanity == EnableOrbsanity.option_off:
world.can_trade = lambda state, required_orbs, required_previous_trade: (
can_trade_vanilla(state, player, required_orbs, required_previous_trade))
else:
return can_reach_orbs_level(state, player, multiworld, level_name)
world.can_trade = lambda state, required_orbs, required_previous_trade: (
can_trade_orbsanity(state, player, required_orbs, required_previous_trade))
def recalculate_reachable_orbs(state: CollectionState, player: int) -> None:
if not state.prog_items[player]["Reachable Orbs Fresh"]:
# Recalculate every level, every time the cache is stale, because you don't know
# when a specific bundle of orbs in one level may unlock access to another.
for level in level_table:
state.prog_items[player][f"{level} Reachable Orbs".strip()] = (
count_reachable_orbs_level(state, player, state.multiworld, level))
# Also recalculate the global count, still used even when Orbsanity is Off.
state.prog_items[player]["Reachable Orbs"] = count_reachable_orbs_global(state, player, state.multiworld)
state.prog_items[player]["Reachable Orbs Fresh"] = True
def count_reachable_orbs_global(state: CollectionState,
player: int,
multiworld: MultiWorld) -> int:
accessible_orbs = 0
for region in multiworld.get_regions(player):
if region.can_reach(state):
# Only cast the region when we need to.
accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count
return accessible_orbs
def count_reachable_orbs_level(state: CollectionState,
player: int,
multiworld: MultiWorld,
level_name: str = "") -> int:
accessible_orbs = 0
# Need to cast all regions upfront.
for region in typing.cast(typing.List[JakAndDaxterRegion], multiworld.get_regions(player)):
if region.level_name == level_name and region.can_reach(state):
accessible_orbs += region.orb_count
return accessible_orbs
def can_reach_orbs_global(state: CollectionState,
player: int,
multiworld: MultiWorld) -> int:
world: JakAndDaxterWorld,
bundle: int) -> 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
return accessible_orbs
recalculate_reachable_orbs(state, player)
return state.has("Reachable Orbs", player, world.orb_bundle_size * (bundle + 1))
def can_reach_orbs_level(state: CollectionState,
player: int,
multiworld: MultiWorld,
level_name: str) -> int:
world: JakAndDaxterWorld,
level_name: str,
bundle: int) -> bool:
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
recalculate_reachable_orbs(state, player)
return state.has(f"{level_name} Reachable Orbs", player, world.orb_bundle_size * (bundle + 1))
# TODO - Until we come up with a better progressive system for the traders (that avoids hard-locking if you pay the
# wrong ones and can't afford the right ones) just make all the traders locked behind the total amount to pay them all.
def can_trade(state: CollectionState,
player: int,
multiworld: MultiWorld,
options: JakAndDaxterOptions,
required_orbs: int,
required_previous_trade: int = None) -> bool:
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
bundle_size = options.level_orbsanity_bundle_size.value
return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade)
elif options.enable_orbsanity == EnableOrbsanity.option_global:
bundle_size = options.global_orbsanity_bundle_size.value
return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade)
else:
return can_trade_regular(state, player, multiworld, required_orbs, required_previous_trade)
def can_trade_regular(state: CollectionState,
def can_trade_vanilla(state: CollectionState,
player: int,
multiworld: MultiWorld,
required_orbs: int,
required_previous_trade: int = None) -> bool:
required_previous_trade: typing.Optional[int] = None) -> bool:
# We know that Orbsanity is off, so count orbs globally.
accessible_orbs = can_reach_orbs_global(state, player, multiworld)
recalculate_reachable_orbs(state, player) # With Orbsanity Off, Reachable Orbs are in fact Tradeable Orbs.
if required_previous_trade:
name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)]
return (accessible_orbs >= required_orbs
and state.can_reach(name_of_previous_trade, "Location", player=player))
else:
return accessible_orbs >= required_orbs
return (state.has("Reachable Orbs", player, required_orbs)
and state.can_reach_location(name_of_previous_trade, player=player))
return state.has("Reachable Orbs", player, required_orbs)
def can_trade_orbsanity(state: CollectionState,
player: int,
orb_bundle_size: int,
required_orbs: int,
required_previous_trade: int = None) -> bool:
required_previous_trade: typing.Optional[int] = None) -> bool:
required_count = math.ceil(required_orbs / orb_bundle_size)
orb_bundle_name = orb_item_table[orb_bundle_size]
recalculate_reachable_orbs(state, player) # Yes, even Orbsanity trades may unlock access to new Reachable Orbs.
if required_previous_trade:
name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)]
return (state.has(orb_bundle_name, player, required_count)
and state.can_reach(name_of_previous_trade, "Location", player=player))
else:
return state.has(orb_bundle_name, player, required_count)
return (state.has("Tradeable Orbs", player, required_orbs)
and state.can_reach_location(name_of_previous_trade, player=player))
return state.has("Tradeable Orbs", player, required_orbs)
def can_free_scout_flies(state: CollectionState, player: int) -> bool:
@@ -105,3 +120,65 @@ def can_free_scout_flies(state: CollectionState, player: int) -> bool:
def can_fight(state: CollectionState, player: int) -> bool:
return state.has_any({"Jump Dive", "Jump Kick", "Punch", "Kick"}, player)
def enforce_multiplayer_limits(options: JakAndDaxterOptions):
friendly_message = ""
if (options.enable_orbsanity == EnableOrbsanity.option_global
and options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum):
friendly_message += (f" "
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
f"{GlobalOrbsanityBundleSize.friendly_minimum} (currently "
f"{options.global_orbsanity_bundle_size.value}).\n")
if (options.enable_orbsanity == EnableOrbsanity.option_per_level
and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum):
friendly_message += (f" "
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (currently "
f"{options.level_orbsanity_bundle_size.value}).\n")
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
friendly_message += (f" "
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
f"{FireCanyonCellCount.friendly_maximum} (currently "
f"{options.fire_canyon_cell_count.value}).\n")
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
friendly_message += (f" "
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
f"{MountainPassCellCount.friendly_maximum} (currently "
f"{options.mountain_pass_cell_count.value}).\n")
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
friendly_message += (f" "
f"{options.lava_tube_cell_count.display_name} must be no greater than "
f"{LavaTubeCellCount.friendly_maximum} (currently "
f"{options.lava_tube_cell_count.value}).\n")
if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum:
friendly_message += (f" "
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
f"{CitizenOrbTradeAmount.friendly_maximum} (currently "
f"{options.citizen_orb_trade_amount.value}).\n")
if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum:
friendly_message += (f" "
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
f"{OracleOrbTradeAmount.friendly_maximum} (currently "
f"{options.oracle_orb_trade_amount.value}).\n")
if friendly_message != "":
raise OptionError(f"Please adjust the following Options for a multiplayer game.\n"
f"{friendly_message}")
def verify_orb_trade_amounts(options: JakAndDaxterOptions):
total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
if total_trade_orbs > 2000:
raise OptionError(f"Required number of orbs for all trades ({total_trade_orbs}) "
f"is more than all the orbs in the game (2000). "
f"Reduce the value of either {options.citizen_orb_trade_amount.display_name} "
f"or {options.oracle_orb_trade_amount.display_name}.")

View File

@@ -1,18 +1,30 @@
from typing import Dict, Any, ClassVar
from typing import Dict, Any, ClassVar, Tuple, Callable, Optional
import settings
from Utils import local_path, visualize_regions
from BaseClasses import Item, ItemClassification, Tutorial
from Utils import local_path
from BaseClasses import Item, ItemClassification, Tutorial, CollectionState
from .GameID import jak1_id, jak1_name, jak1_max
from .JakAndDaxterOptions import JakAndDaxterOptions, EnableOrbsanity
from .Locations import JakAndDaxterLocation, location_table
from .Items import JakAndDaxterItem, item_table
from .Locations import (JakAndDaxterLocation,
location_table,
cell_location_table,
scout_location_table,
special_location_table,
cache_location_table,
orb_location_table)
from .Items import (JakAndDaxterItem,
item_table,
cell_item_table,
scout_item_table,
special_item_table,
move_item_table,
orb_item_table)
from .Levels import level_table, level_table_with_global
from .locs import (CellLocations as Cells,
ScoutLocations as Scouts,
SpecialLocations as Specials,
OrbCacheLocations as Caches,
OrbLocations as Orbs)
from .Regions import create_regions
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths
@@ -79,48 +91,62 @@ class JakAndDaxterWorld(World):
item_name_to_id = {item_table[k]: k for k in item_table}
location_name_to_id = {location_table[k]: k for k in location_table}
item_name_groups = {
"Power Cells": {item_table[k]: k for k in item_table
if k in range(jak1_id, jak1_id + Scouts.fly_offset)},
"Scout Flies": {item_table[k]: k for k in item_table
if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)},
"Specials": {item_table[k]: k for k in item_table
if k in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset)},
"Moves": {item_table[k]: k for k in item_table
if k in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset)},
"Precursor Orbs": {item_table[k]: k for k in item_table
if k in range(jak1_id + Orbs.orb_offset, jak1_max)},
"Power Cells": set(cell_item_table.values()),
"Scout Flies": set(scout_item_table.values()),
"Specials": set(special_item_table.values()),
"Moves": set(move_item_table.values()),
"Precursor Orbs": set(orb_item_table.values()),
}
location_name_groups = {
"Power Cells": {location_table[k]: k for k in location_table
if k in range(jak1_id, jak1_id + Scouts.fly_offset)},
"Scout Flies": {location_table[k]: k for k in location_table
if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)},
"Specials": {location_table[k]: k for k in location_table
if k in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset)},
"Orb Caches": {location_table[k]: k for k in location_table
if k in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset)},
"Precursor Orbs": {location_table[k]: k for k in location_table
if k in range(jak1_id + Orbs.orb_offset, jak1_max)},
"Trades": {location_table[k]: k for k in location_table
if k in {Cells.to_ap_id(t) for t in {11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}}},
"Power Cells": set(cell_location_table.values()),
"Scout Flies": set(scout_location_table.values()),
"Specials": set(special_location_table.values()),
"Orb Caches": set(cache_location_table.values()),
"Precursor Orbs": set(orb_location_table.values()),
"Trades": {location_table[Cells.to_ap_id(k)] for k in
{11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}},
}
# Functions and Variables that are Options-driven, keep them as instance variables here so that we don't clog up
# the seed generation routines with options checking. So we set these once, and then just use them as needed.
can_trade: Callable[[CollectionState, int, Optional[int]], bool]
orb_bundle_size: int = 0
orb_bundle_item_name: str = ""
def generate_early(self) -> None:
# For the fairness of other players in a multiworld game, enforce some friendly limitations on our options,
# so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen.
if self.multiworld.players > 1:
from .Rules import enforce_multiplayer_limits
enforce_multiplayer_limits(self.options)
# Verify that we didn't overload the trade amounts with more orbs than exist in the world.
# This is easy to do by accident even in a single-player world.
from .Rules import verify_orb_trade_amounts
verify_orb_trade_amounts(self.options)
# Cache the orb bundle size and item name for quicker reference.
if self.options.enable_orbsanity == EnableOrbsanity.option_per_level:
self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value
self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size]
elif self.options.enable_orbsanity == EnableOrbsanity.option_global:
self.orb_bundle_size = self.options.global_orbsanity_bundle_size.value
self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size]
# Options drive which trade rules to use, so they need to be setup before we create_regions.
from .Rules import set_orb_trade_rule
set_orb_trade_rule(self)
# This will also set Locations, Location access rules, Region access rules, etc.
def create_regions(self) -> None:
create_regions(self.multiworld, self.options, self.player)
visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml")
from .Regions import create_regions
create_regions(self)
# Helper function to get the correct orb bundle size.
def get_orb_bundle_size(self) -> int:
if self.options.enable_orbsanity == EnableOrbsanity.option_per_level:
return self.options.level_orbsanity_bundle_size.value
elif self.options.enable_orbsanity == EnableOrbsanity.option_global:
return self.options.global_orbsanity_bundle_size.value
else:
return 0
# from Utils import visualize_regions
# visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml")
# Helper function to reuse some nasty if/else trees.
def item_type_helper(self, item) -> (int, ItemClassification):
def item_type_helper(self, item) -> Tuple[int, ItemClassification]:
# Make 101 Power Cells.
if item in range(jak1_id, jak1_id + Scouts.fly_offset):
classification = ItemClassification.progression_skip_balancing
@@ -144,8 +170,7 @@ class JakAndDaxterWorld(World):
# Make N Precursor Orb bundles, where N is 2000 / bundle size.
elif item in range(jak1_id + Orbs.orb_offset, jak1_max):
classification = ItemClassification.progression_skip_balancing
size = self.get_orb_bundle_size()
count = int(2000 / size) if size > 0 else 0 # Don't divide by zero!
count = 2000 // self.orb_bundle_size if self.orb_bundle_size > 0 else 0 # Don't divide by zero!
# Under normal circumstances, we will create 0 filler items.
# We will manually create filler items as needed.
@@ -168,15 +193,15 @@ class JakAndDaxterWorld(World):
# then fill the item pool with a corresponding amount of filler items.
if item_name in self.item_name_groups["Moves"] and not self.options.enable_move_randomizer:
self.multiworld.push_precollected(self.create_item(item_name))
self.multiworld.itempool += [self.create_item(self.get_filler_item_name())]
self.multiworld.itempool += [self.create_filler()]
continue
# Handle Orbsanity option.
# If it is OFF, don't add any orbs to the item pool.
# If it is OFF, don't add any orb bundles to the item pool, period.
# If it is ON, don't add any orb bundles that don't match the chosen option.
if (item_name in self.item_name_groups["Precursor Orbs"]
and ((self.options.enable_orbsanity == EnableOrbsanity.option_off
or Orbs.to_game_id(item_id) != self.get_orb_bundle_size()))):
and (self.options.enable_orbsanity == EnableOrbsanity.option_off
or item_name != self.orb_bundle_item_name)):
continue
# In every other scenario, do this.
@@ -192,6 +217,41 @@ class JakAndDaxterWorld(World):
def get_filler_item_name(self) -> str:
return "Green Eco Pill"
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change:
# No matter the option, no matter the item, set the caches to stale.
state.prog_items[self.player]["Reachable Orbs Fresh"] = False
# Matching the item name implies Orbsanity is ON, so we don't need to check the option.
# When Orbsanity is OFF, there won't even be any orb bundle items to collect.
# Give the player the appropriate number of Tradeable Orbs based on bundle size.
if item.name == self.orb_bundle_item_name:
state.prog_items[self.player]["Tradeable Orbs"] += self.orb_bundle_size
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change:
# No matter the option, no matter the item, set the caches to stale.
state.prog_items[self.player]["Reachable Orbs Fresh"] = False
# The opposite of what we did in collect: Take away from the player
# the appropriate number of Tradeable Orbs based on bundle size.
if item.name == self.orb_bundle_item_name:
state.prog_items[self.player]["Tradeable Orbs"] -= self.orb_bundle_size
# TODO - 3.8 compatibility, remove this block when no longer required.
if state.prog_items[self.player]["Tradeable Orbs"] < 1:
del state.prog_items[self.player]["Tradeable Orbs"]
if state.prog_items[self.player]["Reachable Orbs"] < 1:
del state.prog_items[self.player]["Reachable Orbs"]
for level in level_table:
if state.prog_items[self.player][f"{level} Reachable Orbs".strip()] < 1:
del state.prog_items[self.player][f"{level} Reachable Orbs".strip()]
return change
def fill_slot_data(self) -> Dict[str, Any]:
return self.options.as_dict("enable_move_randomizer",
"enable_orbsanity",

View File

@@ -99,7 +99,6 @@ def as_float(value: int) -> int:
# "Jak" to be replaced by player name in the Client.
def autopsy(died: int) -> str:
assert died > 0, f"Tried to find Jak's cause of death, but he's still alive!"
if died in [1, 2, 3, 4]:
return random.choice(["Jak said goodnight.",
"Jak stepped into the light.",
@@ -147,7 +146,7 @@ class JakAndDaxterMemoryReader:
# The memory reader just needs the game running.
gk_process: pymem.process = None
location_outbox = []
location_outbox: List[int] = []
outbox_index: int = 0
finished_game: bool = False
@@ -224,7 +223,7 @@ 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 a uint64).
# and the struct address is 8 bytes long (it's an uint64).
goal_pointer = marker_address + len(self.marker) + 4
self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, sizeof_uint64),
byteorder="little",

View File

@@ -2,7 +2,7 @@ import json
import time
import struct
import random
from typing import Dict, Callable
from typing import Dict, Optional
import pymem
from pymem.exception import ProcessNotFound, ProcessError
@@ -41,10 +41,10 @@ class JakAndDaxterReplClient:
item_inbox: Dict[int, NetworkItem] = {}
inbox_index = 0
my_item_name: str = None
my_item_finder: str = None
their_item_name: str = None
their_item_owner: str = None
my_item_name: Optional[str] = None
my_item_finder: Optional[str] = None
their_item_name: Optional[str] = None
their_item_owner: Optional[str] = None
def __init__(self, ip: str = "127.0.0.1", port: int = 8181):
self.ip = ip
@@ -353,6 +353,8 @@ class JakAndDaxterReplClient:
logger.error(f"Unable to subtract {orb_count} traded orbs!")
return ok
return True
async def setup_options(self,
os_option: int, os_bundle: int,
fc_count: int, mp_count: int,

View File

@@ -180,14 +180,14 @@ Here is how the HUD works:
Depending on the nature of the bug, there are a couple of different options.
* If you found a logical error in the randomizer, please create a new Issue
[here.](https://github.com/ArchipelaGOAL/Archipelago/issues) Use this page if:
[here](https://github.com/ArchipelaGOAL/Archipelago/issues). Use this page if:
* An item required for progression is unreachable.
* The randomizer did not respect one of the Options you chose.
* You see a mistake, typo, etc. on this webpage.
* You see an error or stack trace appear on the text client.
* If you encountered an error in OpenGOAL, please create a new Issue
[here.](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues) Use this page if:
[here](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues). Use this page if:
* You encounter a crash, freeze, reset, etc. in the game.
* You fail to send Items you find in the game to the Archipelago server.
* You fail to receive Items the server sends to you.

View File

@@ -26,8 +26,7 @@ At this time, this method of setup works on Windows only, but Linux support is a
- Click `View Folder`.
- In the new file explorer window, take note of the current path. It should contain `gk.exe` and `goalc.exe`.
- Verify that the mod launcher copied the extracted ISO files to the mod directory:
- `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` should have *all* the same files as
- `%appdata%/OpenGOAL-Mods/_iso_data`, if it doesn't, copy those files over manually.
- `%appdata%/OpenGOAL-Mods/archipelagoal/iso_data` and `%appdata%/OpenGOAL-Mods/_iso_data` should have *all* the same files; if they don't, copy those files over manually.
- And then `Recompile` if you needed to copy the files over.
- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE MOD LAUNCHER.** It will run in retail mode, which is incompatible with Archipelago. We need it to run in debug mode (see below).
@@ -43,7 +42,7 @@ At this time, this method of setup works on Windows only, but Linux support is a
- Back in the Archipelago Launcher, click `Open host.yaml`.
- In the text file that opens, search for `jakanddaxter_options`.
- You should see the block of YAML below. If you do not see it, you will need to add it.
- If the default path does not contain `gk.exe` and `goalc.exe`, you will need to provide the path you noted earlier. **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/ `.**
- If the default path does not contain `gk.exe` and `goalc.exe`, you will need to provide the path you noted earlier. **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.**
```
jakanddaxter_options:
@@ -162,8 +161,7 @@ PAL versions of the game seem to require additional troubleshooting/setup in ord
### Known Issues
- The game needs to run in debug mode in order to allow the repl to connect to it. We hide the debug text on screen and play the game's introductory cutscenes properly.
- The game needs to boot in debug mode in order to allow the repl to connect to it. We disable debug mode once we connect to the AP server.
- The powershell windows cannot be run as background processes due to how the repl works, so the best we can do is minimize them.
- The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server.
- Orbsanity checks may show up out of order in the text client.
- Large item releases may take up to several minutes for the game to process them all.

View File

@@ -11,12 +11,14 @@ from ..GameID import jak1_id
# These helper functions do all the math required to get information about each
# power cell and translate its ID between AP and OpenGOAL.
def to_ap_id(game_id: int) -> int:
assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
if game_id >= jak1_id:
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
return jak1_id + game_id
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
if ap_id < jak1_id:
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
return ap_id - jak1_id

View File

@@ -19,13 +19,15 @@ orb_cache_offset = 4096
# special check and translate its ID between AP and OpenGOAL. Similar to Scout Flies, these large numbers are not
# necessary, and we can flatten out the range in which these numbers lie.
def to_ap_id(game_id: int) -> int:
assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
if game_id >= jak1_id:
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
uncompressed_id = jak1_id + orb_cache_offset + game_id # Add the offsets and the orb cache Actor ID.
return uncompressed_id - 10344 # Subtract the smallest Actor ID.
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
if ap_id < jak1_id:
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
uncompressed_id = ap_id + 10344 # Reverse process, add back the smallest Actor ID.
return uncompressed_id - jak1_id - orb_cache_offset # Subtract the offsets.

View File

@@ -1,6 +1,5 @@
from dataclasses import dataclass
from ..GameID import jak1_id
from ..Levels import level_table_with_global
# Precursor Orbs are not necessarily given ID's by the game.
@@ -24,12 +23,14 @@ orb_offset = 32768
# These helper functions do all the math required to get information about each
# precursor orb and translate its ID between AP and OpenGOAL.
def to_ap_id(game_id: int) -> int:
assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
if game_id >= jak1_id:
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
return jak1_id + orb_offset + game_id # Add the offsets and the orb Actor ID.
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
if ap_id < jak1_id:
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets.
@@ -50,79 +51,8 @@ def create_address(level_index: int, bundle_index: int) -> int:
# What follows is our method of generating all the name/ID pairs for location_name_to_id.
# Remember that not every bundle will be used in the actual seed, we just need this as a static map of strings to ints.
level_info = {
"": {
"level_index": 16, # Global
"orbs": 2000
},
"Geyser Rock": {
"level_index": 0,
"orbs": 50
},
"Sandover Village": {
"level_index": 1,
"orbs": 50
},
"Sentinel Beach": {
"level_index": 2,
"orbs": 150
},
"Forbidden Jungle": {
"level_index": 3,
"orbs": 150
},
"Misty Island": {
"level_index": 4,
"orbs": 150
},
"Fire Canyon": {
"level_index": 5,
"orbs": 50
},
"Rock Village": {
"level_index": 6,
"orbs": 50
},
"Lost Precursor City": {
"level_index": 7,
"orbs": 200
},
"Boggy Swamp": {
"level_index": 8,
"orbs": 200
},
"Precursor Basin": {
"level_index": 9,
"orbs": 200
},
"Mountain Pass": {
"level_index": 10,
"orbs": 50
},
"Volcanic Crater": {
"level_index": 11,
"orbs": 50
},
"Snowy Mountain": {
"level_index": 12,
"orbs": 200
},
"Spider Cave": {
"level_index": 13,
"orbs": 200
},
"Lava Tube": {
"level_index": 14,
"orbs": 50
},
"Gol and Maia's Citadel": {
"level_index": 15,
"orbs": 200
}
}
loc_orbBundleTable = {
create_address(level_info[name]["level_index"], index): f"{name} Orb Bundle {index + 1}".strip()
for name in level_info
for index in range(level_info[name]["orbs"])
create_address(level_table_with_global[name]["level_index"], index): f"{name} Orb Bundle {index + 1}".strip()
for name in level_table_with_global
for index in range(level_table_with_global[name]["orbs"])
}

View File

@@ -23,7 +23,8 @@ fly_offset = 1024
# These helper functions do all the math required to get information about each
# scout fly and translate its ID between AP and OpenGOAL.
def to_ap_id(game_id: int) -> int:
assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
if game_id >= jak1_id:
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
cell_id = get_cell_id(game_id) # Get the power cell ID from the lowest 7 bits.
buzzer_index = (game_id - cell_id) >> 9 # Get the index, bit shift it down 9 places.
compressed_id = fly_offset + buzzer_index + cell_id # Add the offset, the bit-shifted index, and the cell ID.
@@ -31,7 +32,8 @@ def to_ap_id(game_id: int) -> int:
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
if ap_id < jak1_id:
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
compressed_id = ap_id - jak1_id # Reverse process. First thing: subtract the game's ID.
cell_id = get_cell_id(compressed_id) # Get the power cell ID from the lowest 7 bits.
buzzer_index = compressed_id - fly_offset - cell_id # Get the bit-shifted index.
@@ -42,7 +44,8 @@ def to_game_id(ap_id: int) -> int:
# Make sure to use this function ONLY when the input argument does NOT include jak1_id,
# because that number may flip some of the bottom 7 bits, and that will throw off this bit mask.
def get_cell_id(buzzer_id: int) -> int:
assert buzzer_id < jak1_id, f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}."
if buzzer_id >= jak1_id:
raise ValueError(f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}.")
return buzzer_id & 0b1111111
@@ -184,7 +187,7 @@ locVC_scoutTable = {
# Spider Cave
locSC_scoutTable = {
327765: "SC: Scout Fly Near Dark Dave Entrance",
327765: "SC: Scout Fly Near Dark Cave Entrance",
262229: "SC: Scout Fly In Dark Cave",
393301: "SC: Scout Fly Main Cave, Overlooking Entrance",
196693: "SC: Scout Fly Main Cave, Near Dark Crystal",

View File

@@ -21,12 +21,14 @@ 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."
if game_id >= jak1_id:
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
return jak1_id + special_offset + game_id # Add the offsets and the orb Actor ID.
def to_game_id(ap_id: int) -> int:
assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
if ap_id < jak1_id:
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
return ap_id - jak1_id - special_offset # Reverse process, subtract the offsets.

View File

@@ -1,11 +1,15 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from BaseClasses import CollectionState
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_fight, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
# This level is full of short-medium gaps that cannot be crossed by single jump alone.
# These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...)
@@ -155,15 +159,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(200 / bundle_size)
bundle_count = 200 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(8,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,12 +1,15 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_reach_orbs_level
from ..locs import CellLocations as Cells, ScoutLocations as Scouts
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
@@ -21,15 +24,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(50 / bundle_size)
bundle_count = 50 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(5,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,11 +1,14 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 25)
@@ -86,15 +89,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(150 / bundle_size)
bundle_count = 150 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(3,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,12 +1,15 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_reach_orbs_level
from ..locs import ScoutLocations as Scouts
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
main_area.add_cell_locations([92, 93])
@@ -30,15 +33,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(50 / bundle_size)
bundle_count = 50 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(0,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,18 +1,21 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from BaseClasses import CollectionState
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level
# God help me... here we go.
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
# This level is full of short-medium gaps that cannot be crossed by single jump alone.
# These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...)
def can_jump_farther(state: CollectionState, p: int) -> bool:
return (state.has("Double Jump", p)
or state.has("Jump Kick", p)
return (state.has_any({"Double Jump", "Jump Kick"}, p)
or state.has_all({"Punch", "Punch Uppercut"}, p))
def can_triple_jump(state: CollectionState, p: int) -> bool:
@@ -115,15 +118,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(200 / bundle_size)
bundle_count = 200 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(15,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,12 +1,15 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_reach_orbs_level
from ..locs import CellLocations as Cells, ScoutLocations as Scouts
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
@@ -21,15 +24,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(50 / bundle_size)
bundle_count = 50 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(14,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,11 +1,14 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
# Just the starting area.
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 4)
@@ -132,15 +135,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(200 / bundle_size)
bundle_count = 200 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(7,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,12 +1,15 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 9)
muse_course = JakAndDaxterRegion("Muse Course", player, multiworld, level_name, 21)
@@ -114,15 +117,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(150 / bundle_size)
bundle_count = 150 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(4,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,12 +1,15 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_reach_orbs_level
from ..locs import ScoutLocations as Scouts
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
# This is basically just Klaww.
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0)
@@ -37,15 +40,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(50 / bundle_size)
bundle_count = 50 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(10,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,12 +1,15 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_reach_orbs_level
from ..locs import CellLocations as Cells, ScoutLocations as Scouts
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 200)
@@ -21,15 +24,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(200 / bundle_size)
bundle_count = 200 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(9,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,7 +1,6 @@
from typing import List, Callable
from typing import Iterable, Callable, Optional
from BaseClasses import MultiWorld, Region
from ..GameID import jak1_name
from ..JakAndDaxterOptions import JakAndDaxterOptions
from ..Locations import JakAndDaxterLocation, location_table
from ..locs import (OrbLocations as Orbs,
CellLocations as Cells,
@@ -26,7 +25,7 @@ class JakAndDaxterRegion(Region):
self.level_name = level_name
self.orb_count = orb_count
def add_cell_locations(self, locations: List[int], access_rule: Callable = None):
def add_cell_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None):
"""
Adds a Power Cell Location to this region with the given access rule.
Converts Game ID's to AP ID's for you.
@@ -35,7 +34,7 @@ class JakAndDaxterRegion(Region):
ap_id = Cells.to_ap_id(loc)
self.add_jak_locations(ap_id, location_table[ap_id], access_rule)
def add_fly_locations(self, locations: List[int], access_rule: Callable = None):
def add_fly_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None):
"""
Adds a Scout Fly Location to this region with the given access rule.
Converts Game ID's to AP ID's for you.
@@ -44,7 +43,7 @@ class JakAndDaxterRegion(Region):
ap_id = Scouts.to_ap_id(loc)
self.add_jak_locations(ap_id, location_table[ap_id], access_rule)
def add_special_locations(self, locations: List[int], access_rule: Callable = None):
def add_special_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None):
"""
Adds a Special Location to this region with the given access rule.
Converts Game ID's to AP ID's for you.
@@ -55,7 +54,7 @@ class JakAndDaxterRegion(Region):
ap_id = Specials.to_ap_id(loc)
self.add_jak_locations(ap_id, location_table[ap_id], access_rule)
def add_cache_locations(self, locations: List[int], access_rule: Callable = None):
def add_cache_locations(self, locations: Iterable[int], access_rule: Optional[Callable] = None):
"""
Adds an Orb Cache Location to this region with the given access rule.
Converts Game ID's to AP ID's for you.
@@ -64,7 +63,7 @@ class JakAndDaxterRegion(Region):
ap_id = Caches.to_ap_id(loc)
self.add_jak_locations(ap_id, location_table[ap_id], access_rule)
def add_orb_locations(self, level_index: int, bundle_index: int, bundle_size: int, access_rule: Callable = None):
def add_orb_locations(self, level_index: int, bundle_index: int, access_rule: Optional[Callable] = None):
"""
Adds Orb Bundle Locations to this region equal to `bundle_count`. Used only when Per-Level Orbsanity is enabled.
The orb factory class will handle AP ID enumeration.
@@ -78,7 +77,7 @@ class JakAndDaxterRegion(Region):
location.access_rule = access_rule
self.locations.append(location)
def add_jak_locations(self, ap_id: int, name: str, access_rule: Callable = None):
def add_jak_locations(self, ap_id: int, name: str, access_rule: Optional[Callable] = None):
"""
Helper function to add Locations. Not to be used directly.
"""

View File

@@ -1,26 +1,24 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
# This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump.
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23)
main_area.add_cell_locations([31], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([32], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([33], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([34], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([35], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs, 34))
main_area.add_cell_locations([31], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
main_area.add_cell_locations([32], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
main_area.add_cell_locations([33], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
main_area.add_cell_locations([34], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
main_area.add_cell_locations([35], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 34))
# These 2 scout fly boxes can be broken by running with nearby blue eco.
main_area.add_fly_locations([196684, 262220])
@@ -64,15 +62,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(50 / bundle_size)
bundle_count = 50 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(6,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,11 +1,14 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
@@ -13,10 +16,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
# Yakows requires no combat.
main_area.add_cell_locations([10])
main_area.add_cell_locations([11], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([12], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([11], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
main_area.add_cell_locations([12], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
# These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach.
main_area.add_fly_locations([262219, 327755, 131147, 65611])
@@ -34,10 +35,8 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
yakow_cliff.add_fly_locations([75], access_rule=lambda state: can_free_scout_flies(state, player))
oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6)
oracle_platforms.add_cell_locations([13], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
oracle_platforms.add_cell_locations([14], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs, 13))
oracle_platforms.add_cell_locations([13], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
oracle_platforms.add_cell_locations([14], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 13))
oracle_platforms.add_fly_locations([393291], access_rule=lambda state:
can_free_scout_flies(state, player))
@@ -70,15 +69,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(50 / bundle_size)
bundle_count = 50 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(1,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,11 +1,14 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 128)
main_area.add_cell_locations([18, 21, 22])
@@ -84,15 +87,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(150 / bundle_size)
bundle_count = 150 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(2,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,12 +1,16 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from BaseClasses import CollectionState
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level
# God help me... here we go.
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
# We need a few helper functions.
def can_cross_main_gap(state: CollectionState, p: int) -> bool:
@@ -19,8 +23,7 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
or state.has_all({"Roll", "Roll Jump"}, p)))
def can_jump_blockers(state: CollectionState, p: int) -> bool:
return (state.has("Double Jump", p)
or state.has("Jump Dive", p)
return (state.has_any({"Double Jump", "Jump Dive"}, p)
or state.has_all({"Crouch", "Crouch Jump"}, p)
or state.has_all({"Crouch", "Crouch Uppercut"}, p)
or state.has_all({"Punch", "Punch Uppercut"}, p))
@@ -190,15 +193,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(200 / bundle_size)
bundle_count = 200 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(12,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,11 +1,14 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_fight, can_reach_orbs_level
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
# A large amount of this area can be covered by single jump, floating platforms, web trampolines, and goggles.
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 63)
@@ -113,15 +116,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(200 / bundle_size)
bundle_count = 200 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(13,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -1,29 +1,26 @@
from typing import List
from BaseClasses import CollectionState, MultiWorld
from .RegionBase import JakAndDaxterRegion
from .. import JakAndDaxterOptions, EnableOrbsanity
from ..Rules import can_free_scout_flies, can_trade, can_reach_orbs
from ..locs import CellLocations as Cells, ScoutLocations as Scouts
from .. import EnableOrbsanity, JakAndDaxterWorld
from ..Rules import can_free_scout_flies, can_reach_orbs_level
from ..locs import ScoutLocations as Scouts
def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxterOptions, player: int) -> List[JakAndDaxterRegion]:
def build_regions(level_name: str, world: JakAndDaxterWorld) -> List[JakAndDaxterRegion]:
multiworld = world.multiworld
options = world.options
player = world.player
total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
# No area is inaccessible in VC even with only running and jumping.
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
main_area.add_cell_locations([96], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([97], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs, 96))
main_area.add_cell_locations([98], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs, 97))
main_area.add_cell_locations([99], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs, 98))
main_area.add_cell_locations([100], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs))
main_area.add_cell_locations([101], access_rule=lambda state:
can_trade(state, player, multiworld, options, total_trade_orbs, 100))
main_area.add_cell_locations([96], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
main_area.add_cell_locations([97], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 96))
main_area.add_cell_locations([98], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 97))
main_area.add_cell_locations([99], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 98))
main_area.add_cell_locations([100], access_rule=lambda state: world.can_trade(state, total_trade_orbs, None))
main_area.add_cell_locations([101], access_rule=lambda state: world.can_trade(state, total_trade_orbs, 100))
# Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping
# and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode).
@@ -43,15 +40,12 @@ def build_regions(level_name: str, multiworld: MultiWorld, options: JakAndDaxter
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
bundle_size = options.level_orbsanity_bundle_size.value
bundle_count = int(50 / bundle_size)
bundle_count = 50 // world.orb_bundle_size
for bundle_index in range(bundle_count):
orbs.add_orb_locations(11,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options, level_name)
>= (bundle_size * (bundle + 1)))
access_rule=lambda state, level=level_name, bundle=bundle_index:
can_reach_orbs_level(state, player, world, level, bundle))
multiworld.regions.append(orbs)
main_area.connect(orbs)

View File

@@ -3,11 +3,8 @@ import typing
from . import JakAndDaxterTestBase
from .. import jak1_id
from ..regs.RegionBase import JakAndDaxterRegion
from ..locs import (OrbLocations as Orbs,
CellLocations as Cells,
ScoutLocations as Scouts,
SpecialLocations as Specials,
OrbCacheLocations as Caches)
from ..locs import (ScoutLocations as Scouts,
SpecialLocations as Specials)
class LocationsTest(JakAndDaxterTestBase):