Skip to content

Commit

Permalink
Merge branch 'main' into top_secret
Browse files Browse the repository at this point in the history
  • Loading branch information
ThePhar committed Dec 22, 2023
2 parents ba5639a + 0d929b8 commit 4f99483
Show file tree
Hide file tree
Showing 27 changed files with 209 additions and 122 deletions.
11 changes: 6 additions & 5 deletions AdventureClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,12 @@ def on_package(self, cmd: str, args: dict):
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "Retrieved":
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
elif cmd == "SetReply":
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
self.freeincarnates_used = args["value"]
Expand Down
39 changes: 22 additions & 17 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,20 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio
range(1, self.players + 1)}

def set_options(self, args: Namespace) -> None:
# TODO - remove this section once all worlds use options dataclasses
all_keys: Set[str] = {key for player in self.player_ids for key in
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys:
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
f"Please use `self.options.{option_key}` instead.")
option.update(getattr(args, option_key, {}))
setattr(self, option_key, option)

for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
for option_key in world_type.options_dataclass.type_hints:
option_values = getattr(args, option_key, {})
setattr(self, option_key, option_values)
# TODO - remove this loop once all worlds use options dataclasses
options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})

Expand Down Expand Up @@ -646,34 +651,34 @@ def __init__(self, parent: MultiWorld):

def update_reachable_regions(self, player: int):
self.stale[player] = False
rrp = self.reachable_regions[player]
bc = self.blocked_connections[player]
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
queue = deque(self.blocked_connections[player])
start = self.multiworld.get_region('Menu', player)
start = self.multiworld.get_region("Menu", player)

# init on first call - this can't be done on construction since the regions don't exist yet
if start not in rrp:
rrp.add(start)
bc.update(start.exits)
if start not in reachable_regions:
reachable_regions.add(start)
blocked_connections.update(start.exits)
queue.extend(start.exits)

# run BFS on all connections, and keep track of those blocked by missing items
while queue:
connection = queue.popleft()
new_region = connection.connected_region
if new_region in rrp:
bc.remove(connection)
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
rrp.add(new_region)
bc.remove(connection)
bc.update(new_region.exits)
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
queue.extend(new_region.exits)
self.path[new_region] = (new_region.name, self.path.get(connection, None))

# Retry connections if the new region can unblock them
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
if new_entrance in bc and new_entrance not in queue:
if new_entrance in blocked_connections and new_entrance not in queue:
queue.append(new_entrance)

def copy(self) -> CollectionState:
Expand Down
19 changes: 19 additions & 0 deletions Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,25 @@ def deprecate(message: str):
import warnings
warnings.warn(message)


class DeprecateDict(dict):
log_message: str
should_error: bool

def __init__(self, message, error: bool = False) -> None:
self.log_message = message
self.should_error = error
super().__init__()

def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
elif __debug__:
import warnings
warnings.warn(self.log_message)
return super().__getitem__(item)


def _extend_freeze_support() -> None:
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
# upstream issue: https://github.com/python/cpython/issues/76327
Expand Down
59 changes: 31 additions & 28 deletions WebHostLib/check.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import zipfile
import base64
from typing import Union, Dict, Set, Tuple
Expand All @@ -6,13 +7,7 @@
from markupsafe import Markup

from WebHostLib import app

banned_zip_contents = (".sfc",)


def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))

from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file

from Generate import roll_settings, PlandoOptions
from Utils import parse_yamls
Expand Down Expand Up @@ -51,33 +46,41 @@ def mysterycheck():
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
options = {}
for uploaded_file in files:
# if user does not select file, browser also
# submit an empty part without filename
if uploaded_file.filename == '':
return 'No selected file'
if banned_file(uploaded_file.filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. "
"Your file was deleted.")
# If the user does not select file, the browser will still submit an empty string without a file name.
elif uploaded_file.filename == "":
return "No selected file."
elif uploaded_file.filename in options:
return f'Conflicting files named {uploaded_file.filename} submitted'
elif uploaded_file and allowed_file(uploaded_file.filename):
return f"Conflicting files named {uploaded_file.filename} submitted."
elif uploaded_file and allowed_options(uploaded_file.filename):
if uploaded_file.filename.endswith(".zip"):

with zipfile.ZipFile(uploaded_file, 'r') as zfile:
infolist = zfile.infolist()

if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')

for file in infolist:
if file.filename.endswith(banned_zip_contents):
return ("Uploaded data contained a rom file, "
"which is likely to contain copyrighted material. "
"Your file was deleted.")
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
if not zipfile.is_zipfile(uploaded_file):
return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened."

uploaded_file.seek(0) # offset from is_zipfile check
with zipfile.ZipFile(uploaded_file, "r") as zfile:
for file in zfile.infolist():
# Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS).
base_filename = os.path.basename(file.filename)

if base_filename.endswith(".archipelago"):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
elif base_filename.endswith(".zip"):
return "Nested .zip files inside a .zip are not supported."
elif banned_file(base_filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted "
"material. Your file was deleted.")
# Ignore dot-files.
elif not base_filename.startswith(".") and allowed_options(base_filename):
options[file.filename] = zfile.open(file, "r").read()
else:
options[uploaded_file.filename] = uploaded_file.read()

if not options:
return "Did not find a .yaml file to process."
return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}"
return options


Expand Down
10 changes: 10 additions & 0 deletions WebHostLib/templates/hostRoom.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
{% block head %}
<title>Multiworld {{ room.id|suuid }}</title>
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
<meta name="og:site_name" content="Archipelago">
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
<meta property="og:type" content="website" />
{% if room.seed.slots|length < 2 %}
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% else %}
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
{% endblock %}

Expand Down
58 changes: 34 additions & 24 deletions WebHostLib/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@
from . import app
from .models import Seed, Room, Slot, GameDataPackage

banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba")
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
allowed_generation_extensions = (".archipelago", ".zip")


def allowed_options(filename: str) -> bool:
return filename.endswith(allowed_options_extensions)


def allowed_generation(filename: str) -> bool:
return filename.endswith(allowed_generation_extensions)


def banned_file(filename: str) -> bool:
return filename.endswith(banned_extensions)


def process_multidata(compressed_multidata, files={}):
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
Expand Down Expand Up @@ -61,8 +76,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
if not owner:
owner = session["_id"]
infolist = zfile.infolist()
if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
flash(Markup("Error: Your .zip file only contains .yaml files. "
if all(allowed_options(file.filename) or file.is_dir() for file in infolist):
flash(Markup("Error: Your .zip file only contains options files. "
'Did you mean to <a href="/generate">generate a game</a>?'))
return

Expand All @@ -73,7 +88,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
# Load files.
for file in infolist:
handler = AutoPatchRegister.get_handler(file.filename)
if file.filename.endswith(banned_zip_contents):
if banned_file(file.filename):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
"Your file was deleted."

Expand Down Expand Up @@ -136,35 +151,34 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
flash("No multidata was found in the zip file, which is required.")


@app.route('/uploads', methods=['GET', 'POST'])
@app.route("/uploads", methods=["GET", "POST"])
def uploads():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part')
if request.method == "POST":
# check if the POST request has a file part.
if "file" not in request.files:
flash("No file part in POST request.")
else:
file = request.files['file']
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
flash('No selected file')
elif file and allowed_file(file.filename):
if zipfile.is_zipfile(file):
with zipfile.ZipFile(file, 'r') as zfile:
uploaded_file = request.files["file"]
# If the user does not select file, the browser will still submit an empty string without a file name.
if uploaded_file.filename == "":
flash("No selected file.")
elif uploaded_file and allowed_generation(uploaded_file.filename):
if zipfile.is_zipfile(uploaded_file):
with zipfile.ZipFile(uploaded_file, "r") as zfile:
try:
res = upload_zip_to_db(zfile)
except VersionException:
flash(f"Could not load multidata. Wrong Version detected.")
else:
if type(res) == str:
if res is str:
return res
elif res:
return redirect(url_for("view_seed", seed=res.id))
else:
file.seek(0) # offset from is_zipfile check
uploaded_file.seek(0) # offset from is_zipfile check
# noinspection PyBroadException
try:
multidata = file.read()
multidata = uploaded_file.read()
slots, multidata = process_multidata(multidata)
except Exception as e:
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
Expand All @@ -182,7 +196,3 @@ def user_content():
rooms = select(room for room in Room if room.owner == session["_id"])
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
return render_template("userContent.html", rooms=rooms, seeds=seeds)


def allowed_file(filename):
return filename.endswith(('.archipelago', ".zip"))
2 changes: 1 addition & 1 deletion data/lua/connector_bizhawk_generic.lua
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ else
-- misaligned, so for GB and GBC we explicitly set the callback on
-- vblank instead.
-- https://github.com/TASEmulators/BizHawk/issues/3711
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then
event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
else
event.onframeend(tick)
Expand Down
4 changes: 4 additions & 0 deletions worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut
# create missing options_dataclass from legacy option_definitions
# TODO - remove this once all worlds use options dataclasses
if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version
if __debug__:
from warnings import warn
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))

Expand Down
2 changes: 1 addition & 1 deletion worlds/alttp/Dungeons.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):

if loc in all_state_base.events:
all_state_base.events.remove(loc)
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True,
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True,
name="LttP Dungeon Items")


Expand Down
3 changes: 2 additions & 1 deletion worlds/alttp/Rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ def mirrorless_path_to_castle_courtyard(world, player):

def set_defeat_dungeon_boss_rule(location):
# Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used
set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))


def set_always_allow(spot, rule):
spot.always_allow = rule
Expand Down
2 changes: 1 addition & 1 deletion worlds/factorio/Mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def flop_random(low, high, base=None):
else:
basepath = os.path.join(os.path.dirname(__file__), "data", "mod")
for dirpath, dirnames, filenames in os.walk(basepath):
base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)
base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\")
for filename in filenames:
mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename,
file_path=os.path.join(dirpath, filename):
Expand Down
4 changes: 2 additions & 2 deletions worlds/ffmq/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,11 @@ class CompanionLevelingType(Choice):
class CompanionSpellbookType(Choice):
"""Update companions' spellbook.
Standard: Original game spellbooks.
Standard Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
Random Balanced: Randomize the spellbooks with an appropriate mix of spells.
Random Chaos: Randomize the spellbooks in total free-for-all."""
option_standard = 0
option_standard_extended = 1
option_extended = 1
option_random_balanced = 2
option_random_chaos = 3
default = 0
Expand Down
4 changes: 2 additions & 2 deletions worlds/ffmq/data/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Final Fantasy Mystic Quest:
Normal: 0
OneAndHalf: 0
Double: 0
DoubleHalf: 0
DoubleAndHalf: 0
Triple: 0
Quadruple: 0
companion_leveling_type:
Expand All @@ -98,7 +98,7 @@ Final Fantasy Mystic Quest:
BenPlus10: 0
companion_spellbook_type:
Standard: 0
StandardExtended: 0
Extended: 0
RandomBalanced: 0
RandomChaos: 0
starting_companion:
Expand Down
Loading

0 comments on commit 4f99483

Please sign in to comment.