Skip to content

Commit

Permalink
Merge branch 'ArchipelagoMW:main' into ttyd
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesbrq authored Apr 21, 2024
2 parents 9563c54 + e22ac85 commit e9d6157
Show file tree
Hide file tree
Showing 148 changed files with 4,745 additions and 4,139 deletions.
27 changes: 27 additions & 0 deletions .github/pyright-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"include": [
"type_check.py",
"../worlds/AutoSNIClient.py",
"../Patch.py"
],

"exclude": [
"**/__pycache__"
],

"stubPath": "../typings",

"typeCheckingMode": "strict",
"reportImplicitOverride": "error",
"reportMissingImports": true,
"reportMissingTypeStubs": true,

"pythonVersion": "3.8",
"pythonPlatform": "Windows",

"executionEnvironments": [
{
"root": ".."
}
]
}
15 changes: 15 additions & 0 deletions .github/type_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pathlib import Path
import subprocess

config = Path(__file__).parent / "pyright-config.json"

command = ("pyright", "-p", str(config))
print(" ".join(command))

try:
result = subprocess.run(command)
except FileNotFoundError as e:
print(f"{e} - Is pyright installed?")
exit(1)

exit(result.returncode)
33 changes: 33 additions & 0 deletions .github/workflows/strict-type-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: type check

on:
pull_request:
paths:
- "**.py"
- ".github/pyright-config.json"
- ".github/workflows/strict-type-check.yml"
- "**.pyi"
push:
paths:
- "**.py"
- ".github/pyright-config.json"
- ".github/workflows/strict-type-check.yml"
- "**.pyi"

jobs:
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: "Install dependencies"
run: |
python -m pip install --upgrade pip pyright==1.1.358
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
- name: "pyright: strict check on specific files"
run: python .github/type_check.py
46 changes: 1 addition & 45 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ def __getattr__(self, name: str) -> Any:
class MultiWorld():
debug_types = False
player_name: Dict[int, str]
difficulty_requirements: dict
required_medallions: dict
dark_room_logic: Dict[int, str]
restrict_dungeon_item_on_boss: Dict[int, bool]
plando_texts: List[Dict[str, str]]
plando_items: List[List[Dict[str, Any]]]
plando_connections: List
Expand Down Expand Up @@ -137,7 +133,6 @@ def __init__(self, players: int):
self.random = ThreadBarrierProxy(random.Random())
self.players = players
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
self.glitch_triforce = False
self.algorithm = 'balanced'
self.groups = {}
self.regions = self.RegionManager(players)
Expand All @@ -164,49 +159,10 @@ def __init__(self, players: int):
for player in range(1, players + 1):
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val

set_player_attr('shuffle', "vanilla")
set_player_attr('logic', "noglitches")
set_player_attr('mode', 'open')
set_player_attr('difficulty', 'normal')
set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False)
set_player_attr('goal', 'ganon')
set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False)
set_player_attr('ganon_at_pyramid', True)
set_player_attr('ganonstower_vanilla', True)
set_player_attr('can_access_trock_eyebridge', None)
set_player_attr('can_access_trock_front', None)
set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', True)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_health', 'default')
set_player_attr('enemy_damage', 'default')
set_player_attr('beemizer_total_chance', 0)
set_player_attr('beemizer_trap_chance', 0)
set_player_attr('escape_assist', [])
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
set_player_attr('clock_mode', False)
set_player_attr('countdown_start_time', 10)
set_player_attr('red_clock_time', -2)
set_player_attr('blue_clock_time', 2)
set_player_attr('green_clock_time', 4)
set_player_attr('can_take_damage', True)
set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off')
set_player_attr('shuffle_prizes', "g")
set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp")
set_player_attr('plando_items', [])
set_player_attr('plando_texts', {})
set_player_attr('plando_connections', [])
set_player_attr('game', "A Link to the Past")
set_player_attr('game', "Archipelago")
set_player_attr('completion_condition', lambda state: True)
self.worlds = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
Expand Down
3 changes: 1 addition & 2 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,10 +495,9 @@ def mark_for_locking(location: Location):
if unplaced or unfilled:
logging.warning(
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}")
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
items_counter = Counter(location.item.player for location in multiworld.get_filled_locations())
locations_counter = Counter(location.player for location in multiworld.get_locations())
items_counter.update(item.player for item in unplaced)
locations_counter.update(location.player for location in unfilled)
print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f"Per-Player counts: {print_data})")

Expand Down
1 change: 0 additions & 1 deletion Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ def main(args=None, callback=ERmain):
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.plando_options = args.plando
erargs.glitch_triforce = options.generator.glitch_triforce_room
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.outputname = seed_name
Expand Down
2 changes: 1 addition & 1 deletion Launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def update_settings():
Component("Open Patch", func=open_patch),
Component("Generate Template Options", func=generate_yamls),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files),
])

Expand Down
27 changes: 1 addition & 26 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,38 +36,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger = logging.getLogger()
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
multiworld.plando_options = args.plando_options

multiworld.shuffle = args.shuffle.copy()
multiworld.logic = args.logic.copy()
multiworld.mode = args.mode.copy()
multiworld.difficulty = args.difficulty.copy()
multiworld.item_functionality = args.item_functionality.copy()
multiworld.timer = args.timer.copy()
multiworld.goal = args.goal.copy()
multiworld.boss_shuffle = args.shufflebosses.copy()
multiworld.enemy_health = args.enemy_health.copy()
multiworld.enemy_damage = args.enemy_damage.copy()
multiworld.beemizer_total_chance = args.beemizer_total_chance.copy()
multiworld.beemizer_trap_chance = args.beemizer_trap_chance.copy()
multiworld.countdown_start_time = args.countdown_start_time.copy()
multiworld.red_clock_time = args.red_clock_time.copy()
multiworld.blue_clock_time = args.blue_clock_time.copy()
multiworld.green_clock_time = args.green_clock_time.copy()
multiworld.dungeon_counters = args.dungeon_counters.copy()
multiworld.triforce_pieces_available = args.triforce_pieces_available.copy()
multiworld.triforce_pieces_required = args.triforce_pieces_required.copy()
multiworld.shop_shuffle = args.shop_shuffle.copy()
multiworld.shuffle_prizes = args.shuffle_prizes.copy()
multiworld.sprite_pool = args.sprite_pool.copy()
multiworld.dark_room_logic = args.dark_room_logic.copy()
multiworld.plando_items = args.plando_items.copy()
multiworld.plando_texts = args.plando_texts.copy()
multiworld.plando_connections = args.plando_connections.copy()
multiworld.required_medallions = args.required_medallions.copy()
multiworld.game = args.game.copy()
multiworld.player_name = args.name.copy()
multiworld.sprite = args.sprite.copy()
multiworld.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
multiworld.sprite_pool = args.sprite_pool.copy()

multiworld.set_options(args)
multiworld.set_item_links()
Expand Down
2 changes: 1 addition & 1 deletion ModuleUpdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def install_pkg_resources(yes=False):
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])


def update(yes=False, force=False):
def update(yes: bool = False, force: bool = False) -> None:
global update_ran
if not update_ran:
update_ran = True
Expand Down
65 changes: 40 additions & 25 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1905,7 +1905,7 @@ def _cmd_exit(self) -> bool:
@mark_raw
def _cmd_alias(self, player_name_then_alias_name):
"""Set a player's alias, by listing their base name and then their intended alias."""
player_name, alias_name = player_name_then_alias_name.split(" ", 1)
player_name, _, alias_name = player_name_then_alias_name.partition(" ")
player_name, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
for (team, slot), name in self.ctx.player_names.items():
Expand Down Expand Up @@ -2134,32 +2134,47 @@ def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool:
self.output(response)
return False

def _cmd_option(self, option_name: str, option: str):
"""Set options for the server."""

attrtype = self.ctx.simple_options.get(option_name, None)
if attrtype:
if attrtype == bool:
def attrtype(input_text: str):
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
elif attrtype == str and option_name.endswith("password"):
def attrtype(input_text: str):
if input_text.lower() in {"null", "none", '""', "''"}:
return None
return input_text
setattr(self.ctx, option_name, attrtype(option))
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
elif option_name in {"hint_cost", "location_check_points"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
return True
else:
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
self.output(f"Unrecognized Option {option_name}, known: "
f"{', '.join(known)}")
def _cmd_option(self, option_name: str, option_value: str):
"""Set an option for the server."""
value_type = self.ctx.simple_options.get(option_name, None)
if not value_type:
known_options = (f"{option}: {option_type}" for option, option_type in self.ctx.simple_options.items())
self.output(f"Unrecognized option '{option_name}', known: {', '.join(known_options)}")
return False

if value_type == bool:
def value_type(input_text: str):
return input_text.lower() not in {"off", "0", "false", "none", "null", "no"}
elif value_type == str and option_name.endswith("password"):
def value_type(input_text: str):
return None if input_text.lower() in {"null", "none", '""', "''"} else input_text
elif value_type == str and option_name.endswith("mode"):
valid_values = {"goal", "enabled", "disabled"}
valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else [])
if option_value.lower() not in valid_values:
self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}")
return False

setattr(self.ctx, option_name, value_type(option_value))
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
elif option_name in {"hint_cost", "location_check_points"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
return True

def _cmd_datastore(self):
"""Debug Tool: list writable datastorage keys and approximate the size of their values with pickle."""
total: int = 0
texts = []
for key, value in self.ctx.stored_data.items():
size = len(pickle.dumps(value))
total += size
texts.append(f"Key: {key} | Size: {size}B")
texts.insert(0, f"Found {len(self.ctx.stored_data)} keys, "
f"approximately totaling {Utils.format_SI_prefix(total, power=1024)}B")
self.output("\n".join(texts))


async def console(ctx: Context):
import sys
Expand Down
4 changes: 3 additions & 1 deletion Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ class Toggle(NumericOption):
default = 0

def __init__(self, value: int):
assert value == 0 or value == 1, "value of Toggle can only be 0 or 1"
# if user puts in an invalid value, make it valid
value = int(bool(value))
self.value = value

@classmethod
Expand Down Expand Up @@ -1124,6 +1125,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P
raise Exception(f"item_link {link['name']} has {intersection} "
f"items in both its local_items and non_local_items pool.")
link.setdefault("link_replacement", None)
link["item_pool"] = list(pool)


class Removed(FreeText):
Expand Down
1 change: 1 addition & 0 deletions SNIClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def _cmd_snes_close(self) -> bool:
"""Close connection to a currently connected snes"""
self.ctx.snes_reconnect_address = None
self.ctx.cancel_snes_autoreconnect()
self.ctx.snes_state = SNESState.SNES_DISCONNECTED
if self.ctx.snes_socket and not self.ctx.snes_socket.closed:
async_start(self.ctx.snes_socket.close())
return True
Expand Down
2 changes: 1 addition & 1 deletion WebHost.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ def get_app():
from WebHostLib import register, cache, app as raw_app
from WebHostLib.models import db

register()
app = raw_app
if os.path.exists(configpath) and not app.config["TESTING"]:
import yaml
Expand All @@ -34,6 +33,7 @@ def get_app():
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")

register()
cache.init_app(app)
db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True)
Expand Down
3 changes: 2 additions & 1 deletion WebHostLib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "SimpleCache"
app.config["HOST_ADDRESS"] = ""
app.config["ASSET_RIGHTS"] = False

cache = Cache()
Compress(app)
Expand Down Expand Up @@ -82,6 +83,6 @@ def register():

from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots

app.register_blueprint(api.api_endpoints)
14 changes: 14 additions & 0 deletions WebHostLib/robots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from WebHostLib import app
from flask import abort
from . import cache


@cache.cached()
@app.route('/robots.txt')
def robots():
# If this host is not official, do not allow search engine crawling
if not app.config["ASSET_RIGHTS"]:
return app.send_static_file('robots.txt')

# Send 404 if the host has affirmed this to be the official WebHost
abort(404)
Loading

0 comments on commit e9d6157

Please sign in to comment.