Files
Archipelago/worlds/stardew_valley/regions/model.py
agilbert1412 1de91fab67 Stardew Valley: 7.x.x - The Jojapocalypse Update (#5432)
Major Content update for Stardew Valley

### Features
- New BundleRandomization Value: Meme Bundles - Over 100 custom bundles, designed to be jokes, references, trolls, etc
- New Setting: Bundles Per Room modifier
- New Setting: Backpack Size
- New Setting: Secretsanity - Checks for triggering easter eggs and secrets
- New Setting: Moviesanity - Checks for watching movies and sharing snacks with Villagers
- New Setting: Eatsanity - Checks for eating items
- New Setting: Hatsanity - Checks for wearing Hats
- New Setting: Start Without - Allows you to select any combination of various "starting" items, that you will actually not start with. Notably, tools, backpack slots, Day5 unlocks, etc.
- New Setting: Allowed Filler Items - Allows you to customize the filler items you'll get
- New Setting: Endgame Locations - Checks for various expensive endgame tasks and purchases
- New Shipsanity value: Crops and Fish
- New Settings: Jojapocalypse and settings to customize it
- Bundle Plando: Replaced with BundleWhitelist and BundleBlacklist, for more customization freedom
- Added a couple of Host.yaml settings to help hosts allow or ban specific difficult settings that could cause problems if the people don't know what they are signing up for.

Plus a truckload of improvements on the mod side, not seen in this PR.

### Removed features
- Integration for Stardew Valley Expanded. It is simply disabled, the code is all still there, but I'm extremely tired of providing tech support for it, plus Stardew Valley 1.7 was announced and that will break it again, so I'm done. When a maintainer steps up, it can be re-enabled.
2026-02-15 18:02:21 +01:00

95 lines
3.1 KiB
Python

from __future__ import annotations
from collections.abc import Container
from dataclasses import dataclass, field
from enum import IntFlag
connector_keyword = " to "
def reverse_connection_name(name: str) -> str | None:
try:
origin, destination = name.split(connector_keyword)
except ValueError:
return None
return f"{destination}{connector_keyword}{origin}"
class MergeFlag(IntFlag):
ADD_EXITS = 0
REMOVE_EXITS = 1
class RandomizationFlag(IntFlag):
NOT_RANDOMIZED = 0
# Randomization options
# The first 4 bits are used to mark if an entrance is eligible for randomization according to the entrance randomization options.
BIT_PELICAN_TOWN = 1 # 0b0001
BIT_NON_PROGRESSION = 1 << 1 # 0b0010
BIT_BUILDINGS = 1 << 2 # 0b0100
BIT_EVERYTHING = 1 << 3 # 0b1000
# Content flag for entrances exclusions
# The next 2 bits are used to mark if an entrance is to be excluded from randomization according to the content options.
# Those bits must be removed from an entrance flags when then entrance must be excluded.
__UNUSED = 1 << 4 # 0b010000
EXCLUDE_MASTERIES = 1 << 5 # 0b100000
# Entrance groups
# The last bit is used to add additional qualifiers on entrances to group them
# Those bits should be added when an entrance need additional qualifiers.
LEAD_TO_OPEN_AREA = 1 << 6
# Tags to apply on connections
EVERYTHING = EXCLUDE_MASTERIES | BIT_EVERYTHING
BUILDINGS = EVERYTHING | BIT_BUILDINGS
NON_PROGRESSION = BUILDINGS | BIT_NON_PROGRESSION
PELICAN_TOWN = NON_PROGRESSION | BIT_PELICAN_TOWN
@dataclass(frozen=True)
class RegionData:
name: str
exits: tuple[str, ...] = field(default_factory=tuple)
flag: MergeFlag = MergeFlag.ADD_EXITS
def __post_init__(self):
assert not isinstance(self.exits, str), "Exits must be a tuple of strings, you probably forgot a trailing comma."
def merge_with(self, other: RegionData) -> RegionData:
assert self.name == other.name, "Regions must have the same name to be merged"
if other.flag == MergeFlag.REMOVE_EXITS:
return self.get_without_exits(other.exits)
merged_exits = self.exits + other.exits
assert len(merged_exits) == len(set(merged_exits)), "Two regions getting merged have duplicated exists..."
return RegionData(self.name, merged_exits)
def get_without_exits(self, exits_to_remove: Container[str]) -> RegionData:
exits = tuple(exit_ for exit_ in self.exits if exit_ not in exits_to_remove)
return RegionData(self.name, exits)
@dataclass(frozen=True)
class ConnectionData:
name: str
destination: str
flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED
@property
def reverse(self) -> str | None:
return reverse_connection_name(self.name)
def is_eligible_for_randomization(self, chosen_randomization_flag: RandomizationFlag) -> bool:
return chosen_randomization_flag and chosen_randomization_flag in self.flag
@dataclass(frozen=True)
class ModRegionsData:
content_pack: str
regions: list[RegionData]
connections: list[ConnectionData]