Factorio: revamped location system (#1147)

This commit is contained in:
Fabian Dill 2022-10-28 21:00:06 +02:00 committed by GitHub
parent ec0389eefb
commit 53974d568b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 294 additions and 233 deletions

View File

@ -37,7 +37,7 @@ class Version(typing.NamedTuple):
build: int build: int
__version__ = "0.3.5" __version__ = "0.3.6"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")

View File

@ -0,0 +1,32 @@
from typing import Dict, List
from .Technologies import factorio_base_id, factorio_id
from .Options import MaxSciencePack
boundary: int = 0xff
total_locations: int = 0xff
assert total_locations <= boundary
assert factorio_base_id != factorio_id
def make_pools() -> Dict[str, List[str]]:
pools: Dict[str, List[str]] = {}
for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1):
max_needed: int = sum(divmod(boundary, i))
scale: float = boundary / max_needed
prefix: str = f"AP-{i}-"
pools[pack] = [prefix + hex(int(x * scale))[2:].upper().zfill(2) for x in range(1, max_needed + 1)]
return pools
location_pools: Dict[str, List[str]] = make_pools()
location_table: Dict[str, int] = {}
end_id: int = factorio_id
for pool in location_pools.values():
location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)})
end_id += len(pool)
assert end_id - len(location_table) == factorio_id
del pool

View File

@ -1,23 +1,23 @@
"""Outputs a Factorio Mod to facilitate integration with Archipelago""" """Outputs a Factorio Mod to facilitate integration with Archipelago"""
import os
import zipfile
from typing import Optional
import threading
import json import json
import os
import shutil
import threading
import zipfile
from typing import Optional, TYPE_CHECKING
import jinja2 import jinja2
import shutil
import Utils import Utils
import Patch
import worlds.AutoWorld
import worlds.Files import worlds.Files
from . import Options from . import Options
from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \ from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \
base_tech_table, tech_to_progressive_lookup, fluids base_tech_table, tech_to_progressive_lookup, fluids
if TYPE_CHECKING:
from . import Factorio
template_env: Optional[jinja2.Environment] = None template_env: Optional[jinja2.Environment] = None
data_template: Optional[jinja2.Template] = None data_template: Optional[jinja2.Template] = None
@ -75,7 +75,7 @@ class FactorioModFile(worlds.Files.APContainer):
super(FactorioModFile, self).write_contents(opened_zipfile) super(FactorioModFile, self).write_contents(opened_zipfile)
def generate_mod(world, output_directory: str): def generate_mod(world: "Factorio", output_directory: str):
player = world.player player = world.player
multiworld = world.world multiworld = world.world
global data_final_template, locale_template, control_template, data_template, settings_template global data_final_template, locale_template, control_template, data_template, settings_template
@ -95,18 +95,10 @@ def generate_mod(world, output_directory: str):
control_template = template_env.get_template("control.lua") control_template = template_env.get_template("control.lua")
settings_template = template_env.get_template("settings.lua") settings_template = template_env.get_template("settings.lua")
# get data for templates # get data for templates
locations = [] locations = [(location, location.item)
for location in multiworld.get_filled_locations(player): for location in world.locations]
if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
tech_cost_scale = {0: 0.1,
1: 0.25,
2: 0.5,
3: 1,
4: 2,
5: 5,
6: 10}[multiworld.tech_cost[player].value]
random = multiworld.slot_seeds[player] random = multiworld.slot_seeds[player]
def flop_random(low, high, base=None): def flop_random(low, high, base=None):
@ -120,18 +112,19 @@ def generate_mod(world, output_directory: str):
return random.uniform(low, high) return random.uniform(low, high)
template_data = { template_data = {
"locations": locations, "player_names": multiworld.player_name, "tech_table": tech_table, "locations": locations,
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, "player_names": multiworld.player_name,
"tech_table": tech_table,
"base_tech_table": base_tech_table,
"tech_to_progressive_lookup": tech_to_progressive_lookup,
"mod_name": mod_name, "mod_name": mod_name,
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost_scale,
"custom_technologies": multiworld.worlds[player].custom_technologies, "custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
"slot_player": player, "slot_player": player,
"starting_items": multiworld.starting_items[player], "recipes": recipes, "starting_items": multiworld.starting_items[player], "recipes": recipes,
"random": random, "flop_random": flop_random, "random": random, "flop_random": flop_random,
"static_nodes": multiworld.worlds[player].static_nodes,
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), "recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None),
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), "recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None),
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, "free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
@ -141,7 +134,7 @@ def generate_mod(world, output_directory: str):
"max_science_pack": multiworld.max_science_pack[player].value, "max_science_pack": multiworld.max_science_pack[player].value,
"liquids": fluids, "liquids": fluids,
"goal": multiworld.goal[player].value, "goal": multiworld.goal[player].value,
"energy_link": multiworld.energy_link[player].value "energy_link": multiworld.energy_link[player].value,
} }
for factorio_option in Options.factorio_options: for factorio_option in Options.factorio_options:

View File

@ -41,17 +41,30 @@ class Goal(Choice):
default = 0 default = 0
class TechCost(Choice): class TechCost(Range):
"""How expensive are the technologies.""" range_start = 1
display_name = "Technology Cost Scale" range_end = 10000
option_very_easy = 0 default = 5
option_easy = 1
option_kind = 2
option_normal = 3 class MinTechCost(TechCost):
option_hard = 4 """The cheapest a Technology can be in Science Packs."""
option_very_hard = 5 display_name = "Minimum Science Pack Cost"
option_insane = 6 default = 5
default = 3
class MaxTechCost(TechCost):
"""The most expensive a Technology can be in Science Packs."""
display_name = "Maximum Science Pack Cost"
default = 500
class TechCostMix(Range):
"""Percent chance that a preceding Science Pack is also required.
Chance is rolled per preceding pack."""
display_name = "Science Pack Cost Mix"
range_end = 100
default = 70
class Silo(Choice): class Silo(Choice):
@ -168,7 +181,7 @@ class FactorioFreeSampleWhitelist(OptionSet):
class TrapCount(Range): class TrapCount(Range):
range_end = 4 range_end = 25
class AttackTrapCount(TrapCount): class AttackTrapCount(TrapCount):
@ -343,7 +356,9 @@ factorio_options: typing.Dict[str, type(Option)] = {
"max_science_pack": MaxSciencePack, "max_science_pack": MaxSciencePack,
"goal": Goal, "goal": Goal,
"tech_tree_layout": TechTreeLayout, "tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost, "min_tech_cost": MinTechCost,
"max_tech_cost": MaxTechCost,
"tech_cost_mix": TechCostMix,
"silo": Silo, "silo": Silo,
"satellite": Satellite, "satellite": Satellite,
"free_samples": FreeSamples, "free_samples": FreeSamples,

View File

@ -1,8 +1,11 @@
from typing import Dict, List, Set from typing import Dict, List, Set, TYPE_CHECKING
from collections import deque from collections import deque
from .Options import TechTreeLayout from .Options import TechTreeLayout
if TYPE_CHECKING:
from . import Factorio, FactorioScienceLocation
funnel_layers = {TechTreeLayout.option_small_funnels: 3, funnel_layers = {TechTreeLayout.option_small_funnels: 3,
TechTreeLayout.option_medium_funnels: 4, TechTreeLayout.option_medium_funnels: 4,
TechTreeLayout.option_large_funnels: 5} TechTreeLayout.option_large_funnels: 5}
@ -12,24 +15,26 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6,
TechTreeLayout.option_large_funnels: 15} TechTreeLayout.option_large_funnels: 15}
def get_shapes(factorio_world) -> Dict[str, List[str]]: def _sorter(location: "FactorioScienceLocation"):
return location.complexity, location.rel_cost
def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]:
world = factorio_world.world world = factorio_world.world
player = factorio_world.player player = factorio_world.player
prerequisites: Dict[str, Set[str]] = {} prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {}
layout = world.tech_tree_layout[player].value layout = world.tech_tree_layout[player].value
custom_technologies = factorio_world.custom_technologies locations: List["FactorioScienceLocation"] = sorted(factorio_world.locations, key=lambda loc: loc.name)
tech_names: List[str] = list(set(custom_technologies) - world.worlds[player].static_nodes) world.random.shuffle(locations)
tech_names.sort()
world.random.shuffle(tech_names)
if layout == TechTreeLayout.option_single: if layout == TechTreeLayout.option_single:
pass pass
elif layout == TechTreeLayout.option_small_diamonds: elif layout == TechTreeLayout.option_small_diamonds:
slice_size = 4 slice_size = 4
while len(tech_names) > slice_size: while len(locations) > slice_size:
slice = tech_names[:slice_size] slice = locations[:slice_size]
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) slice.sort(key=_sorter)
diamond_0, diamond_1, diamond_2, diamond_3 = slice diamond_0, diamond_1, diamond_2, diamond_3 = slice
# 0 | # 0 |
@ -40,10 +45,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
elif layout == TechTreeLayout.option_medium_diamonds: elif layout == TechTreeLayout.option_medium_diamonds:
slice_size = 9 slice_size = 9
while len(tech_names) > slice_size: while len(locations) > slice_size:
slice = tech_names[:slice_size] slice = locations[:slice_size]
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) slice.sort(key=_sorter)
# 0 | # 0 |
# 1 2 | # 1 2 |
@ -65,10 +70,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
elif layout == TechTreeLayout.option_large_diamonds: elif layout == TechTreeLayout.option_large_diamonds:
slice_size = 16 slice_size = 16
while len(tech_names) > slice_size: while len(locations) > slice_size:
slice = tech_names[:slice_size] slice = locations[:slice_size]
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) slice.sort(key=_sorter)
# 0 | # 0 |
# 1 2 | # 1 2 |
@ -101,10 +106,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
elif layout == TechTreeLayout.option_small_pyramids: elif layout == TechTreeLayout.option_small_pyramids:
slice_size = 6 slice_size = 6
while len(tech_names) > slice_size: while len(locations) > slice_size:
slice = tech_names[:slice_size] slice = locations[:slice_size]
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) slice.sort(key=_sorter)
# 0 | # 0 |
# 1 2 | # 1 2 |
@ -119,10 +124,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
elif layout == TechTreeLayout.option_medium_pyramids: elif layout == TechTreeLayout.option_medium_pyramids:
slice_size = 10 slice_size = 10
while len(tech_names) > slice_size: while len(locations) > slice_size:
slice = tech_names[:slice_size] slice = locations[:slice_size]
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) slice.sort(key=_sorter)
# 0 | # 0 |
# 1 2 | # 1 2 |
@ -144,10 +149,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
elif layout == TechTreeLayout.option_large_pyramids: elif layout == TechTreeLayout.option_large_pyramids:
slice_size = 15 slice_size = 15
while len(tech_names) > slice_size: while len(locations) > slice_size:
slice = tech_names[:slice_size] slice = locations[:slice_size]
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) slice.sort(key=_sorter)
# 0 | # 0 |
# 1 2 | # 1 2 |
@ -176,17 +181,17 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
elif layout in funnel_layers: elif layout in funnel_layers:
slice_size = funnel_slice_sizes[layout] slice_size = funnel_slice_sizes[layout]
world.random.shuffle(tech_names) world.random.shuffle(locations)
while len(tech_names) > slice_size: while len(locations) > slice_size:
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
current_tech_names = tech_names[:slice_size] current_locations = locations[:slice_size]
layer_size = funnel_layers[layout] layer_size = funnel_layers[layout]
previous_slice = [] previous_slice = []
current_tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) current_locations.sort(key=_sorter)
for layer in range(funnel_layers[layout]): for layer in range(funnel_layers[layout]):
slice = current_tech_names[:layer_size] slice = current_locations[:layer_size]
current_tech_names = current_tech_names[layer_size:] current_locations = current_locations[layer_size:]
if previous_slice: if previous_slice:
for i, tech_name in enumerate(slice): for i, tech_name in enumerate(slice):
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2]) prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2])
@ -202,10 +207,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
# 15 | # 15 |
# 16 | # 16 |
slice_size = 17 slice_size = 17
while len(tech_names) > slice_size: while len(locations) > slice_size:
slice = tech_names[:slice_size] slice = locations[:slice_size]
tech_names = tech_names[slice_size:] locations = locations[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) slice.sort(key=_sorter)
prerequisites[slice[1]] = {slice[0]} prerequisites[slice[1]] = {slice[0]}
prerequisites[slice[2]] = {slice[0]} prerequisites[slice[2]] = {slice[0]}
@ -229,13 +234,13 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
prerequisites[slice[15]] = {slice[9], slice[10], slice[11], slice[12], slice[13], slice[14]} prerequisites[slice[15]] = {slice[9], slice[10], slice[11], slice[12], slice[13], slice[14]}
prerequisites[slice[16]] = {slice[15]} prerequisites[slice[16]] = {slice[15]}
elif layout == TechTreeLayout.option_choices: elif layout == TechTreeLayout.option_choices:
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) locations.sort(key=_sorter)
current_choices = deque([tech_names[0]]) current_choices = deque([locations[0]])
tech_names = tech_names[1:] locations = locations[1:]
while len(tech_names) > 1: while len(locations) > 1:
source = current_choices.pop() source = current_choices.pop()
choices = tech_names[:2] choices = locations[:2]
tech_names = tech_names[2:] locations = locations[2:]
for choice in choices: for choice in choices:
prerequisites[choice] = {source} prerequisites[choice] = {source}
current_choices.extendleft(choices) current_choices.extendleft(choices)

View File

@ -36,7 +36,7 @@ technology_table: Dict[str, Technology] = {}
always = lambda state: True always = lambda state: True
class FactorioElement(): class FactorioElement:
name: str name: str
def __repr__(self): def __repr__(self):
@ -98,7 +98,7 @@ class CustomTechnology(Technology):
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
or origin.name == "rocket-silo") or origin.name == "rocket-silo")
self.player = player self.player = player
if origin.name not in world.worlds[player].static_nodes: if origin.name not in world.worlds[player].special_nodes:
if military_allowed: if military_allowed:
ingredients.add("military-science-pack") ingredients.add("military-science-pack")
ingredients = list(ingredients) ingredients = list(ingredients)

View File

@ -1,19 +1,20 @@
from __future__ import annotations
import collections import collections
import logging
import typing import typing
from worlds.AutoWorld import World, WebWorld
from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
from .Shapes import get_shapes
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
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, \
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
fluids, stacking_items, valid_ingredients fluids, stacking_items, valid_ingredients, progressive_rows
from .Shapes import get_shapes from .Locations import location_pools, location_table
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
import logging
class FactorioWeb(WebWorld): class FactorioWeb(WebWorld):
@ -43,89 +44,75 @@ class Factorio(World):
research new technologies, and become more efficient in your quest to build a rocket and return home. research new technologies, and become more efficient in your quest to build a rocket and return home.
""" """
game: str = "Factorio" game: str = "Factorio"
static_nodes = {"automation", "logistics", "rocket-silo"} special_nodes = {"automation", "logistics", "rocket-silo"}
custom_recipes: typing.Dict[str, Recipe] custom_recipes: typing.Dict[str, Recipe]
location_pool: typing.List[FactorioScienceLocation]
advancement_technologies: typing.Set[str] advancement_technologies: typing.Set[str]
web = FactorioWeb() web = FactorioWeb()
item_name_to_id = all_items item_name_to_id = all_items
location_name_to_id = base_tech_table # TODO: remove base_tech_table ~ 0.3.7
location_name_to_id = {**base_tech_table, **location_table}
item_name_groups = { item_name_groups = {
"Progressive": set(progressive_tech_table.keys()), "Progressive": set(progressive_tech_table.keys()),
} }
data_version = 5 data_version = 6
required_client_version = (0, 3, 0) required_client_version = (0, 3, 6)
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
tech_mix: int = 0
skip_silo: bool = False
def __init__(self, world, player: int): def __init__(self, world, player: int):
super(Factorio, self).__init__(world, player) super(Factorio, self).__init__(world, player)
self.advancement_technologies = set() self.advancement_technologies = set()
self.custom_recipes = {} self.custom_recipes = {}
self.locations = []
def generate_basic(self):
player = self.player
want_progressives = collections.defaultdict(lambda: self.world.progressive[player].
want_progressives(self.world.random))
skip_silo = self.world.silo[player].value == Silo.option_spawn
evolution_traps_wanted = self.world.evolution_traps[player].value
attack_traps_wanted = self.world.attack_traps[player].value
traps_wanted = ["Evolution Trap"] * evolution_traps_wanted + ["Attack Trap"] * attack_traps_wanted
self.world.random.shuffle(traps_wanted)
for tech_name in base_tech_table:
if traps_wanted and tech_name in useless_technologies:
self.world.itempool.append(self.create_item(traps_wanted.pop()))
elif skip_silo and tech_name == "rocket-silo":
pass
else:
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
want_progressive = want_progressives[progressive_item_name]
item_name = progressive_item_name if want_progressive else tech_name
tech_item = self.create_item(item_name)
if tech_name in self.static_nodes:
self.world.get_location(tech_name, player).place_locked_item(tech_item)
else:
self.world.itempool.append(tech_item)
map_basic_settings = self.world.world_gen[player].value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
# used to be called "sending_visible"
if self.world.tech_tree_information[player] == TechTreeInformation.option_full:
# mark all locations as pre-hinted
self.world.start_location_hints[self.player].value.update(base_tech_table)
generate_output = generate_mod generate_output = generate_mod
def generate_early(self) -> None:
self.world.max_tech_cost[self.player] = max(self.world.max_tech_cost[self.player],
self.world.min_tech_cost[self.player])
self.tech_mix = self.world.tech_cost_mix[self.player]
self.skip_silo = self.world.silo[self.player].value == Silo.option_spawn
def create_regions(self): def create_regions(self):
player = self.player player = self.player
random = self.world.random
menu = Region("Menu", RegionType.Generic, "Menu", player, self.world) menu = Region("Menu", RegionType.Generic, "Menu", player, self.world)
crash = Entrance(player, "Crash Land", menu) crash = Entrance(player, "Crash Land", menu)
menu.exits.append(crash) menu.exits.append(crash)
nauvis = Region("Nauvis", RegionType.Generic, "Nauvis", player, self.world) nauvis = Region("Nauvis", RegionType.Generic, "Nauvis", player, self.world)
skip_silo = self.world.silo[self.player].value == Silo.option_spawn location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
for tech_name, tech_id in base_tech_table.items(): self.world.evolution_traps[player].value + self.world.attack_traps[player].value
if skip_silo and tech_name == "rocket-silo":
continue location_pool = []
tech = Location(player, tech_name, tech_id, nauvis)
nauvis.locations.append(tech) for pack in self.world.max_science_pack[self.player].get_allowed_packs():
tech.game = "Factorio" location_pool.extend(location_pools[pack])
location = Location(player, "Rocket Launch", None, nauvis) location_names = self.world.random.sample(location_pool, location_count)
self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names]
rand_values = sorted(random.randint(self.world.min_tech_cost[self.player],
self.world.max_tech_cost[self.player]) for _ in self.locations)
for i, location in enumerate(sorted(self.locations, key=lambda loc: loc.rel_cost)):
location.count = rand_values[i]
del rand_values
nauvis.locations.extend(self.locations)
location = FactorioLocation(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location) nauvis.locations.append(location)
location.game = "Factorio"
event = FactorioItem("Victory", ItemClassification.progression, None, player) event = FactorioItem("Victory", ItemClassification.progression, None, player)
event.game = "Factorio" location.place_locked_item(event)
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(): for ingredient in self.world.max_science_pack[self.player].get_allowed_packs():
location = Location(player, f"Automate {ingredient}", None, nauvis) location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis)
location.game = "Factorio"
nauvis.locations.append(location) nauvis.locations.append(location)
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
self.world.push_item(location, event, False) location.place_locked_item(event)
location.event = location.locked = True
crash.connect(nauvis) crash.connect(nauvis)
self.world.regions += [menu, nauvis] self.world.regions += [menu, nauvis]
@ -151,17 +138,13 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=ingredient: \ location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient]) all(state.has(technology.name, player) for technology in required_technologies[ingredient])
skip_silo = self.world.silo[self.player].value == Silo.option_spawn for location in self.locations:
for tech_name, technology in self.custom_technologies.items(): Rules.set_rule(location, lambda state, ingredients=location.ingredients:
if skip_silo and tech_name == "rocket-silo": all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients))
continue prerequisites = shapes.get(location)
location = world.get_location(tech_name, player) if prerequisites:
Rules.set_rule(location, technology.build_rule(player)) Rules.add_rule(location, lambda state, locations=
prequisites = shapes.get(tech_name) prerequisites: all(state.can_reach(loc) for loc in locations))
if prequisites:
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))
silo_recipe = None silo_recipe = None
if self.world.silo[self.player] == Silo.option_spawn: if self.world.silo[self.player] == Silo.option_spawn:
@ -179,6 +162,48 @@ class Factorio(World):
world.completion_condition[player] = lambda state: state.has('Victory', player) world.completion_condition[player] = lambda state: state.has('Victory', player)
def generate_basic(self):
player = self.player
want_progressives = collections.defaultdict(lambda: self.world.progressive[player].
want_progressives(self.world.random))
self.world.itempool.extend(self.create_item("Evolution Trap") for _ in
range(self.world.evolution_traps[player].value))
self.world.itempool.extend(self.create_item("Attack Trap") for _ in
range(self.world.attack_traps[player].value))
cost_sorted_locations = sorted(self.locations, key=lambda location: location.name)
special_index = {"automation": 0,
"logistics": 1,
"rocket-silo": -1}
loc: FactorioScienceLocation
if self.skip_silo:
removed = useless_technologies | {"rocket-silo"}
else:
removed = useless_technologies
for tech_name in base_tech_table:
if tech_name not in removed:
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
want_progressive = want_progressives[progressive_item_name]
item_name = progressive_item_name if want_progressive else tech_name
tech_item = self.create_item(item_name)
index = special_index.get(tech_name, None)
if index is None:
self.world.itempool.append(tech_item)
else:
loc = cost_sorted_locations[index]
loc.place_locked_item(tech_item)
loc.revealed = True
map_basic_settings = self.world.world_gen[player].value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
if self.world.tech_tree_information[player] == TechTreeInformation.option_full:
# mark all locations as pre-hinted
self.world.start_location_hints[self.player].value.update(base_tech_table)
for loc in self.locations:
loc.revealed = True
def collect_item(self, state, item, remove=False): def collect_item(self, state, item, remove=False):
if item.advancement and item.name in progressive_technology_table: if item.advancement and item.name in progressive_technology_table:
prog_table = progressive_technology_table[item.name].progressive prog_table = progressive_technology_table[item.name].progressive
@ -400,3 +425,33 @@ class Factorio(World):
ItemClassification.trap if "Trap" in name else ItemClassification.filler, ItemClassification.trap if "Trap" in name else ItemClassification.filler,
all_items[name], self.player) all_items[name], self.player)
return item return item
class FactorioLocation(Location):
game: str = Factorio.game
class FactorioScienceLocation(FactorioLocation):
complexity: int
revealed: bool = False
# Factorio technology properties:
ingredients: typing.Dict[str, int]
count: int
def __init__(self, player: int, name: str, address: int, parent: Region):
super(FactorioScienceLocation, self).__init__(player, name, address, parent)
# "AP-{Complexity}-{Cost}"
self.complexity = int(self.name[3]) - 1
self.rel_cost = int(self.name[5:], 16)
self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1}
for complexity in range(self.complexity):
if parent.world.tech_cost_mix[self.player] > parent.world.random.randint(0, 99):
self.ingredients[Factorio.ordered_science_packs[complexity]] = 1
self.count = parent.world.random.randint(parent.world.min_tech_cost[self.player],
parent.world.max_tech_cost[self.player])
@property
def factorio_ingredients(self) -> typing.List[typing.Tuple[str, int]]:
return [(name, count) for name, count in self.ingredients.items()]

View File

@ -1,4 +1,4 @@
{% from "macros.lua" import dict_to_recipe %} {% from "macros.lua" import dict_to_recipe, variable_to_lua %}
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require('lib') require('lib')
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
@ -50,16 +50,8 @@ data.raw["recipe"]["{{recipe_name}}"].ingredients = {{ dict_to_recipe(recipe.ing
{%- endfor %} {%- endfor %}
local technologies = data.raw["technology"] local technologies = data.raw["technology"]
local original_tech
local new_tree_copy local new_tree_copy
allowed_ingredients = {}
{%- for tech_name, technology in custom_technologies.items() %}
allowed_ingredients["{{ tech_name }}"] = {
{%- for ingredient in technology.ingredients %}
["{{ingredient}}"] = 1,
{%- endfor %}
}
{% endfor %}
local template_tech = table.deepcopy(technologies["automation"]) local template_tech = table.deepcopy(technologies["automation"])
{#- ensure the copy unlocks nothing #} {#- ensure the copy unlocks nothing #}
template_tech.unlocks = {} template_tech.unlocks = {}
@ -87,39 +79,6 @@ template_tech.prerequisites = {}
data.raw["recipe"]["rocket-silo"].enabled = true data.raw["recipe"]["rocket-silo"].enabled = true
{% endif %} {% endif %}
function prep_copy(new_copy, old_tech)
old_tech.hidden = true
local ingredient_filter = allowed_ingredients[old_tech.name]
if ingredient_filter ~= nil then
if mods["science-not-invited"] then
local weights = {
["automation-science-pack"] = 0, -- Red science
["logistic-science-pack"] = 0, -- Green science
["military-science-pack"] = 0, -- Black science
["chemical-science-pack"] = 0, -- Blue science
["production-science-pack"] = 0, -- Purple science
["utility-science-pack"] = 0, -- Yellow science
["space-science-pack"] = 0 -- Space science
}
for key, value in pairs(ingredient_filter) do
weights[key] = value
end
SNI.setWeights(weights)
-- Just in case an ingredient is being added to an existing tech. Found the root cause of the 9.223e+18 problem.
-- Turns out science-not-invited was ultimately dividing by zero, due to it being unaware of there being added ingredients.
old_tech.unit.ingredients = add_ingredients(old_tech.unit.ingredients, ingredient_filter)
SNI.sendInvite(old_tech)
-- SCIENCE-not-invited could potentially make tech cost 9.223e+18.
old_tech.unit.count = math.min(100000, old_tech.unit.count)
end
new_copy.unit = table.deepcopy(old_tech.unit)
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
new_copy.unit.ingredients = add_ingredients(new_copy.unit.ingredients, ingredient_filter)
else
new_copy.unit = table.deepcopy(old_tech.unit)
end
end
function set_ap_icon(tech) function set_ap_icon(tech)
tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png" tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
tech.icons = nil tech.icons = nil
@ -198,38 +157,40 @@ end
data.raw["ammo"]["artillery-shell"].stack_size = 10 data.raw["ammo"]["artillery-shell"].stack_size = 10
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #} {# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name, item_name, receiving_player, advancement in locations %} {%- for original_tech_name in base_tech_table -%}
original_tech = technologies["{{original_tech_name}}"] technologies["{{ original_tech_name }}"].hidden = true
{% endfor %}
{%- for location, item in locations %}
{#- the tech researched by the local player #} {#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech) new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #} new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #}
prep_copy(new_tree_copy, original_tech) new_tree_copy.unit.count = {{ location.count }}
{% if tech_cost_scale != 1 %} new_tree_copy.unit.ingredients = {{ variable_to_lua(location.factorio_ingredients) }}
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
{% endif %} {%- if location.revealed and item.name in base_tech_table -%}
{%- 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 Technology Icon -#} copy_factorio_icon(new_tree_copy, "{{ item.name }}")
copy_factorio_icon(new_tree_copy, "{{ item_name }}") {%- if item.name == "rocket-silo" and item.player == location.player %}
{%- if original_tech_name == "rocket-silo" and original_tech_name in static_nodes %}
{%- for ingredient in custom_recipes["rocket-part"].ingredients %} {%- for ingredient in custom_recipes["rocket-part"].ingredients %}
table.insert(new_tree_copy.effects, {type = "nothing", effect_description = "Ingredient {{ loop.index }}: {{ ingredient }}"}) table.insert(new_tree_copy.effects, {type = "nothing", effect_description = "Ingredient {{ loop.index }}: {{ ingredient }}"})
{% endfor -%} {% endfor -%}
{% endif -%} {% endif -%}
{%- elif (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in progressive_technology_table -%} {%- elif location.revealed and item.name in progressive_technology_table -%}
copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] }}") copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item.name][0] }}")
{%- else -%} {%- else -%}
{#- use default AP icon if no Factorio graphics exist -#} {#- 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 %} {% if item.advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
{%- endif -%} {%- endif -%}
{#- connect Technology #} {#- connect Technology #}
{%- if original_tech_name in tech_tree_layout_prerequisites %} {%- if location in tech_tree_layout_prerequisites %}
{%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %} {%- for prerequisite in tech_tree_layout_prerequisites[location] %}
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-") table.insert(new_tree_copy.prerequisites, "ap-{{ prerequisite.address }}-")
{% endfor %} {% endfor %}
{% endif -%} {% endif -%}
{#- add new Technology to game #} {#- add new Technology to game #}
data:extend{new_tree_copy} data:extend{new_tree_copy}
{% endfor %} {% endfor %}
{#- Recipe Rando #}
{% if recipe_time_scale %} {% if recipe_time_scale %}
{%- for recipe_name, recipe in recipes.items() %} {%- for recipe_name, recipe in recipes.items() %}
{%- if recipe.category not in ("basic-solid", "basic-fluid") %} {%- if recipe.category not in ("basic-solid", "basic-fluid") %}

View File

@ -5,22 +5,22 @@ archipelago=Archipelago
archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos. archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos.
[technology-name] [technology-name]
{% for original_tech_name, item_name, receiving_player, advancement in locations %} {% for location, item in locations %}
{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} {%- if location.revealed %}
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }})
{%- else %} {%- else %}
ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable ap-{{ location.address }}-= {{location.name}}
{%- endif -%} {%- endif -%}
{% endfor %} {% endfor %}
[technology-description] [technology-description]
{% for original_tech_name, item_name, receiving_player, advancement in locations %} {% for location, item in locations %}
{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} {%- if location.revealed %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}.
{%- elif tech_tree_information == 1 and advancement %} {%- elif tech_tree_information == 1 and item.advancement %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. For purposes of hints, this location is called "{{ original_tech_name }}". ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement.
{%- else %} {%- else %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}". ap-{{ location.address }}-=Researching this technology sends something to someone.
{%- endif -%} {%- endif -%}
{% endfor %} {% endfor %}

View File

@ -4,7 +4,7 @@
["{{ key }}"] = {{ variable_to_lua(value) }}{% if not loop.last %},{% endif %} ["{{ key }}"] = {{ variable_to_lua(value) }}{% if not loop.last %},{% endif %}
{% endfor -%} {% endfor -%}
} }
{%- endmacro %} {% endmacro %}
{% macro list_to_lua(list) -%} {% macro list_to_lua(list) -%}
{ {
{%- for key in list -%} {%- for key in list -%}