diff --git a/worlds/satisfactory/CriticalPathCalculator.py b/worlds/satisfactory/CriticalPathCalculator.py index d774717f7d..d730d4a9f2 100644 --- a/worlds/satisfactory/CriticalPathCalculator.py +++ b/worlds/satisfactory/CriticalPathCalculator.py @@ -3,12 +3,12 @@ from typing import Optional from collections.abc import Iterable from .GameLogic import GameLogic, Recipe from .Options import SatisfactoryOptions -from .Options import SatisfactoryOptions class CriticalPathCalculator: logic: GameLogic random: Random - options: SatisfactoryOptions + final_elevator_package: int + randomize_starter_recipes: bool required_parts: set[str] required_buildings: set[str] @@ -25,11 +25,13 @@ class CriticalPathCalculator: handcraftable_parts: dict[str, list[Recipe]] tier_0_recipes: set[str] - def __init__(self, logic: GameLogic, random: Random, options: SatisfactoryOptions): + def __init__(self, logic: GameLogic, seed: float, options: SatisfactoryOptions): self.logic = logic - self.random = random - self.options = options + self.random = Random(seed) + self.final_elevator_package = options.final_elevator_package.value + self.randomize_starter_recipes = bool(options.randomize_starter_recipes.value) + def calculate(self) -> None: self.required_parts = set() self.required_buildings = set() self.required_power_level: int = 1 @@ -38,13 +40,14 @@ class CriticalPathCalculator: self.configure_implicitly_unlocked_and_handcraftable_parts() - self.select_minimal_required_parts_for(self.logic.space_elevator_tiers[options.final_elevator_package-1].keys()) + self.select_minimal_required_parts_for( + self.logic.space_elevator_tiers[self.final_elevator_package-1].keys()) for tree in self.logic.man_trees.values(): self.select_minimal_required_parts_for(tree.access_items) for node in tree.nodes: - if node.minimal_tier > options.final_elevator_package: + if node.minimal_tier > self.final_elevator_package: continue self.select_minimal_required_parts_for(node.unlock_cost.keys()) @@ -64,15 +67,15 @@ class CriticalPathCalculator: self.select_minimal_required_parts_for_building("Pipes Mk.2") self.select_minimal_required_parts_for_building("Pipeline Pump Mk.1") self.select_minimal_required_parts_for_building("Pipeline Pump Mk.2") - - if self.logic.recipes["Uranium"][0].minimal_tier <= options.final_elevator_package: + + if self.logic.recipes["Uranium"][0].minimal_tier <= self.final_elevator_package: self.select_minimal_required_parts_for(("Hazmat Suit", "Iodine-Infused Filter")) for i in range(1, self.__potential_required_belt_speed + 1): self.select_minimal_required_parts_for_building(f"Conveyor Mk.{i}") for i in range(1, self.required_power_level + 1): - power_recipe = random.choice(self.logic.requirement_per_powerlevel[i]) + power_recipe = self.random.choice(self.logic.requirement_per_powerlevel[i]) self.select_minimal_required_parts_for(power_recipe.inputs) self.select_minimal_required_parts_for_building(power_recipe.building) @@ -80,7 +83,7 @@ class CriticalPathCalculator: recipe.name for part in self.required_parts for recipe in self.logic.recipes[part] - if recipe.minimal_tier <= self.options.final_elevator_package + if recipe.minimal_tier <= self.final_elevator_package } self.required_item_names.update({"Building: "+ building for building in self.required_buildings}) @@ -104,7 +107,7 @@ class CriticalPathCalculator: self.required_parts.add(part) for recipe in self.logic.recipes[part]: - if recipe.minimal_tier > self.options.final_elevator_package: + if recipe.minimal_tier > self.final_elevator_package: continue self.__potential_required_belt_speed = \ @@ -129,7 +132,7 @@ class CriticalPathCalculator: recipe.name for part in self.logic.recipes for recipe in self.logic.recipes[part] - if recipe.minimal_tier > self.options.final_elevator_package + if recipe.minimal_tier > self.final_elevator_package } excluded_count = len(self.recipes_to_exclude) @@ -191,7 +194,7 @@ class CriticalPathCalculator: def select_starter_recipes(self) -> None: # cable is left unaffected as all its alternative recipes require refinery - if not self.options.randomize_starter_recipes: + if not self.randomize_starter_recipes: self.tier_0_recipes = { "Recipe: Iron Ingot", "Recipe: Iron Plate", diff --git a/worlds/satisfactory/ItemData.py b/worlds/satisfactory/ItemData.py index b62dd1acd4..92b9a029fb 100644 --- a/worlds/satisfactory/ItemData.py +++ b/worlds/satisfactory/ItemData.py @@ -31,7 +31,7 @@ class ItemGroups(IntFlag): Vehicles = 1 << 26 Customizer = 1 << 27 ConveyorMk6 = 1 << 28 - AlwaysUseful = 1 << 29 + NeverExclude = 1 << 29 class ItemData(NamedTuple): diff --git a/worlds/satisfactory/Items.py b/worlds/satisfactory/Items.py index 998a086376..d0a60c5b77 100644 --- a/worlds/satisfactory/Items.py +++ b/worlds/satisfactory/Items.py @@ -466,7 +466,7 @@ class Items: "Recipe: Charcoal": ItemData(G.Recipe, 1338461, C.useful), "Recipe: Sloppy Alumina": ItemData(G.Recipe, 1338462, C.progression), "Recipe: Hoverpack": ItemData(G.Recipe, 1338463, C.useful), - "Recipe: Jetpack": ItemData(G.Recipe, 1338464, C.useful), + "Recipe: Jetpack": ItemData(G.Recipe | G.NeverExclude, 1338464, C.useful), "Recipe: Nobelisk Detonator": ItemData(G.Recipe, 1338465, C.progression), "Recipe: Portable Miner": ItemData(G.Recipe, 1338466, C.progression), # @@ -494,28 +494,28 @@ class Items: "Building: Fuel Generator": ItemData(G.Building, 1338618, C.progression), "Building: Resource Well Pressurizer": ItemData(G.Building, 1338619, C.progression), "Building: Equipment Workshop": ItemData(G.Building, 1338620, C.progression), - "Building: AWESOME Sink": ItemData(G.Building | G.AlwaysUseful, 1338621, C.progression), - "Building: AWESOME Shop": ItemData(G.Building | G.AlwaysUseful, 1338622, C.progression), + "Building: AWESOME Sink": ItemData(G.Building | G.NeverExclude, 1338621, C.progression), + "Building: AWESOME Shop": ItemData(G.Building | G.NeverExclude, 1338622, C.progression), "Building: Structural Beam Pack": ItemData(G.Beams, 1338623, C.filler), "Building: Blueprint Designer": ItemData(G.Building, 1338624, C.filler, 0), # unlocked by default - "Building: Fluid Buffer": ItemData(G.Building, 1338625, C.filler), - "Building: Industrial Fluid Buffer": ItemData(G.Building, 1338626, C.filler), + "Building: Fluid Buffer": ItemData(G.Building | G.NeverExclude, 1338625, C.useful), + "Building: Industrial Fluid Buffer": ItemData(G.Building | G.NeverExclude, 1338626, C.useful), "Building: Jump Pad": ItemData(G.Building, 1338627, C.filler), "Building: Ladder": ItemData(G.Building, 1338628, C.filler), - "Building: MAM": ItemData(G.Building | G.AlwaysUseful, 1338629, C.progression), + "Building: MAM": ItemData(G.Building | G.NeverExclude, 1338629, C.progression), "Building: Personal Storage Box": ItemData(G.Building, 1338630, C.filler), - "Building: Power Storage": ItemData(G.Building | G.AlwaysUseful, 1338631, C.progression), + "Building: Power Storage": ItemData(G.Building | G.NeverExclude, 1338631, C.progression), "Building: U-Jelly Landing Pad": ItemData(G.Building, 1338632, C.useful), - "Building: Power Switch": ItemData(G.Building, 1338633, C.useful), - "Building: Priority Power Switch": ItemData(G.Building, 1338634, C.useful), + "Building: Power Switch": ItemData(G.Building | G.NeverExclude, 1338633, C.useful), + "Building: Priority Power Switch": ItemData(G.Building | G.NeverExclude, 1338634, C.useful), "Building: Storage Container": ItemData(G.Building, 1338635, C.useful, 0), "Building: Lookout Tower": ItemData(G.Building, 1338636, C.filler), #"Building: Power Pole Mk.1": ItemData(G.Building, 1338637, C.progression), # unlocked by default - "Building: Power Pole Mk.2": ItemData(G.Building, 1338638, C.useful), - "Building: Power Pole Mk.3": ItemData(G.Building, 1338639, C.useful), - "Building: Industrial Storage Container": ItemData(G.Building, 1338640, C.filler), - "Building: Conveyor Merger": ItemData(G.Building | G.AlwaysUseful, 1338641, C.progression), - "Building: Conveyor Splitter": ItemData(G.Building | G.AlwaysUseful, 1338642, C.progression), + "Building: Power Pole Mk.2": ItemData(G.Building | G.NeverExclude, 1338638, C.useful), + "Building: Power Pole Mk.3": ItemData(G.Building | G.NeverExclude, 1338639, C.useful), + "Building: Industrial Storage Container": ItemData(G.Building | G.NeverExclude, 1338640, C.useful), + "Building: Conveyor Merger": ItemData(G.Building | G.NeverExclude, 1338641, C.progression), + "Building: Conveyor Splitter": ItemData(G.Building | G.NeverExclude, 1338642, C.progression), "Building: Conveyor Mk.1": ItemData(G.Building | G.ConveyorMk1, 1338643, C.progression), # unlocked by default "Building: Conveyor Mk.2": ItemData(G.Building | G.ConveyorMk2, 1338644, C.progression), "Building: Conveyor Mk.3": ItemData(G.Building | G.ConveyorMk3, 1338645, C.progression), @@ -546,11 +546,11 @@ class Items: "Building: Street Light": ItemData(G.Building | G.Lights, 1338670, C.filler, 0), "Building: Flood Light Tower": ItemData(G.Building | G.Lights, 1338671, C.filler, 0), "Building: Ceiling Light": ItemData(G.Building | G.Lights, 1338672, C.filler, 0), - "Building: Power Tower": ItemData(G.Building, 1338673, C.useful), + "Building: Power Tower": ItemData(G.Building | G.NeverExclude, 1338673, C.useful), "Building: Walls Orange": ItemData(G.Building | G.Walls, 1338674, C.progression), "Building: Radar Tower": ItemData(G.Building, 1338675, C.useful), - "Building: Smart Splitter": ItemData(G.Building, 1338676, C.useful), - "Building: Programmable Splitter": ItemData(G.Building, 1338677, C.useful), + "Building: Smart Splitter": ItemData(G.Building | G.NeverExclude, 1338676, C.useful), + "Building: Programmable Splitter": ItemData(G.Building | G.NeverExclude, 1338677, C.useful), "Building: Label Sign Bundle": ItemData(G.Building | G.Signs, 1338678, C.filler, 0), "Building: Display Sign Bundle": ItemData(G.Building | G.Signs, 1338679, C.filler, 0), "Building: Billboard Set": ItemData(G.Building | G.Signs, 1338680, C.filler, 0), @@ -559,7 +559,7 @@ class Items: "Building: Concrete Pillar": ItemData(G.Pilars, 1338683, C.filler, 0), "Building: Frame Pillar": ItemData(G.Pilars, 1338684, C.filler, 0), #1338685 - 1338691 Moved to cosmetics - 1.1 - "Building: Foundation": ItemData(G.Building | G.Foundations | G.AlwaysUseful, 1338692, C.progression), + "Building: Foundation": ItemData(G.Building | G.Foundations | G.NeverExclude, 1338692, C.progression), "Building: Half Foundation": ItemData(G.Foundations, 1338693, C.filler, 0), "Building: Corner Ramp Pack": ItemData(G.Foundations, 1338694, C.filler, 0), "Building: Inverted Ramp Pack": ItemData(G.Foundations, 1338695, C.filler, 0), @@ -591,13 +591,13 @@ class Items: "Building: Roof Corners": ItemData(G.Walls, 1338721, C.filler, 0), "Building: Converter": ItemData(G.Building, 1338722, C.progression), "Building: Quantum Encoder": ItemData(G.Building, 1338723, C.progression), - "Building: Portal": ItemData(G.Building, 1338724, C.filler), + "Building: Portal": ItemData(G.Building, 1338724, C.useful), "Building: Conveyor Mk.6": ItemData(G.Building | G.ConveyorMk6, 1338725, C.progression), "Building: Conveyor Lift Mk.6": ItemData(G.Building | G.ConveyorMk6, 1338726, C.useful), "Building: Alien Power Augmenter": ItemData(G.Building, 1338727, C.progression), "Building: Dimensional Depot Uploader": ItemData(G.Building, 1338728, C.useful), # Added in 1.1 - "Building: Priority Merger": ItemData(G.Building, 1338729, C.useful), + "Building: Priority Merger": ItemData(G.Building | G.NeverExclude, 1338729, C.useful), "Building: Conveyor Wall Hole": ItemData(G.Building, 1338730, C.useful), "Building: Conveyor Throughput Monitor": ItemData(G.Building, 1338731, C.useful), "Building: Basic Shelf Unit": ItemData(G.Building, 1338732, C.useful), @@ -873,7 +873,8 @@ class Items: for name, data in cls.item_data.items(): for category in data.category: - groups.setdefault(category.name, set()).add(name) + if category != G.NeverExclude: + groups.setdefault(category.name, set()).add(name) return groups @@ -888,6 +889,7 @@ class Items: self.logic = logic self.random = random self.critical_path = critical_path + self.options = options @classmethod @@ -895,18 +897,18 @@ class Items: data: ItemData = cls.item_data[name] type = data.type - if type == C.progression \ + if (type == C.progression or type == C.useful) \ and instance and instance.critical_path.required_item_names \ - and (data.category & (G.Recipe | G.Building)) and not (data.category & G.AlwaysUseful) \ + and (data.category & (G.Recipe | G.Building)) and not (data.category & G.NeverExclude) \ and name not in instance.critical_path.required_item_names: type = C.useful return Item(name, type, data.code, player) - def get_filler_item_name(self, filler_items: tuple[str, ...], random: Random, options: SatisfactoryOptions) -> str: - trap_chance: int = options.trap_chance.value - enabled_traps: list[str] = list(options.trap_selection_override.value) + def get_filler_item_name(self, filler_items: tuple[str, ...], random: Random) -> str: + trap_chance: int = self.options.trap_chance.value + enabled_traps: list[str] = list(self.options.trap_selection_override.value) if enabled_traps and random.random() < (trap_chance / 100): return random.choice(enabled_traps) @@ -914,7 +916,7 @@ class Items: return random.choice(filler_items) - def get_excluded_items(self, multiworld: MultiWorld, options: SatisfactoryOptions) -> set[str]: + def get_excluded_items(self, multiworld: MultiWorld) -> set[str]: excluded_items: set[str] = set() excluded_items.update("Bundle: "+ part for part in self.critical_path.parts_to_exclude) excluded_items.update(recipe for recipe in self.critical_path.recipes_to_exclude) @@ -923,16 +925,15 @@ class Items: for item in multiworld.precollected_items[self.player]: if item.name in self.item_data \ and not (self.item_data[item.name].category & self.non_unique_item_categories) \ - and item.name not in options.start_inventory_from_pool: + and item.name not in self.options.start_inventory_from_pool: excluded_items.add(item.name) return excluded_items - def build_item_pool(self, random: Random, multiworld: MultiWorld, - options: SatisfactoryOptions, number_of_locations: int) -> list[Item]: - excluded_from_pool: set[str] = self.get_excluded_items(multiworld, options) \ + def build_item_pool(self, random: Random, multiworld: MultiWorld, number_of_locations: int) -> list[Item]: + excluded_from_pool: set[str] = self.get_excluded_items(multiworld) \ .union(self.critical_path.implicitly_unlocked) pool: list[Item] = [] @@ -953,7 +954,7 @@ class Items: filtered_filler_items = tuple(item for item in self.filler_items if item not in excluded_from_pool) pool += [ - self.create_item(self, self.get_filler_item_name(filtered_filler_items, random, options), self.player) + self.create_item(self, self.get_filler_item_name(filtered_filler_items, random), self.player) for _ in range(filler_pool_size) ] diff --git a/worlds/satisfactory/Regions.py b/worlds/satisfactory/Regions.py index 5b22a47041..9a8efb9344 100644 --- a/worlds/satisfactory/Regions.py +++ b/worlds/satisfactory/Regions.py @@ -94,13 +94,15 @@ def create_regions_and_return_locations(world: MultiWorld, options: Satisfactory if options.final_elevator_package == 1: super_early_game_buildings.extend(early_game_buildings) + is_ut = getattr(world, "generation_is_fake", False) + connect(regions, "Overworld", "Hub Tier 1") connect(regions, "Hub Tier 1", "Hub Tier 2", - lambda state: state_logic.can_build_all(state, super_early_game_buildings)) + lambda state: is_ut or state_logic.can_build_all(state, super_early_game_buildings)) if options.final_elevator_package >= 2: connect(regions, "Hub Tier 2", "Hub Tier 3", lambda state: state.has("Elevator Tier 1", player) - and state_logic.can_build_all(state, early_game_buildings)) + and (is_ut or state_logic.can_build_all(state, early_game_buildings))) connect(regions, "Hub Tier 3", "Hub Tier 4") if options.final_elevator_package >= 3: connect(regions, "Hub Tier 4", "Hub Tier 5", lambda state: state.has("Elevator Tier 2", player)) diff --git a/worlds/satisfactory/StateLogic.py b/worlds/satisfactory/StateLogic.py index 893c822488..01f00dd588 100644 --- a/worlds/satisfactory/StateLogic.py +++ b/worlds/satisfactory/StateLogic.py @@ -80,9 +80,12 @@ class StateLogic: and self.can_produce_all(state, recipe.inputs) def is_elevator_tier(self, state: CollectionState, phase: int) -> bool: - limited_phase = min(self.options.final_elevator_package, phase) - - return state.has(f"Elevator Tier {limited_phase}", self.player) + limited_phase = min(self.options.final_elevator_package - 1, phase) + + if limited_phase != 0: + return state.has(f"Elevator Tier {limited_phase}", self.player) + else: + return True @staticmethod def to_part_event(part: str) -> str: diff --git a/worlds/satisfactory/__init__.py b/worlds/satisfactory/__init__.py index 65faa90d20..d1f01322aa 100644 --- a/worlds/satisfactory/__init__.py +++ b/worlds/satisfactory/__init__.py @@ -1,4 +1,4 @@ -from typing import TextIO, ClassVar +from typing import TextIO, ClassVar, Any from BaseClasses import Item, ItemClassification, CollectionState from .GameLogic import GameLogic from .Items import Items @@ -24,18 +24,27 @@ class SatisfactoryWorld(World): web = SatisfactoryWebWorld() origin_region_name = "Overworld" required_client_version = (0, 6, 0) + ut_can_gen_without_yaml = True game_logic: ClassVar[GameLogic] = GameLogic() state_logic: StateLogic items: Items critical_path: CriticalPathCalculator + critical_path_seed: float | None = None item_name_to_id = Items.item_names_and_ids location_name_to_id = Locations().get_locations_for_data_package() item_name_groups = Items.get_item_names_per_category(game_logic) def generate_early(self) -> None: - self.critical_path = CriticalPathCalculator(self.game_logic, self.random, self.options) + self.interpret_slot_data(None) + + if self.critical_path_seed == None: + self.critical_path_seed = self.random.random() + + self.critical_path = CriticalPathCalculator(self.game_logic, self.critical_path_seed, self.options) + self.critical_path.calculate() + self.state_logic = StateLogic(self.player, self.options, self.critical_path) self.items = Items(self.player, self.game_logic, self.random, self.options, self.critical_path) @@ -71,7 +80,7 @@ class SatisfactoryWorld(World): number_of_locations: int = len(self.multiworld.get_unfilled_locations(self.player)) self.multiworld.itempool += \ - self.items.build_item_pool(self.random, self.multiworld, self.options, number_of_locations) + self.items.build_item_pool(self.random, self.multiworld, number_of_locations) def set_rules(self) -> None: @@ -120,7 +129,6 @@ class SatisfactoryWorld(World): return { "Data": { "HubLayout": slot_hub_layout, - "SlotsPerMilestone": self.game_logic.slots_per_milestone, "ExplorationCosts": { self.item_id_str("Mercer Sphere"): int(self.options.goal_exploration_collectables_amount.value * 2), self.item_id_str("Somersloop"): self.options.goal_exploration_collectables_amount.value, @@ -135,7 +143,6 @@ class SatisfactoryWorld(World): "FinalElevatorTier": self.options.final_elevator_package.value, "FinalResourceSinkPointsTotal": self.options.goal_awesome_sink_points_total.value, "FinalResourceSinkPointsPerMinute": self.options.goal_awesome_sink_points_per_minute.value, - "FinalExplorationCollectionAmount": self.options.goal_exploration_collectables_amount.value, "FreeSampleEquipment": self.options.free_sample_equipment.value, "FreeSampleBuildings": self.options.free_sample_buildings.value, "FreeSampleParts": self.options.free_sample_parts.value, @@ -143,11 +150,52 @@ class SatisfactoryWorld(World): "EnergyLink": bool(self.options.energy_link), "StartingRecipies": starting_recipes }, - "SlotDataVersion": 1 + "SlotDataVersion": 1, + "UT": { + "Seed": self.critical_path_seed, + "RandomizeTier0": bool(self.options.randomize_starter_recipes) + } }, "DeathLink": bool(self.options.death_link) } + def interpret_slot_data(self, slot_data: dict[str, Any] | None) -> dict[str, Any] | None: + """Used by Universal Tracker to correctly rebuild state""" + + if not slot_data \ + and hasattr(self.multiworld, "re_gen_passthrough") \ + and isinstance(self.multiworld.re_gen_passthrough, dict) \ + and "Satisfactory" in self.multiworld.re_gen_passthrough: + slot_data = self.multiworld.re_gen_passthrough["Satisfactory"] + + if not slot_data: + return None + + if (slot_data["Data"]["SlotDataVersion"] != 1): + raise Exception("The slot_data version mismatch, the UT's Satisfactory .apworld is different from the one used during generation") + + self.options.goal_selection.value = slot_data["Data"]["Options"]["GoalSelection"] + self.options.goal_requirement.value = slot_data["Data"]["Options"]["GoalRequirement"] + self.options.final_elevator_package.value = slot_data["Data"]["Options"]["FinalElevatorTier"] + self.options.goal_awesome_sink_points_total.value = slot_data["Data"]["Options"]["FinalResourceSinkPointsTotal"] + self.options.goal_awesome_sink_points_per_minute.value = \ + slot_data["Data"]["Options"]["FinalResourceSinkPointsPerMinute"] + self.options.free_sample_equipment.value = slot_data["Data"]["Options"]["FreeSampleEquipment"] + self.options.free_sample_buildings.value = slot_data["Data"]["Options"]["FreeSampleBuildings"] + self.options.free_sample_parts.value = slot_data["Data"]["Options"]["FreeSampleParts"] + self.options.free_sample_radioactive.value = int(slot_data["Data"]["Options"]["FreeSampleRadioactive"]) + self.options.energy_link.value = int(slot_data["Data"]["Options"]["EnergyLink"]) + + self.options.milestone_cost_multiplier.value = 100 * \ + (slot_data["Data"]["HubLayout"][0][0][self.item_id_str("Concrete")] + / self.game_logic.hub_layout[0][0]["Concrete"]) + self.options.goal_exploration_collectables_amount.value = \ + slot_data["Data"]["ExplorationCosts"][self.item_id_str("Somersloop")] + + self.critical_path_seed = slot_data["Data"]["UT"]["Seed"] + self.options.randomize_starter_recipes.value = slot_data["Data"]["UT"]["RandomizeTier0"] + + return slot_data def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.randomize_starter_recipes: @@ -155,7 +203,7 @@ class SatisfactoryWorld(World): def get_filler_item_name(self) -> str: - return self.items.get_filler_item_name(self.items.filler_items, self.random, self.options) + return self.items.get_filler_item_name(self.items.filler_items, self.random) def setup_events(self) -> None: @@ -178,6 +226,7 @@ class SatisfactoryWorld(World): item = self.create_item(item_name) self.multiworld.push_precollected(item) + def item_id_str(self, item_name: str) -> str: # ItemIDs of bundles are shared with their component item bundled_name = f"Bundle: {item_name}"