From f0a6b5a8e4495f2cda5ba75f7bdc4e127187ce36 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 8 Apr 2021 19:53:24 +0200 Subject: [PATCH] Factorio: add visibility option fix tech_cost using the wrong variable name fix yaml defaults not init'ing the Option class LttP: fix potential pathing confusion in maseya palette shuffler Server: Minimum version per team made no sense, removed --- CommonClient.py | 2 +- Main.py | 160 +----------------- MultiServer.py | 15 +- Mystery.py | 2 +- Options.py | 11 +- Utils.py | 2 +- data/factorio/mod_template/control.lua | 12 +- .../mod_template/data-final-fixes.lua | 5 +- .../mod_template/locale/en/locale.cfg | 9 + worlds/__init__.py | 2 +- worlds/alttp/Rom.py | 2 +- worlds/factorio/Mod.py | 5 +- worlds/factorio/Technologies.py | 31 +++- worlds/factorio/__init__.py | 2 +- 14 files changed, 80 insertions(+), 180 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index e834e83a..2b15c3ff 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -316,7 +316,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): logger.info(f' Team #{network_player.team + 1}') current_team = network_player.team logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) - if args["datapackage_version"] > network_data_package["version"]: + if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0: await ctx.send_msgs([{"cmd": "GetDataPackage"}]) await ctx.server_auth(args['password']) diff --git a/Main.py b/Main.py index 575bed86..c3a9c2ed 100644 --- a/Main.py +++ b/Main.py @@ -10,8 +10,7 @@ import pickle from typing import Dict from BaseClasses import MultiWorld, CollectionState, Region, Item -from worlds.alttp import ALttPLocation -from worlds.alttp.Items import ItemFactory, item_table, item_name_groups +from worlds.alttp.Items import ItemFactory, item_name_groups from worlds.alttp.Regions import create_regions, mark_light_world_regions, \ lookup_vanilla_location_to_entrance from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions @@ -23,7 +22,7 @@ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_p from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple -from worlds.hk import gen_hollow, set_rules as set_hk_rules +from worlds.hk import gen_hollow from worlds.hk import create_regions as hk_create_regions from worlds.factorio import gen_factorio, factorio_create_regions from worlds.factorio.Mod import generate_mod @@ -492,7 +491,10 @@ def main(args, seed=None): for future in roms: rom_name = future.result() rom_names.append(rom_name) - minimum_versions = {"server": (0, 0, 2)} + client_versions = {} + minimum_versions = {"server": (0, 0, 3), "clients": client_versions} + for slot in world.player_ids: + client_versions[slot] = (0, 0, 3) connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for slot, team, rom_name in rom_names} @@ -500,6 +502,7 @@ def main(args, seed=None): for player, name in enumerate(team, 1): if player not in world.alttp_player_ids: connect_names[name] = (i, player) + multidata = zlib.compress(pickle.dumps({"names": parsed_names, "connect_names": connect_names, "remote_items": {player for player in range(1, world.players + 1) if @@ -544,155 +547,8 @@ def main(args, seed=None): return world - -def copy_world(world): - # ToDo: Not good yet - # delete now? - ret = MultiWorld(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.item_functionality, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) - ret.teams = world.teams - ret.player_names = copy.deepcopy(world.player_names) - ret.remote_items = world.remote_items.copy() - ret.required_medallions = world.required_medallions.copy() - ret.swamp_patch_required = world.swamp_patch_required.copy() - ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() - ret.powder_patch_required = world.powder_patch_required.copy() - ret.ganonstower_vanilla = world.ganonstower_vanilla.copy() - ret.treasure_hunt_count = world.treasure_hunt_count.copy() - ret.treasure_hunt_icon = world.treasure_hunt_icon.copy() - ret.sewer_light_cone = world.sewer_light_cone.copy() - ret.light_world_light_cone = world.light_world_light_cone - ret.dark_world_light_cone = world.dark_world_light_cone - ret.seed = world.seed - ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy() - ret.can_access_trock_front = world.can_access_trock_front.copy() - ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy() - ret.can_access_trock_middle = world.can_access_trock_middle.copy() - ret.can_take_damage = world.can_take_damage - ret.difficulty_requirements = world.difficulty_requirements.copy() - ret.fix_fake_world = world.fix_fake_world.copy() - ret.mapshuffle = world.mapshuffle.copy() - ret.compassshuffle = world.compassshuffle.copy() - ret.keyshuffle = world.keyshuffle.copy() - ret.bigkeyshuffle = world.bigkeyshuffle.copy() - ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() - ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() - ret.open_pyramid = world.open_pyramid.copy() - ret.boss_shuffle = world.boss_shuffle.copy() - ret.enemy_shuffle = world.enemy_shuffle.copy() - ret.enemy_health = world.enemy_health.copy() - ret.enemy_damage = world.enemy_damage.copy() - ret.beemizer = world.beemizer.copy() - ret.timer = world.timer.copy() - ret.shufflepots = world.shufflepots.copy() - ret.shuffle_prizes = world.shuffle_prizes.copy() - ret.shop_shuffle = world.shop_shuffle.copy() - ret.shop_shuffle_slots = world.shop_shuffle_slots.copy() - ret.dark_room_logic = world.dark_room_logic.copy() - ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy() - ret.game = world.game.copy() - ret.completion_condition = world.completion_condition.copy() - - for player in world.alttp_player_ids: - if world.mode[player] != 'inverted': - create_regions(ret, player) - else: - create_inverted_regions(ret, player) - create_shops(ret, player) - create_dungeons(ret, player) - - for player in world.hk_player_ids: - hk_create_regions(ret, player) - - copy_dynamic_regions_and_locations(world, ret) - - # copy bosses - for dungeon in world.dungeons: - for level, boss in dungeon.bosses.items(): - ret.get_dungeon(dungeon.name, dungeon.player).bosses[level] = boss - - for shop in world.shops: - copied_shop = ret.get_region(shop.region.name, shop.region.player).shop - copied_shop.inventory = copy.copy(shop.inventory) - - # connect copied world - for region in world.regions: - copied_region = ret.get_region(region.name, region.player) - copied_region.is_light_world = region.is_light_world - copied_region.is_dark_world = region.is_dark_world - for exit in copied_region.exits: - old_connection = world.get_entrance(exit.name, exit.player).connected_region - exit.connect(ret.get_region(old_connection.name, old_connection.player)) - - # fill locations - for location in world.get_locations(): - if location.item is not None: - item = Item(location.item.name, location.item.advancement, location.item.code, player = location.item.player) - ret.get_location(location.name, location.player).item = item - item.location = ret.get_location(location.name, location.player) - item.world = ret - item.type = location.item.type - item.game = location.item.game - - if location.event: - ret.get_location(location.name, location.player).event = True - if location.locked: - ret.get_location(location.name, location.player).locked = True - - - # copy remaining itempool. No item in itempool should have an assigned location - for old_item in world.itempool: - item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player) - item.type = old_item.type - ret.itempool.append(item) - - for old_item in world.precollected_items: - item = Item(old_item.name, old_item.advancement, old_item.code, player = old_item.player) - item.type = old_item.type - ret.push_precollected(item) - - # copy progress items in state - ret.state.prog_items = world.state.prog_items.copy() - ret.state.stale = {player: True for player in range(1, world.players + 1)} - - for player in world.alttp_player_ids: - set_rules(ret, player) - - for player in world.hk_player_ids: - set_hk_rules(ret, player) - - - return ret - - -def copy_dynamic_regions_and_locations(world, ret): - for region in world.dynamic_regions: - new_reg = Region(region.name, region.type, region.hint_text, region.player) - ret.regions.append(new_reg) - ret.initialize_regions([new_reg]) - ret.dynamic_regions.append(new_reg) - - # Note: ideally exits should be copied here, but the current use case (Take anys) do not require this - - if region.shop: - new_reg.shop = region.shop.__class__(new_reg, region.shop.room_id, region.shop.shopkeeper_config, - region.shop.custom, region.shop.locked, region.shop.sram_offset) - ret.shops.append(new_reg.shop) - - for location in world.dynamic_locations: - new_reg = ret.get_region(location.parent_region.name, location.parent_region.player) - new_loc = ALttPLocation(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg) - # todo: this is potentially dangerous. later refactor so we - # can apply dynamic region rules on top of copied world like other rules - new_loc.access_rule = location.access_rule - new_loc.always_allow = location.always_allow - new_loc.item_rule = location.item_rule - new_reg.locations.append(new_loc) - - ret.clear_location_cache() - - def create_playthrough(world): - """Destructive to the world it is run on.""" + """Destructive to the world while it is run, damage gets repaired afterwards.""" # get locations containing progress items prog_locations = {location for location in world.get_filled_locations() if location.item.advancement} state_cache = [None] diff --git a/MultiServer.py b/MultiServer.py index 6d0bfedb..4cd97557 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -110,7 +110,7 @@ class Context(Node): self.auto_saver_thread = None self.save_dirty = False self.tags = ['AP'] - self.minimum_client_versions: typing.Dict[typing.Tuple[int, int], Utils.Version] = {} + self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} def load(self, multidatapath: str, use_embedded_server_options: bool = False): with open(multidatapath, 'rb') as f: @@ -131,10 +131,10 @@ class Context(Node): if mdata_ver > Utils._version_tuple: raise RuntimeError(f"Supplied Multidata requires a server of at least version {mdata_ver}," f"however this server is of version {Utils._version_tuple}") - clients_ver = decoded_obj["minimum_versions"].get("clients", []) + clients_ver = decoded_obj["minimum_versions"].get("clients", {}) self.minimum_client_versions = {} - for team, player, version in clients_ver: - self.minimum_client_versions[team, player] = Utils.Version(*version) + for player, version in clients_ver.items(): + self.minimum_client_versions[player] = Utils.Version(*version) for team, names in enumerate(decoded_obj['names']): for player, name in enumerate(names, 1): @@ -991,13 +991,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): client.name = ctx.player_names[(team, slot)] client.team = team client.slot = slot - minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0)))) + minver = ctx.minimum_client_versions[slot] if minver > args['version']: errors.add('IncompatibleVersion') if ctx.compatibility == 1 and "AP" not in args['tags']: errors.add('IncompatibleVersion') - #only exact version match allowed + # only exact version match allowed elif ctx.compatibility == 0 and args['version'] != _version_tuple: errors.add('IncompatibleVersion') if errors: @@ -1013,7 +1013,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): "team": client.team, "slot": client.slot, "players": ctx.get_players_package(), "missing_locations": get_missing_checks(ctx, client), - "checked_locations": get_checked_checks(ctx, client)}] + "checked_locations": get_checked_checks(ctx, client), + }] items = get_received_items(ctx, client.team, client.slot) if items: reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)}) diff --git a/Mystery.py b/Mystery.py index 689d1904..ddd4a1c3 100644 --- a/Mystery.py +++ b/Mystery.py @@ -536,7 +536,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b if option_name in weights: setattr(ret, option_name, option.from_any(get_choice(option_name, weights))) else: - setattr(ret, option_name, option.default) + setattr(ret, option_name, option.from_any(option.default)) else: raise Exception(f"Unsupported game {ret.game}") return ret diff --git a/Options.py b/Options.py index ea32ca51..667c2ac6 100644 --- a/Options.py +++ b/Options.py @@ -104,7 +104,9 @@ class Choice(Option): @classmethod def from_any(cls, data: typing.Any): - return cls.from_text(data) + if type(data) == int and data in cls.options.values(): + return cls(data) + return cls.from_text(str(data)) class Logic(Choice): @@ -273,11 +275,16 @@ class TechTreeLayout(Choice): option_single = 0 default = 0 +class Visibility(Choice): + option_none = 0 + option_sending = 1 + default = 0 factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack, "tech_tree_layout": TechTreeLayout, "tech_cost": TechCost, - "free_samples": FreeSamples} + "free_samples": FreeSamples, + "visibility": Visibility} if __name__ == "__main__": import argparse diff --git a/Utils.py b/Utils.py index 35c9cc58..e6c6392e 100644 --- a/Utils.py +++ b/Utils.py @@ -12,7 +12,7 @@ class Version(typing.NamedTuple): minor: int build: int -__version__ = "0.0.2" +__version__ = "0.0.3" _version_tuple = tuplize_version(__version__) import builtins diff --git a/data/factorio/mod_template/control.lua b/data/factorio/mod_template/control.lua index d5571e2d..dbeab051 100644 --- a/data/factorio/mod_template/control.lua +++ b/data/factorio/mod_template/control.lua @@ -1,10 +1,10 @@ require "lib" -- for testing --- script.on_event(defines.events.on_tick, function(event) --- if event.tick%600 == 0 then --- dumpTech(game.forces["player"]) --- end --- end) +script.on_event(defines.events.on_tick, function(event) + if event.tick%600 == 0 then + dumpTech(game.forces["player"]) + end +end) -- hook into researches done script.on_event(defines.events.on_research_finished, function(event) @@ -54,7 +54,7 @@ function dumpGameInfo() local data_collection = {} local force = game.forces["player"] for tech_name, tech in pairs(force.technologies) do - if tech.enabled then + if tech.enabled and tech.research_unit_count_formula == nil then local tech_data = {} local unlocks = {} tech_data["unlocks"] = unlocks diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index 17a9861b..66704239 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -31,10 +31,10 @@ new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #} prep_copy(new_tree_copy, original_tech) {% if tech_cost != 1 %} if new_tree_copy.unit.count then - new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost }})) + new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }})) end {% endif %} -{% if item_name in tech_table %} +{% if item_name in tech_table and visibility %} {#- copy Factorio Technology Icon #} new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon) new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons) @@ -44,6 +44,7 @@ new_tree_copy.icon_size = table.deepcopy(technologies["{{ item_name }}"].icon_si new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png" new_tree_copy.icons = nil new_tree_copy.icon_size = 512 + {% endif %} {#- add new technology to game #} data:extend{new_tree_copy} diff --git a/data/factorio/mod_template/locale/en/locale.cfg b/data/factorio/mod_template/locale/en/locale.cfg index 990f34ed..7e1dd94f 100644 --- a/data/factorio/mod_template/locale/en/locale.cfg +++ b/data/factorio/mod_template/locale/en/locale.cfg @@ -1,8 +1,17 @@ + [technology-name] {% for original_tech_name, item_name, receiving_player in locations %} +{%- if visibility %} ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} +{%- else %} +ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable +{%- endif %} {% endfor %} [technology-description] {% for original_tech_name, item_name, receiving_player in locations %} +{%- if visibility %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}. +{%- else %} +ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. +{%- endif %} {% endfor %} \ No newline at end of file diff --git a/worlds/__init__.py b/worlds/__init__.py index 937c40d5..5b983ac7 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -25,7 +25,7 @@ lookup_any_location_name_to_id = {name: id for id, name in lookup_any_location_i network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name, "lookup_any_item_id_to_name": lookup_any_item_id_to_name, - "version": 2} + "version": 3} @enum.unique class Games(str, enum.Enum): diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 87993c58..8ffa3d18 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1775,7 +1775,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr option_name: True } - data_dir = local_path("../../data") if is_bundled() else None + data_dir = local_path("data") if is_bundled() else None offsets_array = build_offset_collections(options, data_dir) restore_maseya_colors(rom, offsets_array) if mode == 'default': diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index dd527245..174dd97d 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -9,6 +9,7 @@ import json import jinja2 import Utils import shutil +import Options from BaseClasses import MultiWorld from .Technologies import tech_table @@ -50,7 +51,9 @@ def generate_mod(world: MultiWorld, player: int): 6: 10}[world.tech_cost[player].value] template_data = {"locations": locations, "player_names" : player_names, "tech_table": tech_table, "mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(), - "tech_cost": tech_cost, "free_samples": world.free_samples[player].value} + "tech_cost_scale": tech_cost} + for factorio_option in Options.factorio_options: + template_data[factorio_option] = getattr(world, factorio_option)[player].value control_code = control_template.render(**template_data) data_final_fixes_code = template.render(**template_data) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 3bca7b33..590cfbe5 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -6,12 +6,13 @@ import Utils factorio_id = 2 ** 17 source_file = Utils.local_path("data", "factorio", "techs.json") - +recipe_source_file = Utils.local_path("data", "factorio", "recipes.json") with open(source_file) as f: raw = json.load(f) +with open(recipe_source_file) as f: + raw_recipes = json.load(f) tech_table = {} technology_table = {} -requirements = {} # tech_name -> Set[required_technologies] class Technology(): # maybe make subclass of Location? @@ -25,13 +26,28 @@ class Technology(): # maybe make subclass of Location? def get_required_technologies(self): requirements = set() for ingredient in self.ingredients: - if ingredient in recipe_sources: # no source likely means starting item - requirements |= recipe_sources[ingredient] # technically any, not all, need to improve later + if ingredient in recipe_sources: # no source likely means starting item + requirements |= recipe_sources[ingredient] # technically any, not all, need to improve later return requirements + def build_rule(self): + ingredient_rules = [] + for ingredient in self.ingredients: + if ingredient in recipe_sources: + technologies = recipe_sources[ingredient] # technologies that unlock the recipe + ingredient_rules.append(lambda state, technologies=technologies: any(state.has(technology) for technology in technologies)) + ingredient_rules = frozenset(ingredient_rules) + return lambda state: all(rule(state) for rule in ingredient_rules) + def __hash__(self): return self.factorio_id +class Recipe(): + def __init__(self, name, category, ingredients, products): + self.name = name + self.category = category + self.products = ingredients + self.ingredients = products # recipes and technologies can share names in Factorio for technology_name in sorted(raw): @@ -56,3 +72,10 @@ for technology, data in raw.items(): del (raw) lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} + + +all_recipes = set() +for recipe_name, recipe_data in raw_recipes.items(): + # example: + # "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"} + all_recipes.add(Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"]))) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 35fb6f21..8b53f889 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -2,7 +2,7 @@ import logging from BaseClasses import Region, Entrance, Location, MultiWorld, Item -from .Technologies import tech_table, requirements, recipe_sources, technology_table +from .Technologies import tech_table, recipe_sources, technology_table static_nodes = {"automation", "logistics"}