Files
Grinch-AP/worlds/factorio/Mod.py
Fabian Dill ac77666f2f Factorio: skip a bunch of file IO (#2444)
In a lot of cases, Factorio would write data to file first, then attach that file into zip. It now directly attaches the data to the zip and encapsulation was used to allow earlier GC in places (rendered templates especially).
2023-11-10 22:02:34 +01:00

202 lines
8.7 KiB
Python

"""Outputs a Factorio Mod to facilitate integration with Archipelago"""
import json
import os
import shutil
import threading
import zipfile
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple
import jinja2
import Utils
import worlds.Files
from . import Options
from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \
base_tech_table, tech_to_progressive_lookup, fluids, useless_technologies
if TYPE_CHECKING:
from . import Factorio
template_env: Optional[jinja2.Environment] = None
data_template: Optional[jinja2.Template] = None
data_final_template: Optional[jinja2.Template] = None
locale_template: Optional[jinja2.Template] = None
control_template: Optional[jinja2.Template] = None
settings_template: Optional[jinja2.Template] = None
template_load_lock = threading.Lock()
base_info = {
"version": Utils.__version__,
"title": "Archipelago",
"author": "Berserker",
"homepage": "https://archipelago.gg",
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "1.1",
"dependencies": [
"base >= 1.1.0",
"? science-not-invited",
"? factory-levels"
]
}
recipe_time_scales = {
# using random.triangular
Options.RecipeTime.option_fast: (0.25, 1),
# 0.5, 2, 0.5 average -> 1.0
Options.RecipeTime.option_normal: (0.5, 2, 0.5),
Options.RecipeTime.option_slow: (1, 4),
# 0.25, 4, 0.25 average -> 1.5
Options.RecipeTime.option_chaos: (0.25, 4, 0.25),
Options.RecipeTime.option_vanilla: None
}
recipe_time_ranges = {
Options.RecipeTime.option_new_fast: (0.25, 2),
Options.RecipeTime.option_new_normal: (0.25, 10),
Options.RecipeTime.option_slow: (5, 10)
}
class FactorioModFile(worlds.Files.APContainer):
game = "Factorio"
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
writing_tasks: List[Callable[[], Tuple[str, str]]]
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.writing_tasks = []
def write_contents(self, opened_zipfile: zipfile.ZipFile):
# directory containing Factorio mod has to come first, or Factorio won't recognize this file as a mod.
mod_dir = self.path[:-4] # cut off .zip
for root, dirs, files in os.walk(mod_dir):
for file in files:
filename = os.path.join(root, file)
opened_zipfile.write(filename,
os.path.relpath(filename,
os.path.join(mod_dir, '..')))
for task in self.writing_tasks:
target, content = task()
opened_zipfile.writestr(target, content)
# now we can add extras.
super(FactorioModFile, self).write_contents(opened_zipfile)
def generate_mod(world: "Factorio", output_directory: str):
player = world.player
multiworld = world.multiworld
global data_final_template, locale_template, control_template, data_template, settings_template
with template_load_lock:
if not data_final_template:
def load_template(name: str):
import pkgutil
data = pkgutil.get_data(__name__, "data/mod_template/" + name).decode()
return data, name, lambda: False
template_env: Optional[jinja2.Environment] = \
jinja2.Environment(loader=jinja2.FunctionLoader(load_template))
data_template = template_env.get_template("data.lua")
data_final_template = template_env.get_template("data-final-fixes.lua")
locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua")
settings_template = template_env.get_template("settings.lua")
# get data for templates
locations = [(location, location.item)
for location in world.science_locations]
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
versioned_mod_name = mod_name + "_" + Utils.__version__
random = multiworld.per_slot_randoms[player]
def flop_random(low, high, base=None):
"""Guarantees 50% below base and 50% above base, uniform distribution in each direction."""
if base:
distance = random.random()
if random.randint(0, 1):
return base + (high - base) * distance
else:
return base - (base - low) * distance
return random.uniform(low, high)
template_data = {
"locations": locations,
"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,
"allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites,
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
"slot_player": player,
"starting_items": multiworld.starting_items[player], "recipes": recipes,
"random": random, "flop_random": flop_random,
"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),
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
"progressive_technology_table": {tech.name: tech.progressive for tech in
progressive_technology_table.values()},
"custom_recipes": world.custom_recipes,
"max_science_pack": multiworld.max_science_pack[player].value,
"liquids": fluids,
"goal": multiworld.goal[player].value,
"energy_link": multiworld.energy_link[player].value,
"useless_technologies": useless_technologies,
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
}
for factorio_option in Options.factorio_options:
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
continue
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
template_data["free_sample_blacklist"]["rocket-silo"] = 1
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
template_data["free_sample_blacklist"]["satellite"] = 1
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
mod_dir = os.path.join(output_directory, versioned_mod_name)
zf_path = os.path.join(mod_dir + ".zip")
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
if world.zip_path:
with zipfile.ZipFile(world.zip_path) as zf:
for file in zf.infolist():
if not file.is_dir() and "/data/mod/" in file.filename:
path_part = Utils.get_text_after(file.filename, "/data/mod/")
mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file):
(arcpath, content))
else:
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua",
data_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/data-final-fixes.lua",
data_final_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/control.lua",
control_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua",
settings_template.render(**template_data)))
mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg",
locale_template.render(**template_data)))
info = base_info.copy()
info["name"] = mod_name
mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json",
json.dumps(info, indent=4)))
# write the mod file
mod.write()
# clean up
shutil.rmtree(mod_dir)