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:
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_()
|
||||
|
||||
@@ -24,7 +24,3 @@ class StardewRule(Protocol):
|
||||
@abstractmethod
|
||||
def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_difficulty(self):
|
||||
...
|
||||
|
||||
164
worlds/stardew_valley/stardew_rule/rule_explain.py
Normal file
164
worlds/stardew_valley/stardew_rule/rule_explain.py
Normal 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"
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user