Skip to content

Commit

Permalink
Merge branch 'main' into datapackage-proposal
Browse files Browse the repository at this point in the history
  • Loading branch information
ThePhar authored Feb 11, 2023
2 parents 143b5fc + 0ff3c69 commit cb32925
Show file tree
Hide file tree
Showing 146 changed files with 5,448 additions and 1,440 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Build
on: workflow_dispatch

env:
SNI_VERSION: v0.0.84
SNI_VERSION: v0.0.88
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13

Expand Down Expand Up @@ -78,9 +78,10 @@ jobs:
- name: Build
run: |
# pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
pip install -r requirements.txt
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
- '*.*.*'

env:
SNI_VERSION: v0.0.84
SNI_VERSION: v0.0.88
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13

Expand Down Expand Up @@ -65,9 +65,10 @@ jobs:
- name: Build
run: |
# pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
pip install -r requirements.txt
python setup.py build --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
Expand Down
100 changes: 85 additions & 15 deletions BaseClasses.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from __future__ import annotations
from argparse import Namespace

import copy
from enum import unique, IntEnum, IntFlag
import logging
import json
import functools
import json
import logging
import random
import secrets
import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace
from collections import OrderedDict, Counter, deque
from enum import unique, IntEnum, IntFlag
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
import typing # this can go away when Python 3.8 support is dropped
import secrets
import random

import NetUtils
import Options
import Utils
import NetUtils


class Group(TypedDict, total=False):
Expand All @@ -29,6 +29,20 @@ class Group(TypedDict, total=False):
link_replacement: bool


class ThreadBarrierProxy():
"""Passes through getattr while passthrough is True"""
def __init__(self, obj: Any):
self.passthrough = True
self.obj = obj

def __getattr__(self, item):
if self.passthrough:
return getattr(self.obj, item)
else:
raise RuntimeError("You are in a threaded context and global random state was removed for your safety. "
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")


class MultiWorld():
debug_types = False
player_name: Dict[int, str]
Expand All @@ -48,6 +62,7 @@ class MultiWorld():
precollected_items: Dict[int, List[Item]]
state: CollectionState

plando_options: PlandoOptions
accessibility: Dict[int, Options.Accessibility]
early_items: Dict[int, Dict[str, int]]
local_early_items: Dict[int, Dict[str, int]]
Expand All @@ -60,6 +75,9 @@ class MultiWorld():

game: Dict[int, str]

random: random.Random
per_slot_randoms: Dict[int, random.Random]

class AttributeProxy():
def __init__(self, rule):
self.rule = rule
Expand All @@ -68,7 +86,8 @@ def __getitem__(self, player) -> bool:
return self.rule(player)

def __init__(self, players: int):
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
# world-local random state is saved for multiple generations running concurrently
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
Expand Down Expand Up @@ -159,7 +178,8 @@ def set_player_attr(attr, val):
set_player_attr('completion_condition', lambda state: True)
self.custom_data = {}
self.worlds = {}
self.slot_seeds = {}
self.per_slot_randoms = {}
self.plando_options = PlandoOptions.none

def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups)
Expand Down Expand Up @@ -204,8 +224,8 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio
else:
self.random.seed(self.seed)
self.seed_name = name if name else str(self.seed)
self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in
range(1, self.players + 1)}
self.per_slot_randoms = {player: random.Random(self.random.getrandbits(64)) for player in
range(1, self.players + 1)}

def set_options(self, args: Namespace) -> None:
for option_key in Options.common_options:
Expand Down Expand Up @@ -289,7 +309,7 @@ def set_default_common_options(self):
self.state = CollectionState(self)

def secure(self):
self.random = secrets.SystemRandom()
self.random = ThreadBarrierProxy(secrets.SystemRandom())
self.is_race = True

@functools.cached_property
Expand Down Expand Up @@ -391,15 +411,25 @@ def get_all_state(self, use_cache: bool) -> CollectionState:
def get_items(self) -> List[Item]:
return [loc.item for loc in self.get_filled_locations()] + self.itempool

def find_item_locations(self, item, player: int) -> List[Location]:
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.player not in player_groups and
(location.item.player == player or location.item.player in player_groups)]
return [location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player]

def find_item(self, item, player: int) -> Location:
return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player)

def find_items_in_locations(self, items: Set[str], player: int) -> List[Location]:
def find_items_in_locations(self, items: Set[str], player: int, resolve_group_locations: bool = False) -> List[Location]:
if resolve_group_locations:
player_groups = self.get_player_groups(player)
return [location for location in self.get_locations() if
location.item and location.item.name in items and location.player not in player_groups and
(location.item.player == player or location.item.player in player_groups)]
return [location for location in self.get_locations() if
location.item and location.item.name in items and location.item.player == player]

Expand Down Expand Up @@ -1558,6 +1588,7 @@ def write_option(option_key: str, option_obj: type(Options.Option)):
Utils.__version__, self.multiworld.seed))
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
outfile.write('Players: %d\n' % self.multiworld.players)
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)

for player in range(1, self.multiworld.players + 1):
Expand Down Expand Up @@ -1674,6 +1705,45 @@ class Tutorial(NamedTuple):
authors: List[str]


class PlandoOptions(IntFlag):
none = 0b0000
items = 0b0001
connections = 0b0010
texts = 0b0100
bosses = 0b1000

@classmethod
def from_option_string(cls, option_string: str) -> PlandoOptions:
result = cls(0)
for part in option_string.split(","):
part = part.strip().lower()
if part:
result = cls._handle_part(part, result)
return result

@classmethod
def from_set(cls, option_set: Set[str]) -> PlandoOptions:
result = cls(0)
for part in option_set:
result = cls._handle_part(part, result)
return result

@classmethod
def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions:
try:
part = cls[part]
except Exception as e:
raise KeyError(f"{part} is not a recognized name for a plando module. "
f"Known options: {', '.join(flag.name for flag in cls)}") from e
else:
return base | part

def __str__(self) -> str:
if self.value:
return ", ".join(flag.name for flag in PlandoOptions if self.value & flag.value)
return "None"


seeddigits = 20


Expand Down
26 changes: 19 additions & 7 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def __init__(self, server_address: typing.Optional[str], password: typing.Option
self.hint_cost = None
self.slot_info = {}
self.permissions = {
"forfeit": "disabled",
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
Expand Down Expand Up @@ -260,7 +260,7 @@ def reset_server_state(self):
self.server_task = None
self.hint_cost = None
self.permissions = {
"forfeit": "disabled",
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
Expand Down Expand Up @@ -494,7 +494,7 @@ def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Op
self._messagebox.open()
return self._messagebox

def _handle_connection_loss(self, msg: str) -> None:
def handle_connection_loss(self, msg: str) -> None:
"""Helper for logging and displaying a loss of connection. Must be called from an except block."""
exc_info = sys.exc_info()
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
Expand Down Expand Up @@ -580,14 +580,22 @@ def reconnect_hint() -> str:
for msg in decode(data):
await process_server_cmd(ctx, msg)
logger.warning(f"Disconnected from multiworld server{reconnect_hint()}")
except websockets.InvalidMessage:
# probably encrypted
if address.startswith("ws://"):
await server_loop(ctx, "ws" + address[1:])
else:
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
f"{reconnect_hint()}")
except ConnectionRefusedError:
ctx._handle_connection_loss("Connection refused by the server. May not be running Archipelago on that address or port.")
ctx.handle_connection_loss("Connection refused by the server. "
"May not be running Archipelago on that address or port.")
except websockets.InvalidURI:
ctx._handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
except OSError:
ctx._handle_connection_loss("Failed to connect to the multiworld server")
ctx.handle_connection_loss("Failed to connect to the multiworld server")
except Exception:
ctx._handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
ctx.handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
finally:
await ctx.connection_closed()
if ctx.server_address and ctx.username and not ctx.disconnected_intentionally:
Expand Down Expand Up @@ -813,6 +821,10 @@ 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

async def disconnect(self, allow_autoreconnect: bool = False):
self.game = ""
await super().disconnect(allow_autoreconnect)


async def main(args):
Expand Down
Loading

0 comments on commit cb32925

Please sign in to comment.