Skip to content

Commit

Permalink
Merge branch 'main' into test_explicit_indirect_condition_spheres
Browse files Browse the repository at this point in the history
  • Loading branch information
Berserker66 authored Sep 18, 2024
2 parents fe7ec36 + 025c550 commit 77e18f5
Show file tree
Hide file tree
Showing 97 changed files with 2,958 additions and 2,248 deletions.
2 changes: 2 additions & 0 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
region = Region("Menu", group_id, self, "ItemLink")
self.regions.append(region)
locations = region.locations
# ensure that progression items are linked first, then non-progression
self.itempool.sort(key=lambda item: item.advancement)
for item in self.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
Expand Down
3 changes: 2 additions & 1 deletion BizHawkClient.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

import sys
import ModuleUpdate
ModuleUpdate.update()

from worlds._bizhawk.context import launch

if __name__ == "__main__":
launch()
launch(*sys.argv[1:])
20 changes: 9 additions & 11 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,28 +475,26 @@ def mark_for_locking(location: Location):
nonlocal lock_later
lock_later.append(location)

single_player = multiworld.players == 1 and not multiworld.groups

if prioritylocations:
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking,
name="Priority")
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

if progitempool:
# "advancement/progression fill"
if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=True,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
name="Progression", single_player_placement=single_player)
elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
name="Progression", single_player_placement=single_player)
elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, allow_partial=True,
name="Progression", single_player_placement=multiworld.players == 1)
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
allow_partial=True, name="Progression", single_player_placement=single_player)
if progitempool:
for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
Expand Down
31 changes: 5 additions & 26 deletions Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ def mystery_argparse():
parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults.plando_options,
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument("--csv_output", action="store_true",
help="Output rolled player options to csv (made for async multiworld).")
parser.add_argument("--plando", default=defaults.plando_options,
help="List of options that can be set manually. Can be combined, for example \"bosses, items\"")
parser.add_argument("--skip_prog_balancing", action="store_true",
help="Skip progression balancing step during generation.")
parser.add_argument("--skip_output", action="store_true",
Expand Down Expand Up @@ -156,6 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output
erargs.name = {}
erargs.csv_output = args.csv_output

settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
Expand Down Expand Up @@ -216,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")

if args.yaml_output:
import yaml
important = {}
for option, player_settings in vars(erargs).items():
if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
else:
logging.debug(f"No player settings defined for option '{option}'")

else:
if player_settings != "": # is not empty name
important[option] = player_settings
else:
logging.debug(f"No player settings defined for option '{option}'")
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)

return erargs, seed


Expand Down
5 changes: 5 additions & 0 deletions LinksAwakeningClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext):

def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient()
self.slot_data = {}

if magpie:
self.magpie_enabled = True
self.magpie = MagpieBridge()
Expand Down Expand Up @@ -564,6 +566,8 @@ async def server_auth(self, password_requested: bool = False):
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {})

# TODO - use watcher_event
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
Expand Down Expand Up @@ -628,6 +632,7 @@ async def deathlink():
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
self.magpie.slot_data = self.slot_data
except Exception:
# Don't let magpie errors take out the client
pass
Expand Down
3 changes: 3 additions & 0 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multiworld.sprite_pool = args.sprite_pool.copy()

multiworld.set_options(args)
if args.csv_output:
from Options import dump_player_options
dump_player_options(multiworld)
multiworld.set_item_links()
multiworld.state = CollectionState(multiworld)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
Expand Down
3 changes: 2 additions & 1 deletion NetUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ def _handle_color(self, node: JSONMessagePart):

color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47,
'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors


def color_code(*args):
Expand Down
46 changes: 43 additions & 3 deletions Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@
import random
import typing
import enum
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass

from schema import And, Optional, Or, Schema
from typing_extensions import Self

from Utils import get_fuzzy_results, is_iterable_except_str
from Utils import get_fuzzy_results, is_iterable_except_str, output_path

if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions
from BaseClasses import MultiWorld, PlandoOptions
from worlds.AutoWorld import World
import pathlib

Expand Down Expand Up @@ -1335,7 +1336,7 @@ class PriorityLocations(LocationSet):


class DeathLink(Toggle):
"""When you die, everyone dies. Of course the reverse is true too."""
"""When you die, everyone who enabled death link dies. Of course, the reverse is true too."""
display_name = "Death Link"
rich_text_doc = True

Expand Down Expand Up @@ -1532,3 +1533,42 @@ def yaml_dump_scalar(scalar) -> str:

with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
f.write(res)


def dump_player_options(multiworld: MultiWorld) -> None:
from csv import DictWriter

game_players = defaultdict(list)
for player, game in multiworld.game.items():
game_players[game].append(player)
game_players = dict(sorted(game_players.items()))

output = []
per_game_option_names = [
getattr(option, "display_name", option_key)
for option_key, option in PerGameCommonOptions.type_hints.items()
]
all_option_names = per_game_option_names.copy()
for game, players in game_players.items():
game_option_names = per_game_option_names.copy()
for player in players:
world = multiworld.worlds[player]
player_output = {
"Game": multiworld.game[player],
"Name": multiworld.get_player_name(player),
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
if issubclass(Removed, option):
continue
display_name = getattr(option, "display_name", option_key)
player_output[display_name] = getattr(world.options, option_key).current_option_name
if display_name not in game_option_names:
all_option_names.append(display_name)
game_option_names.append(display_name)

with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
fields = ["Game", "Name", *all_option_names]
writer = DictWriter(file, fields)
writer.writeheader()
writer.writerows(output)
2 changes: 1 addition & 1 deletion Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def as_simple_string(self) -> str:
return ".".join(str(item) for item in self)


__version__ = "0.5.0"
__version__ = "0.5.1"
version_tuple = tuplize_version(__version__)

is_linux = sys.platform.startswith("linux")
Expand Down
42 changes: 3 additions & 39 deletions WebHostLib/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,15 @@
"""API endpoints package."""
from typing import List, Tuple
from uuid import UUID

from flask import Blueprint, abort, url_for
from flask import Blueprint

import worlds.Files
from ..models import Room, Seed
from ..models import Seed

api_endpoints = Blueprint('api', __name__, url_prefix="/api")

# unsorted/misc endpoints


def get_players(seed: Seed) -> List[Tuple[str, str]]:
return [(slot.player_name, slot.game) for slot in seed.slots]


@api_endpoints.route('/room_status/<suuid:room>')
def room_info(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)

def supports_apdeltapatch(game: str):
return game in worlds.Files.AutoPatchRegister.patch_types
downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}


from . import generate, user, datapackage # trigger registration
from . import datapackage, generate, room, user # trigger registration
42 changes: 42 additions & 0 deletions WebHostLib/api/room.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Any, Dict
from uuid import UUID

from flask import abort, url_for

import worlds.Files
from . import api_endpoints, get_players
from ..models import Room


@api_endpoints.route('/room_status/<suuid:room_id>')
def room_info(room_id: UUID) -> Dict[str, Any]:
room = Room.get(id=room_id)
if room is None:
return abort(404)

def supports_apdeltapatch(game: str) -> bool:
return game in worlds.Files.AutoPatchRegister.patch_types

downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)

return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}
1 change: 1 addition & 0 deletions WebHostLib/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def task():
{"bosses", "items", "connections", "texts"}))
erargs.skip_prog_balancing = False
erargs.skip_output = False
erargs.csv_output = False

name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
Expand Down
35 changes: 25 additions & 10 deletions WebHostLib/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,26 +132,41 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
return "Access Denied", 403


@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
@app.post("/room/<suuid:room>")
def host_room_command(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)

if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))


@app.get("/room/<suuid:room>")
def host_room(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))

now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
with db_session:
room.last_activity = now # will trigger a spinup, if it's not already running

def get_log(max_size: int = 1024000) -> str:
browser_tokens = "Mozilla", "Chrome", "Safari"
automated = ("update" in request.args
or "Discordbot" in request.user_agent.string
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))

def get_log(max_size: int = 0 if automated else 1024000) -> str:
if max_size == 0:
return "…"
try:
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
raw_size = 0
Expand Down
Loading

0 comments on commit 77e18f5

Please sign in to comment.