Merge branch 'main' into player-tracker

This commit is contained in:
Chris Wilson
2022-07-31 11:13:14 -04:00
20 changed files with 325 additions and 56 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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:

View File

@@ -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/)

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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" %}

View File

@@ -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 |
| ---- | ---- | ----- |

View 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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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",

View File

@@ -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