Skip to content

Commit

Permalink
Merge branch 'ArchipelagoMW:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
BlastSlimey authored Dec 5, 2024
2 parents d1436a8 + f4b926e commit 09d3cef
Show file tree
Hide file tree
Showing 261 changed files with 8,391 additions and 4,120 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
worlds/blasphemous/region_data.py linguist-generated=true
worlds/yachtdice/YachtWeights.py linguist-generated=true
2 changes: 1 addition & 1 deletion .github/pyright-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"reportMissingImports": true,
"reportMissingTypeStubs": true,

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

"executionEnvironments": [
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/analyze-modified-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- uses: actions/setup-python@v5
if: env.diff != ''
with:
python-version: 3.8
python-version: '3.10'

- name: "Install dependencies"
if: env.diff != ''
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ env:
jobs:
# build-release-macos: # LF volunteer

build-win-py38: # RCs will still be built and signed by hand
build-win: # RCs will still be built and signed by hand
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install python
uses: actions/setup-python@v5
with:
python-version: '3.8'
python-version: '~3.12.7'
check-latest: true
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand Down Expand Up @@ -111,10 +112,11 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '~3.12.7'
check-latest: true
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.11" >> $GITHUB_ENV
echo "PYTHON=python3.12" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '~3.12.7'
check-latest: true
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.11" >> $GITHUB_ENV
echo "PYTHON=python3.12" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,11 @@ jobs:
matrix:
os: [ubuntu-latest]
python:
- {version: '3.8'}
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
- {version: '3.12'}
include:
- python: {version: '3.8'} # win7 compat
- python: {version: '3.10'} # old compat
os: windows-latest
- python: {version: '3.12'} # current
os: windows-latest
Expand Down
98 changes: 77 additions & 21 deletions BaseClasses.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
from __future__ import annotations

import collections
import itertools
import functools
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 Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, Type)
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)

from typing_extensions import NotRequired, TypedDict

import NetUtils
import Options
import Utils

if typing.TYPE_CHECKING:
if TYPE_CHECKING:
from worlds import AutoWorld


Expand Down Expand Up @@ -231,7 +229,7 @@ def set_options(self, args: Namespace) -> None:
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
options_dataclass: 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 @@ -606,6 +604,49 @@ def get_spheres(self) -> Iterator[Set[Location]]:
state.collect(location.item, True, location)
locations -= sphere

def get_sendable_spheres(self) -> Iterator[Set[Location]]:
"""
yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere
If there are unreachable locations, the last sphere of reachable locations is followed by an empty set,
and then a set of all of the unreachable locations.
"""
state = CollectionState(self)
locations: Set[Location] = set()
events: Set[Location] = set()
for location in self.get_filled_locations():
if type(location.item.code) is int:
locations.add(location)
else:
events.add(location)

while locations:
sphere: Set[Location] = set()

# cull events out
done_events: Set[Union[Location, None]] = {None}
while done_events:
done_events = set()
for event in events:
if event.can_reach(state):
state.collect(event.item, True, event)
done_events.add(event)
events -= done_events

for location in locations:
if location.can_reach(state):
sphere.add(location)

yield sphere
if not sphere:
if locations:
yield locations # unreachable locations
break

for location in sphere:
state.collect(location.item, True, location)
locations -= sphere

def fulfills_accessibility(self, state: Optional[CollectionState] = None):
"""Check if accessibility rules are fulfilled with current or supplied state."""
if not state:
Expand Down Expand Up @@ -975,7 +1016,7 @@ class Region:
entrances: List[Entrance]
exits: List[Entrance]
locations: List[Location]
entrance_type: ClassVar[Type[Entrance]] = Entrance
entrance_type: ClassVar[type[Entrance]] = Entrance

class Register(MutableSequence):
region_manager: MultiWorld.RegionManager
Expand Down Expand Up @@ -1075,7 +1116,7 @@ def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool])
return entrance.parent_region.get_connecting_entrance(is_main_entrance)

def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[Type[Location]] = None) -> None:
location_type: Optional[type[Location]] = None) -> None:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.
Expand Down Expand Up @@ -1112,7 +1153,7 @@ def create_exit(self, name: str) -> Entrance:
return exit_

def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
Expand All @@ -1122,10 +1163,14 @@ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
"""
if not isinstance(exits, Dict):
exits = dict.fromkeys(exits)
for connecting_region, name in exits.items():
self.connect(self.multiworld.get_region(connecting_region, self.player),
name,
rules[connecting_region] if rules and connecting_region in rules else None)
return [
self.connect(
self.multiworld.get_region(connecting_region, self.player),
name,
rules[connecting_region] if rules and connecting_region in rules else None,
)
for connecting_region, name in exits.items()
]

def __repr__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
Expand Down Expand Up @@ -1264,6 +1309,10 @@ def useful(self) -> bool:
def trap(self) -> bool:
return ItemClassification.trap in self.classification

@property
def filler(self) -> bool:
return not (self.advancement or self.useful or self.trap)

@property
def excludable(self) -> bool:
return not (self.advancement or self.useful)
Expand Down Expand Up @@ -1386,14 +1435,21 @@ def create_playthrough(self, create_paths: bool = True) -> None:

# second phase, sphere 0
removed_precollected: List[Item] = []
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
multiworld.precollected_items[item.player].remove(item)
multiworld.state.remove(item)
if not multiworld.can_beat_game():
multiworld.push_precollected(item)
else:
removed_precollected.append(item)

for precollected_items in multiworld.precollected_items.values():
# The list of items is mutated by removing one item at a time to determine if each item is required to beat
# the game, and re-adding that item if it was required, so a copy needs to be made before iterating.
for item in precollected_items.copy():
if not item.advancement:
continue
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
precollected_items.remove(item)
multiworld.state.remove(item)
if not multiworld.can_beat_game():
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
multiworld.push_precollected(item)
else:
removed_precollected.append(item)

# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
Expand Down Expand Up @@ -1532,7 +1588,7 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
[f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else
[f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n\nUnreachable Progression Items:\n\n')
outfile.write(
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))

Expand Down
17 changes: 15 additions & 2 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from MultiServer import CommandProcessor
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType)
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
from Utils import Version, stream_input, async_start
from worlds import network_data_package, AutoWorldRegister
import os
Expand Down Expand Up @@ -412,6 +412,7 @@ async def disconnect(self, allow_autoreconnect: bool = False):
await self.server.socket.close()
if self.server_task is not None:
await self.server_task
self.ui.update_hints()

async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """
Expand Down Expand Up @@ -551,7 +552,14 @@ async def shutdown(self):
await self.ui_task
if self.input_task:
self.input_task.cancel()


# Hints
def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None:
msg = {"cmd": "UpdateHint", "location": location, "player": finding_player}
if status is not None:
msg["status"] = status
async_start(self.send_msgs([msg]), name="update_hint")

# DataPackage
async def prepare_data_package(self, relevant_games: typing.Set[str],
remote_date_package_versions: typing.Dict[str, int],
Expand Down Expand Up @@ -710,6 +718,11 @@ def run_gui(self):

def run_cli(self):
if sys.stdin:
if sys.stdin.fileno() != 0:
from multiprocessing import parent_process
if parent_process():
return # ignore MultiProcessing pipe

# steam overlay breaks when starting console_loop
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")
Expand Down
Loading

0 comments on commit 09d3cef

Please sign in to comment.