Files
dockipelago/worlds/satisfactory/__init__.py
Jarno ebbdd7bfda Satisfactory: Add New Game (#5190)
* Added Satisfactory to latest master

* Fixed hard drive from containing the mam + incremented default value for harddrive progression

* Apply cherry pick of 3076259

* Apply cherry pick of 6114a55

* Clarify Point goal behavior (https://github.com/Jarno458/SatisfactoryArchipelagoMod/issues/98)

* Update Setup guide and info page

* Add links to Gifting and Energy Link compatible games. Add info on Hard Drive behavior

* Fix typos

* Update hard drive behavior description

* Hopefully fixed the mam from getting placed behind harddrives

* Add 1 "Bundle: Solid Biofuel" to default starting items (for later chainsaw usage or early power gen)

* Add info/warning about save setup failure bug

* Add notes about dedicated server setup

* Fixes: `TypeError: 'set' object is not subscriptable`

random.choice does not work over set objects, cast to a list to allow 'trap_selection_override'

* progrees i think

* Fixed some bugs

* Progress commmit incase my pc crashes

* progress i think as test passed

* I guess test pass, game still unbeatable tho

* its generating

* Some refactorings

* Fixed generation with different elevator tiers

* Remove debug statement

* Fix this link.

* Implemented abstract base classes + some fixes

* Implemented many many new options

* Yay more stuff

* Fixed renaming of filters

* Added 1.1 stuffs

* Added options groups and presets

* Fixes after variable renmame

* Added recipy groups for easyer hinting

* Implemented random Tier 0

* Updated slot_data

* Latest update for 1.1

* Applied cheaper building costs of assembler and foundry

* Implemented exploration cost in slot_data

* Fixed exposing option type

* Add goal time estimates

* Trap info

* Added support for Universal Tracker
Put more things in the never exclude pool for a more familiar gameplay

* Added iron ore to build hub

* Added Dark Matter Crystals

* Added Single Dark Matter Crystals

* Fixed typo in options preset

* Update setup directions and info

* Options formatting fixes, lower minimum ExplorationCollectableCount, add new Explorer starting inventory items preset

* Fixed incorrect description on the options

* Reduce Portable Miner and Reinforced Iron Plate quantities in "Skip Tutorial Inspired" starting preset

* Fixed options pickling error

* Reworked logic to no longer include Single: items as filler
Reworked logic for more performance
Reworked logic to always put useful equipment in pool

* Fixed Itemlinks
Removed space elevator parts from fillers
Removed more AWESOME shop purchaseables from minimal item pool
Added all equipment to minimal item pool
Removed non fissile and fertile uranium from minimal item pool
Removed portal from minimal item pool
Removed Ionized fuel from minimal item pool
Removed recipes for Hoverpack and Turbo Rifle Ammo from minimal item pool
Lowered the chance for rolling steel on randomized starter recipes

* Fixed hub milestone item leaking to into wrong milestones

* Fixed unlock cost of geothermal generator

* Fixed itemlinks again

* Add troubleshooting note about hoverpacks

* Add starting inventory bundle delivery info

* Added hint generation at generation time
Harddrive locations now go from 1-100 rather then 0-99

* Update __init__.py

Fixed mistake

* Cleaned docs to be better suited to get verified

* Update CODEOWNERS

Added Satisfactory

* Update README.md

Added Satisfactory

* Restructure and expand setup page to instruct both players and hosts

* Add terms entry for Archipelago mod

* Fixed generation of traps

* Added Robb as code owner

* Restore tests to original state

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix additional typos from code review

* Implemented fix for itterating enum flags on python 3.10

* Update en_Satisfactory.md

* Update setup_en.md

* Apply suggestions from code review

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* more world > multiworld

* Clarify universal tracker behavior

* Fix typos

* Info on smart hinting system

* Move list of additional mods to a page on the mod GitHub

* Restore revamped setup guide that other commits  overwrote
Originally from be26511205, d8bd1aaf04

* Removed bundle of ficsit coupons from the from the item pool
added estimated completion times to space elevator option description

* Apply suggestions from code review

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Wording

* Fix typo

* Update with changes from ToBeVerified branch

* Update note about gameplay options

* Update note about gameplay options

* Improved universal tracker handling

* Improved universal tracker + modernized code a bit

* Fixed bugs that where re-introduced

* Added Recipe: Excited Photonic Matter

* Removed python 3.9 workaround

* Fixed

* Apply suggestions from code review

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Streamlined handle craftable logic by using itterable rather then tuple
Removed dict.keys as the dict itzelf already enumerates over keys

* Updated option description

* Fixed typing

* More info on goal completion conditions

* More info on goal completion conditions (093fe38b6e)

* Apply suggestions from code review

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Implemented review results

* PEP8 stuff

* More PEP8

* Rename ElevatorTier->ElevatorPhase and related for clarity and consistency.
Untested

* speedups part1

* speedsups on part rules

* Fix formatting

* fix `Elevator Tier #` string literals missed in rename

* Remove unused/duplicate imports + organize imports, `== None` to `is None`

* Fixed after merge

* Updated values + removed TODO

* PEPed up the code

* Small refactorings

* Updated name slot data to phase

* Fix hint creation

* Clarify wording of elevator goal

* Review result

* Fixed minor typo in option

* Update option time estimates

---------

Co-authored-by: Rob B <computerguy440+gh@gmail.com>
Co-authored-by: ProverbialPennance <36955346+ProverbialPennance@users.noreply.github.com>
Co-authored-by: Joe Amenta <airbreather@linux.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-12-19 14:48:03 +01:00

265 lines
13 KiB
Python

from typing import TextIO, ClassVar, Any
from collections.abc import Iterable
from BaseClasses import Item, ItemClassification, CollectionState
from NetUtils import Hint
from .GameLogic import GameLogic
from .Items import Items
from .Locations import Locations, LocationData
from .StateLogic import EventId, StateLogic
from .Options import SatisfactoryOptions, Placement
from .Regions import SatisfactoryLocation, create_regions_and_return_locations
from .CriticalPathCalculator import CriticalPathCalculator
from .Web import SatisfactoryWebWorld
from ..AutoWorld import World
class SatisfactoryWorld(World):
"""
Satisfactory is a first-person open-world factory building game with a dash of exploration and combat.
Explore an alien planet, create multi-story factories, and enter conveyor belt heaven!
"""
game = "Satisfactory"
options_dataclass = SatisfactoryOptions
options: SatisfactoryOptions
topology_present = False
web = SatisfactoryWebWorld()
origin_region_name = "Overworld"
required_client_version = (0, 6, 0)
ut_can_gen_without_yaml = True
game_logic: ClassVar[GameLogic] = GameLogic()
# These are set in generate_early and thus aren't always available
state_logic: StateLogic | None = None
items: Items | None = None
critical_path: CriticalPathCalculator | None = None
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.process_universal_tracker_slot_data_if_available()
if not self.critical_path_seed:
self.critical_path_seed = self.random.random()
if self.options.mam_logic_placement.value == Placement.starting_inventory:
self.push_precollected_by_name("Building: MAM")
if self.options.awesome_logic_placement.value == Placement.starting_inventory:
self.push_precollected_by_name("Building: AWESOME Sink")
self.push_precollected_by_name("Building: AWESOME Shop")
if self.options.energy_link_logic_placement.value == Placement.starting_inventory:
self.push_precollected_by_name("Building: Power Storage")
if self.options.splitter_placement == Placement.starting_inventory:
self.push_precollected_by_name("Building: Conveyor Splitter")
self.push_precollected_by_name("Building: Conveyor Merger")
if not self.options.trap_selection_override.value:
self.options.trap_selection_override.value = set(self.options.trap_selection_preset.get_selected_list())
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)
starting_inventory: list[str] = self.options.starting_inventory_preset.get_selected_list()
for item_name in starting_inventory:
self.push_precollected_by_name(item_name)
def create_regions(self) -> None:
locations: list[LocationData] = \
Locations(self.game_logic, self.options, self.state_logic, self.items, self.critical_path).get_locations()
create_regions_and_return_locations(
self.multiworld, self.options, self.player, self.game_logic, self.state_logic, self.critical_path,
locations)
def create_items(self) -> None:
self.setup_events()
number_of_locations: int = len(self.multiworld.get_unfilled_locations(self.player))
precollected_items: list[Item] = self.multiworld.precollected_items[self.player]
self.multiworld.itempool += \
self.items.build_item_pool(self.random, precollected_items, number_of_locations)
def set_rules(self) -> None:
resource_sink_goal: bool = "AWESOME Sink Points (total)" in self.options.goal_selection \
or "AWESOME Sink Points (per minute)" in self.options.goal_selection
required_parts = set(self.game_logic.space_elevator_phases[self.options.final_elevator_phase.value - 1].keys())
if resource_sink_goal:
required_parts.union(self.game_logic.buildings["AWESOME Sink"].inputs)
self.multiworld.completion_condition[self.player] = \
lambda state: self.state_logic.can_produce_all(state, required_parts)
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change and item.name in self.game_logic.indirect_recipes:
state.prog_items[self.player][self.game_logic.indirect_recipes[item.name]] += 1
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change and item.name in self.game_logic.indirect_recipes:
state.prog_items[self.player][self.game_logic.indirect_recipes[item.name]] -= 1
return change
def fill_slot_data(self) -> dict[str, object]:
slot_hub_layout: list[list[dict[str, int]]] = []
for tier, milestones in enumerate(self.game_logic.hub_layout, 1):
slot_hub_layout.append([])
for milestone, parts in enumerate(milestones, 1):
slot_hub_layout[tier - 1].append({})
for part, amount in parts.items():
multiplied_amount = int(max(amount * (self.options.milestone_cost_multiplier / 100), 1))
slot_hub_layout[tier - 1][milestone - 1][self.item_id_str(part)] = multiplied_amount
starting_recipes: tuple[int, ...] = tuple(
self.item_name_to_id[recipe_name]
for recipe_name in self.critical_path.tier_0_recipes
)
return {
"Data": {
"HubLayout": slot_hub_layout,
"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,
self.item_id_str("Hard Drive"): int(self.options.goal_exploration_collectables_amount.value / 5),
self.item_id_str("Paleberry"): self.options.goal_exploration_collectables_amount.value * 10,
self.item_id_str("Beryl Nut"): self.options.goal_exploration_collectables_amount.value * 20,
self.item_id_str("Bacon Agaric"): self.options.goal_exploration_collectables_amount.value,
},
"Options": {
"GoalSelection": self.options.goal_selection.value,
"GoalRequirement": self.options.goal_requirement.value,
"FinalElevatorPhase": self.options.final_elevator_phase.value,
"FinalResourceSinkPointsTotal": self.options.goal_awesome_sink_points_total.value,
"FinalResourceSinkPointsPerMinute": self.options.goal_awesome_sink_points_per_minute.value,
"FreeSampleEquipment": self.options.free_sample_equipment.value,
"FreeSampleBuildings": self.options.free_sample_buildings.value,
"FreeSampleParts": self.options.free_sample_parts.value,
"FreeSampleRadioactive": bool(self.options.free_sample_radioactive),
"EnergyLink": bool(self.options.energy_link),
"StartingRecipies": starting_recipes
},
"SlotDataVersion": 1,
"UT": {
"Seed": self.critical_path_seed,
"RandomizeTier0": bool(self.options.randomize_starter_recipes)
}
},
"DeathLink": bool(self.options.death_link)
}
@staticmethod
def interpret_slot_data(slot_data: dict[str, Any] | None) -> dict[str, Any] | None:
"""Used by Universal Tracker, return value is passed to self.multiworld.re_gen_passthrough["Satisfactory"]"""
return slot_data
def process_universal_tracker_slot_data_if_available(self) -> None:
"""Used by Universal Tracker to correctly rebuild state"""
slot_data: dict[str, Any] | None = None
if (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
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"]
# TODO rename slot data FinalElevatorTier to FinalElevatorPhase in the mod, then here
self.options.final_elevator_phase.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"]
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.options.randomize_starter_recipes:
spoiler_handle.write(f'Starter Recipes: {sorted(self.critical_path.tier_0_recipes)}\n')
def setup_events(self) -> None:
location: SatisfactoryLocation
for location in self.get_locations():
if location.address == EventId:
item_name = location.event_name
item = Item(item_name, ItemClassification.progression, EventId, self.player)
location.place_locked_item(item)
location.show_in_spoiler = False
def get_filler_item_name(self) -> str:
if self.items:
return self.items.get_filler_item_name(self.random, None)
else:
return Items.get_filler_item_name_uninitialized(self.random)
def create_item(self, name: str) -> Item:
if self.items:
return self.items.create_item(name, self.player)
else:
return Items.create_item_uninitialized(name, self.player)
def extend_hint_information(self, _: dict[int, dict[int, str]]):
"""
Normally used for adding entrance information,
but in this case we want to create hints for locations that hold usefull items.
Since we only know item placements after generation is completed it was either this
or fill_slot_data or modify_multidata, and this method seemed the best fit
"""
locations_visible_from_start: set[int] = set(range(1338000, 1338099)) # ids of Hub 1-1,1 to 2-5,10
if "Building: AWESOME Shop" in self.options.start_inventory \
or "Building: AWESOME Shop" in self.options.start_inventory_from_pool \
or self.options.awesome_logic_placement.value == Placement.starting_inventory:
locations_visible_from_start.update(range(1338700, 1338709)) # ids of shop locations 1 to 10
location_names_with_useful_items: Iterable[str] = [
location.name
for location in self.get_locations()
if location.address in locations_visible_from_start and location.item \
and location.item.flags & (ItemClassification.progression | ItemClassification.useful) > 0
]
self.options.start_location_hints.value.update(location_names_with_useful_items)
def push_precollected_by_name(self, item_name: str) -> None:
item = self.create_item(item_name)
self.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}"
return str(self.item_name_to_id[bundled_name])