Skip to content

Commit

Permalink
Merge remote-tracking branch 'archipelago/main' into StardewValley/fi…
Browse files Browse the repository at this point in the history
…x-unresolved-reference-warning
  • Loading branch information
Jouramie committed Jan 23, 2025
2 parents 59d34dc + 5a42c70 commit e354828
Show file tree
Hide file tree
Showing 309 changed files with 22,928 additions and 2,946 deletions.
16 changes: 14 additions & 2 deletions .github/pyright-config.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
{
"include": [
"type_check.py",
"../BizHawkClient.py",
"../Patch.py",
"../test/general/test_groups.py",
"../test/general/test_helpers.py",
"../test/general/test_memory.py",
"../test/general/test_names.py",
"../test/multiworld/__init__.py",
"../test/multiworld/test_multiworlds.py",
"../test/netutils/__init__.py",
"../test/programs/__init__.py",
"../test/programs/test_multi_server.py",
"../test/utils/__init__.py",
"../test/webhost/test_descriptions.py",
"../worlds/AutoSNIClient.py",
"../Patch.py"
"type_check.py"
],

"exclude": [
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/scan-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ jobs:
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x ./llvm.sh
sudo ./llvm.sh 17
sudo ./llvm.sh 19
- name: Install scan-build command
run: |
sudo apt install clang-tools-17
sudo apt install clang-tools-19
- name: Get a recent python
uses: actions/setup-python@v5
with:
Expand All @@ -56,7 +56,7 @@ jobs:
- name: scan-build
run: |
source venv/bin/activate
scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
- name: Store report
if: failure()
uses: actions/upload-artifact@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/strict-type-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:

- name: "Install dependencies"
run: |
python -m pip install --upgrade pip pyright==1.1.358
python -m pip install --upgrade pip pyright==1.1.392.post0
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
- name: "pyright: strict check on specific files"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest "pytest-subtests<0.14.0" pytest-xdist
pip install pytest pytest-subtests pytest-xdist
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
python Launcher.py --update_settings # make sure host.yaml exists for tests
- name: Unittests
Expand Down
93 changes: 80 additions & 13 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import Utils

if TYPE_CHECKING:
from entrance_rando import ERPlacementState
from worlds import AutoWorld


Expand Down Expand Up @@ -426,12 +427,12 @@ def get_entrance(self, entrance_name: str, player: int) -> Entrance:
def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]

def get_all_state(self, use_cache: bool) -> CollectionState:
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
return cached.copy()

ret = CollectionState(self)
ret = CollectionState(self, allow_partial_entrances)

for item in self.itempool:
self.worlds[item.player].collect(ret, item)
Expand Down Expand Up @@ -717,10 +718,11 @@ class CollectionState():
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
stale: Dict[int, bool]
allow_partial_entrances: bool
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []

def __init__(self, parent: MultiWorld):
def __init__(self, parent: MultiWorld, allow_partial_entrances: bool = False):
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
self.multiworld = parent
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
Expand All @@ -729,6 +731,7 @@ def __init__(self, parent: MultiWorld):
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in parent.get_all_ids()}
self.allow_partial_entrances = allow_partial_entrances
for function in self.additional_init_functions:
function(self, parent)
for items in parent.precollected_items.values():
Expand Down Expand Up @@ -763,6 +766,8 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
Expand All @@ -788,7 +793,9 @@ def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue:
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
if self.allow_partial_entrances and not new_region:
continue
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
Expand All @@ -808,6 +815,7 @@ def copy(self) -> CollectionState:
ret.advancements = self.advancements.copy()
ret.path = self.path.copy()
ret.locations_checked = self.locations_checked.copy()
ret.allow_partial_entrances = self.allow_partial_entrances
for function in self.additional_copy_functions:
ret = function(self, ret)
return ret
Expand Down Expand Up @@ -972,26 +980,36 @@ def remove(self, item: Item):
self.stale[item.player] = True


class EntranceType(IntEnum):
ONE_WAY = 1
TWO_WAY = 2


class Entrance:
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
hide_path: bool = False
player: int
name: str
parent_region: Optional[Region]
connected_region: Optional[Region] = None
randomization_group: int
randomization_type: EntranceType
# LttP specific, TODO: should make a LttPEntrance
addresses = None
target = None

def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
self.name = name
self.parent_region = parent
self.player = player
self.randomization_group = randomization_group
self.randomization_type = randomization_type

def can_reach(self, state: CollectionState) -> bool:
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
if not self.hide_path and self not in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True

Expand All @@ -1003,6 +1021,32 @@ def connect(self, region: Region, addresses: Any = None, target: Any = None) ->
self.addresses = addresses
region.entrances.append(self)

def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
"""
Determines whether this is a valid source transition, that is, whether the entrance
randomizer is allowed to pair it to place any other regions. By default, this is the
same as a reachability check, but can be modified by Entrance implementations to add
other restrictions based on the placement state.
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
return self.can_reach(er_state.collection_state)

def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool:
"""
Determines whether a given Entrance is a valid target transition, that is, whether
the entrance randomizer is allowed to pair this Entrance to that Entrance. By default,
only allows connection between entrances of the same type (one ways only go to one ways,
two ways always go to two ways) and prevents connecting an exit to itself in coupled mode.
:param other: The proposed Entrance to connect to
:param dead_end: Whether the other entrance considered a dead end by Entrance randomization
:param er_state: The current (partial) state of the ongoing entrance randomization
"""
# the implementation of coupled causes issues for self-loops since the reverse entrance will be the
# same as the forward entrance. In uncoupled they are ok.
return self.randomization_type == other.randomization_type and (not er_state.coupled or self.name != other.name)

def __repr__(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 @@ -1152,6 +1196,16 @@ def create_exit(self, name: str) -> Entrance:
self.exits.append(exit_)
return exit_

def create_er_target(self, name: str) -> Entrance:
"""
Creates and returns an Entrance object as an entrance to this region
:param name: name of the Entrance being created
"""
entrance = self.entrance_type(self.player, name)
entrance.connect(self)
return entrance

def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
"""
Expand Down Expand Up @@ -1254,13 +1308,26 @@ def hint_text(self) -> str:


class ItemClassification(IntFlag):
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
progression = 0b0001 # Item that is logically relevant
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
trap = 0b0100 # detrimental item
skip_balancing = 0b1000 # should technically never occur on its own
# Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
filler = 0b0000
""" aka trash, as in filler items like ammo, currency etc """

progression = 0b0001
""" Item that is logically relevant.
Protects this item from being placed on excluded or unreachable locations. """

useful = 0b0010
""" Item that is especially useful.
Protects this item from being placed on excluded or unreachable locations.
When combined with another flag like "progression", it means "an especially useful progression item". """

trap = 0b0100
""" Item that is detrimental in some way. """

skip_balancing = 0b1000
""" should technically never occur on its own
Item that is logically relevant, but progression balancing should not touch.
Typically currency or other counted items. """

progression_skip_balancing = 0b1001 # only progression gets balanced

def as_flag(self) -> int:
Expand Down
46 changes: 35 additions & 11 deletions CommonClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

if typing.TYPE_CHECKING:
import kvui
import argparse

logger = logging.getLogger("Client")

Expand Down Expand Up @@ -459,6 +460,13 @@ async def send_connect(self, **kwargs: typing.Any) -> None:
await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])

async def check_locations(self, locations: typing.Collection[int]) -> set[int]:
"""Send new location checks to the server. Returns the set of actually new locations that were sent."""
locations = set(locations) & self.missing_locations
if locations:
await self.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(locations)}])
return locations

async def console_input(self) -> str:
if self.ui:
self.ui.focus_textinput()
Expand Down Expand Up @@ -1041,6 +1049,32 @@ def get_base_parser(description: typing.Optional[str] = None):
return parser


def handle_url_arg(args: "argparse.Namespace",
parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace":
"""
Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient
If alternate data is required the urlparse response is saved back to args.url if valid
"""
if not args.url:
return args

url = urllib.parse.urlparse(args.url)
if url.scheme != "archipelago":
if not parser:
parser = get_base_parser()
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
return args

args.url = url
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)

return args


def run_as_textclient(*args):
class TextContext(CommonContext):
# Text Mode to use !hint and such with games that have no text entry
Expand Down Expand Up @@ -1082,17 +1116,7 @@ async def main(args):
parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args)

# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
if args.url:
url = urllib.parse.urlparse(args.url)
if url.scheme == "archipelago":
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
args = handle_url_arg(args, parser=parser)

# use colorama to display colored text highlighting on windows
colorama.init()
Expand Down
Loading

0 comments on commit e354828

Please sign in to comment.