Skip to content

Commit

Permalink
Wargroove 2: Merge PR changes into 1.1
Browse files Browse the repository at this point in the history
  • Loading branch information
FlySniper committed Sep 5, 2024
2 parents 01be6c2 + 7d21172 commit 1e46141
Show file tree
Hide file tree
Showing 104 changed files with 3,852 additions and 3,079 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ jobs:
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
- {version: '3.12'}
include:
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.11'} # current
- python: {version: '3.12'} # current
os: windows-latest
- python: {version: '3.11'} # current
- python: {version: '3.12'} # current
os: macos-latest

steps:
Expand Down Expand Up @@ -70,7 +71,7 @@ jobs:
os:
- ubuntu-latest
python:
- {version: '3.11'} # current
- {version: '3.12'} # current

steps:
- uses: actions/checkout@v4
Expand Down
91 changes: 45 additions & 46 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
TypedDict, Union, Type, ClassVar
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, Type)

from typing_extensions import NotRequired, TypedDict

import NetUtils
import Options
Expand All @@ -22,16 +24,16 @@
from worlds import AutoWorld


class Group(TypedDict, total=False):
class Group(TypedDict):
name: str
game: str
world: "AutoWorld.World"
players: Set[int]
item_pool: Set[str]
replacement_items: Dict[int, Optional[str]]
local_items: Set[str]
non_local_items: Set[str]
link_replacement: bool
players: AbstractSet[int]
item_pool: NotRequired[Set[str]]
replacement_items: NotRequired[Dict[int, Optional[str]]]
local_items: NotRequired[Set[str]]
non_local_items: NotRequired[Set[str]]
link_replacement: NotRequired[bool]


class ThreadBarrierProxy:
Expand All @@ -48,6 +50,11 @@ def __getattr__(self, name: str) -> Any:
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")


class HasNameAndPlayer(Protocol):
name: str
player: int


class MultiWorld():
debug_types = False
player_name: Dict[int, str]
Expand Down Expand Up @@ -156,7 +163,7 @@ def __init__(self, players: int):
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}

for player in range(1, players + 1):
def set_player_attr(attr, val):
def set_player_attr(attr: str, val) -> None:
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('plando_items', [])
set_player_attr('plando_texts', {})
Expand All @@ -165,13 +172,13 @@ def set_player_attr(attr, val):
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 "
"world's random object instead (usually self.random)")
"world's random object instead (usually self.random)")
self.plando_options = PlandoOptions.none

def get_all_ids(self) -> Tuple[int, ...]:
return self.player_ids + tuple(self.groups)

def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
"""Create a group with name and return the assigned player ID and group.
If a group of this name already exists, the set of players is extended instead of creating a new one."""
from worlds import AutoWorld
Expand All @@ -195,7 +202,7 @@ def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tu

return new_id, new_group

def get_player_groups(self, player) -> Set[int]:
def get_player_groups(self, player: int) -> Set[int]:
return {group_id for group_id, group in self.groups.items() if player in group["players"]}

def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
Expand Down Expand Up @@ -258,7 +265,7 @@ def set_item_links(self):
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
}

for name, item_link in item_links.items():
for _name, item_link in item_links.items():
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
pool = set()
local_items = set()
Expand Down Expand Up @@ -388,7 +395,7 @@ def get_game_worlds(self, game_name: str):
return tuple(world for player, world in self.worlds.items() if
player not in self.groups and self.game[player] == game_name)

def get_name_string_for_object(self, obj) -> str:
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'

def get_player_name(self, player: int) -> str:
Expand Down Expand Up @@ -439,7 +446,7 @@ 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, resolve_group_locations: bool = False) -> List[Location]:
def find_item_locations(self, item: 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
Expand All @@ -448,7 +455,7 @@ def find_item_locations(self, item, player: int, resolve_group_locations: bool =
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:
def find_item(self, item: str, 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)

Expand Down Expand Up @@ -806,7 +813,7 @@ def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool:
if found >= count:
return True
return False

def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
Ignores duplicates of the same item."""
Expand All @@ -821,7 +828,7 @@ def has_from_list_unique(self, items: Iterable[str], player: int, count: int) ->
def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)

def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
Expand Down Expand Up @@ -900,7 +907,7 @@ class Entrance:
addresses = None
target = None

def __init__(self, player: int, name: str = '', parent: Region = None):
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
self.name = name
self.parent_region = parent
self.player = player
Expand All @@ -920,9 +927,6 @@ def connect(self, region: Region, addresses: Any = None, target: Any = None) ->
region.entrances.append(self)

def __repr__(self):
return self.__str__()

def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'

Expand Down Expand Up @@ -1048,7 +1052,7 @@ def add_locations(self, locations: Dict[str, Optional[int]],
self.locations.append(location_type(self.player, location, address, self))

def connect(self, connecting_region: Region, name: Optional[str] = None,
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
"""
Connects this Region to another Region, placing the provided rule on the connection.
Expand Down Expand Up @@ -1088,9 +1092,6 @@ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules[connecting_region] if rules and connecting_region in rules else None)

def __repr__(self):
return self.__str__()

def __str__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'


Expand All @@ -1109,9 +1110,9 @@ class Location:
locked: bool = False
show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda state, item: False)
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True)
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
item: Optional[Item] = None

def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
Expand All @@ -1120,11 +1121,15 @@ def __init__(self, player: int, name: str = '', address: Optional[int] = None, p
self.address = address
self.parent_region = parent

def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))))
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
return ((
self.always_allow(state, item)
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
) or (
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
and self.item_rule(item)
and (not check_access or self.can_reach(state))
))

def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
Expand All @@ -1139,9 +1144,6 @@ def place_locked_item(self, item: Item):
self.locked = True

def __repr__(self):
return self.__str__()

def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'

Expand All @@ -1163,7 +1165,7 @@ def is_event(self) -> bool:
@property
def native_item(self) -> bool:
"""Returns True if the item in this location matches game."""
return self.item and self.item.game == self.game
return self.item is not None and self.item.game == self.game

@property
def hint_text(self) -> str:
Expand Down Expand Up @@ -1246,9 +1248,6 @@ def __hash__(self) -> int:
return hash((self.name, self.player))

def __repr__(self) -> str:
return self.__str__()

def __str__(self) -> str:
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
return self.location.parent_region.multiworld.get_name_string_for_object(self)
return f"{self.name} (Player {self.player})"
Expand Down Expand Up @@ -1326,9 +1325,9 @@ def create_playthrough(self, create_paths: bool = True) -> None:

# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later = {}
restore_later: Dict[Location, Item] = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete = set()
to_delete: Set[Location] = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
Expand All @@ -1346,7 +1345,7 @@ def create_playthrough(self, create_paths: bool = True) -> None:
sphere -= to_delete

# second phase, sphere 0
removed_precollected = []
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)
Expand Down Expand Up @@ -1499,9 +1498,9 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:

if self.paths:
outfile.write('\n\nPaths:\n\n')
path_listings = []
path_listings: List[str] = []
for location, path in sorted(self.paths.items()):
path_lines = []
path_lines: List[str] = []
for region, exit in path:
if exit is not None:
path_lines.append("{} -> {}".format(region, exit))
Expand Down
2 changes: 1 addition & 1 deletion Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights)

Expand Down
6 changes: 3 additions & 3 deletions ModuleUpdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ def update(yes: bool = False, force: bool = False) -> None:
if not update_ran:
update_ran = True

install_pkg_resources(yes=yes)
import pkg_resources

if force:
update_command()
return

install_pkg_resources(yes=yes)
import pkg_resources

prev = "" # if a line ends in \ we store here and merge later
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
Expand Down
24 changes: 24 additions & 0 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ def update_dict(dictionary, entries):
return dictionary


def queue_gc():
import gc
from threading import Thread

gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None)
def async_collect():
time.sleep(2)
setattr(queue_gc, "_thread", None)
gc.collect()
if not gc_thread:
gc_thread = Thread(target=async_collect)
setattr(queue_gc, "_thread", gc_thread)
gc_thread.start()


# functions callable on storable data on the server by clients
modify_functions = {
# generic:
Expand Down Expand Up @@ -551,6 +566,9 @@ def get_datetime_second():
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
else:
self.save_dirty = False
if not atexit_save: # if atexit is used, that keeps a reference anyway
queue_gc()

self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
self.auto_saver_thread.start()

Expand Down Expand Up @@ -1203,6 +1221,10 @@ def _cmd_countdown(self, seconds: str = "10") -> bool:
timer = int(seconds, 10)
except ValueError:
timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")

async_start(countdown(self.ctx, timer))
return True

Expand Down Expand Up @@ -2039,6 +2061,8 @@ def _cmd_send_multiple(self, amount: typing.Union[int, str], player_name: str, *
item_name, usable, response = get_intended_text(item_name, names)
if usable:
amount: int = int(amount)
if amount > 100:
raise ValueError(f"{amount} is invalid. Maximum is 100.")
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
send_items_to(self.ctx, team, slot, *new_items)

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Currently, the following games are supported:
* Old School Runescape
* Kingdom Hearts 1
* Mega Man 2
* Yacht Dice
* Wargroove 2

For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Expand Down
Loading

0 comments on commit 1e46141

Please sign in to comment.