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
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()
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:
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/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:
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
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/stats.py b/WebHostLib/stats.py
index 9a164d02cb..a647be5ee5 100644
--- a/WebHostLib/stats.py
+++ b/WebHostLib/stats.py
@@ -1,23 +1,27 @@
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=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:
@@ -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)
diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html
index 31bafe3931..4188ccd78c 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" %}
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 |
| ---- | ---- | ----- |
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.
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
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):
diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py
index 1de316269a..a7445c01a5 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:
@@ -596,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/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
diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py
index e440eab2c9..7c519ec068 100644
--- a/worlds/smz3/__init__.py
+++ b/worlds/smz3/__init__.py
@@ -8,8 +8,10 @@ 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
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 +70,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)
@@ -77,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()
@@ -334,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))
@@ -364,6 +462,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 +476,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 +517,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)
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