The Messenger: Universal Tracker support (#5344)

This commit is contained in:
Jérémie Bolduc
2026-03-09 23:56:05 -04:00
committed by GitHub
parent 123e1f5d95
commit 0b6ba103c5
4 changed files with 83 additions and 16 deletions

View File

@@ -1,7 +1,8 @@
import logging
from typing import Any, ClassVar, TextIO
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial, \
PlandoOptions
from Options import Accessibility
from Utils import output_path
from settings import FilePath, Group
@@ -18,6 +19,7 @@ from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation
from .transitions import disconnect_entrances, shuffle_transitions
from .universal_tracker import reverse_portal_exits_into_portal_plando, reverse_transitions_into_plando_connections
components.append(
Component(
@@ -151,6 +153,10 @@ class MessengerWorld(World):
reachable_locs: bool = False
filler: dict[str, int]
@staticmethod
def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
return slot_data
def generate_early(self) -> None:
if self.options.goal == Goal.option_power_seal_hunt:
self.total_seals = self.options.total_seals.value
@@ -188,6 +194,11 @@ class MessengerWorld(World):
self.spoiler_portal_mapping = {}
self.transitions = []
if hasattr(self.multiworld, "re_gen_passthrough"):
slot_data = self.multiworld.re_gen_passthrough.get(self.game)
if slot_data:
self.starting_portals = slot_data["starting_portals"]
def create_regions(self) -> None:
# MessengerRegion adds itself to the multiworld
# create simple regions
@@ -279,6 +290,16 @@ class MessengerWorld(World):
def connect_entrances(self) -> None:
if self.options.shuffle_transitions:
disconnect_entrances(self)
keep_entrance_logic = False
if hasattr(self.multiworld, "re_gen_passthrough"):
slot_data = self.multiworld.re_gen_passthrough.get(self.game)
if slot_data:
self.multiworld.plando_options |= PlandoOptions.connections
self.options.portal_plando.value = reverse_portal_exits_into_portal_plando(slot_data["portal_exits"])
self.options.plando_connections.value = reverse_transitions_into_plando_connections(slot_data["transitions"])
keep_entrance_logic = True
add_closed_portal_reqs(self)
# i need portal shuffle to happen after rules exist so i can validate it
attempts = 20
@@ -295,7 +316,7 @@ class MessengerWorld(World):
raise RuntimeError("Unable to generate valid portal output.")
if self.options.shuffle_transitions:
shuffle_transitions(self)
shuffle_transitions(self, keep_entrance_logic)
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.options.available_portals < 6:
@@ -463,7 +484,7 @@ class MessengerWorld(World):
"loc_data": {loc.address: {loc.item.name: [loc.item.code, loc.item.flags]}
for loc in multiworld.get_filled_locations() if loc.address},
}
output = orjson.dumps(data, option=orjson.OPT_NON_STR_KEYS)
with open(out_path, "wb") as f:
f.write(output)

View File

@@ -1,8 +1,7 @@
from functools import cached_property
from typing import TYPE_CHECKING
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region
from entrance_rando import ERPlacementState
from BaseClasses import CollectionState, Item, ItemClassification, Location, Region
from .regions import LOCATIONS, MEGA_SHARDS
from .shop import FIGURINES, SHOP_ITEMS

View File

@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from BaseClasses import Entrance, Region
from BaseClasses import Region, CollectionRule
from entrance_rando import EntranceType, randomize_entrances
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
from .options import ShuffleTransitions, TransitionPlando
@@ -26,7 +26,6 @@ def disconnect_entrances(world: "MessengerWorld") -> None:
entrance.randomization_type = er_type
mock_entrance.randomization_type = er_type
for parent, child in RANDOMIZED_CONNECTIONS.items():
if child == "Corrupted Future":
entrance = world.get_entrance("Artificer's Portal")
@@ -36,8 +35,9 @@ def disconnect_entrances(world: "MessengerWorld") -> None:
entrance = world.get_entrance(f"{parent} -> {child}")
disconnect_entrance()
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
def remove_dangling_exit(region: Region) -> None:
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando, keep_logic: bool = False) -> None:
def remove_dangling_exit(region: Region) -> CollectionRule:
# find the disconnected exit and remove references to it
for _exit in region.exits:
if not _exit.connected_region:
@@ -45,6 +45,7 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando
else:
raise ValueError(f"Unable to find randomized transition for {plando_connection}")
region.exits.remove(_exit)
return _exit.access_rule
def remove_dangling_entrance(region: Region) -> None:
# find the disconnected entrance and remove references to it
@@ -65,30 +66,35 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando
else:
dangling_exit = world.get_entrance("Artificer's Challenge")
reg1.exits.remove(dangling_exit)
access_rule = dangling_exit.access_rule
else:
reg1 = world.get_region(plando_connection.entrance)
remove_dangling_exit(reg1)
access_rule = remove_dangling_exit(reg1)
reg2 = world.get_region(plando_connection.exit)
remove_dangling_entrance(reg2)
# connect the regions
reg1.connect(reg2)
new_exit1 = reg1.connect(reg2)
if keep_logic:
new_exit1.access_rule = access_rule
# pretend the user set the plando direction as "both" regardless of what they actually put on coupled
if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled
or plando_connection.direction == "both")
and plando_connection.exit in RANDOMIZED_CONNECTIONS):
remove_dangling_exit(reg2)
access_rule = remove_dangling_exit(reg2)
remove_dangling_entrance(reg1)
reg2.connect(reg1)
new_exit2 = reg2.connect(reg1)
if keep_logic:
new_exit2.access_rule = access_rule
def shuffle_transitions(world: "MessengerWorld") -> None:
def shuffle_transitions(world: "MessengerWorld", keep_logic: bool = False) -> None:
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
plando = world.options.plando_connections
if plando:
connect_plando(world, plando)
connect_plando(world, plando, keep_logic)
result = randomize_entrances(world, coupled, {0: [0]})

View File

@@ -0,0 +1,41 @@
from Options import PlandoConnection
from .connections import RANDOMIZED_CONNECTIONS
from .portals import REGION_ORDER, SHOP_POINTS, CHECKPOINTS
from .transitions import TRANSITIONS
REVERSED_RANDOMIZED_CONNECTIONS = {v: k for k, v in RANDOMIZED_CONNECTIONS.items()}
def find_spot(portal_key: int) -> str:
"""finds the spot associated with the portal key"""
parent = REGION_ORDER[portal_key // 100]
if portal_key % 100 == 0:
return f"{parent} Portal"
if portal_key % 100 // 10 == 1:
return SHOP_POINTS[parent][portal_key % 10]
return CHECKPOINTS[parent][portal_key % 10]
def reverse_portal_exits_into_portal_plando(portal_exits: list[int]) -> list[PlandoConnection]:
return [
PlandoConnection("Autumn Hills", find_spot(portal_exits[0]), "both"),
PlandoConnection("Riviere Turquoise", find_spot(portal_exits[1]), "both"),
PlandoConnection("Howling Grotto", find_spot(portal_exits[2]), "both"),
PlandoConnection("Sunken Shrine", find_spot(portal_exits[3]), "both"),
PlandoConnection("Searing Crags", find_spot(portal_exits[4]), "both"),
PlandoConnection("Glacial Peak", find_spot(portal_exits[5]), "both"),
]
def reverse_transitions_into_plando_connections(transitions: list[list[int]]) -> list[PlandoConnection]:
plando_connections = []
for connection in [
PlandoConnection(REVERSED_RANDOMIZED_CONNECTIONS[TRANSITIONS[transition[0]]], TRANSITIONS[transition[1]], "both")
for transition in transitions
]:
if connection.exit in {con.entrance for con in plando_connections}:
continue
plando_connections.append(connection)
return plando_connections