diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py index 1903e589be09..f9db5f4a2bd8 100644 --- a/worlds/factorio/Locations.py +++ b/worlds/factorio/Locations.py @@ -1,32 +1,30 @@ from typing import Dict, List -from .Technologies import factorio_base_id, factorio_id +from .Technologies import factorio_base_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 + max_needed: int = 0xff prefix: str = f"AP-{i}-" - pools[pack] = [prefix + hex(int(x * scale))[2:].upper().zfill(2) for x in range(1, max_needed + 1)] + pools[pack] = [prefix + hex(x)[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 +end_id: int = factorio_base_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 +assert end_id - len(location_table) == factorio_base_id del pool diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index bce4bb2d160b..4f1f3fd9d0bd 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -96,7 +96,7 @@ def load_template(name: str): settings_template = template_env.get_template("settings.lua") # get data for templates locations = [(location, location.item) - for location in world.locations] + for location in world.science_locations] mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" random = multiworld.per_slot_randoms[player] diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 7ec6f07a86fd..84bcb06cab1a 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -24,7 +24,7 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se player = factorio_world.player prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} layout = world.tech_tree_layout[player].value - locations: List["FactorioScienceLocation"] = sorted(factorio_world.locations, key=lambda loc: loc.name) + locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name) world.random.shuffle(locations) if layout == TechTreeLayout.option_single: diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 1c1939ee24de..d68c6f2f779e 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -4,6 +4,7 @@ import logging import os import string +import pkgutil from collections import Counter from concurrent.futures import ThreadPoolExecutor from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any @@ -11,7 +12,7 @@ import Utils from . import Options -factorio_id = factorio_base_id = 2 ** 17 +factorio_tech_id = factorio_base_id = 2 ** 17 # Factorio technologies are imported from a .json document in /data source_folder = os.path.join(os.path.dirname(__file__), "data") @@ -19,7 +20,6 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: - import pkgutil return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode()) @@ -33,7 +33,9 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: tech_table: Dict[str, int] = {} technology_table: Dict[str, Technology] = {} -always = lambda state: True + +def always(state): + return True class FactorioElement: @@ -49,7 +51,6 @@ def __hash__(self): class Technology(FactorioElement): # maybe make subclass of Location? has_modifier: bool factorio_id: int - name: str ingredients: Set[str] progressive: Tuple[str] unlocks: Union[Set[str], bool] # bool case is for progressive technologies @@ -192,9 +193,9 @@ def __init__(self, name, categories): # recipes and technologies can share names in Factorio for technology_name, data in sorted(techs_future.result().items()): current_ingredients = set(data["ingredients"]) - technology = Technology(technology_name, current_ingredients, factorio_id, + technology = Technology(technology_name, current_ingredients, factorio_tech_id, has_modifier=data["has_modifier"], unlocks=set(data["unlocks"])) - factorio_id += 1 + factorio_tech_id += 1 tech_table[technology_name] = technology.factorio_id technology_table[technology_name] = technology for recipe_name in technology.unlocks: @@ -391,17 +392,13 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ progressive_rows["progressive-wall"] = ("stone-wall", "gate") progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer") progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter") - -sorted_rows = sorted(progressive_rows) -# to keep ID mappings the same. -# If there's a breaking change at some point, then this should be moved in with the sorted ordering progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret") -sorted_rows.append("progressive-turret") progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing -sorted_rows.append("progressive-flamethrower") progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment", "personal-roboport-mk2-equipment") -sorted_rows.append("progressive-personal-roboport-equipment") + +sorted_rows = sorted(progressive_rows) + # integrate into source_target_mapping: Dict[str, str] = { "progressive-braking-force": "progressive-train-network", @@ -421,8 +418,8 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ for root in sorted_rows: progressive = progressive_rows[root] assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" - factorio_id += 1 - progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_id, + factorio_tech_id += 1 + progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id, progressive, has_modifier=any(technology_table[tech].has_modifier for tech in progressive), unlocks=any(technology_table[tech].unlocks for tech in progressive)) @@ -504,3 +501,4 @@ def get_estimated_difficulty(recipe: Recipe): # cleanup async helpers pool.shutdown() del pool +del factorio_tech_id diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 567ab0bbda25..269ec4556652 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -6,6 +6,9 @@ from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import Component, components +from worlds.generic import Rules +from .Locations import location_pools, location_table from .Mod import generate_mod from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Shapes import get_shapes @@ -14,8 +17,6 @@ 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, \ fluids, stacking_items, valid_ingredients, progressive_rows -from .Locations import location_pools, location_table -from worlds.LauncherComponents import Component, components components.append(Component("Factorio Client", "FactorioClient")) @@ -64,18 +65,19 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 7 - required_client_version = (0, 3, 6) + data_version = 8 + required_client_version = (0, 4, 0) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() tech_mix: int = 0 skip_silo: bool = False + science_locations: typing.List[FactorioScienceLocation] def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) self.advancement_technologies = set() self.custom_recipes = {} - self.locations = [] + self.science_locations = [] generate_output = generate_mod @@ -115,18 +117,18 @@ def create_regions(self): raise Exception("Too many traps for too few locations. Either decrease the trap count, " f"or increase the location count (higher max science pack). (Player {self.player})") from e - self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) - for loc_name in location_names] + self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) + for loc_name in location_names] distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] min_cost = self.multiworld.min_tech_cost[self.player] max_cost = self.multiworld.max_tech_cost[self.player] if distribution == distribution.option_even: - rand_values = (random.randint(min_cost, max_cost) for _ in self.locations) + rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations) else: mode = {distribution.option_low: min_cost, distribution.option_middle: (min_cost+max_cost)//2, distribution.option_high: max_cost}[distribution.value] - rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.locations) + rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations) rand_values = sorted(rand_values) if self.multiworld.ramping_tech_costs[self.player]: def sorter(loc: FactorioScienceLocation): @@ -134,10 +136,10 @@ def sorter(loc: FactorioScienceLocation): else: def sorter(loc: FactorioScienceLocation): return loc.rel_cost - for i, location in enumerate(sorted(self.locations, key=sorter)): + for i, location in enumerate(sorted(self.science_locations, key=sorter)): location.count = rand_values[i] del rand_values - nauvis.locations.extend(self.locations) + nauvis.locations.extend(self.science_locations) location = FactorioLocation(player, "Rocket Launch", None, nauvis) nauvis.locations.append(location) event = FactorioItem("Victory", ItemClassification.progression, None, player) @@ -154,73 +156,25 @@ def sorter(loc: FactorioScienceLocation): def create_items(self) -> None: player = self.player + self.custom_technologies = self.set_custom_technologies() + self.set_custom_recipes() traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") for trap_name in traps: self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in range(getattr(self.multiworld, f"{trap_name.lower().replace(' ', '_')}_traps")[player])) - def set_rules(self): - world = self.multiworld - player = self.player - self.custom_technologies = self.set_custom_technologies() - self.set_custom_recipes() - shapes = get_shapes(self) - if world.logic[player] != 'nologic': - from worlds.generic import Rules - for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): - location = world.get_location(f"Automate {ingredient}", player) - - if self.multiworld.recipe_ingredients[self.player]: - custom_recipe = self.custom_recipes[ingredient] - - location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ - (ingredient not in technology_table or state.has(ingredient, player)) and \ - all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients - for technology in required_technologies[sub_ingredient]) - else: - location.access_rule = lambda state, ingredient=ingredient: \ - all(state.has(technology.name, player) for technology in required_technologies[ingredient]) - - for location in self.locations: - Rules.set_rule(location, lambda state, ingredients=location.ingredients: - all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) - prerequisites = shapes.get(location) - if prerequisites: - Rules.add_rule(location, lambda state, locations= - prerequisites: all(state.can_reach(loc) for loc in locations)) - - silo_recipe = None - if self.multiworld.silo[self.player] == Silo.option_spawn: - silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ - else next(iter(all_product_sources.get("rocket-silo"))) - part_recipe = self.custom_recipes["rocket-part"] - satellite_recipe = None - if self.multiworld.goal[self.player] == Goal.option_satellite: - satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ - else next(iter(all_product_sources.get("satellite"))) - victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) - if self.multiworld.silo[self.player] != Silo.option_spawn: - victory_tech_names.add("rocket-silo") - world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in - victory_tech_names) - - world.completion_condition[player] = lambda state: state.has('Victory', player) - - def generate_basic(self): - player = self.player want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. want_progressives(self.multiworld.random)) - cost_sorted_locations = sorted(self.locations, key=lambda location: location.name) + cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name) special_index = {"automation": 0, "logistics": 1, "rocket-silo": -1} loc: FactorioScienceLocation if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full: # mark all locations as pre-hinted - for loc in self.locations: + for loc in self.science_locations: loc.revealed = True if self.skip_silo: removed = useless_technologies | {"rocket-silo"} @@ -244,13 +198,60 @@ def generate_basic(self): loc.place_locked_item(tech_item) loc.revealed = True - map_basic_settings = self.multiworld.world_gen[player].value["basic"] + def set_rules(self): + world = self.multiworld + player = self.player + shapes = get_shapes(self) + + for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): + location = world.get_location(f"Automate {ingredient}", player) + + if self.multiworld.recipe_ingredients[self.player]: + custom_recipe = self.custom_recipes[ingredient] + + location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ + (ingredient not in technology_table or state.has(ingredient, player)) and \ + all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients + for technology in required_technologies[sub_ingredient]) + else: + location.access_rule = lambda state, ingredient=ingredient: \ + all(state.has(technology.name, player) for technology in required_technologies[ingredient]) + + for location in self.science_locations: + Rules.set_rule(location, lambda state, ingredients=location.ingredients: + all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) + prerequisites = shapes.get(location) + if prerequisites: + Rules.add_rule(location, lambda state, locations= + prerequisites: all(state.can_reach(loc) for loc in locations)) + + silo_recipe = None + if self.multiworld.silo[self.player] == Silo.option_spawn: + silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ + else next(iter(all_product_sources.get("rocket-silo"))) + part_recipe = self.custom_recipes["rocket-part"] + satellite_recipe = None + if self.multiworld.goal[self.player] == Goal.option_satellite: + satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ + else next(iter(all_product_sources.get("satellite"))) + victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) + if self.multiworld.silo[self.player] != Silo.option_spawn: + victory_tech_names.add("rocket-silo") + world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) + for technology in + victory_tech_names) + + world.completion_condition[player] = lambda state: state.has('Victory', player) + + def generate_basic(self): + map_basic_settings = self.multiworld.world_gen[self.player].value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 - map_basic_settings["seed"] = self.multiworld.per_slot_randoms[player].randint(0, 2 ** 32 - 1) # 32 bit uint + # 32 bit uint + map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1) start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value - for loc in self.locations: + for loc in self.science_locations: # show start_location_hints ingame if loc.name in start_location_hints: loc.revealed = True