| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | """Outputs a Factorio Mod to facilitate integration with Archipelago""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import os | 
					
						
							| 
									
										
										
										
											2021-04-03 15:06:32 +02:00
										 |  |  | import zipfile | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | from typing import Optional | 
					
						
							|  |  |  | import threading | 
					
						
							|  |  |  | import json | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import jinja2 | 
					
						
							|  |  |  | import shutil | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | import Utils | 
					
						
							|  |  |  | import Patch | 
					
						
							| 
									
										
										
										
											2022-09-30 00:36:30 +02:00
										 |  |  | import worlds.AutoWorld | 
					
						
							|  |  |  | import worlds.Files | 
					
						
							| 
									
										
										
										
											2021-06-25 23:32:13 +02:00
										 |  |  | from . import Options | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-19 18:06:27 +02:00
										 |  |  | from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \ | 
					
						
							| 
									
										
										
										
											2022-06-18 13:40:02 +02:00
										 |  |  |     base_tech_table, tech_to_progressive_lookup, fluids | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											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", | 
					
						
							| 
									
										
										
										
											2021-11-08 10:00:00 -08:00
										 |  |  |     "factorio_version": "1.1", | 
					
						
							|  |  |  |     "dependencies": [ | 
					
						
							|  |  |  |         "base >= 1.1.0", | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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: | 
					
						
							|  |  |  |                 opened_zipfile.write(os.path.join(root, file), | 
					
						
							|  |  |  |                                      os.path.relpath(os.path.join(root, file), | 
					
						
							|  |  |  |                                                      os.path.join(mod_dir, '..'))) | 
					
						
							|  |  |  |         # now we can add extras. | 
					
						
							|  |  |  |         super(FactorioModFile, self).write_contents(opened_zipfile) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  | def generate_mod(world, output_directory: str): | 
					
						
							| 
									
										
										
										
											2021-07-07 10:14:58 +02:00
										 |  |  |     player = world.player | 
					
						
							|  |  |  |     multiworld = world.world | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  |     locations = [] | 
					
						
							| 
									
										
										
										
											2021-07-07 10:14:58 +02:00
										 |  |  |     for location in multiworld.get_filled_locations(player): | 
					
						
							| 
									
										
										
										
											2021-05-22 07:54:12 +02:00
										 |  |  |         if location.address: | 
					
						
							| 
									
										
										
										
											2021-05-22 10:46:27 +02:00
										 |  |  |             locations.append((location.name, location.item.name, location.item.player, location.item.advancement)) | 
					
						
							| 
									
										
										
										
											2022-04-02 22:47:42 -05:00
										 |  |  |     mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" | 
					
						
							| 
									
										
										
										
											2021-06-15 15:32:40 +02:00
										 |  |  |     tech_cost_scale = {0: 0.1, | 
					
						
							|  |  |  |                        1: 0.25, | 
					
						
							|  |  |  |                        2: 0.5, | 
					
						
							|  |  |  |                        3: 1, | 
					
						
							|  |  |  |                        4: 2, | 
					
						
							|  |  |  |                        5: 5, | 
					
						
							| 
									
										
										
										
											2021-07-07 10:14:58 +02:00
										 |  |  |                        6: 10}[multiworld.tech_cost[player].value] | 
					
						
							| 
									
										
										
										
											2021-07-31 20:20:59 +02:00
										 |  |  |     random = multiworld.slot_seeds[player] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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-03-14 20:26:16 +01:00
										 |  |  |         "locations": locations, "player_names": multiworld.player_name, "tech_table": tech_table, | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |         "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(), | 
					
						
							|  |  |  |         "tech_cost_scale": tech_cost_scale, | 
					
						
							|  |  |  |         "custom_technologies": multiworld.worlds[player].custom_technologies, | 
					
						
							|  |  |  |         "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], | 
					
						
							|  |  |  |         "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, | 
					
						
							|  |  |  |         "static_nodes": multiworld.worlds[player].static_nodes, | 
					
						
							|  |  |  |         "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), | 
					
						
							| 
									
										
										
										
											2022-06-19 18:06:27 +02:00
										 |  |  |         "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, | 
					
						
							| 
									
										
										
										
											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, | 
					
						
							|  |  |  |         "max_science_pack": multiworld.max_science_pack[player].value, | 
					
						
							| 
									
										
										
										
											2022-06-18 13:40:02 +02:00
										 |  |  |         "liquids": fluids, | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  |         "goal": multiworld.goal[player].value, | 
					
						
							|  |  |  |         "energy_link": multiworld.energy_link[player].value | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  |     for factorio_option in Options.factorio_options: | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2021-07-07 10:14:58 +02:00
										 |  |  |         template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-24 21:26:11 -07:00
										 |  |  |     if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: | 
					
						
							|  |  |  |         template_data["free_sample_blacklist"]["rocket-silo"] = 1 | 
					
						
							| 
									
										
										
										
											2022-02-24 22:40:16 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-20 16:27:17 -08:00
										 |  |  |     if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: | 
					
						
							|  |  |  |         template_data["free_sample_blacklist"]["satellite"] = 1 | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-02 15:27:00 -08:00
										 |  |  |     template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) | 
					
						
							| 
									
										
										
										
											2021-12-02 15:27:48 -08:00
										 |  |  |     template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) | 
					
						
							| 
									
										
										
										
											2021-12-02 09:26:51 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-07 01:55:53 +02:00
										 |  |  |     control_code = control_template.render(**template_data) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:29:49 +02:00
										 |  |  |     data_template_code = data_template.render(**template_data) | 
					
						
							|  |  |  |     data_final_fixes_code = data_final_template.render(**template_data) | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |     settings_code = settings_template.render(**template_data) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |     mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     en_locale_dir = os.path.join(mod_dir, "locale", "en") | 
					
						
							|  |  |  |     os.makedirs(en_locale_dir, exist_ok=True) | 
					
						
							| 
									
										
										
										
											2022-08-18 01:33:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if world.zip_path: | 
					
						
							|  |  |  |         # Maybe investigate read from zip, write to zip, without temp file? | 
					
						
							|  |  |  |         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/") | 
					
						
							|  |  |  |                     target = os.path.join(mod_dir, path_part) | 
					
						
							|  |  |  |                     os.makedirs(os.path.split(target)[0], exist_ok=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     with open(target, "wb") as f: | 
					
						
							|  |  |  |                         f.write(zf.read(file)) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-02 01:29:49 +02:00
										 |  |  |     with open(os.path.join(mod_dir, "data.lua"), "wt") as f: | 
					
						
							|  |  |  |         f.write(data_template_code) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f: | 
					
						
							| 
									
										
										
										
											2021-04-07 01:55:53 +02:00
										 |  |  |         f.write(data_final_fixes_code) | 
					
						
							| 
									
										
										
										
											2021-07-02 01:29:49 +02:00
										 |  |  |     with open(os.path.join(mod_dir, "control.lua"), "wt") as f: | 
					
						
							|  |  |  |         f.write(control_code) | 
					
						
							| 
									
										
										
										
											2021-11-24 01:55:36 -08:00
										 |  |  |     with open(os.path.join(mod_dir, "settings.lua"), "wt") as f: | 
					
						
							|  |  |  |         f.write(settings_code) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     locale_content = locale_template.render(**template_data) | 
					
						
							|  |  |  |     with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f: | 
					
						
							|  |  |  |         f.write(locale_content) | 
					
						
							|  |  |  |     info = base_info.copy() | 
					
						
							|  |  |  |     info["name"] = mod_name | 
					
						
							|  |  |  |     with open(os.path.join(mod_dir, "info.json"), "wt") as f: | 
					
						
							|  |  |  |         json.dump(info, f, indent=4) | 
					
						
							| 
									
										
										
										
											2021-04-03 15:06:32 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # zip the result | 
					
						
							| 
									
										
										
										
											2021-06-07 11:32:39 +02:00
										 |  |  |     zf_path = os.path.join(mod_dir + ".zip") | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |     mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) | 
					
						
							|  |  |  |     mod.write() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-03 15:06:32 +02:00
										 |  |  |     shutil.rmtree(mod_dir) |