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
SnolidIce authored Feb 15, 2024
2 parents 8643360 + 4d92025 commit d0c5b67
Show file tree
Hide file tree
Showing 17 changed files with 421 additions and 320 deletions.
20 changes: 13 additions & 7 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
import Options
import Utils

if typing.TYPE_CHECKING:
from worlds import AutoWorld


class Group(TypedDict, total=False):
name: str
game: str
world: auto_world
world: "AutoWorld.World"
players: Set[int]
item_pool: Set[str]
replacement_items: Dict[int, Optional[str]]
Expand Down Expand Up @@ -55,7 +58,7 @@ class MultiWorld():
plando_texts: List[Dict[str, str]]
plando_items: List[List[Dict[str, Any]]]
plando_connections: List
worlds: Dict[int, auto_world]
worlds: Dict[int, "AutoWorld.World"]
groups: Dict[int, Group]
regions: RegionManager
itempool: List[Item]
Expand Down Expand Up @@ -219,6 +222,8 @@ def get_all_ids(self) -> Tuple[int, ...]:
def add_group(self, name: str, game: str, players: Set[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

for group_id, group in self.groups.items():
if group["name"] == name:
group["players"] |= players
Expand Down Expand Up @@ -253,6 +258,8 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio

def set_options(self, args: Namespace) -> None:
# TODO - remove this section once all worlds use options dataclasses
from worlds import AutoWorld

all_keys: Set[str] = {key for player in self.player_ids for key in
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys:
Expand All @@ -270,6 +277,8 @@ def set_options(self, args: Namespace) -> None:
for option_key in options_dataclass.type_hints})

def set_item_links(self):
from worlds import AutoWorld

item_links = {}
replacement_prio = [False, True, None]
for player in self.player_ids:
Expand Down Expand Up @@ -1327,6 +1336,8 @@ def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, st
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))

def to_file(self, filename: str) -> None:
from worlds import AutoWorld

def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
res = getattr(self.multiworld.worlds[player].options, option_key)
display_name = getattr(option_obj, "display_name", option_key)
Expand Down Expand Up @@ -1450,8 +1461,3 @@ def get_seed(seed: Optional[int] = None) -> int:
random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1)
return seed


from worlds import AutoWorld

auto_world = AutoWorld.World
4 changes: 2 additions & 2 deletions Patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import ModuleUpdate
ModuleUpdate.update()

from worlds.Files import AutoPatchRegister, APDeltaPatch
from worlds.Files import AutoPatchRegister, APPatch


class RomMeta(TypedDict):
Expand All @@ -20,7 +20,7 @@ class RomMeta(TypedDict):
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
auto_handler = AutoPatchRegister.get_handler(patch_file)
if auto_handler:
handler: APDeltaPatch = auto_handler(patch_file)
handler: APPatch = auto_handler(patch_file)
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
handler.patch(target)
return {"server": handler.server,
Expand Down
38 changes: 23 additions & 15 deletions test/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from Generate import get_seed_name
from test.general import gen_steps
from worlds import AutoWorld
from worlds.AutoWorld import call_all
from worlds.AutoWorld import World, call_all

from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
from worlds.alttp.Items import ItemFactory
Expand Down Expand Up @@ -105,9 +105,15 @@ def _get_items_partial(self, item_pool, missing_item):

class WorldTestBase(unittest.TestCase):
options: typing.Dict[str, typing.Any] = {}
"""Define options that should be used when setting up this TestBase."""
multiworld: MultiWorld
"""The constructed MultiWorld instance after setup."""
world: World
"""The constructed World instance after setup."""
player: typing.ClassVar[int] = 1

game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
game: typing.ClassVar[str]
"""Define game name in subclass, example "Secret of Evermore"."""
auto_construct: typing.ClassVar[bool] = True
""" automatically set up a world for each test in this class """
memory_leak_tested: typing.ClassVar[bool] = False
Expand Down Expand Up @@ -150,18 +156,19 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None:
if not hasattr(self, "game"):
raise NotImplementedError("didn't define game name")
self.multiworld = MultiWorld(1)
self.multiworld.game[1] = self.game
self.multiworld.player_name = {1: "Tester"}
self.multiworld.game[self.player] = self.game
self.multiworld.player_name = {self.player: "Tester"}
self.multiworld.set_seed(seed)
self.multiworld.state = CollectionState(self.multiworld)
random.seed(self.multiworld.seed)
self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
setattr(args, name, {
1: option.from_any(self.options.get(name, getattr(option, "default")))
1: option.from_any(self.options.get(name, option.default))
})
self.multiworld.set_options(args)
self.world = self.multiworld.worlds[self.player]
for step in gen_steps:
call_all(self.multiworld, step)

Expand Down Expand Up @@ -220,19 +227,19 @@ def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:

def can_reach_location(self, location: str) -> bool:
"""Determines if the current state can reach the provided location name"""
return self.multiworld.state.can_reach(location, "Location", 1)
return self.multiworld.state.can_reach(location, "Location", self.player)

def can_reach_entrance(self, entrance: str) -> bool:
"""Determines if the current state can reach the provided entrance name"""
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
return self.multiworld.state.can_reach(entrance, "Entrance", self.player)

def can_reach_region(self, region: str) -> bool:
"""Determines if the current state can reach the provided region name"""
return self.multiworld.state.can_reach(region, "Region", 1)
return self.multiworld.state.can_reach(region, "Region", self.player)

def count(self, item_name: str) -> int:
"""Returns the amount of an item currently in state"""
return self.multiworld.state.count(item_name, 1)
return self.multiworld.state.count(item_name, self.player)

def assertAccessDependency(self,
locations: typing.List[str],
Expand All @@ -246,10 +253,11 @@ def assertAccessDependency(self,
self.collect_all_but(all_items, state)
if only_check_listed:
for location in locations:
self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
self.assertFalse(state.can_reach(location, "Location", self.player),
f"{location} is reachable without {all_items}")
else:
for location in self.multiworld.get_locations():
loc_reachable = state.can_reach(location, "Location", 1)
loc_reachable = state.can_reach(location, "Location", self.player)
self.assertEqual(loc_reachable, location.name not in locations,
f"{location.name} is reachable without {all_items}" if loc_reachable
else f"{location.name} is not reachable without {all_items}")
Expand All @@ -258,7 +266,7 @@ def assertAccessDependency(self,
for item in items:
state.collect(item)
for location in locations:
self.assertTrue(state.can_reach(location, "Location", 1),
self.assertTrue(state.can_reach(location, "Location", self.player),
f"{location} not reachable with {item_names}")
for item in items:
state.remove(item)
Expand All @@ -285,7 +293,7 @@ def test_all_state_can_reach_everything(self):
if not (self.run_default_tests and self.constructed):
return
with self.subTest("Game", game=self.game):
excluded = self.multiworld.worlds[1].options.exclude_locations.value
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
state = self.multiworld.get_all_state(False)
for location in self.multiworld.get_locations():
if location.name not in excluded:
Expand All @@ -302,7 +310,7 @@ def test_empty_state_can_reach_something(self):
return
with self.subTest("Game", game=self.game):
state = CollectionState(self.multiworld)
locations = self.multiworld.get_reachable_locations(state, 1)
locations = self.multiworld.get_reachable_locations(state, self.player)
self.assertGreater(len(locations), 0,
"Need to be able to reach at least one location to get started.")

Expand All @@ -328,7 +336,7 @@ def fulfills_accessibility() -> bool:
for location in sphere:
if location.item:
state.collect(location.item, True, location)
return self.multiworld.has_beaten_game(state, 1)
return self.multiworld.has_beaten_game(state, self.player)

with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
Expand Down
2 changes: 1 addition & 1 deletion worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def collect_item(self, state: "CollectionState", item: "Item", remove: bool = Fa
def get_pre_fill_items(self) -> List["Item"]:
return []

# following methods should not need to be overridden.
# these two methods can be extended for pseudo-items on state
def collect(self, state: "CollectionState", item: "Item") -> bool:
name = self.collect_item(state, item)
if name:
Expand Down
20 changes: 16 additions & 4 deletions worlds/Files.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import abc
import json
import zipfile
import os
Expand All @@ -15,7 +16,7 @@
del os


class AutoPatchRegister(type):
class AutoPatchRegister(abc.ABCMeta):
patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {}
file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {}

Expand Down Expand Up @@ -112,14 +113,25 @@ def get_manifest(self) -> Dict[str, Any]:
}


class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
"""An APContainer that additionally has delta.bsdiff4
class APPatch(APContainer, abc.ABC, metaclass=AutoPatchRegister):
"""
An abstract `APContainer` that defines the requirements for an object
to be used by the `Patch.create_rom_file` function.
"""
result_file_ending: str = ".sfc"

@abc.abstractmethod
def patch(self, target: str) -> None:
""" create the output file with the file name `target` """


class APDeltaPatch(APPatch):
"""An APPatch that additionally has delta.bsdiff4
containing a delta patch to get the desired file, often a rom."""

hash: Optional[str] # base checksum of source file
patch_file_ending: str = ""
delta: Optional[bytes] = None
result_file_ending: str = ".sfc"
source_data: bytes

def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
Expand Down
Loading

0 comments on commit d0c5b67

Please sign in to comment.