Factorio: support 2.0 update (#4110)

- removed tutorialization (Craft/Do X to unlock tech)
- start with  everything needed for power, electric mining drills, science lab and automation science already unlocked
- updated world gen
- updated mod api use
   - updated fluid boxes (CaitSith2)
- new option: free sample quality (needs quality mod)
- removed old gruft, faster gen speed, faster load time
- lists space age as explicitly not supported, so it prevents the game from trying to load both
- fixes Y offset of traps being wrong (way higher than intended)
- client now has a 5 second timeout to communicate with the bound factorio server, so it aborts actions if the server died
- savegames are now stored  in write_data_directory -> saves -> Archipelago
- add cargo-landing-pad  handling
- starting rocket silo and cargo landing pad respect free sample quality 
- supports Factorio 2.0

---------

Co-authored-by: CaitSith2 <d_good@caitsith2.com>
This commit is contained in:
Fabian Dill
2024-11-11 11:43:16 +01:00
committed by GitHub
parent b3e5ef876a
commit f3413e9cef
17 changed files with 263 additions and 200 deletions

View File

@@ -1,13 +1,13 @@
from __future__ import annotations
import orjson
import logging
import os
import string
import functools
import pkgutil
import string
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any
from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any, Optional
import orjson
import Utils
from . import Options
@@ -32,8 +32,23 @@ items_future = pool.submit(load_json_data, "items")
tech_table: Dict[str, int] = {}
technology_table: Dict[str, Technology] = {}
start_unlocked_recipes = {
"offshore-pump",
"boiler",
"steam-engine",
"automation-science-pack",
"inserter",
"small-electric-pole",
"copper-cable",
"lab",
"electronic-circuit",
"electric-mining-drill",
"pipe",
"pipe-to-ground",
}
def always(state):
def always(state) -> bool:
return True
@@ -50,15 +65,13 @@ class FactorioElement:
class Technology(FactorioElement): # maybe make subclass of Location?
has_modifier: bool
factorio_id: int
ingredients: Set[str]
progressive: Tuple[str]
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = (),
def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (),
has_modifier: bool = False, unlocks: Union[Set[str], bool] = None):
self.name = name
self.name = technology_name
self.factorio_id = factorio_id
self.ingredients = ingredients
self.progressive = progressive
self.has_modifier = has_modifier
if unlocks:
@@ -66,19 +79,6 @@ class Technology(FactorioElement): # maybe make subclass of Location?
else:
self.unlocks = set()
def build_rule(self, player: int):
logging.debug(f"Building rules for {self.name}")
return lambda state: all(state.has(f"Automated {ingredient}", player)
for ingredient in self.ingredients)
def get_prior_technologies(self) -> Set[Technology]:
"""Get Technologies that have to precede this one to resolve tree connections."""
technologies = set()
for ingredient in self.ingredients:
technologies |= required_technologies[ingredient] # technologies that unlock the recipes
return technologies
def __hash__(self):
return self.factorio_id
@@ -91,22 +91,22 @@ class Technology(FactorioElement): # maybe make subclass of Location?
class CustomTechnology(Technology):
"""A particularly configured Technology for a world."""
ingredients: Set[str]
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
ingredients = origin.ingredients & allowed_packs
military_allowed = "military-science-pack" in allowed_packs \
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
or origin.name == "rocket-silo")
ingredients = allowed_packs
self.player = player
if origin.name not in world.special_nodes:
if military_allowed:
ingredients.add("military-science-pack")
ingredients = list(ingredients)
ingredients.sort() # deterministic sample
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
elif origin.name == "rocket-silo" and military_allowed:
ingredients.add("military-science-pack")
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
ingredients = set(world.random.sample(list(ingredients), world.random.randint(1, len(ingredients))))
self.ingredients = ingredients
super(CustomTechnology, self).__init__(origin.name, origin.factorio_id)
def get_prior_technologies(self) -> Set[Technology]:
"""Get Technologies that have to precede this one to resolve tree connections."""
technologies = set()
for ingredient in self.ingredients:
technologies |= required_technologies[ingredient] # technologies that unlock the recipes
return technologies
class Recipe(FactorioElement):
@@ -149,19 +149,22 @@ class Recipe(FactorioElement):
ingredients = sum(self.ingredients.values())
return min(ingredients / amount for product, amount in self.products.items())
@property
@functools.cached_property
def base_cost(self) -> Dict[str, int]:
ingredients = Counter()
for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources:
for recipe in all_product_sources[ingredient]:
if recipe.ingredients:
ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
recipe.base_cost.items()})
else:
ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient]
else:
ingredients[ingredient] += cost
try:
for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources:
for recipe in all_product_sources[ingredient]:
if recipe.ingredients:
ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
recipe.base_cost.items()})
else:
ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient]
else:
ingredients[ingredient] += cost
except RecursionError as e:
raise Exception(f"Infinite recursion in ingredients of {self}.") from e
return ingredients
@property
@@ -191,9 +194,12 @@ recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
# recipes and technologies can share names in Factorio
for technology_name, data in sorted(techs_future.result().items()):
current_ingredients = set(data["ingredients"])
technology = Technology(technology_name, current_ingredients, factorio_tech_id,
has_modifier=data["has_modifier"], unlocks=set(data["unlocks"]))
technology = Technology(
technology_name,
factorio_tech_id,
has_modifier=data["has_modifier"],
unlocks=set(data["unlocks"]) - start_unlocked_recipes,
)
factorio_tech_id += 1
tech_table[technology_name] = technology.factorio_id
technology_table[technology_name] = technology
@@ -226,11 +232,12 @@ for recipe_name, recipe_data in raw_recipes.items():
recipes[recipe_name] = recipe
if set(recipe.products).isdisjoint(
# prevents loop recipes like uranium centrifuging
set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \
set(recipe.ingredients)) and ("barrel" not in recipe.products or recipe.name == "barrel") and \
not recipe_name.endswith("-reprocessing"):
for product_name in recipe.products:
all_product_sources.setdefault(product_name, set()).add(recipe)
assert all(recipe_name in raw_recipes for recipe_name in start_unlocked_recipes), "Unknown Recipe defined."
machines: Dict[str, Machine] = {}
@@ -248,9 +255,7 @@ del machines_future
# build requirements graph for all technology ingredients
all_ingredient_names: Set[str] = set()
for technology in technology_table.values():
all_ingredient_names |= technology.ingredients
all_ingredient_names: Set[str] = set(Options.MaxSciencePack.get_ordered_science_packs())
def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
@@ -319,13 +324,17 @@ required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_recipe: Recipe) -> Set[str]:
def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Recipe,
satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]:
techs = set()
if silo_recipe:
for ingredient in silo_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
for ingredient in part_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
if cargo_landing_pad_recipe:
for ingredient in cargo_landing_pad_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
if satellite_recipe:
techs |= satellite_recipe.unlocking_technologies
for ingredient in satellite_recipe.ingredients:
@@ -382,15 +391,15 @@ progressive_rows["progressive-processing"] = (
"uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing")
progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb")
progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron")
progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon",
"automated-rail-transportation", "rail-signals")
progressive_rows["progressive-fluid-handling"] = ("fluid-handling", "fluid-wagon")
progressive_rows["progressive-train-network"] = ("railway", "automated-rail-transportation")
progressive_rows["progressive-engine"] = ("engine", "electric-engine")
progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2")
progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment")
progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "energy-shield-mk2-equipment")
progressive_rows["progressive-wall"] = ("stone-wall", "gate")
progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer")
progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter")
progressive_rows["progressive-inserter"] = ("fast-inserter", "bulk-inserter")
progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret")
progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing
progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment",
@@ -402,7 +411,7 @@ sorted_rows = sorted(progressive_rows)
source_target_mapping: Dict[str, str] = {
"progressive-braking-force": "progressive-train-network",
"progressive-inserter-capacity-bonus": "progressive-inserter",
"progressive-refined-flammables": "progressive-flamethrower"
"progressive-refined-flammables": "progressive-flamethrower",
}
for source, target in source_target_mapping.items():
@@ -416,12 +425,14 @@ progressive_technology_table: Dict[str, Technology] = {}
for root in sorted_rows:
progressive = progressive_rows[root]
assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology"
assert all(tech in tech_table for tech in progressive), \
(f"Declared a progressive technology ({root}) without base technology. "
f"Missing: f{tuple(tech for tech in progressive if tech not in tech_table)}")
factorio_tech_id += 1
progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id,
progressive,
progressive_technology = Technology(root, factorio_tech_id,
tuple(progressive),
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
unlocks=any(technology_table[tech].unlocks for tech in progressive))
unlocks=any(technology_table[tech].unlocks for tech in progressive),)
progressive_tech_table[root] = progressive_technology.factorio_id
progressive_technology_table[root] = progressive_technology