From 73afab67c802b9c5085258254ed1bc07ce283dba Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 22:21:06 +0200 Subject: [PATCH 01/16] LttP: fix deprecated use of isSet() (#831) --- LttPAdjuster.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 3368620e6c..3de6e3b13a 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -289,7 +289,7 @@ def run_sprite_update(): else: top.withdraw() task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set()) - while not done.isSet(): + while not done.is_set(): task.do_events() logging.info("Done updating sprites") @@ -300,6 +300,7 @@ def update_sprites(task, on_finish=None): sprite_dir = user_path("data", "sprites", "alttpr") os.makedirs(sprite_dir, exist_ok=True) ctx = get_cert_none_ssl_context() + def finished(): task.close_window() if on_finish: From 489450d3fabe3ca639199d865ad26b8f35f64e7b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 26 Jul 2022 16:44:32 +0200 Subject: [PATCH 02/16] SNIClient: fix program not exiting if SNI does not exist nor is running --- SNIClient.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 072d04cc99..aad231691b 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -188,7 +188,10 @@ class Context(CommonContext): async def shutdown(self): await super(Context, self).shutdown() if self.snes_connect_task: - await self.snes_connect_task + try: + await asyncio.wait_for(self.snes_connect_task, 1) + except asyncio.TimeoutError: + self.snes_connect_task.cancel() def on_package(self, cmd: str, args: dict): if cmd in {"Connected", "RoomUpdate"}: @@ -598,7 +601,7 @@ class SNESState(enum.IntEnum): SNES_ATTACHED = 3 -def launch_sni(ctx: Context): +def launch_sni(): sni_path = Utils.get_options()["lttp_options"]["sni"] if not os.path.isdir(sni_path): @@ -636,11 +639,9 @@ async def _snes_connect(ctx: Context, address: str): address = f"ws://{address}" if "://" not in address else address snes_logger.info("Connecting to SNI at %s ..." % address) seen_problems = set() - succesful = False - while not succesful: + while 1: try: snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None) - succesful = True except Exception as e: problem = "%s" % e # only tell the user about new problems, otherwise silently lay in wait for a working connection @@ -650,7 +651,7 @@ async def _snes_connect(ctx: Context, address: str): if len(seen_problems) == 1: # this is the first problem. Let's try launching SNI if it isn't already running - launch_sni(ctx) + launch_sni() await asyncio.sleep(1) else: From e5b868e0e974ec480e61ed547cc2af8cdf2154cd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 23:09:40 +0200 Subject: [PATCH 03/16] WebHost: fix 30 days cutoff for stats (#826) --- WebHostLib/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 9a164d02cb..131f807d2d 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -17,7 +17,7 @@ from .models import Room def get_db_data(): games_played = defaultdict(Counter) total_games = Counter() - cutoff = date.today()-timedelta(days=30000) + cutoff = date.today()-timedelta(days=30) room: Room for room in select(room for room in Room if room.creation_time >= cutoff): for slot in room.seed.slots: From 4565b3af8dc8bccefc417bdc9724190db8495b73 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 26 Jul 2022 19:25:49 +0200 Subject: [PATCH 04/16] DKC3: fix missing default options in Utils.py --- Utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index e13a5fe773..423bb3b469 100644 --- a/Utils.py +++ b/Utils.py @@ -277,7 +277,12 @@ def get_default_options() -> dict: }, "oot_options": { "rom_file": "The Legend of Zelda - Ocarina of Time.z64", - } + }, + "dkc3_options": { + "rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc", + "sni": "SNI", + "rom_start": True, + }, } return options From e849e4792dda4d97e657a8b542149d0eb1fc32d1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 23:36:20 +0200 Subject: [PATCH 05/16] WebHost: games played per day plot per game on stats page (#827) * WebHost: generate stats page palette for maximum hue difference between neighbours. * WebHost: add per game played stats --- WebHostLib/stats.py | 74 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/WebHostLib/stats.py b/WebHostLib/stats.py index 131f807d2d..a647be5ee5 100644 --- a/WebHostLib/stats.py +++ b/WebHostLib/stats.py @@ -1,20 +1,24 @@ from collections import Counter, defaultdict -from itertools import cycle +from colorsys import hsv_to_rgb from datetime import datetime, timedelta, date from math import tau +import typing from bokeh.embed import components -from bokeh.palettes import Dark2_8 as palette +from bokeh.models import HoverTool from bokeh.plotting import figure, ColumnDataSource from bokeh.resources import INLINE +from bokeh.colors import RGB from flask import render_template from pony.orm import select from . import app, cache from .models import Room +PLOT_WIDTH = 600 -def get_db_data(): + +def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]: games_played = defaultdict(Counter) total_games = Counter() cutoff = date.today()-timedelta(days=30) @@ -26,29 +30,72 @@ def get_db_data(): return total_games, games_played +def get_color_palette(colors_needed: int) -> typing.List[RGB]: + colors = [] + # colors_needed +1 to prevent first and last color being too close to each other + colors_needed += 1 + + for x in range(0, 361, 360 // colors_needed): + # a bit of noise on value to add some luminosity difference + colors.append(RGB(*(val * 255 for val in hsv_to_rgb(x / 360, 0.8, 0.8 + (x / 1800))))) + + # splice colors for maximum hue contrast. + colors = colors[::2] + colors[1::2] + + return colors + + +def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.Dict[str, int]], + game: str, color: RGB) -> figure: + occurences = [] + days = [day for day, game_data in all_games_data.items() if game_data[game]] + for day in days: + occurences.append(all_games_data[day][game]) + data = { + "days": [datetime.combine(day, datetime.min.time()) for day in days], + "played": occurences + } + + plot = figure( + title=f"{game} Played Per Day", x_axis_type='datetime', x_axis_label="Date", + y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500, + toolbar_location=None, tools="", + # setting legend to False seems broken in bokeh currently? + # legend=False + ) + + hover = HoverTool(tooltips=[("Date:", "@days{%F}"), ("Played:", "@played")], formatters={"@days": "datetime"}) + plot.add_tools(hover) + plot.vbar(x="days", top="played", legend_label=game, color=color, source=ColumnDataSource(data=data), width=1) + return plot + + @app.route('/stats') -@cache.memoize(timeout=60*60) # regen once per hour should be plenty +@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty def stats(): plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date", - y_axis_label="Games Played", sizing_mode="scale_both", width=500, height=500) + y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500) total_games, games_played = get_db_data() days = sorted(games_played) - cyc_palette = cycle(palette) + color_palette = get_color_palette(len(total_games)) + game_to_color: typing.Dict[str, RGB] = {game: color for game, color in zip(total_games, color_palette)} for game in sorted(total_games): occurences = [] for day in days: occurences.append(games_played[day][game]) plot.line([datetime.combine(day, datetime.min.time()) for day in days], - occurences, legend_label=game, line_width=2, color=next(cyc_palette)) + occurences, legend_label=game, line_width=2, color=game_to_color[game]) total = sum(total_games.values()) pie = figure(plot_height=350, title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None, tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")], - sizing_mode="scale_both", width=500, height=500) + sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2)) pie.axis.visible = False + pie.xgrid.visible = False + pie.ygrid.visible = False data = { "games": [], @@ -65,12 +112,15 @@ def stats(): current_angle += angle data["end_angles"].append(current_angle) - data["colors"] = [element[1] for element in sorted((game, color) for game, color in - zip(data["games"], cycle(palette)))] - pie.wedge(x=0.5, y=0.5, radius=0.5, + data["colors"] = [game_to_color[game] for game in data["games"]] + + pie.wedge(x=0, y=0, radius=0.5, start_angle="start_angles", end_angle="end_angles", fill_color="colors", source=ColumnDataSource(data=data), legend_field="games") - script, charts = components((plot, pie)) + per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in total_games + if total_games[game] > 1] + + script, charts = components((plot, pie, *per_game_charts)) return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(), chart_data=script, charts=charts) From 7d9203ef84699955defab95fa4a65502edcf4636 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 28 Jul 2022 00:05:47 +0200 Subject: [PATCH 06/16] CI: update SNI to 0.0.82 --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb9fda69b4..4138f93f04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: python-version: '3.8' - name: Download run-time dependencies run: | - Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-windows-amd64.zip -OutFile sni.zip + Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip Expand-Archive -Path sni.zip -DestinationPath SNI -Force Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force @@ -63,7 +63,7 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e5095cff2..aa82883ff1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI From c02f355479e85acaa2b9e85e19a52f67637c533d Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Jul 2022 06:04:48 -0400 Subject: [PATCH 07/16] Smz3 no progression gt fix (#818) --- worlds/smz3/__init__.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index e440eab2c9..77168d5674 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -10,6 +10,7 @@ from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassi from worlds.generic.Rules import set_rule import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World +from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray @@ -68,6 +69,8 @@ class SMZ3World(World): remote_items: bool = False remote_start_inventory: bool = False + locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations} + # first added for 0.2.6 required_client_version = (0, 2, 6) @@ -364,6 +367,7 @@ class SMZ3World(World): item.item.Progression = False item.location.event = False self.unreachable.append(item.location) + self.JunkFillGT() def get_pre_fill_items(self): if (not self.smz3World.Config.Keysanity): @@ -377,6 +381,23 @@ class SMZ3World(World): def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) + def JunkFillGT(self): + for loc in self.locations.values(): + if loc.name in self.locationNamesGT and loc.item is None: + poolLength = len(self.world.itempool) + # start looking at a random starting index and loop at start if no match found + for i in range(self.world.random.randint(0, poolLength), poolLength): + if not self.world.itempool[i].advancement: + itemFromPool = self.world.itempool.pop(i) + break + else: + for i in range(0, poolLength): + if not self.world.itempool[i].advancement: + itemFromPool = self.world.itempool.pop(i) + break + self.world.push_item(loc, itemFromPool, False) + loc.event = False + def FillItemAtLocation(self, itemPool, itemType, location): itemToPlace = TotalSMZ3Item.Item.Get(itemPool, itemType, self.smz3World) if (itemToPlace == None): @@ -401,7 +422,7 @@ class SMZ3World(World): raise Exception(f"Tried to front fill {item.Name} in, but no location was available") location.Item = item - itemFromPool = next((i for i in self.world.itempool if i.player == self.player and i.name == item.Type.name), None) + itemFromPool = next((i for i in self.world.itempool if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None) if itemFromPool is not None: self.world.get_location(location.Name, self.player).place_locked_item(itemFromPool) self.world.itempool.remove(itemFromPool) From fd6a0b547fc0d9925a8cd16ececd44e16b69835f Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 28 Jul 2022 23:43:35 +0200 Subject: [PATCH 08/16] Witness: Fatal logic bug fix (#837) * Renamed some event items * Fatal logic bug: Door panels did not check their symbol items --- worlds/witness/player_logic.py | 6 +++--- worlds/witness/rules.py | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index eb57f2c6a0..3639e836df 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -319,9 +319,9 @@ class WitnessPlayerLogic: "0x00037": "Monastery Branch Panels Activate", "0x0A079": "Access to Bunker Laser", "0x0A3B5": "Door to Tutorial Discard Opens", - "0x00139": "Keep Hedges 2 Turns On", - "0x019DC": "Keep Hedges 3 Turns On", - "0x019E7": "Keep Hedges 4 Turns On", + "0x00139": "Keep Hedges 1 Knowledge", + "0x019DC": "Keep Hedges 2 Knowledge", + "0x019E7": "Keep Hedges 3 Knowledge", "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Mill Requirement Met", diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index cd1fae1235..2b9888b361 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -116,12 +116,7 @@ class WitnessLogic(LogicMixin): valid_option = True for panel in option: - if panel in player_logic.DOOR_ITEMS_BY_ID: - if all({not self.has(item, player) for item in player_logic.DOOR_ITEMS_BY_ID[panel]}): - valid_option = False - break - - elif not self._witness_can_solve_panel(panel, world, player, player_logic, locat): + if not self._witness_can_solve_panel(panel, world, player, player_logic, locat): valid_option = False break From 9acaf1c27938cba28d46530cc70a13318afc7577 Mon Sep 17 00:00:00 2001 From: Jarno Date: Fri, 29 Jul 2022 01:11:52 +0200 Subject: [PATCH 09/16] [Docs] Further explained the mythical `InvalidPacket` (#828) * [Docs] Further explained the mythical `InvalidPacket` * Fixed header category * Update docs/network protocol.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update docs/network protocol.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Hussein Farran Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Hussein Farran --- docs/network protocol.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index b5553c372c..05a58d0598 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -191,8 +191,23 @@ Sent to clients after a client requested this message be sent to them, more info ### InvalidPacket Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for. +#### Arguments +| Name | Type | Notes | +| ---- | ---- | ----- | +| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. | +| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. | +| text | str | A descriptive message of the problem at hand. | + +##### PacketProblemType +`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future. + +| Type | Notes | +| ---- | ----- | +| cmd | `cmd` argument of the faulty packet that could not be parsed correctly. | +| arguments | Arguments of the faulty packet which were not correct. | + ### Retrieved -Sent to clients as a response the a [Get](#Get) package +Sent to clients as a response the a [Get](#Get) package. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | From f3d966897f3bf4111bd548fc562d9ea3004a4027 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu, 28 Jul 2022 19:13:00 -0400 Subject: [PATCH 10/16] Prevent Krematoa Crash (#832) * Prevent Krematoa Crash, add crash robustness * Remove print statements * Don't remove ctx.rom if save file dies * Consolidate logic for readability --- worlds/dkc3/Client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 12643a5fcf..8ac20131f3 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -77,9 +77,9 @@ async def dkc3_game_watcher(ctx: Context): # DKC3_TODO: Handle non-included checks new_checks.append(loc_id) - save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) - if save_file_name is None or save_file_name[0] == 0x00: - # We have somehow exited the save file + verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5) + if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name: + # We have somehow exited the save file (or worse) return rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE) @@ -116,17 +116,18 @@ async def dkc3_game_watcher(ctx: Context): # Handle Coin Displays current_level = await snes_read(ctx, WRAM_START + 0x5E3, 0x5) - if item.item == 0xDC3002 and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03): + overworld_locked = ((await snes_read(ctx, WRAM_START + 0x5FC, 0x1))[0] == 0x01) + if item.item == 0xDC3002 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03): # Bazaar and Barter item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) new_item_count = item_count[0] + 1 snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count])) - elif item.item == 0xDC3002 and current_level[0] == 0x04: + elif item.item == 0xDC3002 and not overworld_locked and current_level[0] == 0x04: # Swanky item_count = await snes_read(ctx, WRAM_START + 0xA26, 0x1) new_item_count = item_count[0] + 1 snes_buffered_write(ctx, WRAM_START + 0xA26, bytes([new_item_count])) - elif item.item == 0xDC3003 and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01): + elif item.item == 0xDC3003 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01): # Boomer item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1) new_item_count = item_count[0] + 1 From d817fdcfdb65138ec589d4c2627a98f9aa1fe397 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 29 Jul 2022 01:18:59 +0200 Subject: [PATCH 11/16] Doc: move Running from source from wiki to docs (#797) * Doc: move "Running from source" from wiki to docs/ * Doc: update links and reformat running from source * Doc: implement suggestions in "Running from source" thanks @alwaysintreble * Doc: update link to "Running from source" also link docs/ folder * Doc: Running from source: Apply suggestions from code review Co-authored-by: KonoTyran Co-authored-by: KonoTyran --- README.md | 4 +-- docs/running from source.md | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 docs/running from source.md diff --git a/README.md b/README.md index a3a06d480b..9403159c74 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt ## Running Archipelago For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only. -If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source). +If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md). ## Related Repositories This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present. @@ -68,7 +68,7 @@ Contributions are welcome. We have a few asks of any new contributors. Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.) -For adding a new game to Archipelago and other documentation on how Archipelago functions, please see the docs folder for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. +For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. ## FAQ For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/) diff --git a/docs/running from source.md b/docs/running from source.md new file mode 100644 index 0000000000..4360b28c16 --- /dev/null +++ b/docs/running from source.md @@ -0,0 +1,58 @@ +# Running From Source + +If you just want to play and there is a compiled version available on the +[Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases), +use that version. These steps are for developers or platforms without compiled releases available. + +## General + +What you'll need: + * Python 3.8.7 or newer + * pip (Depending on platform may come included) + * A C compiler + * possibly optional, read OS-specific sections + +Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the +required modules and after pressing enter proceed to install everything automatically. +After this, you should be able to run the programs. + + +## Windows + +Recommended steps + * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) + * Download and install full Visual Studio from + [Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/) + or an older "Build Tools for Visual Studio" from + [Visual Studio Older Downloads](https://visualstudio.microsoft.com/vs/older-downloads/). + + * Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details + * This step is optional. Pre-compiled modules are pinned on + [Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808) + + * It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) + * Run Generate.py which will prompt installation of missing modules, press enter to confirm + + +## macOS + +Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md). + + +## Optional: A Link to the Past Enemizer + +Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an +error if it is required. + +You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases). +It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer +setting in host.yaml at your Enemizer executable. + + +## Optional: SNI + +SNI is required to use SNIClient. If not integrated into the project, it has to be started manually. + +You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). +It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in +host.yaml at your SNI folder. From 2ff7e83ad970235c8106a120898661da1b9eeddd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Jul 2022 12:01:26 +0200 Subject: [PATCH 12/16] WebHost: make a deeply buried if tree for games a bit more automatic --- WebHostLib/__init__.py | 4 ++++ WebHostLib/templates/macros.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index c0179b2238..a44afc744a 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -69,6 +69,10 @@ class B64UUIDConverter(BaseConverter): app.url_map.converters["suuid"] = B64UUIDConverter app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') +# has automatic patch integration +import Patch +app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types + def get_world_theme(game_name: str): if game_name in AutoWorldRegister.world_types: diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 6ed2ca492a..9a92edbbf1 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -40,7 +40,7 @@ {% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %} Download APSM64EX File... - {% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %} + {% elif patch.game | supports_apdeltapatch %} Download Patch File... {% elif patch.game == "Dark Souls III" %} From 07450bb83dd0db8828f51a88a7275aa2d0a42423 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu, 28 Jul 2022 19:51:22 -0400 Subject: [PATCH 13/16] Migrate DKC3 to APDeltaPatch (#838) * Add DKC3 to APDeltaPatch * Undo unintended commit * More undoing * Remove else clause --- worlds/dkc3/Rom.py | 11 ++++++++++- worlds/dkc3/__init__.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 821143090b..7e83589ffa 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -1,5 +1,5 @@ import Utils -from Patch import read_rom +from Patch import read_rom, APDeltaPatch from .Locations import lookup_id_to_name, all_locations from .Levels import level_list, level_dict @@ -529,6 +529,15 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x3B97EA, 0xEA) +class DKC3DeltaPatch(APDeltaPatch): + hash = USHASH + game = "Donkey Kong Country 3" + patch_file_ending = ".apdkc3" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + def get_base_rom_bytes(file_name: str = "") -> bytes: base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index d9e73a7ec3..423693470d 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -12,7 +12,7 @@ from .Levels import level_list from .Rules import set_rules from .Names import ItemName, LocationName from ..AutoWorld import WebWorld, World -from .Rom import LocalRom, patch_rom, get_base_rom_path +from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch import Patch @@ -138,19 +138,23 @@ class DKC3World(World): patch_rom(self.world, rom, self.player, self.active_level_list) self.active_level_list.append(LocationName.rocket_rush_region) - + outfilepname = f'_P{player}' outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \ if world.player_name[player] != 'Player%d' % player else '' rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc') rom.write_to_file(rompath) - Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player], game=Patch.GAME_DKC3) - os.unlink(rompath) self.rom_name = rom.name + + patch = DKC3DeltaPatch(os.path.splitext(rompath)[0]+DKC3DeltaPatch.patch_file_ending, player=player, + player_name=world.player_name[player], patched_path=rompath) + patch.write() except: raise finally: + if os.path.exists(rompath): + os.unlink(rompath) self.rom_name_available_event.set() # make sure threading continues and errors are collected def modify_multidata(self, multidata: dict): From afc9c772be12241b0559511b47063ff7dfae2030 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 30 Jul 2022 12:42:02 -0400 Subject: [PATCH 14/16] Sm broken start location fix (#841) * - fixed basepatches application order breaking (at least) starting location --- worlds/sm/__init__.py | 6 ++++-- worlds/sm/variaRandomizer/randomizer.py | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 1de316269a..bdb4b11eeb 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -271,7 +271,7 @@ class SMWorld(World): data.append(w1) return data - def APPatchRom(self, romPatcher): + def APPrePatchRom(self, romPatcher): # first apply the sm multiworld code patch named 'basepatch' (also has empty tables that we'll overwrite), # + apply some patches from varia that we want to be always-on. # basepatch and variapatches are both generated from https://github.com/lordlou/SMBasepatch @@ -279,6 +279,8 @@ class SMWorld(World): "data", "SMBasepatch_prebuilt", "multiworld-basepatch.ips")) romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), "data", "SMBasepatch_prebuilt", "variapatches.ips")) + + def APPostPatchRom(self, romPatcher): symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__), "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json")) multiWorldLocations = [] @@ -504,7 +506,7 @@ class SMWorld(World): outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc') try: - self.variaRando.PatchRom(outputFilename, self.APPatchRom) + self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom) self.write_crc(outputFilename) self.rom_name = self.romName except: diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index ebb87c520b..3f6a9cc5fa 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -697,7 +697,7 @@ class VariaRandomizer: #if args.patchOnly == False: # randoExec.postProcessItemLocs(itemLocs, args.hideItems) - def PatchRom(self, outputFilename, customPatchApply = None): + def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None): args = self.args optErrMsgs = self.optErrMsgs @@ -749,6 +749,9 @@ class VariaRandomizer: else: romPatcher = RomPatcher(magic=args.raceMagic) + if customPrePatchApply != None: + customPrePatchApply(romPatcher) + if args.hud == True or args.majorsSplit == "FullWithHUD": args.patches.append("varia_hud.ips") if args.patchOnly == False: @@ -767,8 +770,8 @@ class VariaRandomizer: # don't color randomize custom ships args.shift_ship_palette = False - if customPatchApply != None: - customPatchApply(romPatcher) + if customPostPatchApply != None: + customPostPatchApply(romPatcher) # we have to write ips to ROM before doing our direct modifications which will rewrite some parts (like in credits), # but in web mode we only want to generate a global ips at the end From 75165803a061db7eb15b61623d0dbedfc3257d13 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 31 Jul 2022 05:08:41 -0400 Subject: [PATCH 15/16] Sm smz3 create item fix (#844) --- worlds/sm/__init__.py | 2 +- worlds/smz3/__init__.py | 97 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index bdb4b11eeb..a7445c01a5 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -598,7 +598,7 @@ class SMWorld(World): def create_item(self, name: str) -> Item: item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Name], + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], player=self.player) def get_filler_item_name(self) -> str: diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 77168d5674..7c519ec068 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -8,6 +8,7 @@ from typing import Dict, Set, TextIO from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, RegionType, CollectionState, \ Tutorial from worlds.generic.Rules import set_rule +from worlds.smz3.TotalSMZ3.Item import ItemType import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower @@ -80,6 +81,99 @@ class SMZ3World(World): self.unreachable = [] super().__init__(world, player) + @classmethod + def isProgression(cls, itemType): + progressionTypes = { + ItemType.ProgressiveShield, + ItemType.ProgressiveSword, + ItemType.Bow, + ItemType.Hookshot, + ItemType.Mushroom, + ItemType.Powder, + ItemType.Firerod, + ItemType.Icerod, + ItemType.Bombos, + ItemType.Ether, + ItemType.Quake, + ItemType.Lamp, + ItemType.Hammer, + ItemType.Shovel, + ItemType.Flute, + ItemType.Bugnet, + ItemType.Book, + ItemType.Bottle, + ItemType.Somaria, + ItemType.Byrna, + ItemType.Cape, + ItemType.Mirror, + ItemType.Boots, + ItemType.ProgressiveGlove, + ItemType.Flippers, + ItemType.MoonPearl, + ItemType.HalfMagic, + + ItemType.Grapple, + ItemType.Charge, + ItemType.Ice, + ItemType.Wave, + ItemType.Plasma, + ItemType.Varia, + ItemType.Gravity, + ItemType.Morph, + ItemType.Bombs, + ItemType.SpringBall, + ItemType.ScrewAttack, + ItemType.HiJump, + ItemType.SpaceJump, + ItemType.SpeedBooster, + + ItemType.ETank, + ItemType.ReserveTank, + + ItemType.BigKeyGT, + ItemType.KeyGT, + ItemType.BigKeyEP, + ItemType.BigKeyDP, + ItemType.BigKeyTH, + ItemType.BigKeyPD, + ItemType.BigKeySP, + ItemType.BigKeySW, + ItemType.BigKeyTT, + ItemType.BigKeyIP, + ItemType.BigKeyMM, + ItemType.BigKeyTR, + + ItemType.KeyHC, + ItemType.KeyCT, + ItemType.KeyDP, + ItemType.KeyTH, + ItemType.KeyPD, + ItemType.KeySP, + ItemType.KeySW, + ItemType.KeyTT, + ItemType.KeyIP, + ItemType.KeyMM, + ItemType.KeyTR, + + ItemType.CardCrateriaL1, + ItemType.CardCrateriaL2, + ItemType.CardCrateriaBoss, + ItemType.CardBrinstarL1, + ItemType.CardBrinstarL2, + ItemType.CardBrinstarBoss, + ItemType.CardNorfairL1, + ItemType.CardNorfairL2, + ItemType.CardNorfairBoss, + ItemType.CardMaridiaL1, + ItemType.CardMaridiaL2, + ItemType.CardMaridiaBoss, + ItemType.CardWreckedShipL1, + ItemType.CardWreckedShipBoss, + ItemType.CardLowerNorfairL1, + ItemType.CardLowerNorfairBoss, + } + return itemType in progressionTypes + @classmethod def stage_assert_generate(cls, world): base_combined_rom = get_base_rom_bytes() @@ -337,7 +431,8 @@ class SMZ3World(World): return False def create_item(self, name: str) -> Item: - return SMZ3Item(name, ItemClassification.progression, + return SMZ3Item(name, + ItemClassification.progression if SMZ3World.isProgression(TotalSMZ3Item.ItemType[name]) else ItemClassification.filler, TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], self.player, TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], self)) From 3bc9392e5b9eb7d4a66a93ef55fe861894a3fec4 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 31 Jul 2022 05:02:36 -0500 Subject: [PATCH 16/16] Core: have generation print plando settings as string instead of numbers (#843) * have generation print plando settings as string instead of numbers * Change to __str__ * Make to_string not a class method * Suggested fix Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Fix the fix * Better quotes Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Generate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Generate.py b/Generate.py index 125fab4163..7cfe7a504c 100644 --- a/Generate.py +++ b/Generate.py @@ -61,6 +61,11 @@ class PlandoSettings(enum.IntFlag): else: return base | part + def __str__(self) -> str: + if self.value: + return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value)) + return "Off" + def mystery_argparse(): options = get_options()