From 9db506ef42b1072a24e28b5c1f80e86280cd3bc1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 7 Jul 2021 10:14:58 +0200 Subject: [PATCH] Factorio: recipe randomization (rocket-part and science-packs only for now) --- BaseClasses.py | 6 + Utils.py | 9 +- .../mod_template/data-final-fixes.lua | 4 +- playerSettings.yaml | 3 + worlds/alttp/ItemPool.py | 5 +- worlds/factorio/Mod.py | 33 ++-- worlds/factorio/Options.py | 20 +++ worlds/factorio/Technologies.py | 142 +++++++++++++----- worlds/factorio/__init__.py | 83 +++++++--- 9 files changed, 224 insertions(+), 81 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 60edf238..eb5e99c1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1492,6 +1492,12 @@ class Spoiler(object): for dungeon, medallion in self.medallions.items(): outfile.write(f'\n{dungeon}: {medallion}') + if self.world.factorio_player_ids: + outfile.write('\n\nRecipes:\n') + for player in self.world.factorio_player_ids: + for recipe in self.world.worlds[player].custom_recipes.values(): + outfile.write(f"{recipe.name}: {recipe.ingredients} -> {recipe.products}\n") + if self.startinventory: outfile.write('\n\nStarting Inventory:\n\n') outfile.write('\n'.join(self.startinventory)) diff --git a/Utils.py b/Utils.py index 9e02799a..644d5369 100644 --- a/Utils.py +++ b/Utils.py @@ -23,6 +23,7 @@ import sys import pickle import functools import io +import collections from yaml import load, dump, safe_load @@ -53,7 +54,6 @@ def snes_to_pc(value): def parse_player_names(names, players, teams): names = tuple(n for n in (n.strip() for n in names.split(",")) if n) if len(names) != len(set(names)): - import collections name_counter = collections.Counter(names) raise ValueError(f"Duplicate Player names is not supported, " f'found multiple "{name_counter.most_common(1)[0][0]}".') @@ -405,4 +405,9 @@ class RestrictedUnpickler(pickle.Unpickler): def restricted_loads(s): """Helper function analogous to pickle.loads().""" - return RestrictedUnpickler(io.BytesIO(s)).load() \ No newline at end of file + return RestrictedUnpickler(io.BytesIO(s)).load() + +class KeyedDefaultDict(collections.defaultdict): + def __missing__(self, key): + self[key] = value = self.default_factory(key) + return value \ No newline at end of file diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index cd8fcb06..73d568c2 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -2,7 +2,9 @@ -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require('lib') -data.raw["recipe"]["rocket-part"].ingredients = {{ dict_to_recipe(rocket_recipe) }} +{%- for recipe_name, recipe in custom_recipes.items() %} +data.raw["recipe"]["{{recipe_name}}"].ingredients = {{ dict_to_recipe(recipe.ingredients) }} +{%- endfor %} local technologies = data.raw["technology"] local original_tech diff --git a/playerSettings.yaml b/playerSettings.yaml index 9c5308a9..479ea5a9 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -70,6 +70,9 @@ Factorio: normal: 0 # 50 % to 200% of original time slow: 0 # 100% to 400% of original time chaos: 0 # 25% to 400% of original time + recipe_ingredients: + rocket: 1 # only randomize rocket part recipe + science_packs: 1 # also randomize science pack ingredients max_science_pack: automation_science_pack: 0 logistic_science_pack: 0 diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index b5a94235..5bd6f566 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -9,7 +9,6 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError, fill_restrictive from worlds.alttp.Items import ItemFactory, GetBeemizerItem -from worlds.generic.Rules import forbid_items_for_player # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. @@ -349,9 +348,7 @@ def generate_itempool(world, player: int): world.escape_assist[player].append('bombs') for (location, item) in placed_items.items(): - world.push_item(world.get_location(location, player), ItemFactory(item, player), False) - world.get_location(location, player).event = True - world.get_location(location, player).locked = True + world.get_location(location, player).place_locked_item(ItemFactory(item, player)) items = ItemFactory(pool, player) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 607db947..c587eafe 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -43,8 +43,9 @@ recipe_time_scales = { Options.RecipeTime.option_vanilla: None } - -def generate_mod(world: MultiWorld, player: int): +def generate_mod(world): + player = world.player + multiworld = world.world global data_final_template, locale_template, control_template, data_template with template_load_lock: if not data_final_template: @@ -56,36 +57,36 @@ def generate_mod(world: MultiWorld, player: int): locale_template = template_env.get_template(r"locale/en/locale.cfg") control_template = template_env.get_template("control.lua") # get data for templates - player_names = {x: world.player_names[x][0] for x in world.player_ids} + player_names = {x: multiworld.player_names[x][0] for x in multiworld.player_ids} locations = [] - for location in world.get_filled_locations(player): + for location in multiworld.get_filled_locations(player): if location.address: locations.append((location.name, location.item.name, location.item.player, location.item.advancement)) - mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}" + mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_names[player][0]}" tech_cost_scale = {0: 0.1, 1: 0.25, 2: 0.5, 3: 1, 4: 2, 5: 5, - 6: 10}[world.tech_cost[player].value] + 6: 10}[multiworld.tech_cost[player].value] template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table, "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, - "mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(), - "tech_cost_scale": tech_cost_scale, "custom_technologies": world.worlds[player].custom_technologies, - "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player], - "rocket_recipe": rocket_recipes[world.max_science_pack[player].value], - "slot_name": world.player_names[player][0], "seed_name": world.seed_name, - "starting_items": world.starting_items[player], "recipes": recipes, - "random": world.slot_seeds[player], "static_nodes": world.worlds[player].static_nodes, - "recipe_time_scale": recipe_time_scales[world.recipe_time[player].value], + "mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), + "tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies, + "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], + "slot_name": multiworld.player_names[player][0], "seed_name": multiworld.seed_name, + "starting_items": multiworld.starting_items[player], "recipes": recipes, + "random": multiworld.slot_seeds[player], "static_nodes": multiworld.worlds[player].static_nodes, + "recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value], "free_sample_blacklist": {item : 1 for item in free_sample_blacklist}, "progressive_technology_table": {tech.name : tech.progressive for tech in - progressive_technology_table.values()}} + progressive_technology_table.values()}, + "custom_recipes": world.custom_recipes} for factorio_option in Options.factorio_options: - template_data[factorio_option] = getattr(world, factorio_option)[player].value + template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value control_code = control_template.render(**template_data) data_template_code = data_template.render(**template_data) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 42bb28ee..6c2f3d9c 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -17,6 +17,12 @@ class MaxSciencePack(Choice): return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \ {"space-science-pack"} # with rocket launch being the goal, post-launch techs don't make sense + @classmethod + def get_ordered_science_packs(cls): + return [option.replace("_", "-") for option, value in sorted(cls.options.items(), key=lambda pair: pair[1])] + + def get_max_pack(self): + return self.get_ordered_science_packs()[self.value].replace("_", "-") class TechCost(Choice): option_very_easy = 0 @@ -68,6 +74,19 @@ class RecipeTime(Choice): option_slow = 4 option_chaos = 5 +# TODO: implement random +class Progressive(Choice): + option_off = 0 + option_random = 1 + option_on = 2 + default = 2 + + def want_progressives(self, random): + return random.choice([True, False]) if self.value == self.option_random else int(self.value) + +class RecipeIngredients(Choice): + option_rocket = 0 + option_science_pack = 1 class FactorioStartItems(OptionDict): default = {"burner-mining-drill": 19, "stone-furnace": 19} @@ -95,6 +114,7 @@ factorio_options: typing.Dict[str, type(Option)] = { "tech_tree_information": TechTreeInformation, "starting_items": FactorioStartItems, "recipe_time": RecipeTime, + "recipe_ingredients": RecipeIngredients, "imported_blueprints": DefaultOnToggle, "world_gen": FactorioWorldGen, "progressive": DefaultOnToggle diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 4a0b579c..73d61495 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,6 +1,7 @@ from __future__ import annotations # Factorio technologies are imported from a .json document in /data from typing import Dict, Set, FrozenSet, Tuple +from collections import Counter, defaultdict import os import json import string @@ -92,15 +93,37 @@ class Recipe(FactorioElement): return f"{self.__class__.__name__}({self.name})" @property - def crafting_machines(self) -> Set[Machine]: - """crafting machines able to run this recipe""" - return machines_per_category[self.category] + def crafting_machine(self) -> str: + """cheapest crafting machine name able to run this recipe""" + return machine_per_category[self.category] @property def unlocking_technologies(self) -> Set[Technology]: """Unlocked by any of the returned technologies. Empty set indicates a starting recipe.""" return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())} + @property + def recursive_unlocking_technologies(self) -> Set[Technology]: + base = {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())} + for ingredient in self.ingredients: + base |= required_technologies[ingredient] + return base + + @property + def rel_cost(self) -> float: + ingredients = sum(self.ingredients.values()) + return min(ingredients/amount for product, amount in self.products.items()) + + @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]: + ingredients.update({name: amount*cost/recipe.products[ingredient] for name, amount in recipe.base_cost.items()}) + else: + ingredients[ingredient] += cost + return ingredients class Machine(FactorioElement): def __init__(self, name, categories): @@ -137,7 +160,9 @@ for recipe_name, recipe_data in raw_recipes.items(): recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"], recipe_data["products"]) recipes[recipe_name] = recipe if set(recipe.products).isdisjoint( - set(recipe.ingredients)) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging + # prevents loop recipes like uranium centrifuging + set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \ + not recipe_name.endswith("-reprocessing"): for product_name in recipe.products: all_product_sources.setdefault(product_name, set()).add(recipe) @@ -151,6 +176,8 @@ for name, categories in raw_machines.items(): # add electric mining drill as a crafting machine to resolve uranium-ore machines["electric-mining-drill"] = Machine("electric-mining-drill", {"mining"}) +machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this +machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported del (raw_machines) # build requirements graph for all technology ingredients @@ -161,18 +188,17 @@ for technology in technology_table.values(): def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]: - current_technologies = set() - current_technologies |= recipe.unlocking_technologies + current_technologies = recipe.unlocking_technologies for ingredient_name in recipe.ingredients: - current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) + current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done, + unlock_func=unlock_just_tech) return current_technologies def unlock(recipe: Recipe, _done) -> Set[Technology]: - current_technologies = set() - current_technologies |= recipe.unlocking_technologies + current_technologies = recipe.unlocking_technologies for ingredient_name in recipe.ingredients: - current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done) + current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done, unlock_func=unlock) current_technologies |= required_category_technologies[recipe.category] return current_technologies @@ -202,48 +228,41 @@ for ingredient_name in machines: required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name)) logical_machines = {} +machine_tech_cost = {} for machine in machines.values(): - logically_useful = True - for pot_source_machine in machines.values(): - if machine != pot_source_machine \ - and machine.categories.issuperset(pot_source_machine.categories) \ - and required_machine_technologies[machine.name].issuperset( - required_machine_technologies[pot_source_machine.name]): - logically_useful = False - break - - if logically_useful: - logical_machines[machine.name] = machine - -del (required_machine_technologies) - -machines_per_category: Dict[str: Set[Machine]] = {} -for machine in logical_machines.values(): for category in machine.categories: - machines_per_category.setdefault(category, set()).add(machine) + current_cost, current_machine = machine_tech_cost.get(category, (10000, "character")) + machine_cost = len(required_machine_technologies[machine.name]) + if machine_cost < current_cost: + machine_tech_cost[category] = machine_cost, machine.name + +machine_per_category: Dict[str: str] = {} +for category, (cost, machine_name) in machine_tech_cost.items(): + machine_per_category[category] = machine_name + +del (machine_tech_cost) # required technologies to be able to craft recipes from a certain category required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {} -for category_name, cat_machines in machines_per_category.items(): +for category_name, machine_name in machine_per_category.items(): techs = set() - for machine in cat_machines: - techs |= recursively_get_unlocking_technologies(machine.name) + techs |= recursively_get_unlocking_technologies(machine_name) required_category_technologies[category_name] = frozenset(techs) -required_technologies: Dict[str, FrozenSet[Technology]] = {} -for ingredient_name in all_ingredient_names: - required_technologies[ingredient_name] = frozenset( - recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)) +required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name : frozenset( + recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) + advancement_technologies: Set[str] = set() -for technologies in required_technologies.values(): +for ingredient_name in all_ingredient_names: + technologies = required_technologies[ingredient_name] advancement_technologies |= {technology.name for technology in technologies} @functools.lru_cache(10) -def get_rocket_requirements(ingredients: Set[str]) -> Set[str]: +def get_rocket_requirements(recipe: Recipe) -> Set[str]: techs = recursively_get_unlocking_technologies("rocket-silo") - for ingredient in ingredients: + for ingredient in recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) return {tech.name for tech in techs} @@ -266,9 +285,8 @@ rocket_recipes = { Options.MaxSciencePack.option_automation_science_pack: {"copper-cable": 10, "iron-plate": 10, "wood": 10} } -for products in rocket_recipes.values(): - requirements = get_rocket_requirements(frozenset(products)) - advancement_technologies |= requirements + +advancement_technologies |= {tech.name for tech in required_technologies["rocket-silo"]} # progressive technologies # auto-progressive @@ -357,4 +375,46 @@ technology_table.update(progressive_technology_table) common_tech_table: Dict[str, int] = {tech_name: tech_id for tech_name, tech_id in base_tech_table.items() if tech_name not in progressive_tech_table} -lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} \ No newline at end of file +lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} + +rel_cost = { + "wood" : 10000, + "iron-ore": 1, + "copper-ore": 1, + "stone": 1, + "crude-oil": 0.5, + "water": 0.001, + "coal": 1, + "raw-fish": 1000, + "steam": 0.01, + "used-up-uranium-fuel-cell": 1000 +} + +# forbid liquids for now, TODO: allow a single liquid per assembler +blacklist = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas", "light-oil", + "heavy-oil", "lubricant", "steam"} + + +def get_estimated_difficulty(recipe: Recipe): + base_ingredients = recipe.base_cost + cost = 0 + + for ingredient_name, amount in base_ingredients.items(): + if ingredient_name not in rel_cost: + raise Exception((recipe.name, ingredient_name)) + cost += rel_cost.get(ingredient_name, 1) * amount + return cost + + +science_pack_pools = {} +already_taken = blacklist.copy() +current_difficulty = 5 +for science_pack in Options.MaxSciencePack.get_ordered_science_packs(): + current = science_pack_pools[science_pack] = set() + for name, recipe in recipes.items(): + if (science_pack != "automation-science-pack" or not recipe.recursive_unlocking_technologies) \ + and get_estimated_difficulty(recipe) < current_difficulty: + current |= set(recipe.products) + current -= already_taken + already_taken |= current + current_difficulty *= 2 diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 2da0cb8e..0e8b666d 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,9 +1,10 @@ from ..AutoWorld import World -from BaseClasses import Region, Entrance, Location, MultiWorld, Item +from BaseClasses import Region, Entrance, Location, Item from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \ all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \ - progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table + progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ + science_pack_pools, Recipe, recipes, technology_table from .Shapes import get_shapes from .Mod import generate_mod from .Options import factorio_options @@ -11,6 +12,8 @@ from .Options import factorio_options class Factorio(World): game: str = "Factorio" static_nodes = {"automation", "logistics", "rocket-silo"} + custom_recipes = {} + additional_advancement_technologies = set() def generate_basic(self): for tech_name, tech_id in base_tech_table.items(): @@ -20,7 +23,8 @@ class Factorio(World): else: item_name = tech_name - tech_item = Item(item_name, item_name in advancement_technologies, + tech_item = Item(item_name, item_name in advancement_technologies or + item_name in self.additional_advancement_technologies, tech_id, self.player) tech_item.game = "Factorio" if tech_name in self.static_nodes: @@ -28,11 +32,10 @@ class Factorio(World): else: self.world.itempool.append(tech_item) world_gen = self.world.world_gen[self.player].value - if world_gen.get("seed", None) is None: # allow seed 0 + if world_gen.get("seed", None) is None: # allow seed 0 world_gen["seed"] = self.world.slot_seeds[self.player].randint(0, 2**32-1) # 32 bit uint - def generate_output(self): - generate_mod(self.world, self.player) + generate_output = generate_mod def create_regions(self): player = self.player @@ -48,11 +51,14 @@ class Factorio(World): tech.game = "Factorio" location = Location(player, "Rocket Launch", None, nauvis) nauvis.locations.append(location) + location.game = "Factorio" event = Item("Victory", True, None, player) + event.game = "Factorio" self.world.push_item(location, event, False) location.event = location.locked = True for ingredient in self.world.max_science_pack[self.player].get_allowed_packs(): location = Location(player, f"Automate {ingredient}", None, nauvis) + location.game = "Factorio" nauvis.locations.append(location) event = Item(f"Automated {ingredient}", True, None, player) self.world.push_item(location, event, False) @@ -63,14 +69,25 @@ class Factorio(World): def set_rules(self): world = self.world player = self.player - self.custom_technologies = set_custom_technologies(self.world, self.player) + self.custom_technologies = self.set_custom_technologies() + self.set_custom_recipes() shapes = get_shapes(self) if world.logic[player] != 'nologic': from worlds.generic import Rules for ingredient in self.world.max_science_pack[self.player].get_allowed_packs(): location = world.get_location(f"Automate {ingredient}", player) - location.access_rule = lambda state, ingredient=ingredient: \ - all(state.has(technology.name, player) for technology in required_technologies[ingredient]) + + if self.world.recipe_ingredients[self.player]: + custom_recipe = self.custom_recipes[ingredient] + + location.access_rule = lambda state, ingredient=ingredient, custom_recipe = custom_recipe: \ + (ingredient not in technology_table or state.has(ingredient, player)) and \ + all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients + for technology in required_technologies[sub_ingredient]) + else: + location.access_rule = lambda state, ingredient=ingredient: \ + all(state.has(technology.name, player) for technology in required_technologies[ingredient]) + for tech_name, technology in self.custom_technologies.items(): location = world.get_location(tech_name, player) Rules.set_rule(location, technology.build_rule(player)) @@ -79,8 +96,8 @@ class Factorio(World): locations = {world.get_location(requisite, player) for requisite in prequisites} Rules.add_rule(location, lambda state, locations=locations: all(state.can_reach(loc) for loc in locations)) - # get all science pack technologies (but not the ability to craft them) - victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value])) + + victory_tech_names = get_rocket_requirements(self.custom_recipes["rocket-part"]) world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) for technology in victory_tech_names) @@ -101,9 +118,41 @@ class Factorio(World): options = factorio_options -def set_custom_technologies(world: MultiWorld, player: int): - custom_technologies = {} - allowed_packs = world.max_science_pack[player].get_allowed_packs() - for technology_name, technology in base_technology_table.items(): - custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player) - return custom_technologies + def set_custom_technologies(self): + custom_technologies = {} + allowed_packs = self.world.max_science_pack[self.player].get_allowed_packs() + for technology_name, technology in base_technology_table.items(): + custom_technologies[technology_name] = technology.get_custom(self.world, allowed_packs, self.player) + return custom_technologies + + def set_custom_recipes(self): + original_rocket_part = recipes["rocket-part"] + valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()]) + self.world.random.shuffle(valid_pool) + self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, + {valid_pool[x] : 10 for x in range(3)}, + original_rocket_part.products)} + self.additional_advancement_technologies = {tech.name for tech in + self.custom_recipes["rocket-part"].recursive_unlocking_technologies} + + if self.world.recipe_ingredients[self.player]: + valid_pool = [] + for pack in self.world.max_science_pack[self.player].get_ordered_science_packs(): + valid_pool += sorted(science_pack_pools[pack]) + self.world.random.shuffle(valid_pool) + if pack in recipes: # skips over space science pack + original = recipes[pack] + new_ingredients = {} + for _ in original.ingredients: + new_ingredients[valid_pool.pop()] = 1 + new_recipe = Recipe(pack, original.category, new_ingredients, original.products) + self.additional_advancement_technologies |= {tech.name for tech in + new_recipe.recursive_unlocking_technologies} + self.custom_recipes[pack] = new_recipe + + # handle marking progressive techs as advancement + prog_add = set() + for tech in self.additional_advancement_technologies: + if tech in tech_to_progressive_lookup: + prog_add.add(tech_to_progressive_lookup[tech]) + self.additional_advancement_technologies |= prog_add