forked from mirror/Archipelago
Merge branch 'main' into player-tracker
This commit is contained in:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/)
|
||||
|
||||
13
SNIClient.py
13
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:
|
||||
|
||||
7
Utils.py
7
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APSM64EX File...</a>
|
||||
{% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %}
|
||||
{% elif patch.game | supports_apdeltapatch %}
|
||||
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
||||
Download Patch File...</a>
|
||||
{% elif patch.game == "Dark Souls III" %}
|
||||
|
||||
@@ -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 |
|
||||
| ---- | ---- | ----- |
|
||||
|
||||
58
docs/running from source.md
Normal file
58
docs/running from source.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user