Stardew Valley 6.x.x: The Content Update (#3478)

Focus of the Update: Compatibility with Stardew Valley 1.6 Released on March 19th 2024
This includes randomization for pretty much all of the new content, including but not limited to
- Raccoon Bundles
- Booksanity
- Skill Masteries
- New Recipes, Craftables, Fish, Maps, Farm Type, Festivals and Quests

This also includes a significant reorganisation of the code into "Content Packs", to allow for easier modularity of various game mechanics between the settings and the supported mods. This improves maintainability quite a bit.

In addition to that, a few **very** requested new features have been introduced, although they weren't the focus of this update
- Walnutsanity
- Player Buffs
- More customizability in settings, such as shorter special orders, ER without farmhouse
- New Remixed Bundles
This commit is contained in:
agilbert1412
2024-07-07 16:04:25 +03:00
committed by GitHub
parent f99ee77325
commit 9b22458f44
210 changed files with 10298 additions and 4540 deletions

View File

@@ -1,7 +1,8 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import deque
from collections import deque, Counter
from dataclasses import dataclass, field
from functools import cached_property
from itertools import chain
from threading import Lock
@@ -295,7 +296,10 @@ class AggregatingStardewRule(BaseStardewRule, ABC):
self.simplification_state.original_simplifiable_rules == self.simplification_state.original_simplifiable_rules)
def __hash__(self):
return hash((id(self.combinable_rules), self.simplification_state.original_simplifiable_rules))
if len(self.combinable_rules) + len(self.simplification_state.original_simplifiable_rules) > 5:
return id(self)
return hash((*self.combinable_rules.values(), self.simplification_state.original_simplifiable_rules))
class Or(AggregatingStardewRule):
@@ -323,9 +327,6 @@ class Or(AggregatingStardewRule):
def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule:
return min(left, right, key=lambda x: x.value)
def get_difficulty(self):
return min(rule.get_difficulty() for rule in self.original_rules)
class And(AggregatingStardewRule):
identity = true_
@@ -352,19 +353,34 @@ class And(AggregatingStardewRule):
def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule:
return max(left, right, key=lambda x: x.value)
def get_difficulty(self):
return max(rule.get_difficulty() for rule in self.original_rules)
class Count(BaseStardewRule):
count: int
rules: List[StardewRule]
counter: Counter[StardewRule]
evaluate: Callable[[CollectionState], bool]
total: Optional[int]
rule_mapping: Optional[Dict[StardewRule, StardewRule]]
def __init__(self, rules: List[StardewRule], count: int):
self.rules = rules
self.count = count
self.counter = Counter(rules)
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
if len(self.counter) / len(rules) < .66:
# Checking if it's worth using the count operation with shortcircuit or not. Value should be fine-tuned when Count has more usage.
self.total = sum(self.counter.values())
self.rules = sorted(self.counter.keys(), key=lambda x: self.counter[x], reverse=True)
self.rule_mapping = {}
self.evaluate = self.evaluate_with_shortcircuit
else:
self.rules = rules
self.evaluate = self.evaluate_without_shortcircuit
def __call__(self, state: CollectionState) -> bool:
return self.evaluate(state)
def evaluate_without_shortcircuit(self, state: CollectionState) -> bool:
c = 0
for i in range(self.rules_count):
self.rules[i], value = self.rules[i].evaluate_while_simplifying(state)
@@ -372,37 +388,58 @@ class Count(BaseStardewRule):
c += 1
if c >= self.count:
return self, True
return True
if c + self.rules_count - i < self.count:
break
return self, False
return False
def __call__(self, state: CollectionState) -> bool:
return self.evaluate_while_simplifying(state)[1]
def evaluate_with_shortcircuit(self, state: CollectionState) -> bool:
c = 0
t = self.total
for rule in self.rules:
evaluation_value = self.call_evaluate_while_simplifying_cached(rule, state)
rule_value = self.counter[rule]
if evaluation_value:
c += rule_value
else:
t -= rule_value
if c >= self.count:
return True
elif t < self.count:
break
return False
def call_evaluate_while_simplifying_cached(self, rule: StardewRule, state: CollectionState) -> bool:
try:
# A mapping table with the original rule is used here because two rules could resolve to the same rule.
# This would require to change the counter to merge both rules, and quickly become complicated.
return self.rule_mapping[rule](state)
except KeyError:
self.rule_mapping[rule], value = rule.evaluate_while_simplifying(state)
return value
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
return self, self(state)
@cached_property
def rules_count(self):
return len(self.rules)
def get_difficulty(self):
self.rules = sorted(self.rules, key=lambda x: x.get_difficulty())
# In an optimal situation, all the simplest rules will be true. Since the rules are sorted, we know that the most difficult rule we might have to do
# is the one at the "self.count".
return self.rules[self.count - 1].get_difficulty()
def __repr__(self):
return f"Received {self.count} {repr(self.rules)}"
@dataclass(frozen=True)
class Has(BaseStardewRule):
item: str
# For sure there is a better way than just passing all the rules everytime
other_rules: Dict[str, StardewRule]
def __init__(self, item: str, other_rules: Dict[str, StardewRule]):
self.item = item
self.other_rules = other_rules
other_rules: Dict[str, StardewRule] = field(repr=False, hash=False, compare=False)
group: str = "item"
def __call__(self, state: CollectionState) -> bool:
return self.evaluate_while_simplifying(state)[1]
@@ -410,21 +447,15 @@ class Has(BaseStardewRule):
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
return self.other_rules[self.item].evaluate_while_simplifying(state)
def get_difficulty(self):
return self.other_rules[self.item].get_difficulty() + 1
def __str__(self):
if self.item not in self.other_rules:
return f"Has {self.item} -> {MISSING_ITEM}"
return f"Has {self.item}"
return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}"
return f"Has {self.item} ({self.group})"
def __repr__(self):
if self.item not in self.other_rules:
return f"Has {self.item} -> {MISSING_ITEM}"
return f"Has {self.item} -> {repr(self.other_rules[self.item])}"
def __hash__(self):
return hash(self.item)
return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}"
return f"Has {self.item} ({self.group}) -> {repr(self.other_rules[self.item])}"
class RepeatableChain(Iterable, Sized):

View File

@@ -6,34 +6,38 @@ from . import StardewRule, Reach, Count, AggregatingStardewRule, Has
def look_for_indirect_connection(rule: StardewRule) -> Set[str]:
required_regions = set()
_find(rule, required_regions)
_find(rule, required_regions, depth=0)
return required_regions
@singledispatch
def _find(rule: StardewRule, regions: Set[str]):
def _find(rule: StardewRule, regions: Set[str], depth: int):
...
@_find.register
def _(rule: AggregatingStardewRule, regions: Set[str]):
def _(rule: AggregatingStardewRule, regions: Set[str], depth: int):
assert depth < 50, "Recursion depth exceeded"
for r in rule.original_rules:
_find(r, regions)
_find(r, regions, depth + 1)
@_find.register
def _(rule: Count, regions: Set[str]):
def _(rule: Count, regions: Set[str], depth: int):
assert depth < 50, "Recursion depth exceeded"
for r in rule.rules:
_find(r, regions)
_find(r, regions, depth + 1)
@_find.register
def _(rule: Has, regions: Set[str]):
def _(rule: Has, regions: Set[str], depth: int):
assert depth < 50, f"Recursion depth exceeded on {rule.item}"
r = rule.other_rules[rule.item]
_find(r, regions)
_find(r, regions, depth + 1)
@_find.register
def _(rule: Reach, regions: Set[str]):
def _(rule: Reach, regions: Set[str], depth: int):
assert depth < 50, "Recursion depth exceeded"
if rule.resolution_hint == "Region":
regions.add(rule.spot)

View File

@@ -33,9 +33,6 @@ class True_(LiteralStardewRule): # noqa
def __and__(self, other) -> StardewRule:
return other
def get_difficulty(self):
return 0
class False_(LiteralStardewRule): # noqa
value = False
@@ -52,9 +49,6 @@ class False_(LiteralStardewRule): # noqa
def __and__(self, other) -> StardewRule:
return self
def get_difficulty(self):
return 999999999
false_ = False_()
true_ = True_()

View File

@@ -24,7 +24,3 @@ class StardewRule(Protocol):
@abstractmethod
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
...
@abstractmethod
def get_difficulty(self):
...

View File

@@ -0,0 +1,164 @@
from __future__ import annotations
from dataclasses import dataclass, field
from functools import cached_property, singledispatch
from typing import Iterable, Set, Tuple, List, Optional
from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule
from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach, true_
@dataclass
class RuleExplanation:
rule: StardewRule
state: CollectionState
expected: bool
sub_rules: Iterable[StardewRule] = field(default_factory=list)
explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set)
current_rule_explored: bool = False
def __post_init__(self):
checkpoint = _rule_key(self.rule)
if checkpoint is not None and checkpoint in self.explored_rules_key:
self.current_rule_explored = True
self.sub_rules = []
def summary(self, depth=0) -> str:
summary = " " * depth + f"{str(self.rule)} -> {self.result}"
if self.current_rule_explored:
summary += " [Already explained]"
return summary
def __str__(self, depth=0):
if not self.sub_rules:
return self.summary(depth)
return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1)
if i.result is not self.expected else i.summary(depth + 1)
for i in sorted(self.explained_sub_rules, key=lambda x: x.result))
def __repr__(self, depth=0):
if not self.sub_rules:
return self.summary(depth)
return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1)
for i in sorted(self.explained_sub_rules, key=lambda x: x.result))
@cached_property
def result(self) -> bool:
try:
return self.rule(self.state)
except KeyError:
return False
@cached_property
def explained_sub_rules(self) -> List[RuleExplanation]:
rule_key = _rule_key(self.rule)
if rule_key is not None:
self.explored_rules_key.add(rule_key)
return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules]
def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation:
if isinstance(rule, StardewRule):
return _explain(rule, state, expected, explored_spots=set())
else:
return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa
@singledispatch
def _explain(rule: StardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
@_explain.register
def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
return RuleExplanation(rule, state, expected, rule.original_rules, explored_rules_key=explored_spots)
@_explain.register
def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
return RuleExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots)
@_explain.register
def _(rule: Has, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
try:
return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]], explored_rules_key=explored_spots)
except KeyError:
return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
@_explain.register
def _(rule: TotalReceived, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items], explored_rules_key=explored_spots)
@_explain.register
def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
access_rules = None
if rule.resolution_hint == 'Location':
spot = state.multiworld.get_location(rule.spot, rule.player)
if isinstance(spot.access_rule, StardewRule):
if spot.access_rule is true_:
access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
else:
access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
elif rule.resolution_hint == 'Entrance':
spot = state.multiworld.get_entrance(rule.spot, rule.player)
if isinstance(spot.access_rule, StardewRule):
if spot.access_rule is true_:
access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
else:
access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
else:
spot = state.multiworld.get_region(rule.spot, rule.player)
access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)]
if not access_rules:
return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots)
@_explain.register
def _(rule: Received, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation:
access_rules = None
if rule.event:
try:
spot = state.multiworld.get_location(rule.item, rule.player)
if spot.access_rule is true_:
access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
else:
access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
except KeyError:
pass
if not access_rules:
return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots)
return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots)
@singledispatch
def _rule_key(_: StardewRule) -> Optional[Tuple[str, str]]:
return None
@_rule_key.register
def _(rule: Reach) -> Tuple[str, str]:
return rule.spot, rule.resolution_hint
@_rule_key.register
def _(rule: Received) -> Optional[Tuple[str, str]]:
if not rule.event:
return None
return rule.item, "Logic Event"

View File

@@ -1,10 +1,9 @@
from dataclasses import dataclass
from typing import Iterable, Union, List, Tuple, Hashable
from BaseClasses import ItemClassification, CollectionState
from BaseClasses import CollectionState
from .base import BaseStardewRule, CombinableStardewRule
from .protocol import StardewRule
from ..items import item_table
class TotalReceived(BaseStardewRule):
@@ -20,11 +19,6 @@ class TotalReceived(BaseStardewRule):
else:
items_list = [items]
assert items_list, "Can't create a Total Received conditions without items"
for item in items_list:
assert item_table[item].classification & ItemClassification.progression, \
f"Item [{item_table[item].name}] has to be progression to be used in logic"
self.player = player
self.items = items_list
self.count = count
@@ -40,9 +34,6 @@ class TotalReceived(BaseStardewRule):
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
return self, self(state)
def get_difficulty(self):
return self.count
def __repr__(self):
return f"Received {self.count} {self.items}"
@@ -52,10 +43,8 @@ class Received(CombinableStardewRule):
item: str
player: int
count: int
def __post_init__(self):
assert item_table[self.item].classification & ItemClassification.progression, \
f"Item [{item_table[self.item].name}] has to be progression to be used in logic"
event: bool = False
"""Helps `explain` to know it can dig into a location with the same name."""
@property
def combination_key(self) -> Hashable:
@@ -73,11 +62,8 @@ class Received(CombinableStardewRule):
def __repr__(self):
if self.count == 1:
return f"Received {self.item}"
return f"Received {self.count} {self.item}"
def get_difficulty(self):
return self.count
return f"Received {'event ' if self.event else ''}{self.item}"
return f"Received {'event ' if self.event else ''}{self.count} {self.item}"
@dataclass(frozen=True)
@@ -97,9 +83,6 @@ class Reach(BaseStardewRule):
def __repr__(self):
return f"Reach {self.resolution_hint} {self.spot}"
def get_difficulty(self):
return 1
@dataclass(frozen=True)
class HasProgressionPercent(CombinableStardewRule):
@@ -122,19 +105,21 @@ class HasProgressionPercent(CombinableStardewRule):
stardew_world = state.multiworld.worlds[self.player]
total_count = stardew_world.total_progression_items
needed_count = (total_count * self.percent) // 100
player_state = state.prog_items[self.player]
if needed_count <= len(player_state):
return True
total_count = 0
for item in state.prog_items[self.player]:
item_count = state.prog_items[self.player][item]
for item, item_count in player_state.items():
total_count += item_count
if total_count >= needed_count:
return True
return False
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
return self, self(state)
def __repr__(self):
return f"HasProgressionPercent {self.percent}"
def get_difficulty(self):
return self.percent
return f"Received {self.percent}% progression items."