| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | """Outputs a Factorio Mod to facilitate integration with Archipelago""" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  | import dataclasses | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  | import json | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  | import shutil | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | import threading | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  | import zipfile | 
					
						
							| 
									
										
										
										
											2023-12-10 19:11:57 +01:00
										 |  |  | from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | import jinja2 | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | import Utils | 
					
						
							| 
									
										
										
										
											2022-09-30 00:36:30 +02:00
										 |  |  | import worlds.Files | 
					
						
							| 
									
										
										
										
											2021-06-25 23:32:13 +02:00
										 |  |  | from . import Options | 
					
						
							| 
									
										
										
										
											2022-06-19 18:06:27 +02:00
										 |  |  | from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \ | 
					
						
							| 
									
										
										
										
											2022-11-05 20:01:02 +01:00
										 |  |  |     base_tech_table, tech_to_progressive_lookup, fluids, useless_technologies | 
					
						
							| 
									
										
										
										
											2021-05-22 10:06:21 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  | if TYPE_CHECKING: | 
					
						
							|  |  |  |     from . import Factorio | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 10:06:21 +02:00
										 |  |  | template_env: Optional[jinja2.Environment] = None | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:29:49 +02:00
										 |  |  | data_template: Optional[jinja2.Template] = None | 
					
						
							|  |  |  | data_final_template: Optional[jinja2.Template] = None | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | locale_template: Optional[jinja2.Template] = None | 
					
						
							| 
									
										
										
										
											2021-05-11 13:28:58 +02:00
										 |  |  | control_template: Optional[jinja2.Template] = None | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  | settings_template: Optional[jinja2.Template] = None | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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", | 
					
						
							| 
									
										
										
										
											2024-11-11 11:43:16 +01:00
										 |  |  |     "factorio_version": "2.0", | 
					
						
							| 
									
										
										
										
											2021-11-08 10:00:00 -08:00
										 |  |  |     "dependencies": [ | 
					
						
							| 
									
										
										
										
											2024-12-29 20:55:40 +01:00
										 |  |  |         "base >= 2.0.28", | 
					
						
							|  |  |  |         "? quality >= 2.0.28", | 
					
						
							| 
									
										
										
										
											2024-11-11 11:43:16 +01:00
										 |  |  |         "! space-age", | 
					
						
							| 
									
										
										
										
											2022-09-24 02:43:00 -07:00
										 |  |  |         "? science-not-invited", | 
					
						
							|  |  |  |         "? factory-levels" | 
					
						
							| 
									
										
										
										
											2021-11-08 10:00:00 -08:00
										 |  |  |     ] | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-06 21:38:53 +02:00
										 |  |  | recipe_time_scales = { | 
					
						
							|  |  |  |     # using random.triangular | 
					
						
							|  |  |  |     Options.RecipeTime.option_fast: (0.25, 1), | 
					
						
							| 
									
										
										
										
											2021-06-07 11:32:39 +02:00
										 |  |  |     # 0.5, 2, 0.5 average -> 1.0 | 
					
						
							|  |  |  |     Options.RecipeTime.option_normal: (0.5, 2, 0.5), | 
					
						
							| 
									
										
										
										
											2021-06-06 21:38:53 +02:00
										 |  |  |     Options.RecipeTime.option_slow: (1, 4), | 
					
						
							| 
									
										
										
										
											2021-06-07 11:32:39 +02:00
										 |  |  |     # 0.25, 4, 0.25 average -> 1.5 | 
					
						
							|  |  |  |     Options.RecipeTime.option_chaos: (0.25, 4, 0.25), | 
					
						
							| 
									
										
										
										
											2021-06-06 21:38:53 +02:00
										 |  |  |     Options.RecipeTime.option_vanilla: None | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2021-05-07 21:58:46 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-23 19:10:26 +01:00
										 |  |  | 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) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-30 00:36:30 +02:00
										 |  |  | class FactorioModFile(worlds.Files.APContainer): | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |     game = "Factorio" | 
					
						
							|  |  |  |     compression_method = zipfile.ZIP_DEFLATED  # Factorio can't load LZMA archives | 
					
						
							| 
									
										
										
										
											2023-12-10 19:11:57 +01:00
										 |  |  |     writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]] | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, *args: Any, **kwargs: Any): | 
					
						
							|  |  |  |         super().__init__(*args, **kwargs) | 
					
						
							|  |  |  |         self.writing_tasks = [] | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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: | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  |                 filename = os.path.join(root, file) | 
					
						
							|  |  |  |                 opened_zipfile.write(filename, | 
					
						
							|  |  |  |                                      os.path.relpath(filename, | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |                                                      os.path.join(mod_dir, '..'))) | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  |         for task in self.writing_tasks: | 
					
						
							|  |  |  |             target, content = task() | 
					
						
							|  |  |  |             opened_zipfile.writestr(target, content) | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |         # now we can add extras. | 
					
						
							|  |  |  |         super(FactorioModFile, self).write_contents(opened_zipfile) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  | def generate_mod(world: "Factorio", output_directory: str): | 
					
						
							| 
									
										
										
										
											2021-07-07 10:14:58 +02:00
										 |  |  |     player = world.player | 
					
						
							| 
									
										
										
										
											2022-10-31 21:41:21 -05:00
										 |  |  |     multiworld = world.multiworld | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |     random = world.random | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |     global data_final_template, locale_template, control_template, data_template, settings_template | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     with template_load_lock: | 
					
						
							| 
									
										
										
										
											2021-07-02 01:29:49 +02:00
										 |  |  |         if not data_final_template: | 
					
						
							| 
									
										
										
										
											2022-08-18 01:33:40 +02:00
										 |  |  |             def load_template(name: str): | 
					
						
							|  |  |  |                 import pkgutil | 
					
						
							|  |  |  |                 data = pkgutil.get_data(__name__, "data/mod_template/" + name).decode() | 
					
						
							|  |  |  |                 return data, name, lambda: False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-22 10:06:21 +02:00
										 |  |  |             template_env: Optional[jinja2.Environment] = \ | 
					
						
							| 
									
										
										
										
											2022-08-18 01:33:40 +02:00
										 |  |  |                 jinja2.Environment(loader=jinja2.FunctionLoader(load_template)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:29:49 +02:00
										 |  |  |             data_template = template_env.get_template("data.lua") | 
					
						
							|  |  |  |             data_final_template = template_env.get_template("data-final-fixes.lua") | 
					
						
							| 
									
										
										
										
											2021-05-22 10:06:21 +02:00
										 |  |  |             locale_template = template_env.get_template(r"locale/en/locale.cfg") | 
					
						
							|  |  |  |             control_template = template_env.get_template("control.lua") | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |             settings_template = template_env.get_template("settings.lua") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     # get data for templates | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  |     locations = [(location, location.item) | 
					
						
							| 
									
										
										
										
											2023-04-09 20:58:24 +02:00
										 |  |  |                  for location in world.science_locations] | 
					
						
							| 
									
										
										
										
											2022-04-02 22:47:42 -05:00
										 |  |  |     mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  |     versioned_mod_name = mod_name + "_" + Utils.__version__ | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-31 20:20:59 +02:00
										 |  |  |     def flop_random(low, high, base=None): | 
					
						
							| 
									
										
										
										
											2022-08-20 19:22:03 +02:00
										 |  |  |         """Guarantees 50% below base and 50% above base, uniform distribution in each direction.""" | 
					
						
							| 
									
										
										
										
											2021-07-31 20:20:59 +02:00
										 |  |  |         if base: | 
					
						
							|  |  |  |             distance = random.random() | 
					
						
							|  |  |  |             if random.randint(0, 1): | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |                 return base + (high - base) * distance | 
					
						
							| 
									
										
										
										
											2021-07-31 20:20:59 +02:00
										 |  |  |             else: | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |                 return base - (base - low) * distance | 
					
						
							| 
									
										
										
										
											2021-07-31 20:20:59 +02:00
										 |  |  |         return random.uniform(low, high) | 
					
						
							| 
									
										
										
										
											2021-06-15 15:32:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |     template_data = { | 
					
						
							| 
									
										
										
										
											2022-10-28 21:00:06 +02:00
										 |  |  |         "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, | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |         "mod_name": mod_name, | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |         "allowed_science_packs": world.options.max_science_pack.get_allowed_packs(), | 
					
						
							|  |  |  |         "custom_technologies": world.custom_technologies, | 
					
						
							| 
									
										
										
										
											2023-04-24 01:58:26 +02:00
										 |  |  |         "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, | 
					
						
							| 
									
										
										
										
											2024-11-11 11:43:16 +01:00
										 |  |  |         "slot_name": world.player_name, | 
					
						
							|  |  |  |         "seed_name": multiworld.seed_name, | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |         "slot_player": player, | 
					
						
							| 
									
										
										
										
											2024-11-11 11:43:16 +01:00
										 |  |  |         "recipes": recipes, | 
					
						
							|  |  |  |         "random": random, | 
					
						
							|  |  |  |         "flop_random": flop_random, | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |         "recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None), | 
					
						
							|  |  |  |         "recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None), | 
					
						
							| 
									
										
										
										
											2022-06-19 18:06:27 +02:00
										 |  |  |         "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, | 
					
						
							| 
									
										
										
										
											2024-11-11 11:43:16 +01:00
										 |  |  |         "free_sample_quality_name": world.options.free_samples_quality.current_key, | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |         "progressive_technology_table": {tech.name: tech.progressive for tech in | 
					
						
							|  |  |  |                                          progressive_technology_table.values()}, | 
					
						
							|  |  |  |         "custom_recipes": world.custom_recipes, | 
					
						
							| 
									
										
										
										
											2022-06-18 13:40:02 +02:00
										 |  |  |         "liquids": fluids, | 
					
						
							| 
									
										
										
										
											2024-11-11 11:43:16 +01:00
										 |  |  |         "removed_technologies": world.removed_technologies, | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |         "chunk_shuffle": 0, | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |     for factorio_option, factorio_option_instance in dataclasses.asdict(world.options).items(): | 
					
						
							| 
									
										
										
										
											2021-12-02 15:27:48 -08:00
										 |  |  |         if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: | 
					
						
							| 
									
										
										
										
											2021-12-02 09:26:51 -08:00
										 |  |  |             continue | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |         template_data[factorio_option] = factorio_option_instance.value | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |     if world.options.silo == Options.Silo.option_randomize_recipe: | 
					
						
							| 
									
										
										
										
											2021-09-24 21:26:11 -07:00
										 |  |  |         template_data["free_sample_blacklist"]["rocket-silo"] = 1 | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |     if world.options.satellite == Options.Satellite.option_randomize_recipe: | 
					
						
							| 
									
										
										
										
											2021-11-20 16:27:17 -08:00
										 |  |  |         template_data["free_sample_blacklist"]["satellite"] = 1 | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |     template_data["free_sample_blacklist"].update({item: 1 for item in world.options.free_sample_blacklist.value}) | 
					
						
							|  |  |  |     template_data["free_sample_blacklist"].update({item: 0 for item in world.options.free_sample_whitelist.value}) | 
					
						
							| 
									
										
										
										
											2021-12-02 09:26:51 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-10 19:11:57 +01:00
										 |  |  |     zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") | 
					
						
							| 
									
										
										
										
											2024-09-18 00:18:17 +02:00
										 |  |  |     mod = FactorioModFile(zf_path, player=player, player_name=world.player_name) | 
					
						
							| 
									
										
										
										
											2022-08-18 01:33:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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/") | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  |                     mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file): | 
					
						
							|  |  |  |                                              (arcpath, content)) | 
					
						
							| 
									
										
										
										
											2022-08-18 01:33:40 +02:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2023-12-10 19:11:57 +01:00
										 |  |  |         basepath = os.path.join(os.path.dirname(__file__), "data", "mod") | 
					
						
							|  |  |  |         for dirpath, dirnames, filenames in os.walk(basepath): | 
					
						
							| 
									
										
										
										
											2023-12-21 04:26:41 +01:00
										 |  |  |             base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\") | 
					
						
							| 
									
										
										
										
											2023-12-10 19:11:57 +01:00
										 |  |  |             for filename in filenames: | 
					
						
							|  |  |  |                 mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename, | 
					
						
							|  |  |  |                                                 file_path=os.path.join(dirpath, filename): | 
					
						
							|  |  |  |                                          (arcpath, open(file_path, "rb").read())) | 
					
						
							| 
									
										
										
										
											2022-08-18 01:33:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  |     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))) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     info = base_info.copy() | 
					
						
							|  |  |  |     info["name"] = mod_name | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  |     mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json", | 
					
						
							|  |  |  |                                       json.dumps(info, indent=4))) | 
					
						
							| 
									
										
										
										
											2021-04-03 15:06:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-10 22:02:34 +01:00
										 |  |  |     # write the mod file | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |     mod.write() |