diff --git a/FactorioClient.py b/FactorioClient.py index 64b8cda3..0607ddc8 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -137,11 +137,12 @@ async def factorio_server_watcher(ctx: FactorioContext): if ctx.rcon_client: while ctx.send_index < len(ctx.items_received): item_id = ctx.items_received[ctx.send_index].item - item_name = lookup_id_to_name[item_id] - factorio_server_logger.info(f"Sending {item_name} to Nauvis.") - response = ctx.rcon_client.send_command(f'/ap-get-technology {item_name}') - if response: - factorio_server_logger.info(response) + if item_id not in lookup_id_to_name: + logging.error(f"Unknown item ID: {item_id}") + else: + item_name = lookup_id_to_name[item_id] + factorio_server_logger.info(f"Sending {item_name} to Nauvis.") + ctx.rcon_client.send_command(f'/ap-get-technology {item_name}') ctx.send_index += 1 await asyncio.sleep(1) diff --git a/LttPClient.py b/LttPClient.py index f2b8117c..67eca31e 100644 --- a/LttPClient.py +++ b/LttPClient.py @@ -982,8 +982,8 @@ async def main(): parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) parser.add_argument('--founditems', default=False, action='store_true', help='Show items found by other players for themselves.') - parser.add_argument('--disable_web_ui', default=False, action='store_true', - help="Turn off emitting a webserver for the webbrowser based user interface.") + parser.add_argument('--web_ui', default=False, action='store_true', + help="Emit a webserver for the webbrowser based user interface.") args = parser.parse_args() logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) if args.diff_file: @@ -1002,7 +1002,7 @@ async def main(): asyncio.create_task(run_game(adjustedromfile if adjusted else romfile)) port = None - if not args.disable_web_ui: + if args.web_ui: # Find an available port on the host system to use for hosting the websocket server while True: port = randrange(49152, 65535) @@ -1015,7 +1015,7 @@ async def main(): ctx = Context(args.snes, args.connect, args.password, args.founditems, port) input_task = asyncio.create_task(console_loop(ctx), name="Input") - if not args.disable_web_ui: + if args.web_ui: ui_socket = websockets.serve(functools.partial(websocket_server, ctx=ctx), 'localhost', port, ping_timeout=None, ping_interval=None) await ui_socket diff --git a/Main.py b/Main.py index f9ff40b3..575bed86 100644 --- a/Main.py +++ b/Main.py @@ -135,6 +135,8 @@ def main(args, seed=None): import Options for hk_option in Options.hollow_knight_options: setattr(world, hk_option, getattr(args, hk_option, {})) + for factorio_option in Options.factorio_options: + setattr(world, factorio_option, getattr(args, factorio_option, {})) world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} diff --git a/MultiServer.py b/MultiServer.py index 51fb74dd..770d644e 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -794,7 +794,7 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_missing_checks(self.ctx, self.client) if locations: - texts = [f'Missing: {get_item_name_from_id(location)}\n' for location in locations] + texts = [f'Missing: {get_item_name_from_id(location)}' for location in locations] texts.append(f"Found {len(locations)} missing location checks") self.ctx.notify_client_multiple(self.client, texts) else: diff --git a/Mystery.py b/Mystery.py index b2a0cba6..534659d3 100644 --- a/Mystery.py +++ b/Mystery.py @@ -198,11 +198,15 @@ def main(args=None, callback=ERmain): pre_rolled["original_seed_name"] = seedname pre_rolled["pre_rolled"] = vars(settings).copy() if "plando_items" in pre_rolled["pre_rolled"]: - pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in pre_rolled["pre_rolled"]["plando_items"]] + pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in + pre_rolled["pre_rolled"]["plando_items"]] if "plando_connections" in pre_rolled["pre_rolled"]: - pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in pre_rolled["pre_rolled"]["plando_connections"]] + pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in + pre_rolled["pre_rolled"][ + "plando_connections"]] - with open(os.path.join(args.outputpath if args.outputpath else ".", f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f: + with open(os.path.join(args.outputpath if args.outputpath else ".", + f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f: yaml.dump(pre_rolled, f) for k, v in vars(settings).items(): if v is not None: @@ -294,7 +298,8 @@ def handle_name(name: str, player: int, name_counter: Counter): name_counter[name] += 1 new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")]) new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name], - NUMBER=(name_counter[name] if name_counter[name] > 1 else ''), + NUMBER=(name_counter[name] if name_counter[ + name] > 1 else ''), player=player, PLAYER=(player if player > 1 else ''))) return new_name.strip().replace(' ', '_')[:16] @@ -315,17 +320,42 @@ available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if leve boss_shuffle_options = {None: 'none', 'none': 'none', 'basic': 'basic', - 'normal': 'normal', + 'full': 'full', 'chaos': 'chaos', 'singularity': 'singularity' } +goals = { + 'ganon': 'ganon', + 'crystals': 'crystals', + 'bosses': 'bosses', + 'pedestal': 'pedestal', + 'ganon_pedestal': 'ganonpedestal', + 'triforce_hunt': 'triforcehunt', + 'local_triforce_hunt': 'localtriforcehunt', + 'ganon_triforce_hunt': 'ganontriforcehunt', + 'local_ganon_triforce_hunt': 'localganontriforcehunt', + 'ice_rod_hunt': 'icerodhunt', +} + +# remove sometime before 1.0.0, warn before +legacy_boss_shuffle_options = { + # legacy, will go away: + 'simple': 'basic', + 'random': 'full', +} + +legacy_goals = { + 'dungeons': 'bosses', + 'fast_ganon': 'crystals', +} def roll_percentage(percentage: typing.Union[int, float]) -> bool: """Roll a percentage chance. percentage is expected to be in range [0, 100]""" return random.random() < (float(percentage) / 100) + def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict: logging.debug(f'Applying {new_weights}') new_options = set(new_weights) - set(weights) @@ -337,6 +367,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di f'This is probably in error.') return weights + def roll_linked_options(weights: dict) -> dict: weights = weights.copy() # make sure we don't write back to other weights sets in same_settings for option_set in weights["linked_options"]: @@ -349,7 +380,8 @@ def roll_linked_options(weights: dict) -> dict: weights = update_weights(weights, option_set["options"], "Linked", option_set["name"]) if "rom_options" in option_set: rom_weights = weights.get("rom", dict()) - rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom", option_set["name"]) + rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom", + option_set["name"]) weights["rom"] = rom_weights else: logging.debug(f"linked option {option_set['name']} skipped.") @@ -358,6 +390,7 @@ def roll_linked_options(weights: dict) -> dict: f"Please fix your linked option.") from e return weights + def roll_triggers(weights: dict) -> dict: weights = weights.copy() # make sure we don't write back to other weights sets in same_settings weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors. @@ -375,7 +408,8 @@ def roll_triggers(weights: dict) -> dict: weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"]) if "rom_options" in option_set: rom_weights = weights.get("rom", dict()) - rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom", option_set["option_name"]) + rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom", + option_set["option_name"]) weights["rom"] = rom_weights weights[key] = result except Exception as e: @@ -385,6 +419,11 @@ def roll_triggers(weights: dict) -> dict: def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str: + if boss_shuffle in legacy_boss_shuffle_options: + new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle] + logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, " + f"please use {new_boss_shuffle} instead") + return new_boss_shuffle if boss_shuffle in boss_shuffle_options: return boss_shuffle_options[boss_shuffle] elif "bosses" in plando_options: @@ -392,6 +431,10 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str remainder_shuffle = "none" # vanilla bosses = [] for boss in options: + if boss in legacy_boss_shuffle_options: + remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle] + logging.warning(f"Boss shuffle {boss} is deprecated, " + f"please use {remainder_shuffle} instead") if boss in boss_shuffle_options: remainder_shuffle = boss_shuffle_options[boss] elif "-" in boss: @@ -419,7 +462,7 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.") -def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses", ))): +def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))): if "pre_rolled" in weights: pre_rolled = weights["pre_rolled"] @@ -435,7 +478,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b if "plando_connections" in pre_rolled: pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"], connection["exit"], - connection["direction"]) for connection in pre_rolled["plando_connections"]] + connection["direction"]) for connection in + pre_rolled["plando_connections"]] if "connections" not in plando_options and pre_rolled["plando_connections"]: raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.") @@ -486,11 +530,16 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b for option_name, option in Options.hollow_knight_options.items(): setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True))) elif ret.game == "Factorio": - pass + for option_name, option in Options.factorio_options.items(): + if option_name in weights: + setattr(ret, option_name, option.from_any(get_choice(option_name, weights))) + else: + setattr(ret, option_name, option.default) else: raise Exception(f"Unsupported game {ret.game}") return ret + def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): glitches_required = get_choice('glitches_required', weights) if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']: @@ -533,17 +582,11 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' goal = get_choice('goals', weights, 'ganon') - ret.goal = {'ganon': 'ganon', - 'crystals': 'crystals', - 'bosses': 'bosses', - 'pedestal': 'pedestal', - 'ganon_pedestal': 'ganonpedestal', - 'triforce_hunt': 'triforcehunt', - 'local_triforce_hunt': 'localtriforcehunt', - 'ganon_triforce_hunt': 'ganontriforcehunt', - 'local_ganon_triforce_hunt': 'localganontriforcehunt', - 'ice_rod_hunt': 'icerodhunt' - }[goal] + + if goal in legacy_goals: + logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.") + goal = legacy_goals[goal] + ret.goal = goals[goal] # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when # fast ganon + ganon at hole @@ -602,6 +645,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False)) + ret.killable_thieves = get_choice('killable_thieves', weights, False) ret.tile_shuffle = get_choice('tile_shuffle', weights, False) ret.bush_shuffle = get_choice('bush_shuffle', weights, False) @@ -772,5 +816,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.quickswap = True ret.sprite = "Link" + if __name__ == '__main__': main() diff --git a/Options.py b/Options.py index 7ddc2611..826fff6c 100644 --- a/Options.py +++ b/Options.py @@ -23,6 +23,7 @@ class AssembleOptions(type): class Option(metaclass=AssembleOptions): value: int name_lookup: typing.Dict[int, str] + default = 0 def __repr__(self): return f"{self.__class__.__name__}({self.get_option_name()})" @@ -47,6 +48,7 @@ class Option(metaclass=AssembleOptions): class Toggle(Option): option_false = 0 option_true = 1 + default = 0 def __init__(self, value: int): self.value = value @@ -86,6 +88,7 @@ class Toggle(Option): def get_option_name(self): return bool(self.value) + class Choice(Option): def __init__(self, value: int): self.value: int = value @@ -233,6 +236,42 @@ hollow_knight_skip_options: typing.Dict[str, type(Option)] = { hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options, **hollow_knight_skip_options} + +class MaxSciencePack(Choice): + option_automation_science_pack = 0 + option_logistic_science_pack = 1 + option_military_science_pack = 2 + option_chemical_science_pack = 3 + option_production_science_pack = 4 + option_utility_science_pack = 5 + option_space_science_pack = 6 + default = 6 + + def get_allowed_packs(self): + return {option.replace("_", "-") for option, value in self.options.items() + if value <= self.value} + + +class TechCost(Choice): + option_very_easy = 0 + option_easy = 1 + option_kind = 2 + option_normal = 3 + option_hard = 4 + option_very_hard = 5 + option_insane = 6 + default = 3 + + +class TechTreeLayout(Choice): + option_single = 0 + default = 0 + + +factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack, + "tech_tree_layout": TechTreeLayout, + "tech_cost": TechCost} + if __name__ == "__main__": import argparse diff --git a/data/factorio/mod_template/data-final-fixes.lua b/data/factorio/mod_template/data-final-fixes.lua index cd847a18..8bacb1fb 100644 --- a/data/factorio/mod_template/data-final-fixes.lua +++ b/data/factorio/mod_template/data-final-fixes.lua @@ -2,11 +2,28 @@ local technologies = data.raw["technology"] local original_tech local new_tree_copy +allowed_ingredients = {} +{%- for ingredient in allowed_science_packs %} +allowed_ingredients["{{ingredient}}"]= 1 +{% endfor %} local template_tech = table.deepcopy(technologies["automation"]) {#- ensure the copy unlocks nothing #} template_tech.unlocks = {} template_tech.upgrade = false template_tech.effects = {} +template_tech.prerequisites = {} + +function filter_ingredients(ingredients) + local new_ingredient_list = {} + for _, ingredient_table in pairs(ingredients) do + if allowed_ingredients[ingredient_table[1]] then -- name of ingredient_table + table.insert(new_ingredient_list, ingredient_table) + end + end + + return new_ingredient_list +end + {# each randomized tech gets set to be invisible, with new nodes added that trigger those #} {%- for original_tech_name, item_name, receiving_player in locations %} original_tech = technologies["{{original_tech_name}}"] @@ -17,6 +34,12 @@ new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #} original_tech.enabled = false {#- copy original tech costs #} new_tree_copy.unit = table.deepcopy(original_tech.unit) +new_tree_copy.unit.ingredients = filter_ingredients(new_tree_copy.unit.ingredients) +{% 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 }})) +end +{% endif %} {% if item_name in tech_table %} {#- copy Factorio Technology Icon #} new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon) diff --git a/playerSettings.yaml b/playerSettings.yaml index 35e85aca..01399eb0 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -35,6 +35,25 @@ accessibility: progression_balancing: on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. +# Factorio options: +tech_tree_layout: + single: 1 +max_science_pack: + automation_science_pack: 0 + logistic_science_pack: 0 + military_science_pack: 0 + chemical_science_pack: 0 + production_science_pack: 0 + utility_science_pack: 0 + space_science_pack: 1 +tech_cost: + very_easy : 0 + easy : 0 + kind : 0 + normal : 1 + hard : 0 + very_hard : 0 + insane : 0 # A Link to the Past options: ### Logic Section ### # Warning: overworld_glitches is not available and minor_glitches is only partially implemented on the door-rando version diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 6a799cb3..5c261365 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -241,7 +241,7 @@ def place_bosses(world, player: int): if shuffle_mode == "none": return # vanilla bosses come pre-placed - if shuffle_mode in ["basic", "normal"]: + if shuffle_mode in ["basic", "full"]: if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] else: # all bosses present, the three duplicates chosen at random diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 57958e91..bf7afdb4 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1651,7 +1651,7 @@ def write_custom_shops(rom, world, player): if item is None: break if not item['item'] in item_table: # item not native to ALTTP - item_code = 0x21 + item_code = 0x09 # Hammer else: item_code = ItemFactory(item['item'], player).code if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]: diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index ea0f7860..f7afe219 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -35,11 +35,19 @@ def generate_mod(world: MultiWorld, player: int): player_names = {x: world.player_names[x][0] for x in world.player_ids} locations = [] for location in world.get_filled_locations(player): - if not location.name.startswith("recipe-"): # introduce this a new location property? + if not location.name.startswith("recipe-"): # introduce this as a new location property? locations.append((location.name, location.item.name, location.item.player)) mod_name = f"archipelago-client-{world.seed}-{player}" + tech_cost = {0: 0.1, + 1: 0.25, + 2: 0.5, + 3: 1, + 4: 2, + 5: 5, + 6: 10}[world.tech_cost[player].value] template_data = {"locations": locations, "player_names" : player_names, "tech_table": tech_table, - "mod_name": mod_name} + "mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(), + "tech_cost": tech_cost} mod_code = template.render(**template_data) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 21d56a1a..169c8aaf 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -29,10 +29,8 @@ for technology in sorted(raw): requirements[technology] = set(data["requires"]) current_ingredients = set(data["ingredients"])-starting_ingredient_recipes if current_ingredients: - all_ingredients |= current_ingredients - current_ingredients = {"recipe-"+ingredient for ingredient in current_ingredients} - ingredients[technology] = current_ingredients + ingredients[technology] = {"recipe-"+ingredient for ingredient in current_ingredients} recipe_sources = {} @@ -41,6 +39,6 @@ for technology, data in raw.items(): for recipe in recipe_source: recipe_sources["recipe-"+recipe] = technology -all_ingredients = {"recipe-"+ingredient for ingredient in all_ingredients} +all_ingredients_recipe = {"recipe-"+ingredient for ingredient in all_ingredients} del(raw) lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} \ No newline at end of file diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index e8382288..3675705b 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, ingredients, all_ingredients, recipe_sources +from .Technologies import tech_table, requirements, ingredients, all_ingredients, recipe_sources, all_ingredients_recipe static_nodes = {"automation", "logistics"} @@ -30,7 +30,7 @@ def factorio_create_regions(world: MultiWorld, player: int): tech = Location(player, tech_name, tech_id, nauvis) nauvis.locations.append(tech) tech.game = "Factorio" - for ingredient in all_ingredients: # register science packs as events + for ingredient in all_ingredients_recipe: # register science packs as events ingredient_location = Location(player, ingredient, 0, nauvis) ingredient_location.item = Item(ingredient, True, 0, player) ingredient_location.event = ingredient_location.locked = True @@ -56,4 +56,4 @@ def set_rules(world: MultiWorld, player: int): world.completion_condition[player] = lambda state: all(state.has(ingredient, player) - for ingredient in all_ingredients) + for ingredient in all_ingredients_recipe)