mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00

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).
202 lines
8.7 KiB
Python
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)
|