diff --git a/CommonClient.py b/CommonClient.py index a43bbb55..072a329e 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -5,6 +5,7 @@ import asyncio import urllib.parse import sys import os +import typing import websockets @@ -91,6 +92,7 @@ class ClientCommandProcessor(CommandProcessor): class CommonContext(): + tags:typing.Set[str] = {"AP"} starting_reconnect_delay: int = 5 current_reconnect_delay: int = starting_reconnect_delay command_processor: int = ClientCommandProcessor @@ -489,7 +491,10 @@ if __name__ == '__main__': # Text Mode to use !hint and such with games that have no text entry init_logging("TextClient") + class TextContext(CommonContext): + tags = {"AP", "IgnoreGame"} + async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(TextContext, self).server_auth(password_requested) @@ -499,10 +504,11 @@ if __name__ == '__main__': await self.send_msgs([{"cmd": 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, - 'tags': ['AP', 'IgnoreGame'], + 'tags': self.tags, 'uuid': Utils.get_unique_identifier(), 'game': self.game }]) + async def main(args): ctx = TextContext(args.connect, args.password) ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") diff --git a/FactorioClient.py b/FactorioClient.py index f07c7ec9..7d7454f2 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -54,10 +54,12 @@ class FactorioContext(CommonContext): def __init__(self, server_address, password): super(FactorioContext, self).__init__(server_address, password) - self.send_index = 0 + self.send_index: int = 0 self.rcon_client = None self.awaiting_bridge = False self.write_data_path = None + self.last_death_link: float = time.time() # last send/received death link on AP layer + self.death_link_tick: int = 0 # last send death link on Factorio layer self.factorio_json_text_parser = FactorioJSONtoTextParser(self) async def server_auth(self, password_requested: bool = False): @@ -76,13 +78,13 @@ class FactorioContext(CommonContext): 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, - 'tags': ['AP'], + 'tags': self.tags, 'uuid': Utils.get_unique_identifier(), 'game': "Factorio" }]) def on_print(self, args: dict): - logger.info(args["text"]) + super(FactorioContext, self).on_print(args) if self.rcon_client: self.print_to_game(args['text']) @@ -107,6 +109,12 @@ class FactorioContext(CommonContext): self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for item_name in args["checked_locations"]}) + elif cmd == "Bounced": + if self.rcon_client: + tags = args.get("tags", []) + if "DeathLink" in tags and self.last_death_link != args["data"]["time"]: + self.rcon_client.send_command(f"/ap-deathlink {args['data']['source']}") + async def game_watcher(ctx: FactorioContext): bridge_logger = logging.getLogger("FactorioWatcher") @@ -139,6 +147,17 @@ async def game_watcher(ctx: FactorioContext): f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") ctx.locations_checked = research_data await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) + death_link_tick = data.get("death_link_tick", 0) + if death_link_tick != ctx.death_link_tick: + ctx.death_link_tick = death_link_tick + ctx.last_death_link = time.time() + await ctx.send_msgs([{ + "cmd": "Bounce", "tags": ["DeathLink"], + "data": { + "time": ctx.last_death_link, + "source": ctx.player_names[ctx.slot] + } + }]) await asyncio.sleep(0.1) except Exception as e: @@ -227,6 +246,10 @@ def get_info(ctx, rcon_client): info = json.loads(rcon_client.send_command("/ap-rcon-info")) ctx.auth = info["slot_name"] ctx.seed_name = info["seed_name"] + # 0.2.0 addition, not present earlier + death_link = bool(info.get("death_link", False)) + if death_link: + ctx.tags.add("DeathLink") async def factorio_spinup_server(ctx: FactorioContext) -> bool: diff --git a/LttPClient.py b/LttPClient.py index 5e060d9a..79b21534 100644 --- a/LttPClient.py +++ b/LttPClient.py @@ -122,7 +122,7 @@ class Context(CommonContext): auth = base64.b64encode(self.rom).decode() await self.send_msgs([{"cmd": 'Connect', 'password': self.password, 'name': auth, 'version': Utils.version_tuple, - 'tags': get_tags(self), + 'tags': self.tags, 'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past" }]) @@ -705,12 +705,6 @@ async def snes_flush_writes(ctx: Context): await snes_write(ctx, writes) -# kept as function for easier wrapping by plugins -def get_tags(ctx: Context): - tags = ['AP'] - return tags - - async def track_locations(ctx: Context, roomid, roomdata): new_locations = [] diff --git a/MultiServer.py b/MultiServer.py index 499f66b0..94c2d829 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -489,7 +489,7 @@ async def on_client_connected(ctx: Context, client: Client): 'cmd': 'RoomInfo', 'password': bool(ctx.password), 'players': players, - 'games': [ctx.games[x] for x in range(1, len(ctx.games)+1)], + 'games': [ctx.games[x] for x in range(1, len(ctx.games) + 1)], # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. 'tags': ctx.tags, @@ -1245,7 +1245,6 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): await ctx.send_msgs(client, reply) - elif cmd == "GetDataPackage": exclusions = set(args.get("exclusions", [])) if exclusions: diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 3f235f82..84c9da25 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -73,7 +73,7 @@ def generate_mod(world, output_directory: str): random = multiworld.slot_seeds[player] def flop_random(low, high, base=None): - """Guarentees 50% bwlo base and 50% above base, uniform distribution in each direction.""" + """Guarentees 50% below base and 50% above base, uniform distribution in each direction.""" if base: distance = random.random() if random.randint(0, 1): diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index b22eaf4e..285835ae 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing -from Options import Choice, OptionDict, ItemDict, Option, DefaultOnToggle, Range +from Options import Choice, OptionDict, ItemDict, Option, DefaultOnToggle, Range, Toggle from schema import Schema, Optional, And, Or # schema helpers @@ -284,6 +284,11 @@ class ImportedBlueprint(DefaultOnToggle): displayname = "Blueprints" +class DeathLink(Toggle): + """When you die, everyone dies. Of course the reverse is true too.""" + displayname = "Death Link" + + factorio_options: typing.Dict[str, type(Option)] = { "max_science_pack": MaxSciencePack, "tech_tree_layout": TechTreeLayout, @@ -300,4 +305,5 @@ factorio_options: typing.Dict[str, type(Option)] = { "evolution_traps": EvolutionTrapCount, "attack_traps": AttackTrapCount, "evolution_trap_increase": EvolutionTrapIncrease, + "death_link": DeathLink } diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 1cd87c96..dd1bfe25 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -8,6 +8,9 @@ SLOT_NAME = "{{ slot_name }}" SEED_NAME = "{{ seed_name }}" FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }} TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100 +DEATH_LINK = {{ death_link | int }} + +CURRENTLY_DEATH_LOCK = 0 {% if not imported_blueprints -%} function set_permissions() @@ -57,6 +60,7 @@ function on_force_created(event) local data = {} data['earned_samples'] = {{ dict_to_lua(starting_items) }} data["victory"] = 0 + data["death_link_tick"] = 0 global.forcedata[event.force] = data {%- if silo == 2 %} check_spawn_silo(force) @@ -250,6 +254,17 @@ function chain_lookup(table, ...) return table end +function kill_players(force) + CURRENTLY_DEATH_LOCK = 1 + local current_character = nil + for _, player in ipairs(force.players) do + current_character = player.character + if current_character ~= nil then + current_character.die() + end + end + CURRENTLY_DEATH_LOCK = 0 +end function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) local prototype = game.entity_prototypes[name] @@ -351,6 +366,20 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) end +if DEATH_LINK == 1 then + script.on_event(defines.events.on_entity_died, function(event) + if CURRENTLY_DEATH_LOCK == 1 then -- don't re-trigger on same event + return + end + + local force = event.entity.force + global.forcedata[force.name].death_link_tick = game.tick + dumpInfo(force) + kill_players(force) + end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}}) +end + + -- add / commands commands.add_command("ap-sync", "Used by the Archipelago client to get progress information", function(call) local force @@ -363,7 +392,8 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress local data_collection = { ["research_done"] = research_done, ["victory"] = chain_lookup(global, "forcedata", force.name, "victory"), - } + ["death_link_tick"] = chain_lookup(global, "forcedata", force.name, "death_link_tick") + } for tech_name, tech in pairs(force.technologies) do if tech.researched and string.find(tech_name, "ap%-") == 1 then @@ -442,7 +472,7 @@ end) commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call) - rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME})) + rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["death_link"] = DEATH_LINK})) end) @@ -453,5 +483,13 @@ end) {% endif -%} +commands.add_command("ap-deathlink", "Kill all players", function(call) + local force = game.forces["player"] + local source = call.parameter or "Archipelago" + kill_players(force) + game.print("Death was granted by " .. source) +end) + + -- data progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}