From e58ae58e243e3e6338b04af98730b830ee2ff8ca Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 4 Jul 2021 22:21:53 +0200 Subject: [PATCH] Factorio: add Progressive Option --- BaseClasses.py | 19 +--- data/factorio/mod_template/control.lua | 47 ++++++--- .../mod_template/data-final-fixes.lua | 4 +- data/factorio/mod_template/macros.lua | 10 +- playerSettings.yaml | 3 + worlds/__init__.py | 2 +- worlds/factorio/Mod.py | 8 +- worlds/factorio/Options.py | 3 +- worlds/factorio/Shapes.py | 1 + worlds/factorio/Technologies.py | 98 ++++++++++++++++++- worlds/factorio/__init__.py | 32 ++++-- 11 files changed, 182 insertions(+), 45 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c7fd5a3b..60edf238 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -155,25 +155,24 @@ class MultiWorld(): @functools.cached_property def player_ids(self): - yield from range(1, self.players + 1) + return tuple(range(1, self.players + 1)) # Todo: make these automatic, or something like get_players_for_game(game_name) @functools.cached_property def alttp_player_ids(self): - yield from (player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past") + return tuple(player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past") @functools.cached_property def hk_player_ids(self): - yield from (player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight") + return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight") @functools.cached_property def factorio_player_ids(self): - yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio") + return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Factorio") @functools.cached_property def minecraft_player_ids(self): - yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft") - + return tuple(player for player in range(1, self.players + 1) if self.game[player] == "Minecraft") def get_name_string_for_object(self, obj) -> str: return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -1337,8 +1336,6 @@ class Spoiler(object): 'shuffle': self.world.shuffle, 'item_pool': self.world.difficulty, 'item_functionality': self.world.item_functionality, - 'gt_crystals': self.world.crystals_needed_for_gt, - 'ganon_crystals': self.world.crystals_needed_for_ganon, 'open_pyramid': self.world.open_pyramid, 'accessibility': self.world.accessibility, 'hints': self.world.hints, @@ -1362,7 +1359,6 @@ class Spoiler(object): 'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_required': self.world.triforce_pieces_required, 'shop_shuffle': self.world.shop_shuffle, - 'shop_item_slots': self.world.shop_item_slots, 'shuffle_prizes': self.world.shuffle_prizes, 'sprite_pool': self.world.sprite_pool, 'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss, @@ -1418,7 +1414,6 @@ class Spoiler(object): res = getattr(self.world, f_option)[player] outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n') - if player in self.world.alttp_player_ids: for team in range(self.world.teams): outfile.write('%s%s\n' % ( @@ -1447,8 +1442,6 @@ class Spoiler(object): outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) if self.metadata['shuffle'][player] != "vanilla": outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player]) - outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player]) - outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player]) outfile.write('Pyramid hole pre-opened: %s\n' % ( 'Yes' if self.metadata['open_pyramid'][player] else 'No')) @@ -1471,8 +1464,6 @@ class Spoiler(object): "f" in self.metadata["shop_shuffle"][player])) outfile.write('Custom Potion Shop: %s\n' % bool_to_text("w" in self.metadata["shop_shuffle"][player])) - outfile.write('Shop Item Slots: %s\n' % - self.metadata["shop_item_slots"][player]) outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) outfile.write( 'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player])) diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index f83f04e0..a6a46705 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -219,25 +219,41 @@ end) commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) + if global.index_sync == nil then + global.index_sync = {} + end + local tech local force = game.forces["player"] chunks = split(call.parameter, "\t") local tech_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - local tech = force.technologies[tech_name] - - if tech ~= nil then - if global.index_sync == nil then - global.index_sync = {} + if progressive_technologies[tech_name] ~= nil then + if global.index_sync[index] == nil then -- not yet received prog item + global.index_sync[index] = tech_name + local tech_stack = progressive_technologies[tech_name] + for _, tech_name in ipairs(tech_stack) do + tech = force.technologies[tech_name] + if tech.researched ~= true then + game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) + game.play_sound({path="utility/research_completed"}) + tech.researched = true + return + end + end end - if global.index_sync[index] ~= nil and global.index_sync[index] ~= tech then - game.print("Warning: Desync Detected. Duplicate/Missing items may occur.") - end - global.index_sync[index] = tech - if tech.researched ~= true then - game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) - game.play_sound({path="utility/research_completed"}) - tech.researched = true + elseif force.technologies[tech_name] ~= nil then + tech = force.technologies[tech_name] + if tech ~= nil then + if global.index_sync[index] ~= nil and global.index_sync[index] ~= tech then + game.print("Warning: Desync Detected. Duplicate/Missing items may occur.") + end + global.index_sync[index] = tech + if tech.researched ~= true then + game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) + game.play_sound({path="utility/research_completed"}) + tech.researched = true + end end else game.print("Unknown Technology " .. tech_name) @@ -247,4 +263,7 @@ end) commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call) rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME})) -end) \ No newline at end of file +end) + +-- data +progressive_technologies = {{ dict_to_lua(progressive_technology_table) }} \ 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 cdcc1230..bb956f11 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -72,9 +72,11 @@ prep_copy(new_tree_copy, original_tech) {% if tech_cost_scale != 1 %} new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }})) {% endif %} -{%- if item_name in tech_table and tech_tree_information == 2 or original_tech_name in static_nodes -%} +{%- if (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in base_tech_table -%} {#- copy Factorio Technology Icon -#} copy_factorio_icon(new_tree_copy, "{{ item_name }}") +{%- elif (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in progressive_technology_table -%} +copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] }}") {%- else -%} {#- use default AP icon if no Factorio graphics exist -#} {% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} diff --git a/data/factorio/mod_template/macros.lua b/data/factorio/mod_template/macros.lua index 9d9416a1..8ae030a7 100644 --- a/data/factorio/mod_template/macros.lua +++ b/data/factorio/mod_template/macros.lua @@ -5,10 +5,18 @@ {% endfor -%} } {%- endmacro %} +{% macro list_to_lua(list) -%} +{ +{%- for key in list -%} + {{ variable_to_lua(key) }}{% if not loop.last %},{% endif %} +{% endfor -%} +} +{%- endmacro %} {%- macro variable_to_lua(value) %} {%- if value is mapping -%}{{ dict_to_lua(value) }} {%- elif value is boolean -%}{{ value | string | lower }} -{%- elif value is string -%} "{{ value | safe }}" +{%- elif value is string -%}"{{ value | safe }}" +{%- elif value is iterable -%}{{ list_to_lua(value) }} {%- else -%} {{ value | safe }} {%- endif -%} {%- endmacro -%} diff --git a/playerSettings.yaml b/playerSettings.yaml index 398151d0..9c5308a9 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -91,6 +91,9 @@ Factorio: single_craft: 0 half_stack: 0 stack: 0 + progressive: + on: 1 + off: 0 tech_tree_information: none: 0 advancement: 0 # show which items are a logical advancement diff --git a/worlds/__init__.py b/worlds/__init__.py index fcd9066d..80a55c12 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -32,7 +32,7 @@ assert len(lookup_any_location_name_to_id) == len(lookup_any_location_id_to_name network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name, "lookup_any_item_id_to_name": lookup_any_item_id_to_name, - "version": 7} + "version": 9} @enum.unique diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index bc78b4f5..607db947 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -11,7 +11,8 @@ import Utils import shutil from . import Options from BaseClasses import MultiWorld -from .Technologies import tech_table, rocket_recipes, recipes, free_sample_blacklist +from .Technologies import tech_table, rocket_recipes, recipes, free_sample_blacklist, progressive_technology_table, \ + base_tech_table, tech_to_progressive_lookup, progressive_tech_table template_env: Optional[jinja2.Environment] = None @@ -70,6 +71,7 @@ def generate_mod(world: MultiWorld, player: int): 6: 10}[world.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], @@ -78,7 +80,9 @@ def generate_mod(world: MultiWorld, player: int): "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], - "free_sample_blacklist": {item : 1 for item in free_sample_blacklist}} + "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()}} for factorio_option in Options.factorio_options: template_data[factorio_option] = getattr(world, factorio_option)[player].value diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 83eb7b5d..42bb28ee 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -96,5 +96,6 @@ factorio_options: typing.Dict[str, type(Option)] = { "starting_items": FactorioStartItems, "recipe_time": RecipeTime, "imported_blueprints": DefaultOnToggle, - "world_gen": FactorioWorldGen + "world_gen": FactorioWorldGen, + "progressive": DefaultOnToggle } \ No newline at end of file diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 4b8a3c3d..12e9f0b6 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -10,6 +10,7 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6, TechTreeLayout.option_medium_funnels: 10, TechTreeLayout.option_large_funnels: 15} + def get_shapes(factorio_world) -> Dict[str, List[str]]: world = factorio_world.world player = factorio_world.player diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index a00a189b..d4d39135 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,8 +1,9 @@ from __future__ import annotations # Factorio technologies are imported from a .json document in /data -from typing import Dict, Set, FrozenSet +from typing import Dict, Set, FrozenSet, Tuple import os import json +import string import Utils import logging @@ -36,10 +37,11 @@ class FactorioElement(): class Technology(FactorioElement): # maybe make subclass of Location? - def __init__(self, name, ingredients, factorio_id): + def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = ()): self.name = name self.factorio_id = factorio_id self.ingredients = ingredients + self.progressive = progressive def build_rule(self, player: int): logging.debug(f"Building rules for {self.name}") @@ -104,7 +106,6 @@ class Machine(FactorioElement): # recipes and technologies can share names in Factorio for technology_name in sorted(raw): data = raw[technology_name] - factorio_id += 1 current_ingredients = set(data["ingredients"]) technology = Technology(technology_name, current_ingredients, factorio_id) factorio_id += 1 @@ -118,7 +119,7 @@ for technology, data in raw.items(): recipe_sources.setdefault(recipe_name, set()).add(technology) del (raw) -lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} + recipes = {} all_product_sources: Dict[str, Set[Recipe]] = {"character": set()} for recipe_name, recipe_data in raw_recipes.items(): @@ -255,3 +256,92 @@ rocket_recipes = { Options.MaxSciencePack.option_automation_science_pack: {"copper-cable": 10, "iron-plate": 10, "wood": 10} } + +# progressive technologies +# auto-progressive +progressive_rows = {} +progressive_incs = set() +for tech_name in tech_table: + if tech_name.endswith("-1"): + progressive_rows[tech_name] = [] + elif tech_name[-2] == "-" and tech_name[-1] in string.digits: + progressive_incs.add(tech_name) + +for root, progressive in progressive_rows.items(): + seeking = root[:-1]+str(int(root[-1])+1) + while seeking in progressive_incs: + progressive.append(seeking) + progressive_incs.remove(seeking) + seeking = seeking[:-1]+str(int(seeking[-1])+1) + +# make root entry the progressive name +for old_name in set(progressive_rows): + prog_name = "progressive-" + old_name.rsplit("-", 1)[0] + progressive_rows[prog_name] = tuple([old_name] + progressive_rows[old_name]) + del(progressive_rows[old_name]) + +# no -1 start +base_starts = set() +for remnant in progressive_incs: + if remnant[-1] == "2": + base_starts.add(remnant[:-2]) + +for root in base_starts: + seeking = root+"-2" + progressive = [root] + while seeking in progressive_incs: + progressive.append(seeking) + seeking = seeking[:-1]+str(int(seeking[-1])+1) + progressive_rows["progressive-"+root] = tuple(progressive) + +# science packs +progressive_rows["progressive-science-pack"] = tuple(sorted(required_technologies, + key=lambda name: len(required_technologies[name]))[1:] + + ["space-science-pack"]) + + +# manual progressive +progressive_rows["progressive-processing"] = ("steel-processing", + "oil-processing", "sulfur-processing", "advanced-oil-processing", + "uranium-processing", "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-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") + +base_tech_table = tech_table.copy() # without progressive techs +base_technology_table = technology_table.copy() + +progressive_tech_table: Dict[str, int] = {} +progressive_technology_table: Dict[str, Technology] = {} + +for root in sorted(progressive_rows): + progressive = progressive_rows[root] + assert all(tech in tech_table for tech in progressive) + factorio_id += 1 + progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id, + progressive) + progressive_tech_table[root] = progressive_technology.factorio_id + progressive_technology_table[root] = progressive_technology + if any(tech in advancement_technologies for tech in progressive): + advancement_technologies.add(root) + +tech_to_progressive_lookup: Dict[str, str] = {} +for technology in progressive_technology_table.values(): + for progressive in technology.progressive: + tech_to_progressive_lookup[progressive] = technology.name + +tech_table.update(progressive_tech_table) +technology_table.update(progressive_technology_table) + +# techs that are never progressive +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 diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 4fc500ce..0a455f89 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,8 +1,9 @@ from ..AutoWorld import World from BaseClasses import Region, Entrance, Location, MultiWorld, Item -from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \ - all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes +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 from .Shapes import get_shapes from .Mod import generate_mod from .Options import factorio_options @@ -15,8 +16,16 @@ class Factorio(World): victory_tech_names = get_rocket_requirements( frozenset(rocket_recipes[self.world.max_science_pack[self.player].value])) - for tech_name, tech_id in tech_table.items(): - tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names, + + + for tech_name, tech_id in base_tech_table.items(): + if self.world.progressive and tech_name in tech_to_progressive_lookup: + item_name = tech_to_progressive_lookup[tech_name] + tech_id = progressive_tech_table[item_name] + else: + item_name = tech_name + + tech_item = Item(item_name, item_name in advancement_technologies or item_name in victory_tech_names, tech_id, self.player) tech_item.game = "Factorio" if tech_name in self.static_nodes: @@ -25,7 +34,7 @@ class Factorio(World): 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 - world_gen["seed"] = self.world.slot_seeds[self.player].randint(0, 2**32-1) # 32 bit uint + 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) @@ -38,7 +47,7 @@ class Factorio(World): nauvis = Region("Nauvis", None, "Nauvis", player) nauvis.world = menu.world = self.world - for tech_name, tech_id in tech_table.items(): + for tech_name, tech_id in base_tech_table.items(): tech = Location(player, tech_name, tech_id, nauvis) nauvis.locations.append(tech) tech.game = "Factorio" @@ -83,6 +92,15 @@ class Factorio(World): world.completion_condition[player] = lambda state: state.has('Victory', player) + def collect(self, state, item) -> bool: + if item.advancement and item.name in progressive_technology_table: + prog_table = progressive_technology_table[item.name].progressive + for item_name in prog_table: + if not state.has(item_name, item.player): + state.prog_items[item_name, item.player] += 1 + return True + return super(Factorio, self).collect(state, item) + def get_required_client_version(self) -> tuple: return max((0, 1, 4), super(Factorio, self).get_required_client_version()) @@ -91,6 +109,6 @@ class Factorio(World): 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 technology_table.items(): + for technology_name, technology in base_technology_table.items(): custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player) return custom_technologies