From fc3b8c40be1bc6f487be67fd2ac555a2165c192c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 13 Nov 2021 20:52:30 +0100 Subject: [PATCH 01/62] WebHost: handle SM and SoE --- Patch.py | 24 +++++++++++++++++++----- Utils.py | 3 +++ WebHostLib/downloads.py | 23 ++++------------------- WebHostLib/templates/viewSeed.html | 28 +++++----------------------- WebHostLib/upload.py | 15 +++++++-------- worlds/soe/__init__.py | 7 ++++++- 6 files changed, 44 insertions(+), 56 deletions(-) diff --git a/Patch.py b/Patch.py index b136e932..af0a5e5e 100644 --- a/Patch.py +++ b/Patch.py @@ -1,3 +1,5 @@ +# TODO: convert this into a system like AutoWorld + import bsdiff4 import yaml import os @@ -14,16 +16,25 @@ current_patch_version = 3 GAME_ALTTP = "A Link to the Past" GAME_SM = "Super Metroid" -supported_games = {"A Link to the Past", "Super Metroid"} +GAME_SOE = "Secret of Evermore" +supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore"} + +preferred_endings = { + GAME_ALTTP: "apbp", + GAME_SM: "apm3", + GAME_SOE: "apsoe" +} def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: if game == GAME_ALTTP: - from worlds.alttp.Rom import JAP10HASH + from worlds.alttp.Rom import JAP10HASH as HASH elif game == GAME_SM: - from worlds.sm.Rom import JAP10HASH + from worlds.sm.Rom import JAP10HASH as HASH + elif game == GAME_SOE: + from worlds.soe.Patch import USHASH as HASH else: - raise RuntimeError("Selected game for base rom not found.") + raise RuntimeError(f"Selected game {game} for base rom not found.") patch = yaml.dump({"meta": metadata, "patch": patch, @@ -31,7 +42,7 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAM # minimum version of patch system expected for patching to be successful "compatible_version": 3, "version": current_patch_version, - "base_checksum": JAP10HASH}) + "base_checksum": HASH}) return patch.encode(encoding="utf-8-sig") @@ -40,6 +51,9 @@ def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME from worlds.alttp.Rom import get_base_rom_bytes elif game == GAME_SM: from worlds.sm.Rom import get_base_rom_bytes + elif game == GAME_SOE: + file_name = Utils.get_options()["soe_options"]["rom"] + get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb"))) else: raise RuntimeError("Selected game for base rom not found.") diff --git a/Utils.py b/Utils.py index c5a0a48c..e7b468b3 100644 --- a/Utils.py +++ b/Utils.py @@ -166,6 +166,9 @@ def get_default_options() -> dict: "sni": "SNI", "rom_start": True, }, + "soe_options": { + "rom_file": "Secret of Evermore (USA).sfc", + }, "lttp_options": { "rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc", "sni": "SNI", diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index e7856a12..ce623c1e 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -1,10 +1,11 @@ from flask import send_file, Response, render_template from pony.orm import select -from Patch import update_patch_data +from Patch import update_patch_data, preferred_endings from WebHostLib import app, Slot, Room, Seed, cache import zipfile + @app.route("/dl_patch//") def download_patch(room_id, patch_id): patch = Slot.get(id=patch_id) @@ -19,7 +20,8 @@ def download_patch(room_id, patch_id): patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}") patch_data = io.BytesIO(patch_data) - fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp" + fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \ + f"{preferred_endings[patch.game]}" return send_file(patch_data, as_attachment=True, attachment_filename=fname) @@ -28,23 +30,6 @@ def download_spoiler(seed_id): return Response(Seed.get(id=seed_id).spoiler, mimetype="text/plain") -@app.route("/dl_raw_patch//") -def download_raw_patch(seed_id, player_id: int): - seed = Seed.get(id=seed_id) - patch = select(patch for patch in seed.slots if - patch.player_id == player_id).first() - - if not patch: - return "Patch not found" - else: - import io - - patch_data = update_patch_data(patch.data, server="") - patch_data = io.BytesIO(patch_data) - - fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp" - return send_file(patch_data, as_attachment=True, attachment_filename=fname) - @app.route("/slot_file//") def download_slot_file(room_id, player_id: int): room = Room.get(id=room_id) diff --git a/WebHostLib/templates/viewSeed.html b/WebHostLib/templates/viewSeed.html index 36271cad..62763629 100644 --- a/WebHostLib/templates/viewSeed.html +++ b/WebHostLib/templates/viewSeed.html @@ -28,34 +28,16 @@ Download {% endif %} - {% if seed.multidata %} - - Rooms:  - - {% call macros.list_rooms(rooms) %} -
  • - Create New Room -
  • - {% endcall %} - - - {% else %} - Files:  - - + {% endcall %} - {% endif %} diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 64537295..8e461071 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -10,10 +10,7 @@ from pony.orm import flush, select from WebHostLib import app, Seed, Room, Slot from Utils import parse_yaml - -accepted_zip_contents = {"patches": ".apbp", - "spoiler": ".txt", - "multidata": ".archipelago"} +from Patch import preferred_endings banned_zip_contents = (".sfc",) @@ -29,15 +26,17 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if file.filename.endswith(banned_zip_contents): return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ "Your file was deleted." - elif file.filename.endswith(".apbp"): + elif file.filename.endswith(tuple(preferred_endings.values())): data = zfile.open(file, "r").read() yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig")) if yaml_data["version"] < 2: - return "Old format cannot be uploaded (outdated .apbp)", 500 + return "Old format cannot be uploaded (outdated .apbp)" metadata = yaml_data["meta"] - slots.add(Slot(data=data, player_name=metadata["player_name"], + + slots.add(Slot(data=data, + player_name=metadata["player_name"], player_id=metadata["player_id"], - game="A Link to the Past")) + game=yaml_data["game"])) elif file.filename.endswith(".apmc"): data = zfile.open(file, "r").read() diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 104f2e86..6fa8e0ae 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -204,7 +204,12 @@ class SoEWorld(World): flags, money, exp)): raise RuntimeError() with lzma.LZMAFile(patch_file, 'wb') as f: - f.write(generate_patch(rom_file, out_file)) + f.write(generate_patch(rom_file, out_file, + { + # used by WebHost + "player_name": self.world.player_name[self.player], + "player_id": self.player + })) except: raise finally: From 586af0de1dddf42d51c2b8250a614d1b0898714c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 13 Nov 2021 23:05:39 +0100 Subject: [PATCH 02/62] SNIClient: remove some debug stuff before release --- SNIClient.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 5afda798..d1f406c0 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -84,20 +84,17 @@ class LttPCommandProcessor(ClientCommandProcessor): else: return False - def _cmd_snes_write(self, address, data): - """Write the specified byte (base10) to the SNES' memory address (base16).""" - if self.ctx.snes_state != SNESState.SNES_ATTACHED: - self.output("No attached SNES Device.") - return False - - snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)])) - asyncio.create_task(snes_flush_writes(self.ctx)) - self.output("Data Sent") - return True - - def _cmd_test_death(self): - self.ctx.on_deathlink({"source": "Console", - "time": time.time()}) + # Left here for quick re-addition for debugging. + # def _cmd_snes_write(self, address, data): + # """Write the specified byte (base10) to the SNES' memory address (base16).""" + # if self.ctx.snes_state != SNESState.SNES_ATTACHED: + # self.output("No attached SNES Device.") + # return False + # + # snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)])) + # asyncio.create_task(snes_flush_writes(self.ctx)) + # self.output("Data Sent") + # return True class Context(CommonContext): From 2ffa0d0e7f9ec1c5e5465fbfaa899043128ae7e9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 13 Nov 2021 23:14:26 +0100 Subject: [PATCH 03/62] Utils: ignore SSL Cert when getting IP --- Utils.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Utils.py b/Utils.py index e7b468b3..b666eed0 100644 --- a/Utils.py +++ b/Utils.py @@ -122,16 +122,25 @@ parse_yaml = safe_load unsafe_parse_yaml = functools.partial(load, Loader=Loader) +def get_cert_none_ssl_context(): + import ssl + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + return ctx + + @cache_argsless def get_public_ipv4() -> str: import socket import urllib.request ip = socket.gethostbyname(socket.gethostname()) + ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip() + ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip() except Exception as e: try: - ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip() + ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip() except: logging.exception(e) pass # we could be offline, in a local game, so no point in erroring out @@ -143,8 +152,9 @@ def get_public_ipv6() -> str: import socket import urllib.request ip = socket.gethostbyname(socket.gethostname()) + ctx = get_cert_none_ssl_context() try: - ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip() + ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip() except Exception as e: logging.exception(e) pass # we could be offline, in a local game, or ipv6 may not be available From 4cd9711de3407e180f731cebad978e7dd55555ce Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 05:27:03 +0100 Subject: [PATCH 04/62] Super Metroid: fix some file paths --- worlds/sm/variaRandomizer/randomizer.py | 2 +- worlds/sm/variaRandomizer/utils/parameters.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 563ed6bf..e3d3bbad 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -327,7 +327,7 @@ class VariaRandomizer: preset = loadRandoPreset(world, self.player, args) # use the skill preset from the rando preset if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None: - args.paramsFileName = '{}/{}/{}.json'.format(appDir, getPresetDir(preset), preset) + args.paramsFileName = os.path.join(appDir, getPresetDir(preset), preset+".json") # if diff preset given, load it if args.paramsFileName is not None: diff --git a/worlds/sm/variaRandomizer/utils/parameters.py b/worlds/sm/variaRandomizer/utils/parameters.py index 7adec798..0f7b62c6 100644 --- a/worlds/sm/variaRandomizer/utils/parameters.py +++ b/worlds/sm/variaRandomizer/utils/parameters.py @@ -1,6 +1,7 @@ from logic.smbool import SMBool import os import sys +from pathlib import Path # the different difficulties available easy = 1 @@ -60,7 +61,7 @@ def diff4solver(difficulty): return "mania" # allow multiple local repo -appDir = sys.path[0] +appDir = Path(__file__).parents[4] def isKnows(knows): return knows[0:len('__')] != '__' and knows[0] == knows[0].upper() From 28f5236719610d085777cf2f0fc2dab42ea623dd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 15:24:01 +0100 Subject: [PATCH 05/62] OoT: fix link in english guide --- WebHostLib/static/assets/tutorial/zelda5/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/tutorial/zelda5/setup_en.md b/WebHostLib/static/assets/tutorial/zelda5/setup_en.md index 747d2e21..eaf1621d 100644 --- a/WebHostLib/static/assets/tutorial/zelda5/setup_en.md +++ b/WebHostLib/static/assets/tutorial/zelda5/setup_en.md @@ -28,7 +28,7 @@ can all have different options. ### Where do I get a YAML file? -A basic OOT yaml will look like this. (There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download (Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] and look for the sample file in the "Players" folder)) +A basic OOT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in the "Players" folder. ```yaml description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit From 5626ff1582a3ef2e3e98b3a94ee65fb4ea76f8cd Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 14 Nov 2021 13:19:53 +0100 Subject: [PATCH 06/62] Fixed some routing logic + make two checks more easily available --- worlds/timespinner/Locations.py | 5 ++--- worlds/timespinner/LogicMixin.py | 3 +++ worlds/timespinner/Regions.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 904240d6..0151a564 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -106,8 +106,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Refugee Camp', 'Refugee camp storage chest 2', 1337088), LocationData('Refugee Camp', 'Refugee camp storage chest 1', 1337089), LocationData('Forest', 'Refugee camp roof', 1337090), - LocationData('Forest', 'Bat jump chest', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player)), - LocationData('Forest', 'Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Forest', 'Bat jump chest', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)), LocationData('Forest', 'Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)), LocationData('Forest', 'Rats guarded chest', 1337093), LocationData('Forest', 'Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Waterfall chest 2', 1337095, lambda state: state.has('Water Mask', player)), @@ -158,7 +157,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Castle Keep', 'Out of the way', 1337139), LocationData('Castle Keep', 'Killed Twins', EventId, lambda state: state._timespinner_has_timestop(world, player)), LocationData('Castle Keep', 'Twins', 1337140, lambda state: state._timespinner_has_timestop(world, player)), - LocationData('Castle Keep', 'Royal guard tiny room', 1337141, lambda state: state._timespinner_has_doublejump(world, player)), + LocationData('Castle Keep', 'Royal guard tiny room', 1337141, lambda state: state._timespinner_has_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world,player)), LocationData('Royal towers (lower)', 'Royal tower floor secret', 1337142, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_can_break_walls(world, player)), LocationData('Royal towers', 'Above the gap', 1337143), LocationData('Royal towers', 'Under the ice mage', 1337144), diff --git a/worlds/timespinner/LogicMixin.py b/worlds/timespinner/LogicMixin.py index 8181b309..7a81c25e 100644 --- a/worlds/timespinner/LogicMixin.py +++ b/worlds/timespinner/LogicMixin.py @@ -15,6 +15,9 @@ class TimespinnerLogic(LogicMixin): def _timespinner_has_doublejump_of_npc(self, world: MultiWorld, player: int) -> bool: return self._timespinner_has_upwarddash(world, player) or (self.has('Timespinner Wheel', player) and self._timespinner_has_doublejump(world, player)) + def _timespinner_has_fastjump_on_npc(self, world: MultiWorld, player: int) -> bool: + return self.has_all(['Timespinner Wheel', 'Talaria Attachment'], player) + def _timespinner_has_multiple_small_jumps_of_npc(self, world: MultiWorld, player: int) -> bool: return self.has('Timespinner Wheel', player) or self._timespinner_has_upwarddash(world, player) diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 827277d8..7fa1ec27 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -72,7 +72,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)') connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)') - connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left') + connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_B(world, player)) connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player)) connect(world, player, names, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: state._timespinner_has_keycard_B(world, player) and state.has('Elevator Keycard', player)) connect(world, player, names, 'Varndagroth tower right (lower)', 'Militairy Fortress', lambda state: state._timespinner_can_kill_all_3_bosses(world, player)) From 4bbf8858b09deea53ffcdcd56a925dbd6202cdf6 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 14 Nov 2021 13:25:28 +0100 Subject: [PATCH 07/62] Fixed missing newline --- worlds/timespinner/Locations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 0151a564..b1895b4c 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -106,7 +106,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Refugee Camp', 'Refugee camp storage chest 2', 1337088), LocationData('Refugee Camp', 'Refugee camp storage chest 1', 1337089), LocationData('Forest', 'Refugee camp roof', 1337090), - LocationData('Forest', 'Bat jump chest', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)), LocationData('Forest', 'Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Forest', 'Bat jump chest', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)), + LocationData('Forest', 'Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)), LocationData('Forest', 'Rats guarded chest', 1337093), LocationData('Forest', 'Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)), LocationData('Forest', 'Waterfall chest 2', 1337095, lambda state: state.has('Water Mask', player)), From d623cd5ce0567ef724a140aa7585798676357090 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 16:04:44 +0100 Subject: [PATCH 08/62] Factorio: fix coop sync printing desync detected --- worlds/factorio/data/mod_template/control.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index dd1bfe25..8c049e14 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -423,8 +423,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) game.play_sound({path="utility/research_completed"}) tech.researched = true - return end + return elseif progressive_technologies[item_name] ~= nil then if global.index_sync[index] == nil then -- not yet received prog item global.index_sync[index] = item_name From 55bfc712695e563eb5866b106e1b246e34554a1e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 14 Nov 2021 15:46:07 +0100 Subject: [PATCH 09/62] SoE: produce useful error if ROM does not exist --- worlds/soe/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 6fa8e0ae..6bf6f803 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -5,6 +5,7 @@ from Utils import get_options, output_path import typing import lzma import os +import os.path import threading try: @@ -200,8 +201,10 @@ class SoEWorld(World): line = f'{loc.type},{loc.index}:{item.type},{item.index}\n' f.write(line.encode('utf-8')) - if (pyevermizer.main(rom_file, out_file, placement_file, self.world.seed_name, self.connect_name, self.evermizer_seed, - flags, money, exp)): + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + if (pyevermizer.main(rom_file, out_file, placement_file, self.world.seed_name, self.connect_name, + self.evermizer_seed, flags, money, exp)): raise RuntimeError() with lzma.LZMAFile(patch_file, 'wb') as f: f.write(generate_patch(rom_file, out_file, From 16cd2760a475bc47cde2fd82623f1c6b89459da3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 20:51:10 +0100 Subject: [PATCH 10/62] Super Metroid: more path fixes --- .../static/assets/tutorial/zelda3/multiworld_en.md | 10 +++++----- worlds/sm/variaRandomizer/patches/patchaccess.py | 5 ++--- worlds/sm/variaRandomizer/randomizer.py | 2 +- worlds/sm/variaRandomizer/utils/utils.py | 10 ++++------ 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md index 1f7a0cfc..737de947 100644 --- a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md +++ b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md @@ -1,10 +1,10 @@ # A Link to the Past Randomizer Setup Guide ## Required Software -- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the LttPClient included with +- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the SNIClient included with [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) - - If installing Archipelago, make sure to check the box for LttPClient during install, or SNI will not be included -- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and LttPClient) + - If installing Archipelago, make sure to check the box for SNIClient -> A Link to the Past Patch Setup during install, or SNI will not be included +- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and SNIClient) - Hardware or software capable of loading and playing SNES ROM files - An emulator capable of connecting to SNI ([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), @@ -76,7 +76,7 @@ Firewall. 4. In the new window, click **Browse...** 5. Select the connector lua file included with your client - Z3Client users should download `sniConnector.lua` from the client download page - - LttPClient users should look in their Archipelago folder for `/sni/Connector.lua` + - SNIClient users should look in their Archipelago folder for `/sni/Connector.lua` ##### BizHawk 1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following @@ -88,7 +88,7 @@ Firewall. 4. Click the button to open a new Lua script. 5. Select the `sniConnector.lua` file you downloaded above - Z3Client users should download `sniConnector.lua` from the client download page - - LttPClient users should look in their Archipelago folder for `/sni/Connector.lua` + - SNIClient users should look in their Archipelago folder for `/sni/Connector.lua` #### With hardware This guide assumes you have downloaded the correct firmware for your device. If you have not diff --git a/worlds/sm/variaRandomizer/patches/patchaccess.py b/worlds/sm/variaRandomizer/patches/patchaccess.py index 857b99e8..bce2d486 100644 --- a/worlds/sm/variaRandomizer/patches/patchaccess.py +++ b/worlds/sm/variaRandomizer/patches/patchaccess.py @@ -2,16 +2,15 @@ import os, importlib from logic.logic import Logic from patches.common.patches import patches, additional_PLMs from utils.parameters import appDir -from Utils import is_frozen class PatchAccess(object): def __init__(self): # load all ips patches self.patchesPath = {} - commonDir = os.path.join(appDir, 'lib' if is_frozen() else '', 'worlds/sm/variaRandomizer/patches/common/ips/') + commonDir = os.path.join(appDir, 'worlds/sm/variaRandomizer/patches/common/ips/') for patch in os.listdir(commonDir): self.patchesPath[patch] = commonDir - logicDir = os.path.join(appDir, 'lib' if is_frozen() else '', 'worlds/sm/variaRandomizer/patches/{}/ips/'.format(Logic.patches)) + logicDir = os.path.join(appDir, 'worlds/sm/variaRandomizer/patches/{}/ips/'.format(Logic.patches)) for patch in os.listdir(logicDir): self.patchesPath[patch] = logicDir diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index e3d3bbad..d87c3e39 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -352,7 +352,7 @@ class VariaRandomizer: sys.exit(-1) else: preset = 'default' - PresetLoader.factory('{}/{}/{}.json'.format(appDir, getPresetDir('casual'), 'casual')).load(self.player) + PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player) diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index d6c71b00..d64cb252 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -1,19 +1,17 @@ -import os, json, sys, re, random +import os, json, re, random from utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton from utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff from logic.smbool import SMBool -from Utils import is_frozen - def isStdPreset(preset): return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021'] -def getPresetDir(preset): +def getPresetDir(preset) -> str: if isStdPreset(preset): - return 'lib/worlds/sm/variaRandomizer/standard_presets' if is_frozen() else 'worlds/sm/variaRandomizer/standard_presets' + return 'worlds/sm/variaRandomizer/standard_presets' else: - return 'lib/worlds/sm/variaRandomizer/community_presets' if is_frozen() else 'worlds/sm/variaRandomizer/community_presets' + return 'worlds/sm/variaRandomizer/community_presets' def removeChars(string, toRemove): return re.sub('[{}]+'.format(toRemove), '', string) From 2dc8b77ddcea943c7f3c8b1f1bf00e0dfee0c4ff Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 21:03:17 +0100 Subject: [PATCH 11/62] Patch: consolidate some if trees --- Patch.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/Patch.py b/Patch.py index af0a5e5e..09f41277 100644 --- a/Patch.py +++ b/Patch.py @@ -47,19 +47,9 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAM def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: - if game == GAME_ALTTP: - from worlds.alttp.Rom import get_base_rom_bytes - elif game == GAME_SM: - from worlds.sm.Rom import get_base_rom_bytes - elif game == GAME_SOE: - file_name = Utils.get_options()["soe_options"]["rom"] - get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb"))) - else: - raise RuntimeError("Selected game for base rom not found.") - if metadata is None: metadata = {} - patch = bsdiff4.diff(get_base_rom_bytes(), rom) + patch = bsdiff4.diff(get_base_rom_data(game), rom) return generate_yaml(patch, metadata, game) @@ -80,27 +70,30 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]: data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) game_name = data["game"] - if game_name in supported_games: - if game_name == GAME_ALTTP: - from worlds.alttp.Rom import get_base_rom_bytes - elif game_name == GAME_SM: - from worlds.sm.Rom import get_base_rom_bytes - else: - raise Exception(f"No Patch handler for game {game_name}") - elif game_name == "alttp": # old version for A Link to the Past - from worlds.alttp.Rom import get_base_rom_bytes - else: - raise Exception(f"Cannot handle game {game_name}") - if not ignore_version and data["compatible_version"] > current_patch_version: raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") - patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"]) + patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"]) rom_hash = patched_data[int(0x7FC0):int(0x7FD5)] data["meta"]["hash"] = "".join(chr(x) for x in rom_hash) target = os.path.splitext(patch_file)[0] + ".sfc" return data["meta"], target, patched_data +def get_base_rom_data(game: str): + if game == GAME_ALTTP: + from worlds.alttp.Rom import get_base_rom_bytes + elif game == "alttp": # old version for A Link to the Past + from worlds.alttp.Rom import get_base_rom_bytes + elif game == GAME_SM: + from worlds.sm.Rom import get_base_rom_bytes + elif game == GAME_SOE: + file_name = Utils.get_options()["soe_options"]["rom"] + get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb"))) + else: + raise RuntimeError("Selected game for base rom not found.") + return get_base_rom_bytes() + + def create_rom_file(patch_file: str) -> Tuple[dict, str]: data, target, patched_data = create_rom_bytes(patch_file) with open(target, "wb") as f: From 71f30b72f4ce36e05a8ce510f3e7cb60d8082cf7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 21:14:22 +0100 Subject: [PATCH 12/62] SNIClient: patch and launch SoE --- SNIClient.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index d1f406c0..6044f11a 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import threading import time import multiprocessing @@ -1080,14 +1081,24 @@ async def main(): meta, romfile = Patch.create_rom_file(args.diff_file) args.connect = meta["server"] logging.info(f"Wrote rom file to {romfile}") - adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled) - if adjusted: - try: - shutil.move(adjustedromfile, romfile) - adjustedromfile = romfile - except Exception as e: - logging.exception(e) - asyncio.create_task(run_game(adjustedromfile if adjusted else romfile)) + if args.diff_file.endswith(".apsoe"): + import webbrowser + webbrowser.open("http://www.evermizer.com/apclient/") + logging.info("Starting Evermizer Client in your Browser...") + import time + time.sleep(3) + sys.exit() + elif args.diff_file.endswith((".apbp", "apz3")): + adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled) + if adjusted: + try: + shutil.move(adjustedromfile, romfile) + adjustedromfile = romfile + except Exception as e: + logging.exception(e) + asyncio.create_task(run_game(adjustedromfile if adjusted else romfile)) + else: + asyncio.create_task(run_game(romfile)) ctx = Context(args.snes, args.connect, args.password) if ctx.server_task is None: From 6a912c128d12a80d3a7bba494de36ab4f25ea257 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 22:37:27 +0100 Subject: [PATCH 13/62] Setup: use GetSNESMD5OfFile (by Black Sliver) --- inno_setup_310.iss | 42 +++++++++++++++++++++++++++--------------- inno_setup_38.iss | 42 +++++++++++++++++++++++++++--------------- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/inno_setup_310.iss b/inno_setup_310.iss index 1da72694..23fa7fc4 100644 --- a/inno_setup_310.iss +++ b/inno_setup_310.iss @@ -48,20 +48,20 @@ Name: "playing"; Description: "Installation for playing purposes" Name: "custom"; Description: "Custom installation"; Flags: iscustom [Components] -Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed -Name: "generator"; Description: "Generator"; Types: full hosting -Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 -Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 -Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 -Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296 -Name: "server"; Description: "Server"; Types: full hosting -Name: "client"; Description: "Clients"; Types: full playing -Name: "client/sni"; Description: "SNI Client"; Types: full playing -Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing -Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing -Name: "client/factorio"; Description: "Factorio"; Types: full playing +Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed +Name: "generator"; Description: "Generator"; Types: full hosting +Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning +Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning +Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 +Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning +Name: "server"; Description: "Server"; Types: full hosting +Name: "client"; Description: "Clients"; Types: full playing +Name: "client/sni"; Description: "SNI Client"; Types: full playing +Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 -Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing +Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; @@ -134,7 +134,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{ap Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server - [Code] const SHCONTCH_NOPROGRESSBOX = 4; @@ -219,6 +218,19 @@ var OoTROMFilePage: TInputFileWizardPage; var MinecraftDownloadPage: TDownloadWizardPage; +function GetSNESMD5OfFile(const rom: string): string; +var data: AnsiString; +begin + if LoadStringFromFile(rom, data) then + begin + if Length(data) mod 1024 = 512 then + begin + data := copy(data, 513, Length(data)-512); + end; + Result := GetMD5OfString(data); + end; +end; + function CheckRom(name: string; hash: string): string; var rom: string; begin @@ -228,7 +240,7 @@ begin begin log('existing ROM found'); log(IntToStr(CompareStr(GetMD5OfFile(rom), hash))); - if CompareStr(GetMD5OfFile(rom), hash) = 0 then + if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then begin log('existing ROM verified'); Result := rom; diff --git a/inno_setup_38.iss b/inno_setup_38.iss index f9387c1a..c036c1cd 100644 --- a/inno_setup_38.iss +++ b/inno_setup_38.iss @@ -48,20 +48,20 @@ Name: "playing"; Description: "Installation for playing purposes" Name: "custom"; Description: "Custom installation"; Flags: iscustom [Components] -Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed -Name: "generator"; Description: "Generator"; Types: full hosting -Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 -Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 -Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 -Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296 -Name: "server"; Description: "Server"; Types: full hosting -Name: "client"; Description: "Clients"; Types: full playing -Name: "client/sni"; Description: "SNI Client"; Types: full playing -Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing -Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing -Name: "client/factorio"; Description: "Factorio"; Types: full playing +Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed +Name: "generator"; Description: "Generator"; Types: full hosting +Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning +Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning +Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 +Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning +Name: "server"; Description: "Server"; Types: full hosting +Name: "client"; Description: "Clients"; Types: full playing +Name: "client/sni"; Description: "SNI Client"; Types: full playing +Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 -Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing +Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing [Dirs] NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; @@ -134,7 +134,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{ap Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server - [Code] const SHCONTCH_NOPROGRESSBOX = 4; @@ -219,6 +218,19 @@ var OoTROMFilePage: TInputFileWizardPage; var MinecraftDownloadPage: TDownloadWizardPage; +function GetSNESMD5OfFile(const rom: string): string; +var data: AnsiString; +begin + if LoadStringFromFile(rom, data) then + begin + if Length(data) mod 1024 = 512 then + begin + data := copy(data, 513, Length(data)-512); + end; + Result := GetMD5OfString(data); + end; +end; + function CheckRom(name: string; hash: string): string; var rom: string; begin @@ -228,7 +240,7 @@ begin begin log('existing ROM found'); log(IntToStr(CompareStr(GetMD5OfFile(rom), hash))); - if CompareStr(GetMD5OfFile(rom), hash) = 0 then + if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then begin log('existing ROM verified'); Result := rom; From 3658c9f8e3e546ccfc6c0ebc2bd0de910407d27e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 22:45:49 +0100 Subject: [PATCH 14/62] Setup: use GetSNESMD5OfFile more --- inno_setup_310.iss | 9 ++++----- inno_setup_38.iss | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/inno_setup_310.iss b/inno_setup_310.iss index 23fa7fc4..9a35073f 100644 --- a/inno_setup_310.iss +++ b/inno_setup_310.iss @@ -239,7 +239,7 @@ begin if Length(rom) > 0 then begin log('existing ROM found'); - log(IntToStr(CompareStr(GetMD5OfFile(rom), hash))); + log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash))); if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then begin log('existing ROM verified'); @@ -337,7 +337,7 @@ begin Result := lttprom else if Assigned(LttPRomFilePage) then begin - R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') + R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') if R <> 0 then MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); @@ -353,7 +353,7 @@ begin Result := smrom else if Assigned(SMRomFilePage) then begin - R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675') + R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675') if R <> 0 then MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); @@ -369,8 +369,7 @@ begin Result := soerom else if Assigned(SoERomFilePage) then begin - R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a') - log(GetMD5OfFile(SoEROMFilePage.Values[0])) + R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a') if R <> 0 then MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); diff --git a/inno_setup_38.iss b/inno_setup_38.iss index c036c1cd..93d9cc64 100644 --- a/inno_setup_38.iss +++ b/inno_setup_38.iss @@ -239,7 +239,7 @@ begin if Length(rom) > 0 then begin log('existing ROM found'); - log(IntToStr(CompareStr(GetMD5OfFile(rom), hash))); + log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash))); if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then begin log('existing ROM verified'); @@ -337,7 +337,7 @@ begin Result := lttprom else if Assigned(LttPRomFilePage) then begin - R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') + R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') if R <> 0 then MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); @@ -353,7 +353,7 @@ begin Result := smrom else if Assigned(SMRomFilePage) then begin - R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675') + R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675') if R <> 0 then MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); @@ -369,8 +369,7 @@ begin Result := soerom else if Assigned(SoERomFilePage) then begin - R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a') - log(GetMD5OfFile(SoEROMFilePage.Values[0])) + R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a') if R <> 0 then MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); From bd8e1f6531cd8f13203e09bddffd7f57c31b8c4e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 14 Nov 2021 23:14:52 +0100 Subject: [PATCH 15/62] Setup: prevent clicking next when no rom file is selected. --- inno_setup_310.iss | 15 ++++++++++++--- inno_setup_38.iss | 11 ++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/inno_setup_310.iss b/inno_setup_310.iss index 9a35073f..4e936728 100644 --- a/inno_setup_310.iss +++ b/inno_setup_310.iss @@ -327,7 +327,16 @@ begin MinecraftDownloadPage.Hide; end; Result := True; - end else + end + else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then + Result := not (LttPROMFilePage.Values[0] = '') + else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then + Result := not (SMROMFilePage.Values[0] = '') + else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then + Result := not (SoEROMFilePage.Values[0] = '') + else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then + Result := not (OoTROMFilePage.Values[0] = '') + else Result := True; end; @@ -383,7 +392,7 @@ function GetOoTROMPath(Param: string): string; begin if Length(ootrom) > 0 then Result := ootrom - else if Assigned(OoTROMFilePage) then + else if (Assigned(OoTROMFilePage)) then begin R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f'); if R <> 0 then @@ -426,4 +435,4 @@ begin Result := not (WizardIsComponentSelected('generator/soe')); if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then Result := not (WizardIsComponentSelected('generator/oot')); -end; \ No newline at end of file +end; diff --git a/inno_setup_38.iss b/inno_setup_38.iss index 93d9cc64..8f005cac 100644 --- a/inno_setup_38.iss +++ b/inno_setup_38.iss @@ -327,7 +327,16 @@ begin MinecraftDownloadPage.Hide; end; Result := True; - end else + end + else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then + Result := not (LttPROMFilePage.Values[0] = '') + else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then + Result := not (SMROMFilePage.Values[0] = '') + else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then + Result := not (SoEROMFilePage.Values[0] = '') + else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then + Result := not (OoTROMFilePage.Values[0] = '') + else Result := True; end; From 97f6003582c452d2ed82927f28895966afa88681 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 15 Nov 2021 20:55:21 +0100 Subject: [PATCH 16/62] MultiServer: fix legacy argument passing in websockets --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index c1be4d9e..1874ae4c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -469,7 +469,7 @@ def update_aliases(ctx: Context, team: int): asyncio.create_task(ctx.send_encoded_msgs(client, cmd)) -async def server(websocket, path, ctx: Context): +async def server(websocket, path: str = "/", ctx: Context = None): client = Client(websocket, ctx) ctx.endpoints.append(client) @@ -1680,7 +1680,7 @@ async def main(args: argparse.Namespace): ctx.init_save(not args.disable_save) - ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None, + ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None, ping_interval=None) ip = args.host if args.host else Utils.get_public_ipv4() logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, From 899e9331fa48a37998fe33aeb68c61d7950629b9 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Tue, 16 Nov 2021 11:00:36 -0800 Subject: [PATCH 17/62] Make /connect archipelago.gg:port reflect PATCH_TARGET. --- WebHostLib/__init__.py | 2 ++ WebHostLib/templates/hostRoom.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index cfce8b05..13269dc0 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -171,6 +171,8 @@ def hostRoom(room: UUID): with db_session: room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running + + room.server = app.config['PATCH_TARGET'] return render_template("hostRoom.html", room=room) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 24b7bbf1..9133cb75 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -20,7 +20,7 @@ later, you can simply refresh this page and the server will be started again.
    {% if room.last_port %} - You can connect to this room by using '/connect archipelago.gg:{{ room.last_port }}' + You can connect to this room by using '/connect {{ room.server }}:{{ room.last_port }}' in the client.
    {% endif %} {{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %} From f4762cb3f24162d38e1491e32b0c71e686527089 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Tue, 16 Nov 2021 11:01:16 -0800 Subject: [PATCH 18/62] Provide a sample webhost configuration yaml. Not fully documented yet. --- webhost configuration sample.yaml | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 webhost configuration sample.yaml diff --git a/webhost configuration sample.yaml b/webhost configuration sample.yaml new file mode 100644 index 00000000..61fdcc26 --- /dev/null +++ b/webhost configuration sample.yaml @@ -0,0 +1,50 @@ +# This is a sample configuration for the Web host. If you wish to change any of these, rename this file to config.yaml + +# TODO +SELFHOST: true + +# Maximum concurrent world gens +GENERATORS: 8 + +# TODO +SELFLAUNCH: true + +# TODO +DEBUG: false + +# Web hosting port +PORT: 80 + +# Place where uploads go. +UPLOAD_FOLDER: uploads + +# Maximum upload size. Default is 64 megabyte (64 * 1024 * 1024) +MAX_CONTENT_LENGTH: 67108864 + +# Secret key used to determine important things like cookie authentication of room/seed page ownership. +# If you wish to deploy, uncomment the following line and set it to something not easily guessable. +# SECRET_KEY: "Your secret key here" + +# TODO +JOB_THRESHOLD: 2 + +# waitress uses one thread for I/O, these are for processing of view that get sent +WAITRESS_THREADS: 10 + +# Database provider details: +PONY: + provider: "sqlite" + filename: "ap.db3" # This MUST be the ABSOLUTE PATH to the file. + create_db: true + +# Maximum number of players that are allowed to be rolled on the server. After this limit, one should roll locally and upload the results. +MAX_ROLL: 20 + +# TODO +CACHE_TYPE: "simple" + +# TODO +JSON_AS_ASCII: false + +# Patch target. This is the address encoded into the patch that will be used for client auto-connect. +PATCH_TARGET: archipelago.gg \ No newline at end of file From 5d29184801fd5f5d687e83b78c443540231bd938 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 16 Nov 2021 21:38:34 +0100 Subject: [PATCH 19/62] WebHost: retrieve PATCH_TARGET from config directly --- WebHostLib/__init__.py | 2 -- WebHostLib/templates/hostRoom.html | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 13269dc0..cfce8b05 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -171,8 +171,6 @@ def hostRoom(room: UUID): with db_session: room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running - - room.server = app.config['PATCH_TARGET'] return render_template("hostRoom.html", room=room) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 9133cb75..9e392af9 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -20,7 +20,7 @@ later, you can simply refresh this page and the server will be started again.
    {% if room.last_port %} - You can connect to this room by using '/connect {{ room.server }}:{{ room.last_port }}' + You can connect to this room by using '/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}' in the client.
    {% endif %} {{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %} From a6cfed0da2ebdea13198ee65430ea309e27fa309 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 16 Nov 2021 21:39:08 +0100 Subject: [PATCH 20/62] reduce playerSettings.yaml to legacy LttP, remove when LttP transition is complete. --- playerSettings.yaml | 1148 +------------------------------------------ 1 file changed, 2 insertions(+), 1146 deletions(-) diff --git a/playerSettings.yaml b/playerSettings.yaml index d1e7e430..e41acaf0 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -24,14 +24,8 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc #{number} will be replaced with the counter value of the name. #{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1. game: # Pick a game to play - A Link to the Past: 0 - Factorio: 0 - Minecraft: 0 - Subnautica: 0 - Slay the Spire: 0 - Ocarina of Time: 0 - Super Metroid: 0 - + A Link to the Past: 1 + requires: version: 0.1.7 # Version of Archipelago required for this yaml to work as expected. # Shared Options supported by all games: @@ -59,493 +53,6 @@ progression_balancing: # exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk. # - "Master Sword Pedestal" -Super Metroid: # see https://randommetroidsolver.pythonanywhere.com/randomizer advanced tab for detailed info on each option -# start_inventory: # Begin the file with the listed items/upgrades -# Screw Attack: 1 -# Bomb: 1 -# Speed Booster: 1 -# Grappling Beam: 1 -# Space Jump: 1 -# Hi-Jump Boots: 1 -# Spring Ball: 1 -# Charge Beam: 1 -# Ice Beam: 1 -# Spazer: 1 -# Reserve Tank: 4 -# Missile: 46 -# Super Missile: 20 -# Power Bomb: 20 -# Energy Tank: 14 -# Morph Ball: 1 -# X-Ray Scope: 1 -# Wave Beam: 1 -# Plasma Beam: 1 -# Varia Suit: 1 -# Gravity Suit: 1 - start_inventory_removes_from_pool: - on: 0 - off: 1 - death_link: - on: 0 - off: 1 - preset: # choose one of the preset or specify "custom" to use customPreset option - newbie: 0 - casual: 0 - regular: 1 - veteran: 0 - expert: 0 - master: 0 - samus: 0 - Season_Races: 0 - SMRAT2021: 0 - solution: 0 - custom: 0 # see https://randommetroidsolver.pythonanywhere.com/presets for detailed info on each preset settings - varia_custom: 0 # use an entry from the preset list on https://randommetroidsolver.pythonanywhere.com/presets - varia_custom_preset: # use an entry from the preset list on https://randommetroidsolver.pythonanywhere.com/presets - regular - start_location: - Ceres: 0 - Landing_Site: 1 - Gauntlet_Top: 0 - Green_Brinstar_Elevator: 0 - Big_Pink: 0 - Etecoons_Supers: 0 - Wrecked_Ship_Main: 0 - Firefleas_Top: 0 - Business_Center: 0 - Bubble_Mountain: 0 - Mama_Turtle: 0 - Watering_Hole: 0 - Aqueduct: 0 - Red_Brinstar_Elevator: 0 - Golden_Four: 0 - max_difficulty: - easy: 0 - medium: 0 - hard: 0 - harder: 0 - hardcore: 1 - mania: 0 - infinity: 0 - morph_placement: - early: 1 - normal: 0 - suits_restriction: - on: 1 - off: 0 - strict_minors: - on: 0 - off: 1 - missile_qty: 30 # a range between 10 and 90 that is divided by 10 as a float - super_qty: 20 # a range between 10 and 90 that is divided by 10 as a float - power_bomb_qty: 10 # a range between 10 and 90 that is divided by 10 as a float - minor_qty: 100 # a range between 7 (minimum to beat the game) and 100 - energy_qty: - ultra_sparse: 0 - sparse: 0 - medium: 0 - vanilla: 1 - area_randomization: - on: 0 - light: 0 - off: 1 - area_layout: - on: 0 - off: 1 - doors_colors_rando: - on: 0 - off: 1 - allow_grey_doors: - on: 0 - off: 1 - boss_randomization: - on: 0 - off: 1 - fun_combat: - on: 0 - off: 1 - fun_movement: - on: 0 - off: 1 - fun_suits: - on: 0 - off: 1 - layout_patches: - on: 1 - off: 0 - varia_tweaks: - on: 0 - off: 1 - nerfed_charge: - on: 0 - off: 1 - gravity_behaviour: - Vanilla: 0 - Balanced: 1 - Progressive: 0 - elevators_doors_speed: - on: 1 - off: 0 - spin_jump_restart: - on: 0 - off: 1 - infinite_space_jump: - on: 0 - off: 1 - refill_before_save: - on: 0 - off: 1 - hud: - on: 0 - off: 1 - animals: - on: 0 - off: 1 - no_music: - on: 0 - off: 1 - random_music: - on: 0 - off: 1 - #item_sounds: always forced on due to a conflict in patching - #majors_split: not supported always "Full" - #scav_num_locs: not supported always off - #scav_randomized: not supported always off - #scav_escape: not supported always off - #progression_speed: not supported always random - #progression_difficulty: not supported always random - #hide_items: not supported always off - #minimizer: not supported always off - #minimizer_qty: not supported always off - #minimizer_tourian: not supported always off - #escape_rando: not supported always off - #remove_escape_enemies: not supported always off - #rando_speed: not supported always off - custom_preset: # see https://randommetroidsolver.pythonanywhere.com/presets for detailed info on each preset settings - Knows: # each skill (know) has a pair [can use, perceived difficulty using one of the following values] - # easy = 1 - # medium = 5 - # hard = 10 - # harder = 25 - # hardcore = 50 - # mania = 100 - Mockball: [True, 1] - SimpleShortCharge: [True, 1] - InfiniteBombJump: [True, 5] - GreenGateGlitch: [True, 5] - ShortCharge: [False, 0] - GravityJump: [True, 10] - SpringBallJump: [True, 10] - SpringBallJumpFromWall: [False, 0] - GetAroundWallJump: [True, 10] - DraygonGrappleKill: [True, 5] - DraygonSparkKill: [False, 0] - MicrowaveDraygon: [True, 1] - MicrowavePhantoon: [True, 5] - IceZebSkip: [False, 0] - SpeedZebSkip: [False, 0] - HiJumpMamaTurtle: [False, 0] - GravLessLevel1: [True, 50] - GravLessLevel2: [False, 0] - GravLessLevel3: [False, 0] - CeilingDBoost: [True, 1] - BillyMays: [True, 1] - AlcatrazEscape: [True, 25] - ReverseGateGlitch: [True, 5] - ReverseGateGlitchHiJumpLess: [False, 0] - EarlyKraid: [True, 1] - XrayDboost: [False, 0] - XrayIce: [True, 10] - RedTowerClimb: [True, 25] - RonPopeilScrew: [False, 0] - OldMBWithSpeed: [False, 0] - Moondance: [False, 0] - HiJumpLessGauntletAccess: [True, 50] - HiJumpGauntletAccess: [True, 25] - LowGauntlet: [False, 0] - IceEscape: [False, 0] - WallJumpCathedralExit: [True, 5] - BubbleMountainWallJump: [True, 5] - NovaBoost: [False, 0] - NorfairReserveDBoost: [False, 0] - CrocPBsDBoost: [False, 0] - CrocPBsIce: [False, 0] - IceMissileFromCroc: [False, 0] - FrogSpeedwayWithoutSpeed: [False, 0] - LavaDive: [True, 50] - LavaDiveNoHiJump: [False, 0] - WorstRoomIceCharge: [False, 0] - ScrewAttackExit: [False, 0] - ScrewAttackExitWithoutScrew: [False, 0] - FirefleasWalljump: [True, 25] - ContinuousWallJump: [False, 0] - DiagonalBombJump: [False, 0] - MockballWs: [False, 0] - SpongeBathBombJump: [False, 0] - SpongeBathHiJump: [True, 1] - SpongeBathSpeed: [True, 5] - TediousMountEverest: [False, 0] - DoubleSpringBallJump: [False, 0] - BotwoonToDraygonWithIce: [False, 0] - DraygonRoomGrappleExit: [False, 0] - DraygonRoomCrystalFlash: [False, 0] - PreciousRoomXRayExit: [False, 0] - MochtroidClip: [True, 5] - PuyoClip: [False, 0] - PuyoClipXRay: [False, 0] - SnailClip: [False, 0] - SuitlessPuyoClip: [False, 0] - KillPlasmaPiratesWithSpark: [False, 0] - KillPlasmaPiratesWithCharge: [True, 5] - AccessSpringBallWithHiJump: [True, 1] - AccessSpringBallWithSpringBallBombJumps: [True, 10] - AccessSpringBallWithBombJumps: [False, 0] - AccessSpringBallWithSpringBallJump: [False, 0] - AccessSpringBallWithXRayClimb: [False, 0] - AccessSpringBallWithGravJump: [False, 0] - Controller: - A: Jump - B: Dash - X: Shoot - Y: Item Cancel - L: Angle Down - R: Angle Up - Select: Item Select - Moonwalk: False - Settings: - Ice: "Gimme energy" - MainUpperNorfair: "Gimme energy" - LowerNorfair: "Default" - Kraid: "Default" - Phantoon: "Default" - Draygon: "Default" - Ridley: "Default" - MotherBrain: "Default" - X-Ray: "I don't like spikes" - Gauntlet: "I don't like acid" -Subnautica: {} -Slay the Spire: - character: # Pick What Character you wish to play with. - ironclad: 50 - silent: 50 - defect: 50 - watcher: 50 - ascension: # What Ascension do you wish to play with. - # you can add additional values between minimum and maximum - 0: 50 # minimum value - 20: 0 # maximum value - random: 0 - random-low: 0 - random-high: 0 - heart_run: # Whether or not you will need to collect they 3 keys to unlock the final act - # and beat the heart to finish the game. - false: 50 - true: 0 -Factorio: - tech_tree_layout: - single: 1 - small_diamonds: 1 - medium_diamonds: 1 - large_diamonds: 1 - small_pyramids: 1 - medium_pyramids: 1 - large_pyramids: 1 - small_funnels: 1 - medium_funnels: 1 - large_funnels: 1 - recipe_time: # randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc. - vanilla: 1 - fast: 0 # 25% to 100% of original time - normal: 0 # 50 % to 200% of original time - slow: 0 # 100% to 400% of original time - chaos: 0 # 25% to 400% of original time - recipe_ingredients: - rocket: 1 # only randomize rocket part recipe - science_pack: 1 # also randomize science pack ingredients - 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 - silo: - vanilla: 1 - randomize_recipe: 0 - spawn: 0 # spawn silo near player spawn point - free_samples: - none: 1 - single_craft: 0 - half_stack: 0 - stack: 0 - progressive: - on: 1 - off: 0 - grouped_random: 0 - tech_tree_information: - none: 0 - advancement: 0 # show which items are a logical advancement - full: 1 # show full info on each tech node - imported_blueprints: # can be turned off to prevent access to blueprints created outside the current world - on: 1 - off: 0 - starting_items: - burner-mining-drill: 19 - stone-furnace: 19 - # Note: Total amount of traps cannot exceed 4, if the sum of them is higher it will get automatically capped. - evolution_traps: - # Trap items that when received increase the enemy evolution. - 0: 1 - 1: 0 - 2: 0 - 3: 0 - 4: 0 - random: 0 - random-low: 0 - random-middle: 0 - random-high: 0 - evolution_trap_increase: - # If present, % increase of Evolution with each trap received. - 5: 0 - 10: 1 - 15: 0 - 20: 0 - 100: 0 - random: 0 - random-low: 0 - random-middle: 0 - random-high: 0 - attack_traps: - # Trap items that when received trigger an attack on your base. - 0: 1 - 1: 0 - 2: 0 - 3: 0 - 4: 0 - random: 0 - random-low: 0 - random-middle: 0 - random-high: 0 - world_gen: - # frequency, size, richness, terrain segmentation, starting area and water are all of https://wiki.factorio.com/Types/MapGenSize - # inverse of water scale - terrain_segmentation: 0.5 - water: 1.5 - autoplace_controls: - # set size to 0 to disable - coal: - frequency: 1 - size: 3 - richness: 6 - copper-ore: - frequency: 1 - size: 3 - richness: 6 - crude-oil: - frequency: 1 - size: 3 - richness: 6 - enemy-base: - frequency: 1 - size: 1 - richness: 1 - iron-ore: - frequency: 1 - size: 3 - richness: 6 - stone: - frequency: 1 - size: 3 - richness: 6 - trees: - frequency: 1 - size: 1 - richness: 1 - uranium-ore: - frequency: 1 - size: 3 - richness: 6 - seed: null # turn into positive number to create specific seed - starting_area: 1 - peaceful_mode: 0 - cliff_settings: - name: cliff - cliff_elevation_0: 10 # base elevation, can't be changed in GUI - cliff_elevation_interval: 40 # 40/frequency - richness: 1 # 0: off, >0: continuity - property_expression_names: - "control-setting:moisture:bias": 0 # grass bias -0.5 to +0.5 - "control-setting:moisture:frequency:multiplier": 1 # 1/scale in GUI - "control-setting:aux:bias": 0 # -sand/+red desert bias -0.5 to +0.5 - "control-setting:aux:frequency:multiplier": 1 # 1/scale in GUI - pollution: - enabled: true - diffusion_ratio: 0.02 - ageing: 1 # GUI dissipation factor - enemy_attack_pollution_consumption_modifier: 1 - min_pollution_to_damage_trees: 60 - pollution_restored_per_tree_damage: 10 - enemy_evolution: - enabled: true - time_factor: 40.0e-7 # GUI value * 0.0000001 - destroy_factor: 200.0e-5 # GUI value * 0.00001 - pollution_factor: 9.0e-7 # GUI value * 0.0000001 - enemy_expansion: - enabled: true - max_expansion_distance: 7 - settler_group_min_size: 5 - settler_group_max_size: 20 - min_expansion_cooldown: 14400 # 1 to 60 min in ticks - max_expansion_cooldown: 216000 # 5 to 180 min in ticks -Minecraft: - advancement_goal: 50 # Number of advancements required (87 max) to spawn the Ender Dragon and complete the game. - egg_shards_required: # Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn. - 0: 1 - 5: 0 - 10: 0 - 20: 0 - egg_shards_available: # Number of egg shards available in the pool (30 max). - 0: 1 - 5: 0 - 10: 0 - 20: 0 - combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses. - easy: 0 - normal: 1 - hard: 0 - include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements. - on: 0 - off: 1 - include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time. - on: 0 - off: 1 - include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items. - on: 0 - off: 1 - shuffle_structures: # Enables shuffling of villages, outposts, fortresses, bastions, and end cities. - on: 0 - off: 1 - structure_compasses: # Adds structure compasses to the item pool, which point to the nearest indicated structure. - on: 0 - off: 1 - bee_traps: # Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when received. - 0: 1 - 25: 0 - 50: 0 - 75: 0 - 100: 0 - send_defeated_mobs: # Send killed mobs to other Minecraft worlds which have this option enabled. - on: 0 - off: 1 A Link to the Past: ### Logic Section ### glitches_required: # Determine the logic required to complete the seed @@ -949,657 +456,6 @@ A Link to the Past: vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal) swordless: 0 # swordless mode -Ocarina of Time: - logic_rules: # Set the logic used for the generator. - glitchless: 50 - glitched: 0 - no_logic: 0 - logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song. - false: 50 - true: 0 - open_forest: # Set the state of Kokiri Forest and the path to Deku Tree. - open: 50 - closed_deku: 0 - closed: 0 - open_kakariko: # Set the state of the Kakariko Village gate. - open: 50 - zelda: 0 - closed: 0 - open_door_of_time: # Open the Door of Time by default, without the Song of Time. - false: 0 - true: 50 - zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain. - open: 0 - adult: 0 - closed: 50 - gerudo_fortress: # Set the requirements for access to Gerudo Fortress. - normal: 0 - fast: 50 - open: 0 - bridge: # Set the requirements for the Rainbow Bridge. - open: 0 - vanilla: 0 - stones: 0 - medallions: 50 - dungeons: 0 - tokens: 0 - trials: # Set the number of required trials in Ganon's Castle. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 6: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - starting_age: # Choose which age Link will start as. - child: 50 - adult: 0 - triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game. - false: 50 - true: 0 - triforce_goal: # Number of Triforce pieces required to complete the game. - # you can add additional values between minimum and maximum - 1: 0 # minimum value - 50: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - extra_triforce_percentage: # Percentage of additional Triforce pieces in the pool, separate from the item pool setting. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 100: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling. - false: 50 - true: 0 - bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 3: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - bridge_medallions: # Set the number of medallions required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 6: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 9: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 100: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses. - remove: 0 - startwith: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_smallkeys: # Control where to shuffle dungeon small keys. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys. - vanilla: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key. - remove: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - on_lacs: 0 - enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is. - false: 50 - true: 0 - lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time. - vanilla: 50 - stones: 0 - medallions: 0 - dungeons: 0 - tokens: 0 - lacs_stones: # Set the number of Spiritual Stones required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 3: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - lacs_medallions: # Set the number of medallions required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 6: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - lacs_rewards: # Set the number of dungeon rewards required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 9: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 100: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - shuffle_song_items: # Set where songs can appear. - song: 50 - dungeon: 0 - any: 0 - shopsanity: # Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop; "random_number" randomizes the value for each shop. - off: 50 - fixed_number: 0 - random_number: 0 - shop_slots: # Number of items per shop to be randomized into the main itempool. Only active if Shopsanity is set to "fixed_number." - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 4: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool. - off: 50 - dungeons: 0 - overworld: 0 - all: 0 - shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices. - off: 50 - low: 0 - affordable: 0 - expensive: 0 - shuffle_cows: # Cows give items when Epona's Song is played. - false: 50 - true: 0 - shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool. - false: 50 - true: 0 - shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool. - false: 50 - true: 0 - shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle. - false: 50 - true: 0 - shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool. - false: 50 - true: 0 - shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees. - false: 50 - true: 0 - shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman. - false: 50 - true: 0 - skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed. - false: 50 - true: 0 - no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights. - false: 0 - true: 50 - no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda. - false: 0 - true: 50 - no_epona_race: # Epona can always be summoned with Epona's Song. - false: 0 - true: 50 - skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt. - false: 0 - true: 50 - complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop. - false: 50 - true: 0 - useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched. - false: 50 - true: 0 - fast_chests: # All chest animations are fast. If disabled, major items have a slow animation. - false: 0 - true: 50 - free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song. - false: 50 - true: 0 - fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask. - false: 50 - true: 0 - chicken_count: # Controls the number of Cuccos for Anju to give an item as child. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 7: 0 # maximum value - random: 50 - random-low: 0 - random-high: 0 - correct_chest_sizes: # Changes chests containing progression into large chests, and nonprogression into small chests. - false: 50 - true: 0 - hints: # Gossip Stones can give hints about item locations. - none: 0 - mask: 0 - agony: 0 - always: 50 - false: 0 - hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc. - balanced: 50 - ddr: 0 - league: 0 - mw2: 0 - scrubs: 0 - strong: 0 - tournament: 0 - useless: 0 - very_strong: 0 - text_shuffle: # Randomizes text in the game for comedic effect. - none: 50 - except_hints: 0 - complete: 0 - damage_multiplier: # Controls the amount of damage Link takes. - half: 0 - normal: 50 - double: 0 - quadruple: 0 - ohko: 0 - no_collectible_hearts: # Hearts will not drop from enemies or objects. - false: 50 - true: 0 - starting_tod: # Change the starting time of day. - default: 50 - sunrise: 0 - morning: 0 - noon: 0 - afternoon: 0 - sunset: 0 - evening: 0 - midnight: 0 - witching_hour: 0 - start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts. - false: 50 - true: 0 - start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet. - false: 50 - true: 0 - item_pool_value: # Changes the number of items available in the game. - plentiful: 0 - balanced: 50 - scarce: 0 - minimal: 0 - junk_ice_traps: # Adds ice traps to the item pool. - off: 0 - normal: 50 - extra: 0 - mayhem: 0 - onslaught: 0 - ice_trap_appearance: # Changes the appearance of ice traps as freestanding items. - major_only: 50 - junk_only: 0 - anything: 0 - logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 50 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 0 - logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 0 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 50 - default_targeting: # Default targeting option. - hold: 50 - switch: 0 - display_dpad: # Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots). - false: 0 - true: 50 - correct_model_colors: # Makes in-game models match their HUD element colors. - false: 0 - true: 50 - background_music: # Randomize or disable background music. - normal: 50 - off: 0 - randomized: 0 - fanfares: # Randomize or disable item fanfares. - normal: 50 - off: 0 - randomized: 0 - ocarina_fanfares: # Enable ocarina songs as fanfares. These are longer than usual fanfares. Does nothing without fanfares randomized. - false: 50 - true: 0 - kokiri_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - kokiri_green: 50 - goron_red: 0 - zora_blue: 0 - black: 0 - white: 0 - azure_blue: 0 - vivid_cyan: 0 - light_red: 0 - fuchsia: 0 - purple: 0 - majora_purple: 0 - twitch_purple: 0 - purple_heart: 0 - persian_rose: 0 - dirty_yellow: 0 - blush_pink: 0 - hot_pink: 0 - rose_pink: 0 - orange: 0 - gray: 0 - gold: 0 - silver: 0 - beige: 0 - teal: 0 - blood_red: 0 - blood_orange: 0 - royal_blue: 0 - sonic_blue: 0 - nes_green: 0 - dark_green: 0 - lumen: 0 - goron_color: # Choose a color. Uses the same options as "kokiri_color". - random_choice: 0 - completely_random: 0 - goron_red: 50 - zora_color: # Choose a color. Uses the same options as "kokiri_color". - random_choice: 0 - completely_random: 0 - zora_blue: 50 - silver_gauntlets_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - silver: 50 - gold: 0 - black: 0 - green: 0 - blue: 0 - bronze: 0 - red: 0 - sky_blue: 0 - pink: 0 - magenta: 0 - orange: 0 - lime: 0 - purple: 0 - golden_gauntlets_color: # Choose a color. Uses the same options as "silver_gauntlets_color". - random_choice: 0 - completely_random: 0 - gold: 50 - mirror_shield_frame_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - red: 50 - green: 0 - blue: 0 - yellow: 0 - cyan: 0 - magenta: 0 - orange: 0 - gold: 0 - purple: 0 - pink: 0 - navi_color_default_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - rainbow: 0 - gold: 0 - white: 50 - green: 0 - light_blue: 0 - yellow: 0 - red: 0 - magenta: 0 - black: 0 - tatl: 0 - tael: 0 - fi: 0 - ciela: 0 - epona: 0 - ezlo: 0 - king_of_red_lions: 0 - linebeck: 0 - loftwing: 0 - midna: 0 - phantom_zelda: 0 - navi_color_default_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner". - random_choice: 0 - completely_random: 0 - match_inner: 50 - navi_color_enemy_inner: # Choose a color. Uses the same options as "navi_color_default_inner". - random_choice: 0 - completely_random: 0 - yellow: 50 - navi_color_enemy_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner". - random_choice: 0 - completely_random: 0 - match_inner: 50 - navi_color_npc_inner: # Choose a color. Uses the same options as "navi_color_default_inner". - random_choice: 0 - completely_random: 0 - light_blue: 50 - navi_color_npc_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner". - random_choice: 0 - completely_random: 0 - match_inner: 50 - navi_color_prop_inner: # Choose a color. Uses the same options as "navi_color_default_inner". - random_choice: 0 - completely_random: 0 - green: 50 - navi_color_prop_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner". - random_choice: 0 - completely_random: 0 - match_inner: 50 - sword_trail_duration: # Set the duration for sword trails. - # you can add additional values between minimum and maximum - 4: 50 # minimum value - 20: 0 # maximum value - random: 0 - random-low: 0 - random-high: 0 - sword_trail_color_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - rainbow: 0 - white: 50 - red: 0 - green: 0 - blue: 0 - cyan: 0 - magenta: 0 - orange: 0 - gold: 0 - purple: 0 - pink: 0 - sword_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner". - random_choice: 0 - completely_random: 0 - match_inner: 50 - bombchu_trail_color_inner: # Uses the same options as "sword_trail_color_inner". - random_choice: 0 - completely_random: 0 - red: 50 - bombchu_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner". - random_choice: 0 - completely_random: 0 - match_inner: 50 - boomerang_trail_color_inner: # Uses the same options as "sword_trail_color_inner". - random_choice: 0 - completely_random: 0 - yellow: 50 - boomerang_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner". - random_choice: 0 - completely_random: 0 - match_inner: 50 - heart_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - red: 50 - green: 0 - blue: 0 - yellow: 0 - magic_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - green: 50 - red: 0 - blue: 0 - purple: 0 - pink: 0 - yellow: 0 - white: 0 - a_button_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code. - random_choice: 0 - completely_random: 0 - n64_blue: 50 - n64_green: 0 - n64_red: 0 - gamecube_green: 0 - gamecube_red: 0 - gamecube_grey: 0 - yellow: 0 - black: 0 - white: 0 - magenta: 0 - ruby: 0 - sapphire: 0 - lime: 0 - cyan: 0 - purple: 0 - orange: 0 - b_button_color: # Choose a color. Uses the same options as "a_button_color". - random_choice: 0 - completely_random: 0 - n64_green: 50 - c_button_color: # Choose a color. Uses the same options as "a_button_color". - random_choice: 0 - completely_random: 0 - yellow: 50 - start_button_color: # Choose a color. Uses the same options as "a_button_color". - random_choice: 0 - completely_random: 0 - n64_red: 50 - sfx_navi_overworld: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - none: 0 - sfx_navi_enemy: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - none: 0 - sfx_low_hp: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - none: 0 - sfx_menu_cursor: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - none: 0 - sfx_menu_select: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - none: 0 - sfx_nightfall: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - none: 0 - sfx_horse_neigh: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - none: 0 - sfx_hover_boots: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound. - default: 50 - completely_random: 0 - random_ear_safe: 0 - random_choice: 0 - sfx_ocarina: # Change the sound of the ocarina. - ocarina: 50 - malon: 0 - whistle: 0 - harp: 0 - grind_organ: 0 - flute: 0 - - # Uncomment this section to enable logical tricks for Ocarina of Time. - # Add logic tricks keyed by "nice" name rather than internal name: "Hidden Grottos without Stone of Agony", not "logic_grottos_without_agony" - # The following is the typical set of racing tricks, though you can add or remove them as desired. - # logic_tricks: - # - Fewer Tunic Requirements - # - Hidden Grottos without Stone of Agony - # - Child Deadhand without Kokiri Sword - # - Man on Roof without Hookshot - # - Dodongo's Cavern Spike Trap Room Jump without Hover Boots - # - Hammer Rusted Switches Through Walls - # - Windmill PoH as Adult with Nothing - # - Crater's Bean PoH with Hover Boots - # - Forest Temple East Courtyard Vines with Hookshot - # - Bottom of the Well without Lens of Truth - # - Ganon's Castle without Lens of Truth - # - Gerudo Training Grounds without Lens of Truth - # - Shadow Temple before Invisible Moving Platform without Lens of Truth - # - Shadow Temple beyond Invisible Moving Platform without Lens of Truth - # - Spirit Temple without Lens of Truth - -# meta_ignore, linked_options and triggers work for any game -meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it - mode: - - inverted # Never play inverted seeds - retro: - - on # Never play retro seeds - swordless: - - on # Never play a swordless seed - linked_options: - name: crosskeys options: # These overwrite earlier options if the percentage chance triggers From 10411466d83ac8811530e1b3dca1cd200c666bbd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 16 Nov 2021 23:59:40 +0100 Subject: [PATCH 21/62] WebHost: make meta attribute LongStr instead of str --- WebHostLib/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/models.py b/WebHostLib/models.py index e03d7666..2c85e5a7 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -40,7 +40,7 @@ class Seed(db.Entity): creation_time = Required(datetime, default=lambda: datetime.utcnow()) slots = Set(Slot) spoiler = Optional(LongStr, lazy=True) - meta = Required(str, default=lambda: "{\"race\": false}") # additional meta information/tags + meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags class Command(db.Entity): @@ -53,5 +53,5 @@ class Generation(db.Entity): id = PrimaryKey(UUID, default=uuid4) owner = Required(UUID) options = Required(buffer, lazy=True) - meta = Required(str, default=lambda: "{\"race\": false}") + meta = Required(LongStr, default=lambda: "{\"race\": false}") state = Required(int, default=0, index=True) From 2ef60c0cd91600bff41f0086d137031c5b2aaa93 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Tue, 16 Nov 2021 20:31:46 -0500 Subject: [PATCH 22/62] [SM] added support for 65535 different player names in ROM (#133) * added support for 65535 different player names in ROM --- worlds/sm/Rom.py | 2 +- worlds/sm/__init__.py | 38 +++++++++++++----- .../patches/common/ips/basepatch.ips | Bin 44859 -> 45282 bytes 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index 5d7ab709..3e191c2e 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -2,7 +2,7 @@ import Utils from Patch import read_rom JAP10HASH = '21f3e98df4780ee1c667b84e57d88675' -ROM_PLAYER_LIMIT = 255 +ROM_PLAYER_LIMIT = 65535 import hashlib import os diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 808f1bde..005844ea 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -159,6 +159,9 @@ class SMWorld(World): def getWord(self, w): return (w & 0x00FF, (w & 0xFF00) >> 8) + + def getWordArray(self, w): + return [w & 0x00FF, (w & 0xFF00) >> 8] # used for remote location Credits Spoiler of local items class DummyLocation: @@ -232,7 +235,10 @@ class SMWorld(World): multiWorldItems = {} idx = 0 itemId = 0 + self.playerIDMap = {} + playerIDCount = 0 # 0 is for "Archipelago" server for itemLoc in self.world.get_locations(): + romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None: if itemLoc.item.type in ItemManager.Items: itemId = ItemManager.Items[itemLoc.item.type].Id @@ -240,12 +246,21 @@ class SMWorld(World): itemId = ItemManager.Items['ArchipelagoItem'].Id + idx multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name) idx += 1 + + if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): + playerIDCount += 1 + self.playerIDMap[romPlayerID] = playerIDCount + (w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1) (w2, w3) = self.getWord(itemId) - (w4, w5) = self.getWord(itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0) + (w4, w5) = self.getWord(romPlayerID) (w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1) multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7] + if itemLoc.item.player == self.player: + if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): + playerIDCount += 1 + self.playerIDMap[romPlayerID] = playerIDCount itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"] idx = 0 @@ -260,21 +275,24 @@ class SMWorld(World): openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]} deathLink = {0x277f04: [int(self.world.death_link[self.player])]} + + playerNames = {} + playerNameIDMap = {} + playerNames[0x1C5000] = "Archipelago".upper().center(16).encode() + playerNameIDMap[0x1C5800] = self.getWordArray(0) + for key,value in self.playerIDMap.items(): + playerNames[0x1C5000 + value * 16] = self.world.player_name[key][:16].upper().center(16).encode() + playerNameIDMap[0x1C5800 + value * 2] = self.getWordArray(key) + patchDict = { 'MultiWorldLocations': multiWorldLocations, 'MultiWorldItems': multiWorldItems, 'offworldSprites': offworldSprites, 'openTourianGreyDoors': openTourianGreyDoors, - 'deathLink': deathLink} + 'deathLink': deathLink, + 'PlayerName': playerNames, + 'PlayerNameIDMap': playerNameIDMap} romPatcher.applyIPSPatchDict(patchDict) - playerNames = {} - playerNames[0x1C5000] = "Archipelago".upper().center(16).encode() - for p in range(1, min(self.world.players, ROM_PLAYER_LIMIT) + 1): - playerNames[0x1C5000 + p * 16] = self.world.player_name[p][:16].upper().center(16).encode() - - - romPatcher.applyIPSPatch('PlayerName', { 'PlayerName': playerNames }) - # set rom name # 21 bytes from Main import __version__ diff --git a/worlds/sm/variaRandomizer/patches/common/ips/basepatch.ips b/worlds/sm/variaRandomizer/patches/common/ips/basepatch.ips index 3877dbaa5829d5360cf5b3e17d7cc661e7b56c12..1acf93ef5f7baf923f71ca7e34cf4fc7ff375a1c 100644 GIT binary patch delta 91 zcmV-h0HpuB-U8y`0 delta 60 zcmaF#kZJclrVZ?Dj8>aD*a{RGohMhS%ZYFS!Kwue4Eq@_?9lwraEoiz-o2X-sV`$< Q;$T>urNy!Nm+^}l0C`Ln4gdfE From 52aebc309451f6d309d08d96792f2dc5c8b41cb1 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Wed, 10 Nov 2021 14:26:31 -0600 Subject: [PATCH 23/62] Add advanced settings guide; add additional info to setup guide --- .../archipelago/advanced_settings_en.md | 92 +++++++++++++++++++ .../assets/tutorial/archipelago/setup_en.md | 12 +++ .../static/assets/tutorial/tutorials.json | 18 +++- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md diff --git a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md new file mode 100644 index 00000000..6b02d6fd --- /dev/null +++ b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md @@ -0,0 +1,92 @@ +# Advanced Game Options Guide + + +The Archipelago system generates games using player configuration files as input. Generally these are going to be +YAML files and each player will have one of these containing their custom settings for the randomized game they want to play. +On the website when you customize your settings from one of the game player settings pages which you can reach from the +[supported games page](/games). Clicking on the export settings button at the bottom will provide you with a pre-filled out +YAML with your options. The player settings page also has an option to download a fully filled out yaml containing every +option with every available setting for the available options. + +## YAML Formatting? +YAML files are a format of human-readable markup config files. The basic syntax +of a yaml file will have `root` and then different levels of `nested` text that the generator "reads" in order to determine +your settings. To nest text, the correct syntax is **two spaces over** from its root option. A YAML file can be edited +with whatever text editor you choose to use though I personally recommend that you use [Sublime Text](https://www.sublimetext.com/). +This program out of the box supports the correct formatting for the YAML file, so you will be able to tab and get proper +highlighting for any potential errors made while editing the file. If using any other text editor such as Notepad or +Notepad++ whenever you move to nest an option that it is done with two spaces and not tabs. + +Typical YAML format will look as follows: +```yaml +root_option: + nested_option_one: + option_one_setting_one: 1 + option_one_setting_two: 0 + nested_option_two: + option_two_setting_one: 14 + option_two_setting_two: 43 +``` + +In Archipelago YAML options are always written out in full lowercase with underscores separating any words. The numbers +following the colons here are weights. The generator will read the weight of every option the roll that option that many +times, the next option as many times as its numbered and so forth. For the above example `nested_option_one` will have +`option_one_setting_one` 1 time and `option_one_setting_two` 0 times so `option_one_setting_one` is guaranteed to occur. +For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43 +times against each other. This means `option_two_setting_two` will be more likely to occur but it isn't guaranteed adding +more randomness and "mystery" to your settings. Every configurable setting supports weights. + +### Root Options +Currently there are only a few options that are root options. Everything else should be nested within one of these root +options or in some cases nested within other nested options. The only options that should exist in root are `description`, +`name`, `game`, `requires`, `accessibility`, `progression_balancing`, `triggers`, and the name of the games you want +settings for. +* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files using +this to detail the intention of the file. +* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can also +be filled with multiple names each having a weight to it. +* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with different +weights. +* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this +is good for detailing the version of Archipelago this YAML was prepared for as if it is rolled on an older version may be +missing settings and as such will not work as expected. If any plando is used in the file then requiring it here to ensure +it will be used is good practice. +* `accessibility` determines the level of access to the game the generation will expect you to have in order to reach your +completion goal. This supports `items`, `locations`, and `none` and is set to `locations` by default. + * `items` will guarantee you can acquire all items in your world but may not be able to access all locations. This mostly +comes into play if there is any entrance shuffle in the seed as locations without items in them can be placed in areas +that make them unreachable. + * `none` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but +may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest +in a dungeon in ALTTP making it impossible to get and finish the dungeon. +* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. This +primarily involves moving necessary progression items into earlier logic spheres to make the games more accessible so that +players almost always have something to do. This can be turned `on` or `off` and is `on` by default. +* `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read more +about this [here](/tutorial/archipelago/triggers/en). + +### Game Options + +One of your root settings will be the name of the game you would like to populate with settings in the format +`GameName`. since it is possible to give a weight to any option it is possible to have one file that can generate a seed +for you where you don't know which game you'll play. For these cases you'll want to fill the game options for every game +that can be rolled by these settings. If a game can be rolled it **must** have a settings section even if it is empty. + +#### Universal Game Options + +Some options in Archipelago can be used by every game but must still be placed within the relevant game's section. +Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`, +and `exclude_locations`. +* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be +the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which +will give you 30 rupees. +* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for +the location without using any hint points. +* `local_items` will force any items you want to be in your world instead of being in another world. +* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be located +in your own. +* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in +it to see how important the location is. +* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk" +item which isn't necessary for progression to go in these locations. + diff --git a/WebHostLib/static/assets/tutorial/archipelago/setup_en.md b/WebHostLib/static/assets/tutorial/archipelago/setup_en.md index 4ce96731..ec8cc273 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/setup_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/setup_en.md @@ -12,6 +12,17 @@ game/games you plan to play are available here go ahead and install these as wel supported by Archipelago but not listed in the installation check the relevant tutorial. ## Generating a game + +### Creating a YAML +In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's +native coop system or using archipelago's coop support. Each world will hold one slot in the multiworld and will have a +slot name and, if the relevant game requires it, files to associate it with that multiworld. If multiple people plan to +play in one world cooperatively then they will only need one YAML for their coop world, but if each player is planning on +playing their own game then they will each need a YAML. These YAML files can be generated by going to the relevant game's +player settings page, entering the name they want to use for the game, setting the options to what they would like to +play with and then clicking on the export settings button. This will then download a YAML file that will contain all of +these options and this can then be given to whoever is going to generate the game. + ### Gather all player YAMLS All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they wish to play with. A YAML is a file which contains human readable markup. In other words, this is a settings file kind of like an INI file or a TOML file. @@ -51,6 +62,7 @@ The generator will put a zip folder into your `Archipelago\output` folder with t This contains the patch files and relevant mods for the players as well as the serverdata for the host. ## Hosting a multiworld + ### Uploading the seed to the website The easiest and most recommended method is to generate the game on the website which will allow you to create a private room with all the necessary files you can share, as well as hosting the game and supporting item trackers for various games. diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index 50e1964f..2762aa7a 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -4,7 +4,7 @@ "tutorials": [ { "name": "Multiworld Setup Tutorial", - "description": "A Guide to setting up the Archipelago software to generate multiworld games on your computer.", + "description": "A guide to setting up the Archipelago software to generate and host multiworld games on your computer and using the website.", "files": [ { "language": "English", @@ -16,9 +16,23 @@ } ] }, + { + "name": "Using Advanced Settings", + "description": "A guide to reading yaml files and editing them to fully customize your game.", + "files": [ + { + "language": "English", + "filename": "archipelago/advanced_settings_en.md", + "link": "archipelago/advanced_settings/en", + "authors": [ + "alwaysintreble" + ] + } + ] + }, { "name": "Archipelago Triggers Guide", - "description": "A Guide to setting up and using triggers in your game settings.", + "description": "A guide to setting up and using triggers in your game settings.", "files": [ { "language": "English", From fc022c98f29b4072cc3f383fbe1303ba54fe9971 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 14 Nov 2021 13:26:30 -0600 Subject: [PATCH 24/62] Add example using the various options presented --- .../archipelago/advanced_settings_en.md | 74 ++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md index 6b02d6fd..0432efb8 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md @@ -8,7 +8,7 @@ On the website when you customize your settings from one of the game player sett YAML with your options. The player settings page also has an option to download a fully filled out yaml containing every option with every available setting for the available options. -## YAML Formatting? +## YAML Formatting YAML files are a format of human-readable markup config files. The basic syntax of a yaml file will have `root` and then different levels of `nested` text that the generator "reads" in order to determine your settings. To nest text, the correct syntax is **two spaces over** from its root option. A YAML file can be edited @@ -75,8 +75,8 @@ that can be rolled by these settings. If a game can be rolled it **must** have a #### Universal Game Options Some options in Archipelago can be used by every game but must still be placed within the relevant game's section. -Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`, -and `exclude_locations`. +Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`, +`exclude_locations`, and various [plando options](tutorial/archipelago/plando/en). * `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which will give you 30 rupees. @@ -90,3 +90,71 @@ it to see how important the location is. * `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk" item which isn't necessary for progression to go in these locations. +### Example + +```yaml + +description: An example using various advanced options +name: Example Player +game: A Link to the Past +requires: + version: 0.2.0 +accessibility: none +progression_balancing: on +A Link to the Past: + smallkey_shuffle: + original_dungeon: 1 + any_world: 1 + start_inventory: + Pegasus Boots: 1 + Bombs (3): 2 + start_hints: + - Hammer + local_items: + - Bombos + - Ether + - Quake + non_local_items: + - Moon Pearl + start_location_hints: + - Spike Cave + exclude_locations: + - Cave 45 +triggers: + - option_category: A Link to the Past + option_name: smallkey_shuffle + option_result: any_world + options: + A Link to the Past: + bigkey_shuffle: any_world + map_shuffle: any_world + compass_shuffle: any_world +``` + +#### This is a fully functional yaml file that will do all the following things: +* `description` gives us a general overview so if we pull up this file later we can understand the intent. +* `name` is `Example Player` and this will be used in the server console when sending and receiving items. +* `game` is set to `A Link to the Past` meaning that is what game we will play with this file. +* `requires` is set to require release version 0.2.0 or higher. +* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be +completely inaccessible but the seed will still be completable. +* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of having +things to do. +* `A Link to the Past` defines a location for us to nest all the game options we would like to use for our game `A Link to the Past`. +* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example +we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere +amongst the multiworld. +* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this example +we have: + * `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots + * `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs +* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use with no cost. +* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we +have to find it ourselves. +* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it. +* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld +that can be used for no cost. +* `exclude_locations` forces a not important item to be placed on the `Cave 45` location. +* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world` +result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the `any_world` +result. \ No newline at end of file From a5ca4f1611de17e61d3b77d22aae447f12846648 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 17 Nov 2021 16:44:20 +0100 Subject: [PATCH 25/62] Options: document exclude locations and start location hints --- Options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index a87618f9..9fc2da5a 100644 --- a/Options.py +++ b/Options.py @@ -379,6 +379,7 @@ class StartHints(ItemSet): class StartLocationHints(OptionSet): + """Start with these locations and their item prefilled into the !hint command""" displayname = "Start Location Hints" @@ -399,7 +400,7 @@ per_game_common_options = { "start_inventory": StartInventory, "start_hints": StartHints, "start_location_hints": StartLocationHints, - "exclude_locations": OptionSet + "exclude_locations": ExcludeLocations } if __name__ == "__main__": From cd1c38515b0e3475c7e3b032b75373deb5f8624d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 17 Nov 2021 16:58:43 +0100 Subject: [PATCH 26/62] WebHost: add remaining and collect to options page --- WebHostLib/api/generate.py | 9 +++--- WebHostLib/generate.py | 16 ++++++++-- WebHostLib/templates/generate.html | 50 ++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index b2caf4d7..2868a079 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -9,6 +9,7 @@ from pony.orm import commit from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR from WebHostLib.check import get_yaml_data, roll_options +from WebHostLib.generate import get_meta @api_endpoints.route('/generate', methods=['POST']) @@ -35,9 +36,6 @@ def generate_api(): if "race" in json_data: race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"])) - hint_cost = int(meta_options_source.get("hint_cost", 10)) - forfeit_mode = meta_options_source.get("forfeit_mode", "goal") - if not options: return {"text": "No options found. Expected file attachment or json weights." }, 400 @@ -45,7 +43,8 @@ def generate_api(): if len(options) > app.config["MAX_ROLL"]: return {"text": "Max size of multiworld exceeded", "detail": app.config["MAX_ROLL"]}, 409 - + meta = get_meta(meta_options_source) + meta["race"] = race results, gen_options = roll_options(options) if any(type(result) == str for result in results.values()): return {"text": str(results), @@ -54,7 +53,7 @@ def generate_api(): gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), # convert to json compatible - meta=json.dumps({"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}), state=STATE_QUEUED, + meta=json.dumps(meta), state=STATE_QUEUED, owner=session["_id"]) commit() return {"text": f"Generation of seed {gen.id} started successfully.", diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index ac16d7c9..c6e2d7d8 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -20,6 +20,16 @@ from .check import get_yaml_data, roll_options from .upload import upload_zip_to_db +def get_meta(options_source: dict) -> dict: + meta = { + "hint_cost": int(options_source.get("hint_cost", 10)), + "forfeit_mode": options_source.get("forfeit_mode", "goal"), + "remaining_mode": options_source.get("forfeit_mode", "disabled"), + "collect_mode": options_source.get("collect_mode", "disabled"), + } + return meta + + @app.route('/generate', methods=['GET', 'POST']) @app.route('/generate/', methods=['GET', 'POST']) def generate(race=False): @@ -35,9 +45,9 @@ def generate(race=False): else: results, gen_options = roll_options(options) # get form data -> server settings - hint_cost = int(request.form.get("hint_cost", 10)) - forfeit_mode = request.form.get("forfeit_mode", "goal") - meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode} + meta = get_meta(request.form) + meta["race"] = race + if race: meta["item_cheat"] = False meta["remaining"] = False diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 72592167..9004bc48 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -34,7 +34,14 @@ - + + + + + + + + + + + From cf20c0781fb4fc537eeca0a9a5ce98ff70ff1e6d Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Wed, 17 Nov 2021 10:26:18 -0600 Subject: [PATCH 27/62] OoT: fixed glitched not rolling set internal value of shuffle_interior_entrances to False instead of 'off' --- worlds/oot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index a97a6a0b..ebd532fc 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -147,7 +147,7 @@ class OOTWorld(World): # Incompatible option handling # ER and glitched logic are not compatible; glitched takes priority if self.logic_rules == 'glitched': - self.shuffle_interior_entrances = False + self.shuffle_interior_entrances = 'off' self.shuffle_grotto_entrances = False self.shuffle_dungeon_entrances = False self.shuffle_overworld_entrances = False From 9cb24280fabf3926b92af2f7c6704f4a623eb374 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 17 Nov 2021 22:46:32 +0100 Subject: [PATCH 28/62] Clients: log exception to logfile --- CommonClient.py | 4 ++-- FactorioClient.py | 2 +- SNIClient.py | 2 +- Utils.py | 18 +++++++++++++++++- kvui.py | 20 +------------------- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index bde3adb5..e8a742bb 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -11,7 +11,7 @@ import websockets import Utils if __name__ == "__main__": - Utils.init_logging("TextClient") + Utils.init_logging("TextClient", exception_logger="Client") from MultiServer import CommandProcessor from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission @@ -271,7 +271,7 @@ class CommonContext(): logger.info(f"DeathLink: Received from {data['source']}") async def send_death(self, death_text: str = ""): - logger.info("Sending death to your friends...") + logger.info("DeathLink: Sending death to your friends...") self.last_death_link = time.time() await self.send_msgs([{ "cmd": "Bounce", "tags": ["DeathLink"], diff --git a/FactorioClient.py b/FactorioClient.py index 5eb39035..1375f737 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -15,7 +15,7 @@ from queue import Queue import Utils if __name__ == "__main__": - Utils.init_logging("FactorioClient") + Utils.init_logging("FactorioClient", exception_logger="Client") from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \ get_base_parser diff --git a/SNIClient.py b/SNIClient.py index 6044f11a..1a251262 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -15,7 +15,7 @@ from json import loads, dumps from Utils import get_item_name_from_id, init_logging if __name__ == "__main__": - init_logging("SNIClient") + init_logging("SNIClient", exception_logger="Client") import colorama diff --git a/Utils.py b/Utils.py index b666eed0..84e4378f 100644 --- a/Utils.py +++ b/Utils.py @@ -427,7 +427,7 @@ loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': log def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w", - log_format: str = "[%(name)s]: %(message)s"): + log_format: str = "[%(name)s]: %(message)s", exception_logger: str = ""): loglevel: int = loglevel_mapping.get(loglevel, loglevel) log_folder = local_path("logs") os.makedirs(log_folder, exist_ok=True) @@ -446,3 +446,19 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri root_logger.addHandler( logging.StreamHandler(sys.stdout) ) + + # Relay unhandled exceptions to logger. + if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified + orig_hook = sys.excepthook + + def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + logging.getLogger(exception_logger).exception("Uncaught exception", + exc_info=(exc_type, exc_value, exc_traceback)) + return orig_hook(exc_type, exc_value, exc_traceback) + + handle_exception._wrapped = True + + sys.excepthook = handle_exception diff --git a/kvui.py b/kvui.py index 1a0071c9..da2923bb 100644 --- a/kvui.py +++ b/kvui.py @@ -208,26 +208,8 @@ class GameManager(App): self.commandprocessor("/help") Clock.schedule_interval(self.update_texts, 1 / 30) self.container.add_widget(self.grid) - self.catch_unhandled_exceptions() return self.container - def catch_unhandled_exceptions(self): - """Relay unhandled exceptions to UI logger.""" - if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified - orig_hook = sys.excepthook - - def handle_exception(exc_type, exc_value, exc_traceback): - if issubclass(exc_type, KeyboardInterrupt): - sys.__excepthook__(exc_type, exc_value, exc_traceback) - return - logging.getLogger("Client").exception("Uncaught exception", - exc_info=(exc_type, exc_value, exc_traceback)) - return orig_hook(exc_type, exc_value, exc_traceback) - - handle_exception._wrapped = True - - sys.excepthook = handle_exception - def update_texts(self, dt): if self.ctx.server: self.title = self.base_title + " " + Utils.__version__ + \ @@ -304,7 +286,7 @@ class TextManager(GameManager): class LogtoUI(logging.Handler): def __init__(self, on_log): - super(LogtoUI, self).__init__(logging.DEBUG) + super(LogtoUI, self).__init__(logging.INFO) self.on_log = on_log def handle(self, record: logging.LogRecord) -> None: From f7e89695e5dbb677b25fa1ea332370a390eab3e0 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 17 Nov 2021 23:38:30 -0800 Subject: [PATCH 29/62] Comment the defaults, with instructions to uncomment and change the values. --- webhost configuration sample.yaml | 38 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/webhost configuration sample.yaml b/webhost configuration sample.yaml index 61fdcc26..f007805b 100644 --- a/webhost configuration sample.yaml +++ b/webhost configuration sample.yaml @@ -1,50 +1,52 @@ -# This is a sample configuration for the Web host. If you wish to change any of these, rename this file to config.yaml +# This is a sample configuration for the Web host. +# If you wish to change any of these, rename this file to config.yaml +# Default values are shown here. Uncomment and change the values as desired. # TODO -SELFHOST: true +#SELFHOST: true # Maximum concurrent world gens -GENERATORS: 8 +#GENERATORS: 8 # TODO -SELFLAUNCH: true +#SELFLAUNCH: true # TODO -DEBUG: false +#DEBUG: false # Web hosting port -PORT: 80 +#PORT: 80 # Place where uploads go. -UPLOAD_FOLDER: uploads +#UPLOAD_FOLDER: uploads # Maximum upload size. Default is 64 megabyte (64 * 1024 * 1024) -MAX_CONTENT_LENGTH: 67108864 +#MAX_CONTENT_LENGTH: 67108864 # Secret key used to determine important things like cookie authentication of room/seed page ownership. # If you wish to deploy, uncomment the following line and set it to something not easily guessable. # SECRET_KEY: "Your secret key here" # TODO -JOB_THRESHOLD: 2 +#JOB_THRESHOLD: 2 # waitress uses one thread for I/O, these are for processing of view that get sent -WAITRESS_THREADS: 10 +#WAITRESS_THREADS: 10 # Database provider details: -PONY: - provider: "sqlite" - filename: "ap.db3" # This MUST be the ABSOLUTE PATH to the file. - create_db: true +#PONY: +# provider: "sqlite" +# filename: "ap.db3" # This MUST be the ABSOLUTE PATH to the file. +# create_db: true # Maximum number of players that are allowed to be rolled on the server. After this limit, one should roll locally and upload the results. -MAX_ROLL: 20 +#MAX_ROLL: 20 # TODO -CACHE_TYPE: "simple" +#CACHE_TYPE: "simple" # TODO -JSON_AS_ASCII: false +#JSON_AS_ASCII: false # Patch target. This is the address encoded into the patch that will be used for client auto-connect. -PATCH_TARGET: archipelago.gg \ No newline at end of file +#PATCH_TARGET: archipelago.gg \ No newline at end of file From 6b4445e1224915fcd18491d27a12800bf642ddaf Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 17 Nov 2021 23:39:21 -0800 Subject: [PATCH 30/62] move webhost configuration sample yaml to docs --- .../webhost configuration sample.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename webhost configuration sample.yaml => docs/webhost configuration sample.yaml (100%) diff --git a/webhost configuration sample.yaml b/docs/webhost configuration sample.yaml similarity index 100% rename from webhost configuration sample.yaml rename to docs/webhost configuration sample.yaml From 6e29101ecf9fd487bd831d475a32956c5f2da770 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 18 Nov 2021 18:54:17 +0100 Subject: [PATCH 31/62] Generate: remove duplicate .txt --- Generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index 99631165..1af56a97 100644 --- a/Generate.py +++ b/Generate.py @@ -126,7 +126,7 @@ def main(args=None, callback=ERmain): erargs.outputname = seed_name erargs.outputpath = args.outputpath - Utils.init_logging(f"Generate_{seed}.txt", loglevel=args.log_level) + Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) erargs.lttp_rom = args.lttp_rom erargs.sm_rom = args.sm_rom From c638a2cfb66017cf2a81d338b128e34bc5bcf4ce Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 18 Nov 2021 18:57:31 +0100 Subject: [PATCH 32/62] LttP: remove SM joke hint to reduce confusion --- worlds/alttp/Text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Text.py b/worlds/alttp/Text.py index 920913d7..2449f2a7 100644 --- a/worlds/alttp/Text.py +++ b/worlds/alttp/Text.py @@ -284,7 +284,7 @@ junk_texts = [ "{C:GREEN}\nThere’s always\nmoney in the\nBanana Stand>", "{C:GREEN}\n \nJust walk away\n >", "{C:GREEN}\neverybody is\nlooking for\nsomething >", - "{C:GREEN}\nSpring Ball\nare behind\nRidley >", + # "{C:GREEN}\nSpring Ball\nare behind\nRidley >", removed as people may assume it's a real hint "{C:GREEN}\nThe gnome asks\nyou to guess\nhis name. >", "{C:GREEN}\nI heard beans\non toast is a\ngreat meal. >", "{C:GREEN}\n> Sweetcorn\non pizza is a\ngreat choice.", From 7c3ba3bc42327f7c2f69d098022f5fbe36776776 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 19 Nov 2021 19:44:34 +0100 Subject: [PATCH 33/62] Factorio: fix cumulative advancement flagging --- worlds/__init__.py | 3 ++- worlds/factorio/__init__.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/worlds/__init__.py b/worlds/__init__.py index f193a9f8..4ab5b473 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -3,7 +3,8 @@ import os __all__ = {"lookup_any_item_id_to_name", "lookup_any_location_id_to_name", - "network_data_package"} + "network_data_package", + "AutoWorldRegister"} # import all submodules to trigger AutoWorldRegister for file in os.scandir(os.path.dirname(__file__)): diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 1834e4ff..437030c6 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -32,13 +32,17 @@ class Factorio(World): game: str = "Factorio" static_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes = {} - additional_advancement_technologies = set() + additional_advancement_technologies: set item_name_to_id = all_items location_name_to_id = base_tech_table data_version = 5 + def __init__(self, world, player: int): + super(Factorio, self).__init__(world, player) + self.additional_advancement_technologies = set() + def generate_basic(self): player = self.player want_progressives = collections.defaultdict(lambda: self.world.progressive[player]. @@ -189,7 +193,7 @@ class Factorio(World): fallback_pool = [] # fill all but one slot with random ingredients, last with a good match - while remaining_num_ingredients > 0 and len(pool) > 0: + while remaining_num_ingredients > 0 and pool: if remaining_num_ingredients == 1: max_raw = 1.1 * remaining_raw min_raw = 0.9 * remaining_raw @@ -226,7 +230,7 @@ class Factorio(World): # fill failed slots with whatever we got pool = fallback_pool - while remaining_num_ingredients > 0 and len(pool) > 0: + while remaining_num_ingredients > 0 and pool: ingredient = pool.pop() if ingredient not in recipes: logging.warning(f"missing recipe for {ingredient}") From 824b932961f9f01ee2a065296f41b5e8e27fcba1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 19 Nov 2021 21:25:01 +0100 Subject: [PATCH 34/62] Clients: copyable log labels --- data/client.kv | 8 ++++---- kvui.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/data/client.kv b/data/client.kv index 5b429e78..200ba024 100644 --- a/data/client.kv +++ b/data/client.kv @@ -1,9 +1,9 @@ tab_width: 200 -: +: canvas.before: Color: - rgba: 0.2, 0.2, 0.2, 1 + rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) Rectangle: size: self.size pos: self.pos @@ -13,10 +13,10 @@ font_size: dp(20) markup: True : - viewclass: 'Row' + viewclass: 'SelectableLabel' scroll_y: 0 effect_cls: "ScrollEffect" - RecycleBoxLayout: + SelectableRecycleBoxLayout: default_size: None, dp(20) default_size_hint: 1, None size_hint_y: None diff --git a/kvui.py b/kvui.py index da2923bb..08059fad 100644 --- a/kvui.py +++ b/kvui.py @@ -2,7 +2,6 @@ import os import logging import typing import asyncio -import sys os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" @@ -11,6 +10,7 @@ os.environ["KIVY_LOG_ENABLE"] = "0" from kivy.app import App from kivy.core.window import Window +from kivy.core.clipboard import Clipboard from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock from kivy.factory import Factory from kivy.properties import BooleanProperty, ObjectProperty @@ -25,6 +25,10 @@ from kivy.uix.label import Label from kivy.uix.progressbar import ProgressBar from kivy.utils import escape_markup from kivy.lang import Builder +from kivy.uix.recycleview.views import RecycleDataViewBehavior +from kivy.uix.behaviors import FocusBehavior +from kivy.uix.recycleboxlayout import RecycleBoxLayout +from kivy.uix.recycleview.layout import LayoutSelectionBehavior import Utils from NetUtils import JSONtoTextParser, JSONMessagePart @@ -140,6 +144,36 @@ class ContainerLayout(FloatLayout): pass +class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior, + RecycleBoxLayout): + """ Adds selection and focus behaviour to the view. """ + + +class SelectableLabel(RecycleDataViewBehavior, Label): + """ Add selection support to the Label """ + index = None + selected = BooleanProperty(False) + + def refresh_view_attrs(self, rv, index, data): + """ Catch and handle the view changes """ + self.index = index + return super(SelectableLabel, self).refresh_view_attrs( + rv, index, data) + + def on_touch_down(self, touch): + """ Add selection on touch down """ + if super(SelectableLabel, self).on_touch_down(touch): + return True + if self.collide_point(*touch.pos): + return self.parent.select_with_touch(self.index, touch) + + def apply_selection(self, rv, index, is_selected): + """ Respond to the selection of items in the view. """ + self.selected = is_selected + if is_selected: + Clipboard.copy(self.text) + + class GameManager(App): logging_pairs = [ ("Client", "Archipelago"), @@ -224,7 +258,11 @@ class GameManager(App): self.progressbar.value = 0 def command_button_action(self, button): - logging.getLogger("Client").info("/help for client commands and !help for server commands.") + if self.ctx.server: + logging.getLogger("Client").info("/help for client commands and !help for server commands.") + else: + logging.getLogger("Client").info("/help for client commands and once you are connected, " + "!help for server commands.") def connect_button_action(self, button): if self.ctx.server: From 80c3b8bbca22c68e9659130f0dea53e5ddd79a03 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 20 Nov 2021 04:47:19 +0100 Subject: [PATCH 35/62] Factorio: always build dynamic advancement flag --- worlds/factorio/Technologies.py | 14 ++------------ worlds/factorio/__init__.py | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index fbf283d3..9e7f88fd 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -59,8 +59,8 @@ class Technology(FactorioElement): # maybe make subclass of Location? def build_rule(self, player: int): logging.debug(f"Building rules for {self.name}") - return lambda state, technologies=technologies: all(state.has(f"Automated {ingredient}", player) - for ingredient in self.ingredients) + return lambda state: all(state.has(f"Automated {ingredient}", player) + for ingredient in self.ingredients) def get_prior_technologies(self) -> Set[Technology]: """Get Technologies that have to precede this one to resolve tree connections.""" @@ -300,12 +300,6 @@ for category_name, machine_name in machine_per_category.items(): required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset( recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) -advancement_technologies: Set[str] = set() -for ingredient_name in all_ingredient_names: - technologies = required_technologies[ingredient_name] - advancement_technologies |= {technology.name for technology in technologies} - - def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe) -> Set[str]: techs = set() if silo_recipe: @@ -335,8 +329,6 @@ rocket_recipes = { {"copper-cable": 10, "iron-plate": 10, "wood": 10} } -advancement_technologies |= {tech.name for tech in required_technologies["rocket-silo"]} - # progressive technologies # auto-progressive progressive_rows: Dict[str, Union[List[str], Tuple[str, ...]]] = {} @@ -430,8 +422,6 @@ for root in sorted_rows: unlocks=any(technology_table[tech].unlocks for tech in progressive)) progressive_tech_table[root] = progressive_technology.factorio_id progressive_technology_table[root] = progressive_technology - if any(tech in advancement_technologies for tech in progressive): - advancement_technologies.add(root) tech_to_progressive_lookup: Dict[str, str] = {} for technology in progressive_technology_table.values(): diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 437030c6..fb40f722 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,9 +1,10 @@ import collections +import typing from ..AutoWorld import World from BaseClasses import Region, Entrance, Location, Item -from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \ +from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, rocket_recipes, \ 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 @@ -32,7 +33,7 @@ class Factorio(World): game: str = "Factorio" static_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes = {} - additional_advancement_technologies: set + advancement_technologies: typing.Set[str] item_name_to_id = all_items location_name_to_id = base_tech_table @@ -41,7 +42,7 @@ class Factorio(World): def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) - self.additional_advancement_technologies = set() + self.advancement_technologies = set() def generate_basic(self): player = self.player @@ -268,8 +269,6 @@ class Factorio(World): {valid_pool[x]: 10 for x in range(3)}, original_rocket_part.products, original_rocket_part.energy)} - self.additional_advancement_technologies = {tech.name for tech in - self.custom_recipes["rocket-part"].recursive_unlocking_technologies} if self.world.recipe_ingredients[self.player]: valid_pool = [] @@ -282,8 +281,6 @@ class Factorio(World): for _ in original.ingredients: new_ingredients[valid_pool.pop()] = 1 new_recipe = Recipe(pack, original.category, new_ingredients, original.products, original.energy) - self.additional_advancement_technologies |= {tech.name for tech in - new_recipe.recursive_unlocking_technologies} self.custom_recipes[pack] = new_recipe if self.world.silo[self.player].value == Silo.option_randomize_recipe: @@ -292,21 +289,24 @@ class Factorio(World): valid_pool += sorted(science_pack_pools[pack]) new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool, factor=(self.world.max_science_pack[self.player].value + 1) / 7) - self.additional_advancement_technologies |= {tech.name for tech in - new_recipe.recursive_unlocking_technologies} self.custom_recipes["rocket-silo"] = new_recipe + needed_recipes = self.world.max_science_pack[self.player].get_allowed_packs() | {"rocket-silo", "rocket-part"} + + for recipe in needed_recipes: + recipe = self.custom_recipes.get(recipe, recipes[recipe]) + self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies} + # handle marking progressive techs as advancement prog_add = set() - for tech in self.additional_advancement_technologies: + for tech in self.advancement_technologies: if tech in tech_to_progressive_lookup: prog_add.add(tech_to_progressive_lookup[tech]) - self.additional_advancement_technologies |= prog_add + self.advancement_technologies |= prog_add def create_item(self, name: str) -> Item: if name in tech_table: - return FactorioItem(name, name in advancement_technologies or - name in self.additional_advancement_technologies, + return FactorioItem(name, name in self.advancement_technologies, tech_table[name], self.player) item = FactorioItem(name, False, all_items[name], self.player) From 3e40de72b2751413e39d73588c5f2f7b71e6cff5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 20 Nov 2021 17:37:08 +0100 Subject: [PATCH 36/62] WebHost: add random choice to options --- WebHostLib/options.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 46386569..c8a7b9a6 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -49,7 +49,7 @@ def create(): game_options = {} for option_name, option in world.options.items(): if option.options: - this_option = { + game_options[option_name] = this_option = { "type": "select", "displayName": option.displayname if hasattr(option, "displayname") else option_name, "description": option.__doc__ if option.__doc__ else "Please document me!", @@ -66,7 +66,10 @@ def create(): if sub_option_id == option.default: this_option["defaultValue"] = sub_option_name - game_options[option_name] = this_option + this_option["options"].append({ + "name": "Random", + "value": "random", + }) elif hasattr(option, "range_start") and hasattr(option, "range_end"): game_options[option_name] = { From a27d09f81a60230f686edd648ce2e13f8a11117e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Nov 2021 02:02:40 +0100 Subject: [PATCH 37/62] CommonClient: consolidate shutdown handling --- CommonClient.py | 41 ++++++++++++++++++++++------------------- FactorioClient.py | 9 +-------- SNIClient.py | 23 +++++++---------------- 3 files changed, 30 insertions(+), 43 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index e8a742bb..2e0b8c5b 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -39,13 +39,13 @@ class ClientCommandProcessor(CommandProcessor): def _cmd_connect(self, address: str = "") -> bool: """Connect to a MultiWorld Server""" self.ctx.server_address = None - asyncio.create_task(self.ctx.connect(address if address else None)) + asyncio.create_task(self.ctx.connect(address if address else None), name="connecting") return True def _cmd_disconnect(self) -> bool: """Disconnect from a MultiWorld Server""" self.ctx.server_address = None - asyncio.create_task(self.ctx.disconnect()) + asyncio.create_task(self.ctx.disconnect(), name="disconnecting") return True def _cmd_received(self) -> bool: @@ -89,10 +89,10 @@ class ClientCommandProcessor(CommandProcessor): else: state = ClientStatus.CLIENT_CONNECTED self.output("Unreadied.") - asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}])) + asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") def default(self, raw: str): - asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}])) + asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") class CommonContext(): @@ -149,7 +149,7 @@ class CommonContext(): self.set_getters(network_data_package) # execution - self.keep_alive_task = asyncio.create_task(keep_alive(self)) + self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy") @property def total_locations(self) -> typing.Optional[int]: @@ -236,7 +236,7 @@ class CommonContext(): async def connect(self, address=None): await self.disconnect() - self.server_task = asyncio.create_task(server_loop(self, address)) + self.server_task = asyncio.create_task(server_loop(self, address), name="server loop") def on_print(self, args: dict): logger.info(args["text"]) @@ -282,6 +282,18 @@ class CommonContext(): } }]) + async def shutdown(self): + self.server_address = None + if self.server and not self.server.socket.closed: + await self.server.socket.close() + if self.server_task: + await self.server_task + + while self.input_requests > 0: + self.input_queue.put_nowait(None) + self.input_requests -= 1 + self.keep_alive_task.cancel() + async def keep_alive(ctx: CommonContext, seconds_between_checks=100): """some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive) @@ -340,14 +352,14 @@ async def server_loop(ctx: CommonContext, address=None): await ctx.connection_closed() if ctx.server_address: logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s") - asyncio.create_task(server_autoreconnect(ctx)) + asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect") ctx.current_reconnect_delay *= 2 async def server_autoreconnect(ctx: CommonContext): await asyncio.sleep(ctx.current_reconnect_delay) if ctx.server_address and ctx.server_task is None: - ctx.server_task = asyncio.create_task(server_loop(ctx)) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") async def process_server_cmd(ctx: CommonContext, args: dict): @@ -555,7 +567,7 @@ if __name__ == '__main__': async def main(args): ctx = TextContext(args.connect, args.password) - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") if gui_enabled: input_task = None from kvui import TextManager @@ -566,16 +578,7 @@ if __name__ == '__main__': ui_task = None await ctx.exit_event.wait() - ctx.server_address = None - if ctx.server and not ctx.server.socket.closed: - await ctx.server.socket.close() - if ctx.server_task: - await ctx.server_task - - while ctx.input_requests > 0: - ctx.input_queue.put_nowait(None) - ctx.input_requests -= 1 - + await ctx.shutdown() if ui_task: await ui_task diff --git a/FactorioClient.py b/FactorioClient.py index 1375f737..096189e1 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -322,14 +322,7 @@ async def main(args): await progression_watcher await factorio_server_task - if ctx.server and not ctx.server.socket.closed: - await ctx.server.socket.close() - if ctx.server_task: - await ctx.server_task - - while ctx.input_requests > 0: - ctx.input_queue.put_nowait(None) - ctx.input_requests -= 1 + await ctx.shutdown() if ui_task: await ui_task diff --git a/SNIClient.py b/SNIClient.py index 1a251262..a49deae2 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -73,7 +73,7 @@ class LttPCommandProcessor(ClientCommandProcessor): pass self.ctx.snes_reconnect_address = None - asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number)) + asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number), name="SNES Connect") return True def _cmd_snes_close(self) -> bool: @@ -1113,28 +1113,19 @@ async def main(): input_task = asyncio.create_task(console_loop(ctx), name="Input") ui_task = None - snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address)) + snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address), name="SNES Connect") watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher") await ctx.exit_event.wait() - if snes_connect_task: - snes_connect_task.cancel() + ctx.server_address = None ctx.snes_reconnect_address = None - - await watcher_task - - if ctx.server and not ctx.server.socket.closed: - await ctx.server.socket.close() - if ctx.server_task: - await ctx.server_task - if ctx.snes_socket is not None and not ctx.snes_socket.closed: await ctx.snes_socket.close() - - while ctx.input_requests > 0: - ctx.input_queue.put_nowait(None) - ctx.input_requests -= 1 + if snes_connect_task: + snes_connect_task.cancel() + await watcher_task + await ctx.shutdown() if ui_task: await ui_task From 47c5c407efeac8f7f3d060c5868bcd0084b78f28 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Nov 2021 02:50:24 +0100 Subject: [PATCH 38/62] CommonClient: consolidate Connect packet sending --- CommonClient.py | 17 ++++++++++++----- FactorioClient.py | 10 +--------- SNIClient.py | 7 +------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 2e0b8c5b..78c17542 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -230,6 +230,17 @@ class CommonContext(): self.password = await self.console_input() return self.password + async def send_connect(self, **kwargs): + payload = { + "cmd": 'Connect', + 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, + 'tags': self.tags, + 'uuid': Utils.get_unique_identifier(), 'game': self.game + } + if kwargs: + payload.update(kwargs) + await self.send_msgs([payload]) + async def console_input(self): self.input_requests += 1 return await self.input_queue.get() @@ -554,11 +565,7 @@ if __name__ == '__main__': logger.info('Enter slot name:') self.auth = await self.console_input() - await self.send_msgs([{"cmd": 'Connect', - 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, - 'tags': self.tags, - 'uuid': Utils.get_unique_identifier(), 'game': self.game - }]) + await self.send_connect() def on_package(self, cmd: str, args: dict): if cmd == "Connected": diff --git a/FactorioClient.py b/FactorioClient.py index 096189e1..056dfc3f 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -72,15 +72,7 @@ class FactorioContext(CommonContext): raise Exception("Cannot connect to a server with unknown own identity, " "bridge to Factorio first.") - await self.send_msgs([{ - "cmd": 'Connect', - 'password': self.password, - 'name': self.auth, - 'version': Utils.version_tuple, - 'tags': self.tags, - 'uuid': Utils.get_unique_identifier(), - 'game': "Factorio" - }]) + await self.send_connect() def on_print(self, args: dict): super(FactorioContext, self).on_print(args) diff --git a/SNIClient.py b/SNIClient.py index a49deae2..121e3653 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -143,12 +143,7 @@ class Context(CommonContext): self.awaiting_rom = False self.auth = self.rom auth = base64.b64encode(self.rom).decode() - await self.send_msgs([{"cmd": 'Connect', - 'password': self.password, 'name': auth, 'version': Utils.version_tuple, - 'tags': self.tags, - 'uuid': Utils.get_unique_identifier(), - 'game': self.game - }]) + await self.send_connect(name=auth) def on_deathlink(self, data: dict): if not self.killing_player_task or self.killing_player_task.done(): From 4f00f5509f1d0348a99212e67d891ab9c135c9fd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Nov 2021 05:47:19 +0100 Subject: [PATCH 39/62] CommonClient: keep command input focus after enter and allow tabbing between inputs --- kvui.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/kvui.py b/kvui.py index 08059fad..f883b8d8 100644 --- a/kvui.py +++ b/kvui.py @@ -198,7 +198,8 @@ class GameManager(App): # top part server_label = ServerLabel() connect_layout.add_widget(server_label) - self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False) + self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False, + write_tab=False) self.server_connect_bar.bind(on_text_validate=self.connect_button_action) connect_layout.add_widget(self.server_connect_bar) self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None) @@ -235,8 +236,14 @@ class GameManager(App): info_button = Button(height=30, text="Command:", size_hint_x=None) info_button.bind(on_release=self.command_button_action) bottom_layout.add_widget(info_button) - textinput = TextInput(size_hint_y=None, height=30, multiline=False) + textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False) textinput.bind(on_text_validate=self.on_message) + + def text_focus(event): + """Needs to be set via delay, as unfocusing happens after on_message""" + textinput.focus = True + + textinput.text_focus = text_focus bottom_layout.add_widget(textinput) self.grid.add_widget(bottom_layout) self.commandprocessor("/help") @@ -289,6 +296,9 @@ class GameManager(App): self.ctx.input_queue.put_nowait(input_text) elif input_text: self.commandprocessor(input_text) + + Clock.schedule_once(textinput.text_focus) + except Exception as e: logging.getLogger("Client").exception(e) From 48ed394d02a3ffcdf7773aa9264e048f827604bb Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Thu, 14 Oct 2021 19:28:05 -0700 Subject: [PATCH 40/62] Require sending a satellite for victory in space-science-pack seeds. --- worlds/factorio/data/mod_template/control.lua | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 8c049e14..cf9fc1da 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -8,6 +8,7 @@ SLOT_NAME = "{{ slot_name }}" SEED_NAME = "{{ seed_name }}" FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }} TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100 +MAX_SCIENCE_PACK = {{ max_science_pack }} DEATH_LINK = {{ death_link | int }} CURRENTLY_DEATH_LOCK = 0 @@ -107,8 +108,19 @@ end script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) - global.forcedata[event.rocket.force.name]['victory'] = 1 - dumpInfo(event.rocket.force) + if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then + if event.rocket.get_item_count("satellite") > 0 or MAX_SCIENCE_PACK < 6 then + global.forcedata[event.rocket.force.name]['victory'] = 1 + dumpInfo(event.rocket.force) + game.set_game_state + { + game_finished = true, + player_won = true, + can_continue = true, + victorious_force = event.rocket.force + } + end + end end script.on_event(defines.events.on_rocket_launched, on_rocket_launched) @@ -198,6 +210,10 @@ script.on_init(function() e.player_index = index on_player_created(e) end + + if remote.interfaces["silo_script"] then + remote.call("silo_script", "set_no_victory", true) + end end) -- hook into researches done From affd7077172b79c7c0697d1b246f493af99dd6f7 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 20 Nov 2021 12:35:51 -0800 Subject: [PATCH 41/62] Add satellite recipe to needed_recipes if required. --- worlds/factorio/Technologies.py | 5 ++++- worlds/factorio/__init__.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 9e7f88fd..31b7ea12 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -300,13 +300,16 @@ for category_name, machine_name in machine_per_category.items(): required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset( recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) -def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe) -> Set[str]: +def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_recipe: Recipe) -> Set[str]: techs = set() if silo_recipe: for ingredient in silo_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) for ingredient in part_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) + if satellite_recipe: + for ingredient in satellite_recipe.ingredients: + techs |= recursively_get_unlocking_technologies(ingredient) return {tech.name for tech in techs} diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index fb40f722..7574396d 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -10,7 +10,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies from .Shapes import get_shapes from .Mod import generate_mod -from .Options import factorio_options, Silo, TechTreeInformation +from .Options import factorio_options, MaxSciencePack, Silo, TechTreeInformation import logging @@ -142,11 +142,13 @@ class Factorio(World): locations=locations: all(state.can_reach(loc) for loc in locations)) silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \ - else self.custom_recipes["rocket-silo"] \ - if "rocket-silo" in self.custom_recipes \ + else 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"] - victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe) + satellite_recipe = None if self.world.max_science_pack[self.player].value != MaxSciencePack.option_space_science_pack \ + else 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) world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) for technology in victory_tech_names) @@ -292,6 +294,8 @@ class Factorio(World): self.custom_recipes["rocket-silo"] = new_recipe needed_recipes = self.world.max_science_pack[self.player].get_allowed_packs() | {"rocket-silo", "rocket-part"} + if self.world.max_science_pack[self.player].value == MaxSciencePack.option_space_science_pack: + needed_recipes |= {"satellite"} for recipe in needed_recipes: recipe = self.custom_recipes.get(recipe, recipes[recipe]) From 1b4659276ce36ea43582b77fdd50bc69630ccbb6 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 20 Nov 2021 16:27:17 -0800 Subject: [PATCH 42/62] Add randomized recipe for Satellite. --- worlds/factorio/Mod.py | 3 +++ worlds/factorio/Options.py | 9 +++++++++ worlds/factorio/Technologies.py | 1 + worlds/factorio/__init__.py | 23 +++++++++++++++++------ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 484ed626..ef9893cb 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -107,6 +107,9 @@ def generate_mod(world, output_directory: str): if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: template_data["free_sample_blacklist"]["rocket-silo"] = 1 + + if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: + template_data["free_sample_blacklist"]["satellite"] = 1 control_code = control_template.render(**template_data) data_template_code = data_template.render(**template_data) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 0c9e3cd8..68858498 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -55,6 +55,14 @@ class Silo(Choice): default = 0 +class Satellite(Choice): + """Ingredients to craft satellite.""" + displayname = "Satellite" + options_vanilla = 0 + option_randomize_recipe = 1 + default = 0 + + class FreeSamples(Choice): """Get free items with your technologies.""" displayname = "Free Samples" @@ -289,6 +297,7 @@ factorio_options: typing.Dict[str, type(Option)] = { "tech_tree_layout": TechTreeLayout, "tech_cost": TechCost, "silo": Silo, + "satellite": Satellite, "free_samples": FreeSamples, "tech_tree_information": TechTreeInformation, "starting_items": FactorioStartItems, diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 31b7ea12..89f50b84 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -308,6 +308,7 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ for ingredient in part_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) if satellite_recipe: + techs |= satellite_recipe.unlocking_technologies for ingredient in satellite_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) return {tech.name for tech in techs} diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 7574396d..0c957180 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -10,7 +10,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies from .Shapes import get_shapes from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, TechTreeInformation +from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation import logging @@ -285,20 +285,31 @@ class Factorio(World): new_recipe = Recipe(pack, original.category, new_ingredients, original.products, original.energy) self.custom_recipes[pack] = new_recipe - if self.world.silo[self.player].value == Silo.option_randomize_recipe: + if self.world.silo[self.player].value == Silo.option_randomize_recipe \ + or self.world.satellite[self.player].value == Satellite.option_randomize_recipe: valid_pool = [] for pack in sorted(self.world.max_science_pack[self.player].get_allowed_packs()): valid_pool += sorted(science_pack_pools[pack]) - new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool, - factor=(self.world.max_science_pack[self.player].value + 1) / 7) - self.custom_recipes["rocket-silo"] = new_recipe - needed_recipes = self.world.max_science_pack[self.player].get_allowed_packs() | {"rocket-silo", "rocket-part"} + if self.world.silo[self.player].value == Silo.option_randomize_recipe: + new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool, + factor=(self.world.max_science_pack[self.player].value + 1) / 7) + self.custom_recipes["rocket-silo"] = new_recipe + + if self.world.satellite[self.player].value == Satellite.option_randomize_recipe: + new_recipe = self.make_balanced_recipe(recipes["satellite"], valid_pool, + factor=(self.world.max_science_pack[self.player].value + 1) / 7) + self.custom_recipes["satellite"] = new_recipe + + needed_recipes = self.world.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} + if self.world.silo[self.player] != Silo.option_spawn: + needed_recipes |= {"rocket-silo"} if self.world.max_science_pack[self.player].value == MaxSciencePack.option_space_science_pack: needed_recipes |= {"satellite"} for recipe in needed_recipes: recipe = self.custom_recipes.get(recipe, recipes[recipe]) + self.advancement_technologies |= {tech.name for tech in recipe.unlocking_technologies} self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies} # handle marking progressive techs as advancement From beb494904474b26508167dc16a2facc64289ffea Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 20 Nov 2021 20:20:38 -0800 Subject: [PATCH 43/62] typo whoops --- worlds/factorio/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 68858498..a3ef1162 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -58,7 +58,7 @@ class Silo(Choice): class Satellite(Choice): """Ingredients to craft satellite.""" displayname = "Satellite" - options_vanilla = 0 + option_vanilla = 0 option_randomize_recipe = 1 default = 0 From 3d19c390017dce523d0e919b702c6b92c2f280c3 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 21 Nov 2021 01:37:23 -0800 Subject: [PATCH 44/62] Include number of death_link connected clients in status. --- MultiServer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index 1874ae4c..05da2f65 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -591,10 +591,12 @@ def get_status_string(ctx: Context, team: int): text = "Player Status on your team:" for slot in ctx.locations: connected = len(ctx.clients[team][slot]) + death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags]) completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" + death_text = f" {death_link} of which are death link" if connected else "" goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ - f"{goal_text} {completion_text}" + f"{death_text}{goal_text} {completion_text}" return text From b205972e44f6cd17b35aef785435295ec54b8e4e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Nov 2021 17:50:12 +0100 Subject: [PATCH 45/62] GitHub Hooks: update python --- .github/workflows/lint.yml | 4 ++-- .github/workflows/unittests.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 70c725ef..d7cc3c74 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,10 +12,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index a4fe93e7..65c01e3b 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -12,10 +12,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip From 45a6598d18453fcab10c930664cffb5f271e9a84 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Nov 2021 18:09:06 +0100 Subject: [PATCH 46/62] Generate: return of the meta mystery --- Generate.py | 25 +++++++++++++------------ meta.yaml | 26 -------------------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/Generate.py b/Generate.py index 1af56a97..04ba3ad3 100644 --- a/Generate.py +++ b/Generate.py @@ -90,7 +90,8 @@ def main(args=None, callback=ERmain): except Exception as e: raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e meta_weights = weights_cache[args.meta_file_path] - print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights, 'No description specified')}") + print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}") + del(meta_weights["meta_description"]) if args.samesettings: raise Exception("Cannot mix --samesettings with --meta") else: @@ -139,17 +140,17 @@ def main(args=None, callback=ERmain): player_path_cache[player] = player_files.get(player, args.weights_file_path) if meta_weights: - for player, path in player_path_cache.items(): - weights_cache[path].setdefault("meta_ignore", []) - for key in meta_weights: - option = get_choice(key, meta_weights) - if option is not None: - for player, path in player_path_cache.items(): - players_meta = weights_cache[path].get("meta_ignore", []) - if key not in players_meta: - weights_cache[path][key] = option - elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]: - weights_cache[path][key] = option + for category_name, category_dict in meta_weights.items(): + for key in category_dict: + option = get_choice(key, category_dict) + if option is not None: + for player, path in player_path_cache.items(): + if category_name is None: + weights_cache[path][key] = option + elif category_name not in weights_cache[path]: + raise Exception(f"Meta: Category {category_name} is not present in {path}.") + else: + weights_cache[path][category_name][key] = option name_counter = Counter() erargs.player_settings = {} diff --git a/meta.yaml b/meta.yaml index 84159c5e..fabb57ef 100644 --- a/meta.yaml +++ b/meta.yaml @@ -4,12 +4,6 @@ # For example, if a meta.yaml fast_ganon result is rolled, every player will have that fast_ganon goal # There is the special case of null, which ignores that part of the meta.yaml, # allowing for a chance for that meta to not take effect -# Players can also have a meta_ignore option to ignore specific options -# Example of ignore that would be in a player's file: -# meta_ignore: -# mode: -# inverted -# This means, if mode is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead. meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience null: progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere" @@ -33,26 +27,6 @@ A Link to the Past: open: 60 inverted: 10 null: 10 # Maintain individual world states - tower_open: - '0': 8 - '1': 7 - '2': 6 - '3': 5 - '4': 4 - '5': 3 - '6': 2 - '7': 1 - random: 10 # A different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open - ganon_open: - '0': 3 - '1': 4 - '2': 5 - '3': 6 - '4': 7 - '5': 8 - '6': 9 - '7': 10 - random: 5 # This will mean differing completion times. But leaving it for that surprise effect triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces. extra: 0 # available = triforce_pieces_extra + triforce_pieces_required percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required From 50264993b092c5c9d1efe9d875db02793f2b1cc0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Nov 2021 18:11:51 +0100 Subject: [PATCH 47/62] MultiServer: allow null exclusions on GetDataPackage --- MultiServer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index 05da2f65..ce132e93 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1285,8 +1285,9 @@ 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", [])) + exclusions = args.get("exclusions", []) if exclusions: + exclusions = set(exclusions) games = {name: game_data for name, game_data in network_data_package["games"].items() if name not in exclusions} package = network_data_package.copy() From f4e34372be62ce8d9637493afda734131de1d268 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Nov 2021 23:45:15 +0100 Subject: [PATCH 48/62] Clients: remove color markup in clipboard copy --- kvui.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kvui.py b/kvui.py index f883b8d8..665466f1 100644 --- a/kvui.py +++ b/kvui.py @@ -11,6 +11,7 @@ os.environ["KIVY_LOG_ENABLE"] = "0" from kivy.app import App from kivy.core.window import Window from kivy.core.clipboard import Clipboard +from kivy.core.text.markup import MarkupLabel from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock from kivy.factory import Factory from kivy.properties import BooleanProperty, ObjectProperty @@ -171,7 +172,10 @@ class SelectableLabel(RecycleDataViewBehavior, Label): """ Respond to the selection of items in the view. """ self.selected = is_selected if is_selected: - Clipboard.copy(self.text) + # Not a fan of the following 2 lines, but they work. + temp = MarkupLabel(text=self.text).markup + text = "".join(part for part in temp if not part.startswith(("[color", "[/color]"))) + Clipboard.copy(text) class GameManager(App): From 415f045fd8e3e3541463659d21273eee6996496a Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 21 Nov 2021 18:24:25 -0800 Subject: [PATCH 49/62] Fix a range bug on min_energy in make_balanced_recipe --- worlds/factorio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 0c957180..8b16afb9 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -201,7 +201,7 @@ class Factorio(World): max_raw = 1.1 * remaining_raw min_raw = 0.9 * remaining_raw max_energy = 1.1 * remaining_energy - min_energy = 1.1 * remaining_energy + min_energy = 0.9 * remaining_energy else: max_raw = remaining_raw * 0.75 min_raw = (remaining_raw - max_raw) / remaining_num_ingredients From b10d9040dfbc1520da92f1a21593cb4d31d7e99b Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 21 Nov 2021 18:25:28 -0800 Subject: [PATCH 50/62] Fix "could not randomize recipe" when both silo and satellite are... ...randomized recipes. --- worlds/factorio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 8b16afb9..e502ddb5 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -292,7 +292,7 @@ class Factorio(World): valid_pool += sorted(science_pack_pools[pack]) if self.world.silo[self.player].value == Silo.option_randomize_recipe: - new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool, + new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool.copy(), factor=(self.world.max_science_pack[self.player].value + 1) / 7) self.custom_recipes["rocket-silo"] = new_recipe From e0d6503590975c637273d40860846184d07674b4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Nov 2021 17:44:14 +0100 Subject: [PATCH 51/62] Clients: allow accepting "Did you mean" by clicking on the question. --- kvui.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/kvui.py b/kvui.py index 665466f1..84ff1c1a 100644 --- a/kvui.py +++ b/kvui.py @@ -166,16 +166,23 @@ class SelectableLabel(RecycleDataViewBehavior, Label): if super(SelectableLabel, self).on_touch_down(touch): return True if self.collide_point(*touch.pos): - return self.parent.select_with_touch(self.index, touch) + if self.selected: + self.parent.clear_selection() + else: + # Not a fan of the following few lines, but they work. + temp = MarkupLabel(text=self.text).markup + text = "".join(part for part in temp if not part.startswith(("[color", "[/color]"))) + cmdinput = App.get_running_app().textinput + if not cmdinput.text and text.startswith("Didn't find something that closely matches, did you mean "): + name = Utils.get_text_between(text, "Didn't find something that closely matches, did you mean ", + "? (") + cmdinput.text = f"!hint {name}" + Clipboard.copy(text) + return self.parent.select_with_touch(self.index, touch) def apply_selection(self, rv, index, is_selected): """ Respond to the selection of items in the view. """ self.selected = is_selected - if is_selected: - # Not a fan of the following 2 lines, but they work. - temp = MarkupLabel(text=self.text).markup - text = "".join(part for part in temp if not part.startswith(("[color", "[/color]"))) - Clipboard.copy(text) class GameManager(App): @@ -240,15 +247,15 @@ class GameManager(App): info_button = Button(height=30, text="Command:", size_hint_x=None) info_button.bind(on_release=self.command_button_action) bottom_layout.add_widget(info_button) - textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False) - textinput.bind(on_text_validate=self.on_message) + self.textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False) + self.textinput.bind(on_text_validate=self.on_message) def text_focus(event): """Needs to be set via delay, as unfocusing happens after on_message""" - textinput.focus = True + self.textinput.focus = True - textinput.text_focus = text_focus - bottom_layout.add_widget(textinput) + self.textinput.text_focus = text_focus + bottom_layout.add_widget(self.textinput) self.grid.add_widget(bottom_layout) self.commandprocessor("/help") Clock.schedule_interval(self.update_texts, 1 / 30) From fbd5bfd382d8031430bbb62650f6570a6d8bb7e5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Nov 2021 17:57:23 +0100 Subject: [PATCH 52/62] WebHost: remove duplicate zfile read --- WebHostLib/upload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 8e461071..eef912c2 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -65,8 +65,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s MultiServer.Context._decompress(multidata) except: flash("Could not load multidata. File may be corrupted or incompatible.") - else: - multidata = zfile.open(file).read() + multidata = None + if multidata: flush() # commit slots seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta), From 26b336d6dba85e16024fd787339bad01f0a77b06 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Nov 2021 20:32:59 +0100 Subject: [PATCH 53/62] MultiServer: fix IncompatibleVersion not triggering --- MultiServer.py | 7 ++++--- SNIClient.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index ce132e93..88a4a4f0 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1244,6 +1244,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): game = ctx.games[slot] if "IgnoreGame" not in args["tags"] and args['game'] != game: errors.add('InvalidGame') + minver = ctx.minimum_client_versions[slot] + if minver > args['version']: + errors.add('IncompatibleVersion') # only exact version match allowed if ctx.compatibility == 0 and args['version'] != version_tuple: @@ -1259,9 +1262,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): client.auth = False # swapping Team/Slot client.team = team client.slot = slot - minver = ctx.minimum_client_versions[slot] - if minver > args['version']: - errors.add('IncompatibleVersion') + ctx.client_ids[client.team, client.slot] = args["uuid"] ctx.clients[team][slot].append(client) client.version = args['version'] diff --git a/SNIClient.py b/SNIClient.py index 121e3653..a4f9ba2c 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -889,10 +889,10 @@ async def game_watcher(ctx: Context): if not ctx.rom: ctx.finished_game = False - gameName = await snes_read(ctx, SM_ROMNAME_START, 2) - if gameName is None: + game_name = await snes_read(ctx, SM_ROMNAME_START, 2) + if game_name is None: continue - elif gameName == b"SM": + elif game_name == b"SM": ctx.game = GAME_SM else: ctx.game = GAME_ALTTP From 33c8d307edc30f08a9d2b77a1cdcc0fa16d6f781 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 23 Nov 2021 02:25:34 -0500 Subject: [PATCH 54/62] Update Factorio Setup tutorial --- WebHostLib/static/assets/tutorial/factorio/setup_en.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WebHostLib/static/assets/tutorial/factorio/setup_en.md b/WebHostLib/static/assets/tutorial/factorio/setup_en.md index cf148d1a..0586278f 100644 --- a/WebHostLib/static/assets/tutorial/factorio/setup_en.md +++ b/WebHostLib/static/assets/tutorial/factorio/setup_en.md @@ -107,6 +107,9 @@ Archipelago if you chose to include it during the installation process. 10. Enter `localhost` into the server address box 11. Click "Connect" +For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP +server, you can also issue the `!help` command to learn about additional commands like `!hint`. + ## Allowing Other People to Join Your Game 1. Ensure your Archipelago Client is running. 2. Ensure port `34197` is forwarded to the computer running the Archipelago Client. From 39ff471772d5507aec7e51a38f46cc19d776c8ab Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Nov 2021 19:10:26 +0100 Subject: [PATCH 55/62] Factorio: add new Recipe Time randomize options --- worlds/factorio/Mod.py | 9 ++++++++- worlds/factorio/Options.py | 14 ++++++++++++- .../data/mod_template/data-final-fixes.lua | 20 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index ef9893cb..b47d7ed4 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -47,6 +47,12 @@ recipe_time_scales = { Options.RecipeTime.option_vanilla: None } +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) +} + def generate_mod(world, output_directory: str): player = world.player multiworld = world.world @@ -95,7 +101,8 @@ def generate_mod(world, output_directory: str): "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[multiworld.recipe_time[player].value], + "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), "free_sample_blacklist": {item : 1 for item in free_sample_blacklist}, "progressive_technology_table": {tech.name : tech.progressive for tech in progressive_technology_table.values()}, diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index a3ef1162..abce73c5 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -99,13 +99,25 @@ class TechTreeInformation(Choice): class RecipeTime(Choice): - """randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.""" + """Randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc. + Fast: 0.25X - 1X + Normal: 0.5X - 2X + Slow: 1X - 4X + Chaos: 0.25X - 4X + New category: ignores vanilla recipe time and rolls new one + New Fast: 0.25 - 2 seconds + New Normal: 0.25 - 10 seconds + New Slow: 5 - 10 seconds + """ displayname = "Recipe Time" option_vanilla = 0 option_fast = 1 option_normal = 2 option_slow = 4 option_chaos = 5 + option_new_fast = 6 + option_new_normal = 7 + option_new_slow = 8 class Progressive(Choice): diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 4b2d67a2..be55d2dc 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -100,6 +100,20 @@ function adjust_energy(recipe_name, factor) end end +function set_energy(recipe_name, energy) + local recipe = data.raw.recipe[recipe_name] + + if (recipe.normal ~= nil) then + recipe.normal.energy_required = energy + end + if (recipe.expensive ~= nil) then + recipe.expensive.energy_required = energy + end + if (recipe.expensive == nil and recipe.normal == nil) then + recipe.energy_required = energy + end +end + data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes) @@ -144,6 +158,12 @@ data:extend{new_tree_copy} adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }}) {%- endif %} {%- endfor -%} +{% elif recipe_time_range %} +{%- for recipe_name, recipe in recipes.items() %} +{%- if recipe.category != "mining" %} +set_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_range) }}) +{%- endif %} +{%- endfor -%} {% endif %} {%- if silo==2 %} From c381df65634427f0343df74adc9b09621d856979 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Nov 2021 20:16:48 +0100 Subject: [PATCH 56/62] MultiServer: filter new locations via sets, instead of if and only echo new checks --- MultiServer.py | 22 +++++++++++----------- docs/network protocol.md | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 88a4a4f0..726a21f1 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -654,27 +654,27 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]): new_locations = set(locations) - ctx.location_checks[team, slot] + new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata if new_locations: ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) for location in new_locations: - if location in ctx.locations[slot]: - item_id, target_player = ctx.locations[slot][location] - new_item = NetworkItem(item_id, location, slot) - if target_player != slot or slot in ctx.remote_items: - get_received_items(ctx, team, target_player).append(new_item) + item_id, target_player = ctx.locations[slot][location] + new_item = NetworkItem(item_id, location, slot) + if target_player != slot or slot in ctx.remote_items: + get_received_items(ctx, team, target_player).append(new_item) - logging.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id), - ctx.player_names[(team, target_player)], get_location_name_from_id(location))) - info_text = json_format_send_event(new_item, target_player) - ctx.broadcast_team(team, [info_text]) + logging.info('(Team #%d) %s sent %s to %s (%s)' % ( + team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id), + ctx.player_names[(team, target_player)], get_location_name_from_id(location))) + info_text = json_format_send_event(new_item, target_player) + ctx.broadcast_team(team, [info_text]) ctx.location_checks[team, slot] |= new_locations send_new_items(ctx) ctx.broadcast(ctx.clients[team][slot], [{ "cmd": "RoomUpdate", "hint_points": get_slot_points(ctx, team, slot), - "checked_locations": locations, # duplicated data, but used for coop + "checked_locations": new_locations, # send back new checks only }]) ctx.save() diff --git a/docs/network protocol.md b/docs/network protocol.md index 42669af4..4673b5b7 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -140,7 +140,7 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring: | ---- | ---- | ----- | | hint_points | int | New argument. The client's current hint points. | | players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. | -| checked_locations | May be a partial update, containing new locations that were checked. | +| checked_locations | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. | | missing_locations | Should never be sent as an update, if needed is the inverse of checked_locations. | All arguments for this packet are optional, only changes are sent. From a3951c2621c61e99a0fe2b81b687f68cf0e03fa4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Nov 2021 20:17:42 +0100 Subject: [PATCH 57/62] Factorio: remove Desync detected message. To my knowledge it has never warned about an actual desync, and even it did, the code right behind it fixes the desync. --- worlds/factorio/data/mod_template/control.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index cf9fc1da..117fe7b9 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -458,9 +458,6 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi elseif force.technologies[item_name] ~= nil then tech = force.technologies[item_name] if tech ~= nil then - if global.index_sync[index] ~= nil and global.index_sync[index] ~= tech then - game.print("Warning: Desync Detected. Duplicate/Missing items may occur.") - end global.index_sync[index] = tech if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) From 0c27dbe7464ebd365b0d6f7b4ab0f525c0857471 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Nov 2021 21:47:23 +0100 Subject: [PATCH 58/62] CommonClient: add /items and /locations --- CommonClient.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index 78c17542..aaba1a41 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -81,6 +81,16 @@ class ClientCommandProcessor(CommandProcessor): self.output("No missing location checks found.") return True + def _cmd_items(self): + self.output(f"Item Names for {self.ctx.game}") + for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id: + self.output(item_name) + + def _cmd_locations(self): + self.output(f"Location Names for {self.ctx.game}") + for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id: + self.output(location_name) + def _cmd_ready(self): self.ctx.ready = not self.ctx.ready if self.ctx.ready: @@ -557,6 +567,7 @@ if __name__ == '__main__': class TextContext(CommonContext): tags = {"AP", "IgnoreGame"} + game = "Archipelago" async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: From 1f61d8322cc619888b044e48fda861aa0d8a3bfd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Nov 2021 22:47:41 +0100 Subject: [PATCH 59/62] LttP: Attribute locations to dark/light world if they are directly present in them, ignoring routing requirements. --- Main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index 671e56de..3fed538d 100644 --- a/Main.py +++ b/Main.py @@ -197,8 +197,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: er_hint_data[region.player][location.address] = main_entrance.name - - checks_in_area = {player: {area: list() for area in ordered_areas} for player in range(1, world.players + 1)} @@ -215,6 +213,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No 'Inverted Ganons Tower': 'Ganons Tower'} \ .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) checks_in_area[location.player][dungeonname].append(location.address) + elif location.parent_region.type == RegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif location.parent_region.type == RegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) elif main_entrance.parent_region.type == RegionType.LightWorld: checks_in_area[location.player]["Light World"].append(location.address) elif main_entrance.parent_region.type == RegionType.DarkWorld: From 1f5d1532e31537dd79663dca2fd271e22971b8be Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 24 Nov 2021 01:38:58 -0800 Subject: [PATCH 60/62] Move Death Link change tag to Common Client --- CommonClient.py | 9 +++++++++ SNIClient.py | 9 +-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index aaba1a41..c4202491 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -315,6 +315,15 @@ class CommonContext(): self.input_requests -= 1 self.keep_alive_task.cancel() + async def update_death_link(self, death_link): + old_tags = self.tags.copy() + if death_link: + self.tags.add("DeathLink") + else: + self.tags -= {"DeathLink"} + if old_tags != self.tags and self.server and not self.server.socket.closed: + await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) + async def keep_alive(ctx: CommonContext, seconds_between_checks=100): """some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive) diff --git a/SNIClient.py b/SNIClient.py index a4f9ba2c..f5d9fba2 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -905,14 +905,7 @@ async def game_watcher(ctx: Context): death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else SM_DEATH_LINK_ACTIVE_ADDR, 1) if death_link: - death_link = bool(death_link[0] & 0b1) - old_tags = ctx.tags.copy() - if death_link: - ctx.tags.add("DeathLink") - else: - ctx.tags -= {"DeathLink"} - if old_tags != ctx.tags and ctx.server and not ctx.server.socket.closed: - await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}]) + await ctx.update_death_link(bool(death_link[0] & 0b1)) if not ctx.prev_rom or ctx.prev_rom != ctx.rom: ctx.locations_checked = set() ctx.locations_scouted = set() From 9dbafd3b4b5946b3d4b6b237e08fd27249165ab0 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 24 Nov 2021 01:55:36 -0800 Subject: [PATCH 61/62] Factorio can now change death link state at runtime. --- FactorioClient.py | 23 ++++---- worlds/factorio/Mod.py | 8 ++- worlds/factorio/data/mod_template/control.lua | 55 ++++++++++++++----- .../data/mod_template/locale/en/locale.cfg | 8 ++- .../factorio/data/mod_template/settings.lua | 12 ++++ 5 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 worlds/factorio/data/mod_template/settings.lua diff --git a/FactorioClient.py b/FactorioClient.py index 056dfc3f..292d926e 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -65,12 +65,11 @@ class FactorioContext(CommonContext): if password_requested and not self.password: await super(FactorioContext, self).server_auth(password_requested) - if not self.auth: - if self.rcon_client: - get_info(self, self.rcon_client) # retrieve current auth code - else: - raise Exception("Cannot connect to a server with unknown own identity, " - "bridge to Factorio first.") + if self.rcon_client: + await get_info(self, self.rcon_client) # retrieve current auth code + else: + raise Exception("Cannot connect to a server with unknown own identity, " + "bridge to Factorio first.") await self.send_connect() @@ -126,6 +125,8 @@ async def game_watcher(ctx: FactorioContext): research_data = data["research_done"] research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} victory = data["victory"] + if "death_link" in data: # TODO: Remove this if statement around version 0.2.4 or so + await ctx.update_death_link(data["death_link"]) if not ctx.finished_game and victory: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) @@ -140,7 +141,8 @@ async def game_watcher(ctx: FactorioContext): death_link_tick = data.get("death_link_tick", 0) if death_link_tick != ctx.death_link_tick: ctx.death_link_tick = death_link_tick - await ctx.send_death() + if "DeathLink" in ctx.tags: + await ctx.send_death() await asyncio.sleep(0.1) @@ -226,14 +228,13 @@ async def factorio_server_watcher(ctx: FactorioContext): factorio_process.wait(5) -def get_info(ctx, rcon_client): +async 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") + await ctx.update_death_link(death_link) async def factorio_spinup_server(ctx: FactorioContext) -> bool: @@ -272,7 +273,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool: rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) if ctx.mod_version == ctx.__class__.mod_version: raise Exception("No Archipelago mod was loaded. Aborting.") - get_info(ctx, rcon_client) + await get_info(ctx, rcon_client) await asyncio.sleep(0.01) except Exception as e: diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index b47d7ed4..3a5dc569 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -56,7 +56,7 @@ recipe_time_ranges = { def generate_mod(world, output_directory: str): player = world.player multiworld = world.world - global data_final_template, locale_template, control_template, data_template + global data_final_template, locale_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template") @@ -66,6 +66,7 @@ def generate_mod(world, output_directory: str): data_final_template = template_env.get_template("data-final-fixes.lua") locale_template = template_env.get_template(r"locale/en/locale.cfg") control_template = template_env.get_template("control.lua") + settings_template = template_env.get_template("settings.lua") # get data for templates player_names = {x: multiworld.player_name[x] for x in multiworld.player_ids} locations = [] @@ -97,7 +98,7 @@ def generate_mod(world, output_directory: str): "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_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, @@ -121,6 +122,7 @@ def generate_mod(world, output_directory: str): control_code = control_template.render(**template_data) data_template_code = data_template.render(**template_data) data_final_fixes_code = data_final_template.render(**template_data) + settings_code = settings_template.render(**template_data) mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) en_locale_dir = os.path.join(mod_dir, "locale", "en") @@ -132,6 +134,8 @@ def generate_mod(world, output_directory: str): f.write(data_final_fixes_code) with open(os.path.join(mod_dir, "control.lua"), "wt") as f: f.write(control_code) + with open(os.path.join(mod_dir, "settings.lua"), "wt") as f: + f.write(settings_code) locale_content = locale_template.render(**template_data) with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f: f.write(locale_content) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 117fe7b9..d53a0b9c 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -9,7 +9,13 @@ SEED_NAME = "{{ seed_name }}" FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }} TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100 MAX_SCIENCE_PACK = {{ max_science_pack }} -DEATH_LINK = {{ death_link | int }} +ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}" + +if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then + DEATH_LINK = 1 +else + DEATH_LINK = 0 +end CURRENTLY_DEATH_LOCK = 0 @@ -77,6 +83,27 @@ function on_force_destroyed(event) global.forcedata[event.force.name] = nil end +function on_runtime_mod_setting_changed(event) + local force + if event.player_index == nil then + force = game.forces.player + else + force = game.players[event.player_index].force + end + + if event.setting == ARCHIPELAGO_DEATH_LINK_SETTING then + if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then + DEATH_LINK = 1 + else + DEATH_LINK = 0 + end + if force ~= nil then + dumpInfo(force) + end + end +end +script.on_event(defines.events.on_runtime_mod_setting_changed, on_runtime_mod_setting_changed) + -- Initialize player data, either from them joining the game or them already being part of the game when the mod was -- added.` function on_player_created(event) @@ -382,18 +409,19 @@ 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 +script.on_event(defines.events.on_entity_died, function(event) + if DEATH_LINK == 0 then + return + end + 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 + 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"}}) -- add / commands @@ -408,7 +436,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") + ["death_link_tick"] = chain_lookup(global, "forcedata", force.name, "death_link_tick"), + ["death_link"] = DEATH_LINK } for tech_name, tech in pairs(force.technologies) do diff --git a/worlds/factorio/data/mod_template/locale/en/locale.cfg b/worlds/factorio/data/mod_template/locale/en/locale.cfg index 25e9eb23..e970dbfa 100644 --- a/worlds/factorio/data/mod_template/locale/en/locale.cfg +++ b/worlds/factorio/data/mod_template/locale/en/locale.cfg @@ -22,4 +22,10 @@ ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends somet {%- else %} ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}". {%- endif -%} -{% endfor %} \ No newline at end of file +{% endfor %} + +[mod-setting-name] +archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link + +[mod-setting-description] +archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die. \ No newline at end of file diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua new file mode 100644 index 00000000..7703ebe2 --- /dev/null +++ b/worlds/factorio/data/mod_template/settings.lua @@ -0,0 +1,12 @@ +data:extend({ + { + type = "bool-setting", + name = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}", + setting_type = "runtime-global", + {% if death_link %} + default_value = true + {% else %} + default_value = false + {% endif %} + } +}) \ No newline at end of file From 5a03c0edd65017f1fd4e591da538e17b779d73a7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 24 Nov 2021 23:49:00 +0100 Subject: [PATCH 62/62] WebHost: remove /hosted redirect, all current rooms should be migrated. --- WebHostLib/__init__.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index cfce8b05..b9a99569 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -141,7 +141,7 @@ def new_room(seed: UUID): abort(404) room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) commit() - return redirect(url_for("hostRoom", room=room.id)) + return redirect(url_for("host_room", room=room.id)) def _read_log(path: str): @@ -159,7 +159,7 @@ def display_log(room: UUID): @app.route('/room/', methods=['GET', 'POST']) -def hostRoom(room: UUID): +def host_room(room: UUID): room = Room.get(id=room) if room is None: return abort(404) @@ -175,20 +175,17 @@ def hostRoom(room: UUID): return render_template("hostRoom.html", room=room) -@app.route('/hosted/', methods=['GET', 'POST']) -def hostRoomRedirect(room: UUID): - return redirect(url_for("hostRoom", room=room)) - - @app.route('/favicon.ico') def favicon(): return send_from_directory(os.path.join(app.root_path, 'static/static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') + @app.route('/discord') def discord(): return redirect("https://discord.gg/archipelago") + from WebHostLib.customserver import run_server_process from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
    + + (?) + +
    + + (?) + + + +
    + + (?) + + + +
    (?)