Compare commits

..

1 Commits

Author SHA1 Message Date
Fabian Dill
8fdad3cbba CommonClient: handle Firefox removing the password part of userinfo 2024-01-28 06:47:34 +01:00
1226 changed files with 25399 additions and 105314 deletions

31
.github/labeler.yml vendored
View File

@@ -1,31 +0,0 @@
'is: documentation':
- changed-files:
- all-globs-to-all-files: '{**/docs/**,**/README.md}'
'affects: webhost':
- changed-files:
- all-globs-to-any-file: 'WebHost.py'
- all-globs-to-any-file: 'WebHostLib/**/*'
'affects: core':
- changed-files:
- all-globs-to-any-file:
- '!*Client.py'
- '!README.md'
- '!LICENSE'
- '!*.yml'
- '!.gitignore'
- '!**/docs/**'
- '!typings/kivy/**'
- '!test/**'
- '!data/**'
- '!.run/**'
- '!.github/**'
- '!worlds_disabled/**'
- '!worlds/**'
- '!WebHost.py'
- '!WebHostLib/**'
- any-glob-to-any-file: # exceptions to the above rules of "stuff that isn't core"
- 'worlds/generic/**/*.py'
- 'worlds/*.py'
- 'CommonClient.py'

View File

@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: "Determine modified files (pull_request)" - name: "Determine modified files (pull_request)"
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
@@ -50,7 +50,7 @@ jobs:
run: | run: |
echo "diff=." >> $GITHUB_ENV echo "diff=." >> $GITHUB_ENV
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
if: env.diff != '' if: env.diff != ''
with: with:
python-version: 3.8 python-version: 3.8

View File

@@ -8,13 +8,11 @@ on:
- '.github/workflows/build.yml' - '.github/workflows/build.yml'
- 'setup.py' - 'setup.py'
- 'requirements.txt' - 'requirements.txt'
- '*.iss'
pull_request: pull_request:
paths: paths:
- '.github/workflows/build.yml' - '.github/workflows/build.yml'
- 'setup.py' - 'setup.py'
- 'requirements.txt' - 'requirements.txt'
- '*.iss'
workflow_dispatch: workflow_dispatch:
env: env:
@@ -27,9 +25,9 @@ jobs:
build-win-py38: # RCs will still be built and signed by hand build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install python - name: Install python
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: '3.8' python-version: '3.8'
- name: Download run-time dependencies - name: Download run-time dependencies
@@ -48,42 +46,25 @@ jobs:
cd build cd build
Rename-Item "exe.$NAME" Archipelago Rename-Item "exe.$NAME" Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago 7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name
- name: Store 7z - name: Store 7z
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.ZIP_NAME }} name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }} path: dist/${{ env.ZIP_NAME }}
retention-days: 7 # keep for 7 days, should be enough retention-days: 7 # keep for 7 days, should be enough
- name: Build Setup
run: |
& "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL
if ( $? -eq $false ) {
Write-Error "Building setup failed!"
exit 1
}
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
$SETUP_NAME=$contents[0].Name
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
- name: Store Setup
uses: actions/upload-artifact@v4
with:
name: ${{ env.SETUP_NAME }}
path: setups/${{ env.SETUP_NAME }}
retention-days: 7 # keep for 7 days, should be enough
build-ubuntu2004: build-ubuntu2004:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
# - copy code below to release.yml - # - copy code below to release.yml -
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install base dependencies - name: Install base dependencies
run: | run: |
sudo apt update sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python - name: Get a recent python
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
- name: Install build-time dependencies - name: Install build-time dependencies
@@ -119,13 +100,13 @@ jobs:
source venv/bin/activate source venv/bin/activate
python setup.py build_exe --yes python setup.py build_exe --yes
- name: Store AppImage - name: Store AppImage
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.APPIMAGE_NAME }} name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }} path: dist/${{ env.APPIMAGE_NAME }}
retention-days: 7 retention-days: 7
- name: Store .tar.gz - name: Store .tar.gz
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{ env.TAR_NAME }} name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }} path: dist/${{ env.TAR_NAME }}

View File

@@ -43,7 +43,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View File

@@ -1,46 +0,0 @@
name: Label Pull Request
on:
pull_request_target:
types: ['opened', 'reopened', 'synchronize', 'ready_for_review', 'converted_to_draft', 'closed']
branches: ['main']
permissions:
contents: read
pull-requests: write
jobs:
labeler:
name: 'Apply content-based labels'
if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
sync-labels: false
peer_review:
name: 'Apply peer review label'
needs: labeler
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened' ||
github.event.action == 'ready_for_review') && !github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: 'Add label'
run: "gh pr edit \"$PR_URL\" --add-label 'waiting-on: peer-review'"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
unblock_draft_prs:
name: 'Remove waiting-on labels'
needs: labeler
if: github.event.action == 'converted_to_draft' || github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: 'Remove labels'
run: |-
gh pr edit "$PR_URL" --remove-label 'waiting-on: peer-review' \
--remove-label 'waiting-on: core-review' \
--remove-label 'waiting-on: world-maintainer' \
--remove-label 'waiting-on: author'
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -18,7 +18,7 @@ jobs:
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z" run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
with: with:
draft: true # don't publish right away, especially since windows build is added by hand draft: true # don't publish right away, especially since windows build is added by hand
prerelease: false prerelease: false
@@ -35,14 +35,14 @@ jobs:
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# - code below copied from build.yml - # - code below copied from build.yml -
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install base dependencies - name: Install base dependencies
run: | run: |
sudo apt update sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0 sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python - name: Get a recent python
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
- name: Install build-time dependencies - name: Install build-time dependencies
@@ -74,7 +74,7 @@ jobs:
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - code above copied from build.yml - # - code above copied from build.yml -
- name: Add to Release - name: Add to Release
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
with: with:
draft: true # see above draft: true # see above
prerelease: false prerelease: false

View File

@@ -1,65 +0,0 @@
name: Native Code Static Analysis
on:
push:
paths:
- '**.c'
- '**.cc'
- '**.cpp'
- '**.cxx'
- '**.h'
- '**.hh'
- '**.hpp'
- '**.pyx'
- 'setup.py'
- 'requirements.txt'
- '.github/workflows/scan-build.yml'
pull_request:
paths:
- '**.c'
- '**.cc'
- '**.cpp'
- '**.cxx'
- '**.h'
- '**.hh'
- '**.hpp'
- '**.pyx'
- 'setup.py'
- 'requirements.txt'
- '.github/workflows/scan-build.yml'
jobs:
scan-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install newer Clang
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x ./llvm.sh
sudo ./llvm.sh 17
- name: Install scan-build command
run: |
sudo apt install clang-tools-17
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip -r requirements.txt
- 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
- name: Store report
if: failure()
uses: actions/upload-artifact@v4
with:
name: scan-build-reports
path: scan-build-reports

View File

@@ -46,9 +46,9 @@ jobs:
os: macos-latest os: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python.version }} - name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python.version }} python-version: ${{ matrix.python.version }}
- name: Install dependencies - name: Install dependencies

View File

@@ -18,14 +18,11 @@ import NetUtils
import Options import Options
import Utils import Utils
if typing.TYPE_CHECKING:
from worlds import AutoWorld
class Group(TypedDict, total=False): class Group(TypedDict, total=False):
name: str name: str
game: str game: str
world: "AutoWorld.World" world: auto_world
players: Set[int] players: Set[int]
item_pool: Set[str] item_pool: Set[str]
replacement_items: Dict[int, Optional[str]] replacement_items: Dict[int, Optional[str]]
@@ -58,7 +55,7 @@ class MultiWorld():
plando_texts: List[Dict[str, str]] plando_texts: List[Dict[str, str]]
plando_items: List[List[Dict[str, Any]]] plando_items: List[List[Dict[str, Any]]]
plando_connections: List plando_connections: List
worlds: Dict[int, "AutoWorld.World"] worlds: Dict[int, auto_world]
groups: Dict[int, Group] groups: Dict[int, Group]
regions: RegionManager regions: RegionManager
itempool: List[Item] itempool: List[Item]
@@ -85,7 +82,7 @@ class MultiWorld():
game: Dict[int, str] game: Dict[int, str]
random: random.Random random: random.Random
per_slot_randoms: Utils.DeprecateDict[int, random.Random] per_slot_randoms: Dict[int, random.Random]
"""Deprecated. Please use `self.random` instead.""" """Deprecated. Please use `self.random` instead."""
class AttributeProxy(): class AttributeProxy():
@@ -110,14 +107,10 @@ class MultiWorld():
return self return self
def append(self, region: Region): def append(self, region: Region):
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region self.region_cache[region.player][region.name] = region
def extend(self, regions: Iterable[Region]): def extend(self, regions: Iterable[Region]):
for region in regions: for region in regions:
assert region.name not in self.region_cache[region.player], \
f"{region.name} already exists in region cache."
self.region_cache[region.player][region.name] = region self.region_cache[region.player][region.name] = region
def add_group(self, new_id: int): def add_group(self, new_id: int):
@@ -163,11 +156,11 @@ class MultiWorld():
self.fix_trock_doors = self.AttributeProxy( self.fix_trock_doors = self.AttributeProxy(
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
self.fix_skullwoods_exit = self.AttributeProxy( self.fix_skullwoods_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_palaceofdarkness_exit = self.AttributeProxy( self.fix_palaceofdarkness_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_trock_exit = self.AttributeProxy( self.fix_trock_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
for player in range(1, players + 1): for player in range(1, players + 1):
def set_player_attr(attr, val): def set_player_attr(attr, val):
@@ -217,8 +210,7 @@ class MultiWorld():
set_player_attr('game', "A Link to the Past") set_player_attr('game', "A Link to the Past")
set_player_attr('completion_condition', lambda state: True) set_player_attr('completion_condition', lambda state: True)
self.worlds = {} self.worlds = {}
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the " self.per_slot_randoms = {}
"world's random object instead (usually self.random)")
self.plando_options = PlandoOptions.none self.plando_options = PlandoOptions.none
def get_all_ids(self) -> Tuple[int, ...]: def get_all_ids(self) -> Tuple[int, ...]:
@@ -227,8 +219,6 @@ class MultiWorld():
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]: 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. """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.""" 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(): for group_id, group in self.groups.items():
if group["name"] == name: if group["name"] == name:
group["players"] |= players group["players"] |= players
@@ -252,18 +242,17 @@ class MultiWorld():
return {group_id for group_id, group in self.groups.items() if player in group["players"]} 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): def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
assert not self.worlds, "seed needs to be initialized before Worlds"
self.seed = get_seed(seed) self.seed = get_seed(seed)
if secure: if secure:
self.secure() self.secure()
else: else:
self.random.seed(self.seed) self.random.seed(self.seed)
self.seed_name = name if name else str(self.seed) self.seed_name = name if name else str(self.seed)
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: def set_options(self, args: Namespace) -> None:
# TODO - remove this section once all worlds use options dataclasses # 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 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} AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys: for option_key in all_keys:
@@ -275,13 +264,12 @@ class MultiWorld():
for player in self.player_ids: for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player) self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints}) for option_key in options_dataclass.type_hints})
def set_item_links(self): def set_item_links(self):
from worlds import AutoWorld
item_links = {} item_links = {}
replacement_prio = [False, True, None] replacement_prio = [False, True, None]
for player in self.player_ids: for player in self.player_ids:
@@ -584,10 +572,9 @@ class MultiWorld():
def location_condition(location: Location): def location_condition(location: Location):
"""Determine if this location has to be accessible, location is already filtered by location_relevant""" """Determine if this location has to be accessible, location is already filtered by location_relevant"""
if location.player in players["locations"] or (location.item and location.item.player not in if location.player in players["minimal"]:
players["minimal"]): return False
return True return True
return False
def location_relevant(location: Location): def location_relevant(location: Location):
"""Determine if this location is relevant to sweep.""" """Determine if this location is relevant to sweep."""
@@ -716,23 +703,14 @@ class CollectionState():
assert isinstance(player, int), "can_reach: player is required if spot is str" assert isinstance(player, int), "can_reach: player is required if spot is str"
# try to resolve a name # try to resolve a name
if resolution_hint == 'Location': if resolution_hint == 'Location':
return self.can_reach_location(spot, player) spot = self.multiworld.get_location(spot, player)
elif resolution_hint == 'Entrance': elif resolution_hint == 'Entrance':
return self.can_reach_entrance(spot, player) spot = self.multiworld.get_entrance(spot, player)
else: else:
# default to Region # default to Region
return self.can_reach_region(spot, player) spot = self.multiworld.get_region(spot, player)
return spot.can_reach(self) return spot.can_reach(self)
def can_reach_location(self, spot: str, player: int) -> bool:
return self.multiworld.get_location(spot, player).can_reach(self)
def can_reach_entrance(self, spot: str, player: int) -> bool:
return self.multiworld.get_entrance(spot, player).can_reach(self)
def can_reach_region(self, spot: str, player: int) -> bool:
return self.multiworld.get_region(spot, player).can_reach(self)
def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None: def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None: if locations is None:
locations = self.multiworld.get_filled_locations() locations = self.multiworld.get_filled_locations()
@@ -845,8 +823,8 @@ class Entrance:
return self.__str__() return self.__str__()
def __str__(self): def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region else None world = 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})' return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
class Region: class Region:
@@ -889,8 +867,6 @@ class Region:
del(self.region_manager.location_cache[location.player][location.name]) del(self.region_manager.location_cache[location.player][location.name])
def insert(self, index: int, value: Location) -> None: def insert(self, index: int, value: Location) -> None:
assert value.name not in self.region_manager.location_cache[value.player], \
f"{value.name} already exists in the location cache."
self._list.insert(index, value) self._list.insert(index, value)
self.region_manager.location_cache[value.player][value.name] = value self.region_manager.location_cache[value.player][value.name] = value
@@ -901,8 +877,6 @@ class Region:
del(self.region_manager.entrance_cache[entrance.player][entrance.name]) del(self.region_manager.entrance_cache[entrance.player][entrance.name])
def insert(self, index: int, value: Entrance) -> None: def insert(self, index: int, value: Entrance) -> None:
assert value.name not in self.region_manager.entrance_cache[value.player], \
f"{value.name} already exists in the entrance cache."
self._list.insert(index, value) self._list.insert(index, value)
self.region_manager.entrance_cache[value.player][value.name] = value self.region_manager.entrance_cache[value.player][value.name] = value
@@ -1032,7 +1006,7 @@ class Location:
locked: bool = False locked: bool = False
show_in_spoiler: bool = True show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda state, item: False) always_allow = staticmethod(lambda item, state: False)
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True) item_rule = staticmethod(lambda item: True)
item: Optional[Item] = None item: Optional[Item] = None
@@ -1066,8 +1040,8 @@ class Location:
return self.__str__() return self.__str__()
def __str__(self): def __str__(self):
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None world = 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})' return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
def __hash__(self): def __hash__(self):
return hash((self.name, self.player)) return hash((self.name, self.player))
@@ -1201,7 +1175,7 @@ class Spoiler:
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction} {"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
def create_playthrough(self, create_paths: bool = True) -> None: def create_playthrough(self, create_paths: bool = True) -> None:
"""Destructive to the multiworld while it is run, damage gets repaired afterwards.""" """Destructive to the world while it is run, damage gets repaired afterwards."""
from itertools import chain from itertools import chain
# get locations containing progress items # get locations containing progress items
multiworld = self.multiworld multiworld = self.multiworld
@@ -1288,12 +1262,12 @@ class Spoiler:
for location in sphere: for location in sphere:
state.collect(location.item, True, location) state.collect(location.item, True, location)
required_locations -= sphere
collection_spheres.append(sphere) collection_spheres.append(sphere)
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations)) len(sphere), len(required_locations))
required_locations -= sphere
if not sphere: if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}') raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
@@ -1352,8 +1326,6 @@ class Spoiler:
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player)) get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
def to_file(self, filename: str) -> None: def to_file(self, filename: str) -> None:
from worlds import AutoWorld
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
res = getattr(self.multiworld.worlds[player].options, option_key) res = getattr(self.multiworld.worlds[player].options, option_key)
display_name = getattr(option_obj, "display_name", option_key) display_name = getattr(option_obj, "display_name", option_key)
@@ -1477,3 +1449,8 @@ def get_seed(seed: Optional[int] = None) -> int:
random.seed(None) random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1) return random.randint(0, pow(10, seeddigits) - 1)
return seed return seed
from worlds import AutoWorld
auto_world = AutoWorld.World

View File

@@ -20,8 +20,8 @@ if __name__ == "__main__":
Utils.init_logging("TextClient", exception_logger="Client") Utils.init_logging("TextClient", exception_logger="Client")
from MultiServer import CommandProcessor from MultiServer import CommandProcessor
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot, from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes) ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
from Utils import Version, stream_input, async_start from Utils import Version, stream_input, async_start
from worlds import network_data_package, AutoWorldRegister from worlds import network_data_package, AutoWorldRegister
import os import os
@@ -72,16 +72,9 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_received(self) -> bool: def _cmd_received(self) -> bool:
"""List all received items""" """List all received items"""
item: NetworkItem self.output(f'{len(self.ctx.items_received)} received items:')
self.output(f'{len(self.ctx.items_received)} received items, sorted by time:')
for index, item in enumerate(self.ctx.items_received, 1): for index, item in enumerate(self.ctx.items_received, 1):
parts = [] self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
add_json_item(parts, item.item, self.ctx.slot, item.flags)
add_json_text(parts, " from ")
add_json_location(parts, item.location, item.player)
add_json_text(parts, " by ")
add_json_text(parts, item.player, type=JSONTypes.player_id)
self.ctx.on_print_json({"data": parts, "cmd": "PrintJSON"})
return True return True
def _cmd_missing(self, filter_text = "") -> bool: def _cmd_missing(self, filter_text = "") -> bool:
@@ -122,15 +115,6 @@ class ClientCommandProcessor(CommandProcessor):
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id: for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name) self.output(item_name)
def _cmd_item_groups(self):
"""List all item group names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing item groups.")
return False
self.output(f"Item Group Names for {self.ctx.game}")
for group_name in AutoWorldRegister.world_types[self.ctx.game].item_name_groups:
self.output(group_name)
def _cmd_locations(self): def _cmd_locations(self):
"""List all location names for the currently running game.""" """List all location names for the currently running game."""
if not self.ctx.game: if not self.ctx.game:
@@ -140,15 +124,6 @@ class ClientCommandProcessor(CommandProcessor):
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id: for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name) self.output(location_name)
def _cmd_location_groups(self):
"""List all location group names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing location groups.")
return False
self.output(f"Location Group Names for {self.ctx.game}")
for group_name in AutoWorldRegister.world_types[self.ctx.game].location_name_groups:
self.output(group_name)
def _cmd_ready(self): def _cmd_ready(self):
"""Send ready status to server.""" """Send ready status to server."""
self.ctx.ready = not self.ctx.ready self.ctx.ready = not self.ctx.ready
@@ -637,19 +612,23 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
address = f"ws://{address}" if "://" not in address \ address = f"ws://{address}" if "://" not in address \
else address.replace("archipelago://", "ws://") else address.replace("archipelago://", "ws://")
uri = urllib.parse.urlparse(address)
if uri.username and uri.password is None:
# Fix for Firefox stripping empty password https://bugzilla.mozilla.org/show_bug.cgi?id=1876952
address = address.replace("@", ":@")
server_url = urllib.parse.urlparse(address) server_url = urllib.parse.urlparse(address)
if server_url.username: if server_url.username:
ctx.username = server_url.username ctx.username = server_url.username
if server_url.password: if server_url.password:
ctx.password = server_url.password ctx.password = server_url.password
port = server_url.port or 38281
def reconnect_hint() -> str: def reconnect_hint() -> str:
return ", type /connect to reconnect" if ctx.server_address else "" return ", type /connect to reconnect" if ctx.server_address else ""
logger.info(f'Connecting to Archipelago server at {address}') logger.info(f'Connecting to Archipelago server at {address}')
try: try:
port = server_url.port or 38281 # raises ValueError if invalid
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None, socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
ssl=get_ssl_context() if address.startswith("wss://") else None) ssl=get_ssl_context() if address.startswith("wss://") else None)
if ctx.ui is not None: if ctx.ui is not None:
@@ -758,10 +737,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
elif cmd == 'ConnectionRefused': elif cmd == 'ConnectionRefused':
errors = args["errors"] errors = args["errors"]
if 'InvalidSlot' in errors: if 'InvalidSlot' in errors:
ctx.disconnected_intentionally = True
ctx.event_invalid_slot() ctx.event_invalid_slot()
elif 'InvalidGame' in errors: elif 'InvalidGame' in errors:
ctx.disconnected_intentionally = True
ctx.event_invalid_game() ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors: elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible. ' raise Exception('Server reported your client version as incompatible. '
@@ -968,5 +945,4 @@ def run_as_textclient():
if __name__ == '__main__': if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
run_as_textclient() run_as_textclient()

226
Fill.py
View File

@@ -27,12 +27,12 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
return new_state return new_state
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location], def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None: allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
""" """
:param multiworld: Multiworld to be filled. :param world: Multiworld to be filled.
:param base_state: State assumed before fill. :param base_state: State assumed before fill.
:param locations: Locations to be filled with item_pool :param locations: Locations to be filled with item_pool
:param item_pool: Items to fill into the locations :param item_pool: Items to fill into the locations
@@ -68,7 +68,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
maximum_exploration_state = sweep_from_pool( maximum_exploration_state = sweep_from_pool(
base_state, item_pool + unplaced_items) base_state, item_pool + unplaced_items)
has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state) has_beaten_game = world.has_beaten_game(maximum_exploration_state)
while items_to_place: while items_to_place:
# if we have run out of locations to fill,break out of this loop # if we have run out of locations to fill,break out of this loop
@@ -80,8 +80,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
spot_to_fill: typing.Optional[Location] = None spot_to_fill: typing.Optional[Location] = None
# if minimal accessibility, only check whether location is reachable if game not beatable # if minimal accessibility, only check whether location is reachable if game not beatable
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal: if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state, perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) \ item_to_place.player) \
if single_player_placement else not has_beaten_game if single_player_placement else not has_beaten_game
else: else:
@@ -122,11 +122,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
# Verify placing this item won't reduce available locations, which would be a useless swap. # Verify placing this item won't reduce available locations, which would be a useless swap.
prev_state = swap_state.copy() prev_state = swap_state.copy()
prev_loc_count = len( prev_loc_count = len(
multiworld.get_reachable_locations(prev_state)) world.get_reachable_locations(prev_state))
swap_state.collect(item_to_place, True) swap_state.collect(item_to_place, True)
new_loc_count = len( new_loc_count = len(
multiworld.get_reachable_locations(swap_state)) world.get_reachable_locations(swap_state))
if new_loc_count >= prev_loc_count: if new_loc_count >= prev_loc_count:
# Add this item to the existing placement, and # Add this item to the existing placement, and
@@ -156,7 +156,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
else: else:
unplaced_items.append(item_to_place) unplaced_items.append(item_to_place)
continue continue
multiworld.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
spot_to_fill.locked = lock spot_to_fill.locked = lock
placements.append(spot_to_fill) placements.append(spot_to_fill)
spot_to_fill.event = item_to_place.advancement spot_to_fill.event = item_to_place.advancement
@@ -173,7 +173,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
# validate all placements and remove invalid ones # validate all placements and remove invalid ones
state = sweep_from_pool(base_state, []) state = sweep_from_pool(base_state, [])
for placement in placements: for placement in placements:
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state): if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
placement.item.location = None placement.item.location = None
unplaced_items.append(placement.item) unplaced_items.append(placement.item)
placement.item = None placement.item = None
@@ -188,7 +188,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
if excluded_locations: if excluded_locations:
for location in excluded_locations: for location in excluded_locations:
location.progress_type = location.progress_type.DEFAULT location.progress_type = location.progress_type.DEFAULT
fill_restrictive(multiworld, base_state, excluded_locations, unplaced_items, single_player_placement, lock, fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
swap, on_place, allow_partial, False) swap, on_place, allow_partial, False)
for location in excluded_locations: for location in excluded_locations:
if not location.item: if not location.item:
@@ -196,26 +196,19 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0: if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
# There are leftover unplaceable items and locations that won't accept them # There are leftover unplaceable items and locations that won't accept them
if multiworld.can_beat_game(): if world.can_beat_game():
logging.warning( logging.warning(
f"Not all items placed. Game beatable anyway.\nCould not place:\n" f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
f"{', '.join(str(item) for item in unplaced_items)}")
else: else:
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f"Unplaced items:\n" f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
item_pool.extend(unplaced_items) item_pool.extend(unplaced_items)
def remaining_fill(multiworld: MultiWorld, def remaining_fill(world: MultiWorld,
locations: typing.List[Location], locations: typing.List[Location],
itempool: typing.List[Item], itempool: typing.List[Item]) -> None:
name: str = "Remaining") -> None:
unplaced_items: typing.List[Item] = [] unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = [] placements: typing.List[Location] = []
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
@@ -268,41 +261,36 @@ def remaining_fill(multiworld: MultiWorld,
unplaced_items.append(item_to_place) unplaced_items.append(item_to_place)
continue continue
multiworld.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
placements.append(spot_to_fill) placements.append(spot_to_fill)
placed += 1 placed += 1
if not placed % 1000: if not placed % 1000:
_log_fill_progress(name, placed, total) _log_fill_progress("Remaining", placed, total)
if total > 1000: if total > 1000:
_log_fill_progress(name, placed, total) _log_fill_progress("Remaining", placed, total)
if unplaced_items and locations: if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them # There are leftover unplaceable items and locations that won't accept them
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f"Unplaced items:\n" f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
itempool.extend(unplaced_items) itempool.extend(unplaced_items)
def fast_fill(multiworld: MultiWorld, def fast_fill(world: MultiWorld,
item_pool: typing.List[Item], item_pool: typing.List[Item],
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]: fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
placing = min(len(item_pool), len(fill_locations)) placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations): for item, location in zip(item_pool, fill_locations):
multiworld.push_item(location, item, False) world.push_item(location, item, False)
return item_pool[placing:], fill_locations[placing:] return item_pool[placing:], fill_locations[placing:]
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]): def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
maximum_exploration_state = sweep_from_pool(state, pool) maximum_exploration_state = sweep_from_pool(state, pool)
minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"} minimal_players = {player for player in world.player_ids if world.worlds[player].options.accessibility == "minimal"}
unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
not location.can_reach(maximum_exploration_state)] not location.can_reach(maximum_exploration_state)]
for location in unreachable_locations: for location in unreachable_locations:
if (location.item is not None and location.item.advancement and location.address is not None and not if (location.item is not None and location.item.advancement and location.address is not None and not
@@ -316,36 +304,36 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
locations.append(location) locations.append(location)
if pool and locations: if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections") fill_restrictive(world, state, locations, pool, name="Accessibility Corrections")
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations): def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
maximum_exploration_state = sweep_from_pool(state) maximum_exploration_state = sweep_from_pool(state)
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
if unreachable_locations: if unreachable_locations:
def forbid_important_item_rule(item: Item): def forbid_important_item_rule(item: Item):
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal') return not ((item.classification & 0b0011) and world.worlds[item.player].options.accessibility != 'minimal')
for location in unreachable_locations: for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule) add_item_rule(location, forbid_important_item_rule)
def distribute_early_items(multiworld: MultiWorld, def distribute_early_items(world: MultiWorld,
fill_locations: typing.List[Location], fill_locations: typing.List[Location],
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]: itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
""" returns new fill_locations and itempool """ """ returns new fill_locations and itempool """
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {} early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
for player in multiworld.player_ids: for player in world.player_ids:
items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player]) items = itertools.chain(world.early_items[player], world.local_early_items[player])
for item in items: for item in items:
early_items_count[item, player] = [multiworld.early_items[player].get(item, 0), early_items_count[item, player] = [world.early_items[player].get(item, 0),
multiworld.local_early_items[player].get(item, 0)] world.local_early_items[player].get(item, 0)]
if early_items_count: if early_items_count:
early_locations: typing.List[Location] = [] early_locations: typing.List[Location] = []
early_priority_locations: typing.List[Location] = [] early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set() loc_indexes_to_remove: typing.Set[int] = set()
base_state = multiworld.state.copy() base_state = world.state.copy()
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations): for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state): if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY: if loc.progress_type == LocationProgressType.PRIORITY:
@@ -357,8 +345,8 @@ def distribute_early_items(multiworld: MultiWorld,
early_prog_items: typing.List[Item] = [] early_prog_items: typing.List[Item] = []
early_rest_items: typing.List[Item] = [] early_rest_items: typing.List[Item] = []
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids} early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids} early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
item_indexes_to_remove: typing.Set[int] = set() item_indexes_to_remove: typing.Set[int] = set()
for i, item in enumerate(itempool): for i, item in enumerate(itempool):
if (item.name, item.player) in early_items_count: if (item.name, item.player) in early_items_count:
@@ -382,28 +370,28 @@ def distribute_early_items(multiworld: MultiWorld,
if len(early_items_count) == 0: if len(early_items_count) == 0:
break break
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove] itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
for player in multiworld.player_ids: for player in world.player_ids:
player_local = early_local_rest_items[player] player_local = early_local_rest_items[player]
fill_restrictive(multiworld, base_state, fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player], [loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}") player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}")
if player_local: if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}") logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_rest_items.extend(early_local_rest_items[player]) early_rest_items.extend(early_local_rest_items[player])
early_locations = [loc for loc in early_locations if not loc.item] early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(multiworld, base_state, early_locations, early_rest_items, lock=True, allow_partial=True, fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
name="Early Items") name="Early Items")
early_locations += early_priority_locations early_locations += early_priority_locations
for player in multiworld.player_ids: for player in world.player_ids:
player_local = early_local_prog_items[player] player_local = early_local_prog_items[player]
fill_restrictive(multiworld, base_state, fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player], [loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}") player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}")
if player_local: if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}") logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_prog_items.extend(player_local) early_prog_items.extend(player_local)
early_locations = [loc for loc in early_locations if not loc.item] early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(multiworld, base_state, early_locations, early_prog_items, lock=True, allow_partial=True, fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
name="Early Progression") name="Early Progression")
unplaced_early_items = early_rest_items + early_prog_items unplaced_early_items = early_rest_items + early_prog_items
if unplaced_early_items: if unplaced_early_items:
@@ -412,18 +400,18 @@ def distribute_early_items(multiworld: MultiWorld,
itempool += unplaced_early_items itempool += unplaced_early_items
fill_locations.extend(early_locations) fill_locations.extend(early_locations)
multiworld.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
return fill_locations, itempool return fill_locations, itempool
def distribute_items_restrictive(multiworld: MultiWorld) -> None: def distribute_items_restrictive(world: MultiWorld) -> None:
fill_locations = sorted(multiworld.get_unfilled_locations()) fill_locations = sorted(world.get_unfilled_locations())
multiworld.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
# get items to distribute # get items to distribute
itempool = sorted(multiworld.itempool) itempool = sorted(world.itempool)
multiworld.random.shuffle(itempool) world.random.shuffle(itempool)
fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool) fill_locations, itempool = distribute_early_items(world, fill_locations, itempool)
progitempool: typing.List[Item] = [] progitempool: typing.List[Item] = []
usefulitempool: typing.List[Item] = [] usefulitempool: typing.List[Item] = []
@@ -437,7 +425,7 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
else: else:
filleritempool.append(item) filleritempool.append(item)
call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations) call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = { locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
loc_type: [] for loc_type in LocationProgressType} loc_type: [] for loc_type in LocationProgressType}
@@ -458,79 +446,75 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
if prioritylocations: if prioritylocations:
# "priority fill" # "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking, fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
name="Priority") name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) accessibility_corrections(world, world.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations defaultlocations = prioritylocations + defaultlocations
if progitempool: if progitempool:
# "advancement/progression fill" # "advancement/progression fill"
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression") fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
if progitempool: if progitempool:
raise FillError( raise FillError(
f"Not enough locations for progression items. " f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
f"There are {len(progitempool)} more progression items than there are available locations." accessibility_corrections(world, world.state, defaultlocations)
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
for location in lock_later: for location in lock_later:
if location.item: if location.item:
location.locked = True location.locked = True
del mark_for_locking, lock_later del mark_for_locking, lock_later
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations) inaccessible_location_rules(world, world.state, defaultlocations)
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded") remaining_fill(world, excludedlocations, filleritempool)
if excludedlocations: if excludedlocations:
raise FillError( raise FillError(
f"Not enough filler items for excluded locations. " f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
)
restitempool = filleritempool + usefulitempool restitempool = filleritempool + usefulitempool
remaining_fill(multiworld, defaultlocations, restitempool) remaining_fill(world, defaultlocations, restitempool)
unplaced = restitempool unplaced = restitempool
unfilled = defaultlocations unfilled = defaultlocations
if unplaced or unfilled: if unplaced or unfilled:
logging.warning( logging.warning(
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}") f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item) items_counter = Counter(location.item.player for location in world.get_locations() if location.item)
locations_counter = Counter(location.player for location in multiworld.get_locations()) locations_counter = Counter(location.player for location in world.get_locations())
items_counter.update(item.player for item in unplaced) items_counter.update(item.player for item in unplaced)
locations_counter.update(location.player for location in unfilled) locations_counter.update(location.player for location in unfilled)
print_data = {"items": items_counter, "locations": locations_counter} print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f"Per-Player counts: {print_data})") logging.info(f'Per-Player counts: {print_data})')
def flood_items(multiworld: MultiWorld) -> None: def flood_items(world: MultiWorld) -> None:
# get items to distribute # get items to distribute
multiworld.random.shuffle(multiworld.itempool) world.random.shuffle(world.itempool)
itempool = multiworld.itempool itempool = world.itempool
progress_done = False progress_done = False
# sweep once to pick up preplaced items # sweep once to pick up preplaced items
multiworld.state.sweep_for_events() world.state.sweep_for_events()
# fill multiworld from top of itempool while we can # fill world from top of itempool while we can
while not progress_done: while not progress_done:
location_list = multiworld.get_unfilled_locations() location_list = world.get_unfilled_locations()
multiworld.random.shuffle(location_list) world.random.shuffle(location_list)
spot_to_fill = None spot_to_fill = None
for location in location_list: for location in location_list:
if location.can_fill(multiworld.state, itempool[0]): if location.can_fill(world.state, itempool[0]):
spot_to_fill = location spot_to_fill = location
break break
if spot_to_fill: if spot_to_fill:
item = itempool.pop(0) item = itempool.pop(0)
multiworld.push_item(spot_to_fill, item, True) world.push_item(spot_to_fill, item, True)
continue continue
# ran out of spots, check if we need to step in and correct things # ran out of spots, check if we need to step in and correct things
if len(multiworld.get_reachable_locations()) == len(multiworld.get_locations()): if len(world.get_reachable_locations()) == len(world.get_locations()):
progress_done = True progress_done = True
continue continue
@@ -540,7 +524,7 @@ def flood_items(multiworld: MultiWorld) -> None:
for item in itempool: for item in itempool:
if item.advancement: if item.advancement:
candidate_item_to_place = item candidate_item_to_place = item
if multiworld.unlocks_new_location(item): if world.unlocks_new_location(item):
item_to_place = item item_to_place = item
break break
@@ -553,15 +537,15 @@ def flood_items(multiworld: MultiWorld) -> None:
raise FillError('No more progress items left to place.') raise FillError('No more progress items left to place.')
# find item to replace with progress item # find item to replace with progress item
location_list = multiworld.get_reachable_locations() location_list = world.get_reachable_locations()
multiworld.random.shuffle(location_list) world.random.shuffle(location_list)
for location in location_list: for location in location_list:
if location.item is not None and not location.item.advancement: if location.item is not None and not location.item.advancement:
# safe to replace # safe to replace
replace_item = location.item replace_item = location.item
replace_item.location = None replace_item.location = None
itempool.append(replace_item) itempool.append(replace_item)
multiworld.push_item(location, item_to_place, True) world.push_item(location, item_to_place, True)
itempool.remove(item_to_place) itempool.remove(item_to_place)
break break
@@ -771,7 +755,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
location_1.event, location_2.event = location_2.event, location_1.event location_1.event, location_2.event = location_2.event, location_1.event
def distribute_planned(multiworld: MultiWorld) -> None: def distribute_planned(world: MultiWorld) -> None:
def warn(warning: str, force: typing.Union[bool, str]) -> None: def warn(warning: str, force: typing.Union[bool, str]) -> None:
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']: if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
logging.warning(f'{warning}') logging.warning(f'{warning}')
@@ -784,24 +768,24 @@ def distribute_planned(multiworld: MultiWorld) -> None:
else: else:
warn(warning, force) warn(warning, force)
swept_state = multiworld.state.copy() swept_state = world.state.copy()
swept_state.sweep_for_events() swept_state.sweep_for_events()
reachable = frozenset(multiworld.get_reachable_locations(swept_state)) reachable = frozenset(world.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
for loc in multiworld.get_unfilled_locations(): for loc in world.get_unfilled_locations():
if loc in reachable: if loc in reachable:
early_locations[loc.player].append(loc.name) early_locations[loc.player].append(loc.name)
else: # not reachable with swept state else: # not reachable with swept state
non_early_locations[loc.player].append(loc.name) non_early_locations[loc.player].append(loc.name)
world_name_lookup = multiworld.world_name_lookup world_name_lookup = world.world_name_lookup
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
plando_blocks: typing.List[typing.Dict[str, typing.Any]] = [] plando_blocks: typing.List[typing.Dict[str, typing.Any]] = []
player_ids = set(multiworld.player_ids) player_ids = set(world.player_ids)
for player in player_ids: for player in player_ids:
for block in multiworld.plando_items[player]: for block in world.plando_items[player]:
block['player'] = player block['player'] = player
if 'force' not in block: if 'force' not in block:
block['force'] = 'silent' block['force'] = 'silent'
@@ -815,12 +799,12 @@ def distribute_planned(multiworld: MultiWorld) -> None:
else: else:
target_world = block['world'] target_world = block['world']
if target_world is False or multiworld.players == 1: # target own world if target_world is False or world.players == 1: # target own world
worlds: typing.Set[int] = {player} worlds: typing.Set[int] = {player}
elif target_world is True: # target any worlds besides own elif target_world is True: # target any worlds besides own
worlds = set(multiworld.player_ids) - {player} worlds = set(world.player_ids) - {player}
elif target_world is None: # target all worlds elif target_world is None: # target all worlds
worlds = set(multiworld.player_ids) worlds = set(world.player_ids)
elif type(target_world) == list: # list of target worlds elif type(target_world) == list: # list of target worlds
worlds = set() worlds = set()
for listed_world in target_world: for listed_world in target_world:
@@ -830,9 +814,9 @@ def distribute_planned(multiworld: MultiWorld) -> None:
continue continue
worlds.add(world_name_lookup[listed_world]) worlds.add(world_name_lookup[listed_world])
elif type(target_world) == int: # target world by slot number elif type(target_world) == int: # target world by slot number
if target_world not in range(1, multiworld.players + 1): if target_world not in range(1, world.players + 1):
failed( failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})", f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
block['force']) block['force'])
continue continue
worlds = {target_world} worlds = {target_world}
@@ -860,7 +844,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
item_list: typing.List[str] = [] item_list: typing.List[str] = []
for key, value in items.items(): for key, value in items.items():
if value is True: if value is True:
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key)) value = world.itempool.count(world.worlds[player].create_item(key))
item_list += [key] * value item_list += [key] * value
items = item_list items = item_list
if isinstance(items, str): if isinstance(items, str):
@@ -910,17 +894,17 @@ def distribute_planned(multiworld: MultiWorld) -> None:
count = block['count'] count = block['count']
failed(f"Plando count {count} greater than locations specified", block['force']) failed(f"Plando count {count} greater than locations specified", block['force'])
block['count'] = len(block['locations']) block['count'] = len(block['locations'])
block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max']) block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max'])
if block['count']['target'] > 0: if block['count']['target'] > 0:
plando_blocks.append(block) plando_blocks.append(block)
# shuffle, but then sort blocks by number of locations minus number of items, # shuffle, but then sort blocks by number of locations minus number of items,
# so less-flexible blocks get priority # so less-flexible blocks get priority
multiworld.random.shuffle(plando_blocks) world.random.shuffle(plando_blocks)
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target'] plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
if len(block['locations']) > 0 if len(block['locations']) > 0
else len(multiworld.get_unfilled_locations(player)) - block['count']['target'])) else len(world.get_unfilled_locations(player)) - block['count']['target']))
for placement in plando_blocks: for placement in plando_blocks:
player = placement['player'] player = placement['player']
@@ -931,19 +915,19 @@ def distribute_planned(multiworld: MultiWorld) -> None:
maxcount = placement['count']['target'] maxcount = placement['count']['target']
from_pool = placement['from_pool'] from_pool = placement['from_pool']
candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds))) candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
multiworld.random.shuffle(candidates) world.random.shuffle(candidates)
multiworld.random.shuffle(items) world.random.shuffle(items)
count = 0 count = 0
err: typing.List[str] = [] err: typing.List[str] = []
successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
for item_name in items: for item_name in items:
item = multiworld.worlds[player].create_item(item_name) item = world.worlds[player].create_item(item_name)
for location in reversed(candidates): for location in reversed(candidates):
if (location.address is None) == (item.code is None): # either both None or both not None if (location.address is None) == (item.code is None): # either both None or both not None
if not location.item: if not location.item:
if location.item_rule(item): if location.item_rule(item):
if location.can_fill(multiworld.state, item, False): if location.can_fill(world.state, item, False):
successful_pairs.append((item, location)) successful_pairs.append((item, location))
candidates.remove(location) candidates.remove(location)
count = count + 1 count = count + 1
@@ -961,21 +945,21 @@ def distribute_planned(multiworld: MultiWorld) -> None:
if count < placement['count']['min']: if count < placement['count']['min']:
m = placement['count']['min'] m = placement['count']['min']
failed( failed(
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}", f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {' '.join(err)}",
placement['force']) placement['force'])
for (item, location) in successful_pairs: for (item, location) in successful_pairs:
multiworld.push_item(location, item, collect=False) world.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill location.event = True # flag location to be checked during fill
location.locked = True location.locked = True
logging.debug(f"Plando placed {item} at {location}") logging.debug(f"Plando placed {item} at {location}")
if from_pool: if from_pool:
try: try:
multiworld.itempool.remove(item) world.itempool.remove(item)
except ValueError: except ValueError:
warn( warn(
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.", f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
placement['force']) placement['force'])
except Exception as e: except Exception as e:
raise Exception( raise Exception(
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e f"Error running plando for player {player} ({world.player_name[player]})") from e

View File

@@ -26,7 +26,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Text import TextTable from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from worlds.generic import PlandoConnection from worlds.generic import PlandoConnection
from worlds import failed_world_loads
def mystery_argparse(): def mystery_argparse():
@@ -303,9 +302,7 @@ def handle_name(name: str, player: int, name_counter: Counter):
NUMBER=(number if number > 1 else ''), NUMBER=(number if number > 1 else ''),
player=player, player=player,
PLAYER=(player if player > 1 else ''))) PLAYER=(player if player > 1 else '')))
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace. new_name = new_name.strip()[:16]
# Could cause issues for some clients that cannot handle the additional whitespace.
new_name = new_name.strip()[:16].strip()
if new_name == "Archipelago": if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"") raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name return new_name
@@ -318,35 +315,33 @@ def prefer_int(input_data: str) -> Union[str, int]:
return input_data return input_data
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}
def roll_percentage(percentage: Union[int, float]) -> bool: def roll_percentage(percentage: Union[int, float]) -> bool:
"""Roll a percentage chance. """Roll a percentage chance.
percentage is expected to be in range [0, 100]""" percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100) return random.random() < (float(percentage) / 100)
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict: def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
logging.debug(f'Applying {new_weights}') logging.debug(f'Applying {new_weights}')
cleaned_weights = {} new_options = set(new_weights) - set(weights)
for option in new_weights: weights.update(new_weights)
option_name = option.lstrip("+")
if option.startswith("+") and option_name in weights:
cleaned_value = weights[option_name]
new_value = new_weights[option]
if isinstance(new_value, (set, dict)):
cleaned_value.update(new_value)
elif isinstance(new_value, list):
cleaned_value.extend(new_value)
else:
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
f" received {type(new_value).__name__}.")
cleaned_weights[option_name] = cleaned_value
else:
cleaned_weights[option_name] = new_weights[option]
new_options = set(cleaned_weights) - set(weights)
weights.update(cleaned_weights)
if new_options: if new_options:
for new_option in new_options: for new_option in new_options:
logging.warning(f'{update_type} Suboption "{new_option}" of "{name}" did not ' logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
f'overwrite a root option. ' f'overwrite a root option. '
f'This is probably in error.') f'This is probably in error.')
return weights return weights
@@ -362,6 +357,15 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if options[option_key].supports_weighting: if options[option_key].supports_weighting:
return get_choice(option_key, category_dict) return get_choice(option_key, category_dict)
return category_dict[option_key] return category_dict[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
"random_sprite_on_event"}:
return get_choice(option_key, category_dict)
raise Exception(f"Error generating meta option {option_key} for {game}.") raise Exception(f"Error generating meta option {option_key} for {game}.")
@@ -459,11 +463,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
ret.game = get_choice("game", weights) ret.game = get_choice("game", weights)
if ret.game not in AutoWorldRegister.world_types: if ret.game not in AutoWorldRegister.world_types:
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
if picks[0] in failed_world_loads:
raise Exception(f"No functional world found to handle game {ret.game}. "
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
f"If so, it appears the world failed to initialize correctly.")
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? " raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
f"Check your spelling or installation of that world.") f"Check your spelling or installation of that world.")
@@ -473,10 +473,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
world_type = AutoWorldRegister.world_types[ret.game] world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game] game_weights = weights[ret.game]
if any(weight.startswith("+") for weight in game_weights) or \
any(weight.startswith("+") for weight in weights):
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
if "triggers" in game_weights: if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"]) weights = roll_triggers(weights, game_weights["triggers"])
game_weights = weights[ret.game] game_weights = weights[ret.game]
@@ -489,23 +485,120 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
handle_option(ret, game_weights, option_key, option, plando_options) handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoOptions.items in plando_options: if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", []) ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "A Link to the Past": if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
roll_alttp_settings(ret, game_weights, plando_options) # bad hardcoded behavior to make this work for now
if PlandoOptions.connections in plando_options:
ret.plando_connections = [] ret.plando_connections = []
options = game_weights.get("plando_connections", []) if PlandoOptions.connections in plando_options:
for placement in options: options = game_weights.get("plando_connections", [])
if roll_percentage(get_choice("percentage", placement, 100)): for placement in options:
ret.plando_connections.append(PlandoConnection( if roll_percentage(get_choice("percentage", placement, 100)):
get_choice("entrance", placement), ret.plando_connections.append(PlandoConnection(
get_choice("exit", placement), get_choice("entrance", placement),
get_choice("direction", placement, "both") get_choice("exit", placement),
)) get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
return ret return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required]
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
ret.dark_room_logic = "torches"
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice_legacy('goals', weights, 'ganon')
ret.goal = goals[goal]
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''
ret.mode = get_choice_legacy("mode", weights)
ret.difficulty = get_choice_legacy('item_pool', weights)
ret.item_functionality = get_choice_legacy('item_functionality', weights)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice_legacy('enemy_damage', weights)]
ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.timer = {'none': False,
None: False,
False: False,
'timed': 'timed',
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice_legacy('timer', weights, False)]
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]
for index, medallion in enumerate(ret.required_medallions):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
.get(medallion.lower(), None)
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.plando_texts = {} ret.plando_texts = {}
if PlandoOptions.texts in plando_options: if PlandoOptions.texts in plando_options:
@@ -519,6 +612,17 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
raise Exception(f"No text target \"{at}\" found.") raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice_legacy("text", placement)) ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if PlandoOptions.connections in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice_legacy("entrance", placement),
get_choice_legacy("exit", placement),
get_choice_legacy("direction", placement, "both")
))
ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice_legacy('sprite', weights, "Link") ret.sprite = get_choice_legacy('sprite', weights, "Link")
if 'random_sprite_on_event' in weights: if 'random_sprite_on_event' in weights:

View File

@@ -100,7 +100,7 @@ components.extend([
# Functions # Functions
Component("Open host.yaml", func=open_host_yaml), Component("Open host.yaml", func=open_host_yaml),
Component("Open Patch", func=open_patch), Component("Open Patch", func=open_patch),
Component("Generate Template Options", func=generate_yamls), Component("Generate Template Settings", func=generate_yamls),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files), Component("Browse Files", func=browse_files),
@@ -161,7 +161,7 @@ def launch(exe, in_terminal=False):
def run_gui(): def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget from kvui import App, ContainerLayout, GridLayout, Button, Label
from kivy.uix.image import AsyncImage from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout from kivy.uix.relativelayout import RelativeLayout
@@ -185,16 +185,11 @@ def run_gui():
self.container = ContainerLayout() self.container = ContainerLayout()
self.grid = GridLayout(cols=2) self.grid = GridLayout(cols=2)
self.container.add_widget(self.grid) self.container.add_widget(self.grid)
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40)) self.grid.add_widget(Label(text="General"))
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40)) self.grid.add_widget(Label(text="Clients"))
tool_layout = ScrollBox() button_layout = self.grid # make buttons fill the window
tool_layout.layout.orientation = "vertical"
self.grid.add_widget(tool_layout)
client_layout = ScrollBox()
client_layout.layout.orientation = "vertical"
self.grid.add_widget(client_layout)
def build_button(component: Component) -> Widget: def build_button(component: Component):
""" """
Builds a button widget for a given component. Builds a button widget for a given component.
@@ -205,26 +200,31 @@ def run_gui():
None. The button is added to the parent grid layout. None. The button is added to the parent grid layout.
""" """
button = Button(text=component.display_name, size_hint_y=None, height=40) button = Button(text=component.display_name)
button.component = component button.component = component
button.bind(on_release=self.component_action) button.bind(on_release=self.component_action)
if component.icon != "icon": if component.icon != "icon":
image = AsyncImage(source=icon_paths[component.icon], image = AsyncImage(source=icon_paths[component.icon],
size=(38, 38), size_hint=(None, 1), pos=(5, 0)) size=(38, 38), size_hint=(None, 1), pos=(5, 0))
box_layout = RelativeLayout(size_hint_y=None, height=40) box_layout = RelativeLayout()
box_layout.add_widget(button) box_layout.add_widget(button)
box_layout.add_widget(image) box_layout.add_widget(image)
return box_layout button_layout.add_widget(box_layout)
return button else:
button_layout.add_widget(button)
for (tool, client) in itertools.zip_longest(itertools.chain( for (tool, client) in itertools.zip_longest(itertools.chain(
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()): self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
# column 1 # column 1
if tool: if tool:
tool_layout.layout.add_widget(build_button(tool[1])) build_button(tool[1])
else:
button_layout.add_widget(Label())
# column 2 # column 2
if client: if client:
client_layout.layout.add_widget(build_button(client[1])) build_button(client[1])
else:
button_layout.add_widget(Label())
return self.container return self.container

View File

@@ -348,8 +348,7 @@ class LinksAwakeningClient():
await asyncio.sleep(1.0) await asyncio.sleep(1.0)
continue continue
self.stop_bizhawk_spam = False self.stop_bizhawk_spam = False
logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} " logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
f"running {rom_name.decode('ascii', errors='replace')}")
return return
except (BlockingIOError, TimeoutError, ConnectionResetError): except (BlockingIOError, TimeoutError, ConnectionResetError):
await asyncio.sleep(1.0) await asyncio.sleep(1.0)

282
Main.py
View File

@@ -30,49 +30,49 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_path.cached_path = args.outputpath output_path.cached_path = args.outputpath
start = time.perf_counter() start = time.perf_counter()
# initialize the multiworld # initialize the world
multiworld = MultiWorld(args.multi) world = MultiWorld(args.multi)
logger = logging.getLogger() logger = logging.getLogger()
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None) world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
multiworld.plando_options = args.plando_options world.plando_options = args.plando_options
multiworld.shuffle = args.shuffle.copy() world.shuffle = args.shuffle.copy()
multiworld.logic = args.logic.copy() world.logic = args.logic.copy()
multiworld.mode = args.mode.copy() world.mode = args.mode.copy()
multiworld.difficulty = args.difficulty.copy() world.difficulty = args.difficulty.copy()
multiworld.item_functionality = args.item_functionality.copy() world.item_functionality = args.item_functionality.copy()
multiworld.timer = args.timer.copy() world.timer = args.timer.copy()
multiworld.goal = args.goal.copy() world.goal = args.goal.copy()
multiworld.boss_shuffle = args.shufflebosses.copy() world.boss_shuffle = args.shufflebosses.copy()
multiworld.enemy_health = args.enemy_health.copy() world.enemy_health = args.enemy_health.copy()
multiworld.enemy_damage = args.enemy_damage.copy() world.enemy_damage = args.enemy_damage.copy()
multiworld.beemizer_total_chance = args.beemizer_total_chance.copy() world.beemizer_total_chance = args.beemizer_total_chance.copy()
multiworld.beemizer_trap_chance = args.beemizer_trap_chance.copy() world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
multiworld.countdown_start_time = args.countdown_start_time.copy() world.countdown_start_time = args.countdown_start_time.copy()
multiworld.red_clock_time = args.red_clock_time.copy() world.red_clock_time = args.red_clock_time.copy()
multiworld.blue_clock_time = args.blue_clock_time.copy() world.blue_clock_time = args.blue_clock_time.copy()
multiworld.green_clock_time = args.green_clock_time.copy() world.green_clock_time = args.green_clock_time.copy()
multiworld.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
multiworld.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_available = args.triforce_pieces_available.copy()
multiworld.triforce_pieces_required = args.triforce_pieces_required.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy()
multiworld.shop_shuffle = args.shop_shuffle.copy() world.shop_shuffle = args.shop_shuffle.copy()
multiworld.shuffle_prizes = args.shuffle_prizes.copy() world.shuffle_prizes = args.shuffle_prizes.copy()
multiworld.sprite_pool = args.sprite_pool.copy() world.sprite_pool = args.sprite_pool.copy()
multiworld.dark_room_logic = args.dark_room_logic.copy() world.dark_room_logic = args.dark_room_logic.copy()
multiworld.plando_items = args.plando_items.copy() world.plando_items = args.plando_items.copy()
multiworld.plando_texts = args.plando_texts.copy() world.plando_texts = args.plando_texts.copy()
multiworld.plando_connections = args.plando_connections.copy() world.plando_connections = args.plando_connections.copy()
multiworld.required_medallions = args.required_medallions.copy() world.required_medallions = args.required_medallions.copy()
multiworld.game = args.game.copy() world.game = args.game.copy()
multiworld.player_name = args.name.copy() world.player_name = args.name.copy()
multiworld.sprite = args.sprite.copy() world.sprite = args.sprite.copy()
multiworld.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
multiworld.set_options(args) world.set_options(args)
multiworld.set_item_links() world.set_item_links()
multiworld.state = CollectionState(multiworld) world.state = CollectionState(world)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:") logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
@@ -103,93 +103,93 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# This assertion method should not be necessary to run if we are not outputting any multidata. # This assertion method should not be necessary to run if we are not outputting any multidata.
if not args.skip_output: if not args.skip_output:
AutoWorld.call_stage(multiworld, "assert_generate") AutoWorld.call_stage(world, "assert_generate")
AutoWorld.call_all(multiworld, "generate_early") AutoWorld.call_all(world, "generate_early")
logger.info('') logger.info('')
for player in multiworld.player_ids: for player in world.player_ids:
for item_name, count in multiworld.worlds[player].options.start_inventory.value.items(): for item_name, count in world.worlds[player].options.start_inventory.value.items():
for _ in range(count): for _ in range(count):
multiworld.push_precollected(multiworld.create_item(item_name, player)) world.push_precollected(world.create_item(item_name, player))
for item_name, count in getattr(multiworld.worlds[player].options, for item_name, count in getattr(world.worlds[player].options,
"start_inventory_from_pool", "start_inventory_from_pool",
StartInventoryPool({})).value.items(): StartInventoryPool({})).value.items():
for _ in range(count): for _ in range(count):
multiworld.push_precollected(multiworld.create_item(item_name, player)) world.push_precollected(world.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early. # remove from_pool items also from early items handling, as starting is plenty early.
early = multiworld.early_items[player].get(item_name, 0) early = world.early_items[player].get(item_name, 0)
if early: if early:
multiworld.early_items[player][item_name] = max(0, early-count) world.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early remaining_count = count-early
if remaining_count > 0: if remaining_count > 0:
local_early = multiworld.early_local_items[player].get(item_name, 0) local_early = world.early_local_items[player].get(item_name, 0)
if local_early: if local_early:
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count) world.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early del local_early
del early del early
logger.info('Creating MultiWorld.') logger.info('Creating World.')
AutoWorld.call_all(multiworld, "create_regions") AutoWorld.call_all(world, "create_regions")
logger.info('Creating Items.') logger.info('Creating Items.')
AutoWorld.call_all(multiworld, "create_items") AutoWorld.call_all(world, "create_items")
logger.info('Calculating Access Rules.') logger.info('Calculating Access Rules.')
for player in multiworld.player_ids: for player in world.player_ids:
# items can't be both local and non-local, prefer local # items can't be both local and non-local, prefer local
multiworld.worlds[player].options.non_local_items.value -= multiworld.worlds[player].options.local_items.value world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value
multiworld.worlds[player].options.non_local_items.value -= set(multiworld.local_early_items[player]) world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player])
AutoWorld.call_all(multiworld, "set_rules") AutoWorld.call_all(world, "set_rules")
for player in multiworld.player_ids: for player in world.player_ids:
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value) exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value)
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value
for location_name in multiworld.worlds[player].options.priority_locations.value: for location_name in world.worlds[player].options.priority_locations.value:
try: try:
location = multiworld.get_location(location_name, player) location = world.get_location(location_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location except KeyError as e: # failed to find the given location. Check if it's a legitimate location
if location_name not in multiworld.worlds[player].location_name_to_id: if location_name not in world.worlds[player].location_name_to_id:
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
else: else:
location.progress_type = LocationProgressType.PRIORITY location.progress_type = LocationProgressType.PRIORITY
# Set local and non-local item rules. # Set local and non-local item rules.
if multiworld.players > 1: if world.players > 1:
locality_rules(multiworld) locality_rules(world)
else: else:
multiworld.worlds[1].options.non_local_items.value = set() world.worlds[1].options.non_local_items.value = set()
multiworld.worlds[1].options.local_items.value = set() world.worlds[1].options.local_items.value = set()
AutoWorld.call_all(multiworld, "generate_basic") AutoWorld.call_all(world, "generate_basic")
# remove starting inventory from pool items. # remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible. # Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids): if any(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
new_items: List[Item] = [] new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = { depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options, player: getattr(world.worlds[player].options,
"start_inventory_from_pool", "start_inventory_from_pool",
StartInventoryPool({})).value.copy() StartInventoryPool({})).value.copy()
for player in multiworld.player_ids for player in world.player_ids
} }
for player, items in depletion_pool.items(): for player, items in depletion_pool.items():
player_world: AutoWorld.World = multiworld.worlds[player] player_world: AutoWorld.World = world.worlds[player]
for count in items.values(): for count in items.values():
for _ in range(count): for _ in range(count):
new_items.append(player_world.create_filler()) new_items.append(player_world.create_filler())
target: int = sum(sum(items.values()) for items in depletion_pool.values()) target: int = sum(sum(items.values()) for items in depletion_pool.values())
for i, item in enumerate(multiworld.itempool): for i, item in enumerate(world.itempool):
if depletion_pool[item.player].get(item.name, 0): if depletion_pool[item.player].get(item.name, 0):
target -= 1 target -= 1
depletion_pool[item.player][item.name] -= 1 depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items # quick abort if we have found all items
if not target: if not target:
new_items.extend(multiworld.itempool[i+1:]) new_items.extend(world.itempool[i+1:])
break break
else: else:
new_items.append(item) new_items.append(item)
@@ -199,19 +199,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player, remaining_items in depletion_pool.items(): for player, remaining_items in depletion_pool.items():
remaining_items = {name: count for name, count in remaining_items.items() if count} remaining_items = {name: count for name, count in remaining_items.items() if count}
if remaining_items: if remaining_items:
raise Exception(f"{multiworld.get_player_name(player)}" raise Exception(f"{world.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}") f" is trying to remove items from their pool that don't exist: {remaining_items}")
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items world.itempool[:] = new_items
# temporary home for item links, should be moved out of Main # temporary home for item links, should be moved out of Main
for group_id, group in multiworld.groups.items(): for group_id, group in world.groups.items():
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
]: ]:
classifications: Dict[str, int] = collections.defaultdict(int) classifications: Dict[str, int] = collections.defaultdict(int)
counters = {player: {name: 0 for name in shared_pool} for player in players} counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in multiworld.itempool: for item in world.itempool:
if item.player in counters and item.name in shared_pool: if item.player in counters and item.name in shared_pool:
counters[item.player][item.name] += 1 counters[item.player][item.name] += 1
classifications[item.name] |= item.classification classifications[item.name] |= item.classification
@@ -246,13 +246,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
new_item.classification |= classifications[item_name] new_item.classification |= classifications[item_name]
new_itempool.append(new_item) new_itempool.append(new_item)
region = Region("Menu", group_id, multiworld, "ItemLink") region = Region("Menu", group_id, world, "ItemLink")
multiworld.regions.append(region) world.regions.append(region)
locations = region.locations locations = region.locations
for item in multiworld.itempool: for item in world.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0) count = common_item_count.get(item.player, {}).get(item.name, 0)
if count: if count:
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}", loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
None, region) None, region)
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
state.has(item_name, group_id_, count_) state.has(item_name, group_id_, count_)
@@ -263,10 +263,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else: else:
new_itempool.append(item) new_itempool.append(item)
itemcount = len(multiworld.itempool) itemcount = len(world.itempool)
multiworld.itempool = new_itempool world.itempool = new_itempool
while itemcount > len(multiworld.itempool): while itemcount > len(world.itempool):
items_to_add = [] items_to_add = []
for player in group["players"]: for player in group["players"]:
if group["link_replacement"]: if group["link_replacement"]:
@@ -274,64 +274,64 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else: else:
item_player = player item_player = player
if group["replacement_items"][player]: if group["replacement_items"][player]:
items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player, items_to_add.append(AutoWorld.call_single(world, "create_item", item_player,
group["replacement_items"][player])) group["replacement_items"][player]))
else: else:
items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player)) items_to_add.append(AutoWorld.call_single(world, "create_filler", item_player))
multiworld.random.shuffle(items_to_add) world.random.shuffle(items_to_add)
multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)]) world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
if any(multiworld.item_links.values()): if any(world.item_links.values()):
multiworld._all_state = None world._all_state = None
logger.info("Running Item Plando.") logger.info("Running Item Plando.")
distribute_planned(multiworld) distribute_planned(world)
logger.info('Running Pre Main Fill.') logger.info('Running Pre Main Fill.')
AutoWorld.call_all(multiworld, "pre_fill") AutoWorld.call_all(world, "pre_fill")
logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.') logger.info(f'Filling the world with {len(world.itempool)} items.')
if multiworld.algorithm == 'flood': if world.algorithm == 'flood':
flood_items(multiworld) # different algo, biased towards early game progress items flood_items(world) # different algo, biased towards early game progress items
elif multiworld.algorithm == 'balanced': elif world.algorithm == 'balanced':
distribute_items_restrictive(multiworld) distribute_items_restrictive(world)
AutoWorld.call_all(multiworld, 'post_fill') AutoWorld.call_all(world, 'post_fill')
if multiworld.players > 1 and not args.skip_prog_balancing: if world.players > 1 and not args.skip_prog_balancing:
balance_multiworld_progression(multiworld) balance_multiworld_progression(world)
else: else:
logger.info("Progression balancing skipped.") logger.info("Progression balancing skipped.")
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use # we're about to output using multithreading, so we're removing the global random state to prevent accidental use
multiworld.random.passthrough = False world.random.passthrough = False
if args.skip_output: if args.skip_output:
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start) logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
return multiworld return world
logger.info(f'Beginning output...') logger.info(f'Beginning output...')
outfilebase = 'AP_' + multiworld.seed_name outfilebase = 'AP_' + world.seed_name
output = tempfile.TemporaryDirectory() output = tempfile.TemporaryDirectory()
with output as temp_dir: with output as temp_dir:
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__ output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
is not multiworld.worlds[player].generate_output.__code__] is not world.worlds[player].generate_output.__code__]
with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool: with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
check_accessibility_task = pool.submit(multiworld.fulfills_accessibility) check_accessibility_task = pool.submit(world.fulfills_accessibility)
output_file_futures = [pool.submit(AutoWorld.call_stage, multiworld, "generate_output", temp_dir)] output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
for player in output_players: for player in output_players:
# skip starting a thread for methods that say "pass". # skip starting a thread for methods that say "pass".
output_file_futures.append( output_file_futures.append(
pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir)) pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
# collect ER hint info # collect ER hint info
er_hint_data: Dict[int, Dict[int, str]] = {} er_hint_data: Dict[int, Dict[int, str]] = {}
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data) AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
def write_multidata(): def write_multidata():
import NetUtils import NetUtils
@@ -340,38 +340,38 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
games = {} games = {}
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions} minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
slot_info = {} slot_info = {}
names = [[name for player, name in sorted(multiworld.player_name.items())]] names = [[name for player, name in sorted(world.player_name.items())]]
for slot in multiworld.player_ids: for slot in world.player_ids:
player_world: AutoWorld.World = multiworld.worlds[slot] player_world: AutoWorld.World = world.worlds[slot]
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version) minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
client_versions[slot] = player_world.required_client_version client_versions[slot] = player_world.required_client_version
games[slot] = multiworld.game[slot] games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], multiworld.game[slot], slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
multiworld.player_types[slot]) world.player_types[slot])
for slot, group in multiworld.groups.items(): for slot, group in world.groups.items():
games[slot] = multiworld.game[slot] games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(group["name"], multiworld.game[slot], multiworld.player_types[slot], slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
group_members=sorted(group["players"])) group_members=sorted(group["players"]))
precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int] precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
for player, world_precollected in multiworld.precollected_items.items()} for player, world_precollected in world.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups))} precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
for slot in multiworld.player_ids: for slot in world.player_ids:
slot_data[slot] = multiworld.worlds[slot].fill_slot_data() slot_data[slot] = world.worlds[slot].fill_slot_data()
def precollect_hint(location): def precollect_hint(location):
entrance = er_hint_data.get(location.player, {}).get(location.address, "") entrance = er_hint_data.get(location.player, {}).get(location.address, "")
hint = NetUtils.Hint(location.item.player, location.player, location.address, hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False, entrance, location.item.flags) location.item.code, False, entrance, location.item.flags)
precollected_hints[location.player].add(hint) precollected_hints[location.player].add(hint)
if location.item.player not in multiworld.groups: if location.item.player not in world.groups:
precollected_hints[location.item.player].add(hint) precollected_hints[location.item.player].add(hint)
else: else:
for player in multiworld.groups[location.item.player]["players"]: for player in world.groups[location.item.player]["players"]:
precollected_hints[player].add(hint) precollected_hints[player].add(hint)
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids} locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
for location in multiworld.get_filled_locations(): for location in world.get_filled_locations():
if type(location.address) == int: if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \ assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \ "location.address should then also be None. Location: " \
@@ -381,18 +381,18 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
f"{locations_data[location.player][location.address]}") f"{locations_data[location.player][location.address]}")
locations_data[location.player][location.address] = \ locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags location.item.code, location.item.player, location.item.flags
if location.name in multiworld.worlds[location.player].options.start_location_hints: if location.name in world.worlds[location.player].options.start_location_hints:
precollect_hint(location) precollect_hint(location)
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints: elif location.item.name in world.worlds[location.item.player].options.start_hints:
precollect_hint(location) precollect_hint(location)
elif any([location.item.name in multiworld.worlds[player].options.start_hints elif any([location.item.name in world.worlds[player].options.start_hints
for player in multiworld.groups.get(location.item.player, {}).get("players", [])]): for player in world.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location) precollect_hint(location)
# embedded data package # embedded data package
data_package = { data_package = {
game_world.game: worlds.network_data_package["games"][game_world.game] game_world.game: worlds.network_data_package["games"][game_world.game]
for game_world in multiworld.worlds.values() for game_world in world.worlds.values()
} }
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {} checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
@@ -400,7 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multidata = { multidata = {
"slot_data": slot_data, "slot_data": slot_data,
"slot_info": slot_info, "slot_info": slot_info,
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()}, "connect_names": {name: (0, player) for player, name in world.player_name.items()},
"locations": locations_data, "locations": locations_data,
"checks_in_area": checks_in_area, "checks_in_area": checks_in_area,
"server_options": baked_server_options, "server_options": baked_server_options,
@@ -410,10 +410,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
"version": tuple(version_tuple), "version": tuple(version_tuple),
"tags": ["AP"], "tags": ["AP"],
"minimum_versions": minimum_versions, "minimum_versions": minimum_versions,
"seed_name": multiworld.seed_name, "seed_name": world.seed_name,
"datapackage": data_package, "datapackage": data_package,
} }
AutoWorld.call_all(multiworld, "modify_multidata", multidata) AutoWorld.call_all(world, "modify_multidata", multidata)
multidata = zlib.compress(pickle.dumps(multidata), 9) multidata = zlib.compress(pickle.dumps(multidata), 9)
@@ -423,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_file_futures.append(pool.submit(write_multidata)) output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result(): if not check_accessibility_task.result():
if not multiworld.can_beat_game(): if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.") raise Exception("Game appears as unbeatable. Aborting.")
else: else:
logger.warning("Location Accessibility requirements not fulfilled.") logger.warning("Location Accessibility requirements not fulfilled.")
@@ -436,12 +436,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if args.spoiler > 1: if args.spoiler > 1:
logger.info('Calculating playthrough.') logger.info('Calculating playthrough.')
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2) world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
if args.spoiler: if args.spoiler:
multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
zipfilename = output_path(f"AP_{multiworld.seed_name}.zip") zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f"Creating final archive at {zipfilename}") logger.info(f"Creating final archive at {zipfilename}")
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf: compresslevel=9) as zf:
@@ -449,4 +449,4 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
zf.write(file.path, arcname=file.name) zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start) logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return multiworld return world

View File

@@ -656,8 +656,7 @@ class Context:
else: else:
return self.player_names[team, slot] return self.player_names[team, slot]
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False, def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
recipients: typing.Sequence[int] = None):
"""Send and remember hints.""" """Send and remember hints."""
if only_new: if only_new:
hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]] hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]]
@@ -686,13 +685,12 @@ class Context:
for slot in new_hint_events: for slot in new_hint_events:
self.on_new_hint(team, slot) self.on_new_hint(team, slot)
for slot, hint_data in concerns.items(): for slot, hint_data in concerns.items():
if recipients is None or slot in recipients: clients = self.clients[team].get(slot)
clients = self.clients[team].get(slot) if not clients:
if not clients: continue
continue client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)] for client in clients:
for client in clients: async_start(self.send_msgs(client, client_hints))
async_start(self.send_msgs(client, client_hints))
# "events" # "events"
@@ -707,17 +705,14 @@ class Context:
self.save() # save goal completion flag self.save() # save goal completion flag
def on_new_hint(self, team: int, slot: int): def on_new_hint(self, team: int, slot: int):
self.on_changed_hints(team, slot)
self.broadcast(self.clients[team][slot], [{
"cmd": "RoomUpdate",
"hint_points": get_slot_points(self, team, slot)
}])
def on_changed_hints(self, team: int, slot: int):
key: str = f"_read_hints_{team}_{slot}" key: str = f"_read_hints_{team}_{slot}"
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key]) targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
if targets: if targets:
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}]) self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
self.broadcast(self.clients[team][slot], [{
"cmd": "RoomUpdate",
"hint_points": get_slot_points(self, team, slot)
}])
def on_client_status_change(self, team: int, slot: int): def on_client_status_change(self, team: int, slot: int):
key: str = f"_read_client_status_{team}_{slot}" key: str = f"_read_client_status_{team}_{slot}"
@@ -978,10 +973,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
"hint_points": get_slot_points(ctx, team, slot), "hint_points": get_slot_points(ctx, team, slot),
"checked_locations": new_locations, # send back new checks only "checked_locations": new_locations, # send back new checks only
}]) }])
old_hints = ctx.hints[team, slot].copy()
ctx.recheck_hints(team, slot)
if old_hints != ctx.hints[team, slot]:
ctx.on_changed_hints(team, slot)
ctx.save() ctx.save()
@@ -1058,19 +1050,17 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
if picks[0][1] == 100: if picks[0][1] == 100:
return picks[0][0], True, "Perfect Match" return picks[0][0], True, "Perfect Match"
elif picks[0][1] < 75: elif picks[0][1] < 75:
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ return picks[0][0], False, f"Didn't find something that closely matches, " \
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)" f"did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
elif dif > 5: elif dif > 5:
return picks[0][0], True, "Close Match" return picks[0][0], True, "Close Match"
else: else:
return picks[0][0], False, f"Too many close matches for '{input_text}', " \ return picks[0][0], False, f"Too many close matches, did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
else: else:
if picks[0][1] > 90: if picks[0][1] > 90:
return picks[0][0], True, "Only Option Match" return picks[0][0], True, "Only Option Match"
else: else:
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \ return picks[0][0], False, f"Did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
class CommandMeta(type): class CommandMeta(type):
@@ -1439,13 +1429,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints = {hint.re_check(self.ctx, self.client.team) for hint in hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]} self.ctx.hints[self.client.team, self.client.slot]}
self.ctx.hints[self.client.team, self.client.slot] = hints self.ctx.hints[self.client.team, self.client.slot] = hints
self.ctx.notify_hints(self.client.team, list(hints), recipients=(self.client.slot,)) self.ctx.notify_hints(self.client.team, list(hints))
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. " self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.") f"You have {points_available} points.")
if hints and Utils.version_tuple < (0, 5, 0):
self.output("It was recently changed, so that the above hints are only shown to you. "
"If you meant to alert another player of an above hint, "
"please let them know of the content or to run !hint themselves.")
return True return True
elif input_text.isnumeric(): elif input_text.isnumeric():
@@ -1972,7 +1958,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
@mark_raw @mark_raw
def _cmd_forbid_release(self, player_name: str) -> bool: def _cmd_forbid_release(self, player_name: str) -> bool:
"""Disallow the specified player from using the !release command.""" """"Disallow the specified player from using the !release command."""
player = self.resolve_player(player_name) player = self.resolve_player(player_name)
if player: if player:
team, slot, name = player team, slot, name = player

View File

@@ -290,8 +290,8 @@ def add_json_item(parts: list, item_id: int, player: int = 0, item_flags: int =
parts.append({"text": str(item_id), "player": player, "flags": item_flags, "type": JSONTypes.item_id, **kwargs}) parts.append({"text": str(item_id), "player": player, "flags": item_flags, "type": JSONTypes.item_id, **kwargs})
def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) -> None: def add_json_location(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs}) parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
class Hint(typing.NamedTuple): class Hint(typing.NamedTuple):

View File

@@ -195,10 +195,10 @@ def set_icon(window):
window.tk.call('wm', 'iconphoto', window._w, logo) window.tk.call('wm', 'iconphoto', window._w, logo)
def adjust(args): def adjust(args):
# Create a fake multiworld and OOTWorld to use as a base # Create a fake world and OOTWorld to use as a base
multiworld = MultiWorld(1) world = MultiWorld(1)
multiworld.per_slot_randoms = {1: random} world.per_slot_randoms = {1: random}
ootworld = OOTWorld(multiworld, 1) ootworld = OOTWorld(world, 1)
# Set options in the fake OOTWorld # Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()): for name, option in chain(cosmetic_options.items(), sfx_options.items()):
result = getattr(args, name, None) result = getattr(args, name, None)

View File

@@ -1,18 +1,19 @@
from __future__ import annotations from __future__ import annotations
import abc import abc
import functools
import logging import logging
from copy import deepcopy
from dataclasses import dataclass
import functools
import math import math
import numbers import numbers
import random import random
import typing import typing
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass
from schema import And, Optional, Or, Schema from schema import And, Optional, Or, Schema
from Utils import get_fuzzy_results, is_iterable_except_str from Utils import get_fuzzy_results
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions from BaseClasses import PlandoOptions
@@ -41,11 +42,6 @@ class AssembleOptions(abc.ABCMeta):
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")} name.startswith("alias_")}
assert (
name in {"Option", "VerifyKeys"} or # base abstract classes don't need default
"default" in attrs or
any(hasattr(base, "default") for base in bases)
), f"Option class {name} needs default value"
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned." assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
# auto-alias Off and On being parsed as True and False # auto-alias Off and On being parsed as True and False
@@ -63,7 +59,6 @@ class AssembleOptions(abc.ABCMeta):
def verify(self, *args, **kwargs) -> None: def verify(self, *args, **kwargs) -> None:
for f in verifiers: for f in verifiers:
f(self, *args, **kwargs) f(self, *args, **kwargs)
attrs["verify"] = verify attrs["verify"] = verify
else: else:
assert verifiers, "class Option is supposed to implement def verify" assert verifiers, "class Option is supposed to implement def verify"
@@ -101,7 +96,7 @@ T = typing.TypeVar('T')
class Option(typing.Generic[T], metaclass=AssembleOptions): class Option(typing.Generic[T], metaclass=AssembleOptions):
value: T value: T
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type default = 0
# convert option_name_long into Name Long as display_name, otherwise name_long is the result. # convert option_name_long into Name Long as display_name, otherwise name_long is the result.
# Handled in get_option_name() # Handled in get_option_name()
@@ -111,9 +106,8 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
supports_weighting = True supports_weighting = True
# filled by AssembleOptions: # filled by AssembleOptions:
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore name_lookup: typing.Dict[T, str]
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore options: typing.Dict[str, int]
options: typing.ClassVar[typing.Dict[str, int]]
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.current_option_name})" return f"{self.__class__.__name__}({self.current_option_name})"
@@ -166,8 +160,6 @@ class FreeText(Option[str]):
"""Text option that allows users to enter strings. """Text option that allows users to enter strings.
Needs to be validated by the world or option definition.""" Needs to be validated by the world or option definition."""
default = ""
def __init__(self, value: str): def __init__(self, value: str):
assert isinstance(value, str), "value of FreeText must be a string" assert isinstance(value, str), "value of FreeText must be a string"
self.value = value self.value = value
@@ -188,18 +180,9 @@ class FreeText(Option[str]):
def get_option_name(cls, value: str) -> str: def get_option_name(cls, value: str) -> str:
return value return value
def __eq__(self, other):
if isinstance(other, self.__class__):
return other.value == self.value
elif isinstance(other, str):
return other == self.value
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class NumericOption(Option[int], numbers.Integral, abc.ABC): class NumericOption(Option[int], numbers.Integral, abc.ABC):
default = 0 default = 0
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards # note: some of the `typing.Any`` here is a result of unresolved issue in python standards
# `int` is not a `numbers.Integral` according to the official typestubs # `int` is not a `numbers.Integral` according to the official typestubs
# (even though isinstance(5, numbers.Integral) == True) # (even though isinstance(5, numbers.Integral) == True)
@@ -615,7 +598,7 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
if isinstance(self.value, int): if isinstance(self.value, int):
return return
from BaseClasses import PlandoOptions from BaseClasses import PlandoOptions
if not (PlandoOptions.bosses & plando_options): if not(PlandoOptions.bosses & plando_options):
# plando is disabled but plando options were given so pull the option and change it to an int # plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1] option = self.value.split(";")[-1]
self.value = self.options[option] self.value = self.options[option]
@@ -744,7 +727,7 @@ class SpecialRange(NamedRange):
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In " "Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
"NamedRange, range_start specifies the lower end of the regular range, while special values can be " "NamedRange, range_start specifies the lower end of the regular range, while special values can be "
"placed anywhere (below, inside, or above the regular range).") "placed anywhere (below, inside, or above the regular range).")
return super().__new__(cls) return super().__new__(cls, value)
@classmethod @classmethod
def weighted_range(cls, text) -> Range: def weighted_range(cls, text) -> Range:
@@ -782,7 +765,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
value: typing.Any value: typing.Any
@classmethod @classmethod
def verify_keys(cls, data: typing.Iterable[str]) -> None: def verify_keys(cls, data: typing.List[str]):
if cls.valid_keys: if cls.valid_keys:
data = set(data) data = set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
@@ -819,7 +802,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
default = {} default: typing.Dict[str, typing.Any] = {}
supports_weighting = False supports_weighting = False
def __init__(self, value: typing.Dict[str, typing.Any]): def __init__(self, value: typing.Dict[str, typing.Any]):
@@ -860,11 +843,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead. # If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
# Not a docstring so it doesn't get grabbed by the options system. # Not a docstring so it doesn't get grabbed by the options system.
default = () default: typing.List[typing.Any] = []
supports_weighting = False supports_weighting = False
def __init__(self, value: typing.Iterable[typing.Any]): def __init__(self, value: typing.List[typing.Any]):
self.value = list(deepcopy(value)) self.value = deepcopy(value)
super(OptionList, self).__init__() super(OptionList, self).__init__()
@classmethod @classmethod
@@ -873,7 +856,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
@classmethod @classmethod
def from_any(cls, data: typing.Any): def from_any(cls, data: typing.Any):
if is_iterable_except_str(data): if type(data) == list:
cls.verify_keys(data) cls.verify_keys(data)
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
@@ -886,7 +869,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
class OptionSet(Option[typing.Set[str]], VerifyKeys): class OptionSet(Option[typing.Set[str]], VerifyKeys):
default = frozenset() default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
supports_weighting = False supports_weighting = False
def __init__(self, value: typing.Iterable[str]): def __init__(self, value: typing.Iterable[str]):
@@ -899,7 +882,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
@classmethod @classmethod
def from_any(cls, data: typing.Any): def from_any(cls, data: typing.Any):
if is_iterable_except_str(data): if isinstance(data, (list, set, frozenset)):
cls.verify_keys(data) cls.verify_keys(data)
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
@@ -949,7 +932,7 @@ class OptionsMetaProperty(type):
bases: typing.Tuple[type, ...], bases: typing.Tuple[type, ...],
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty": attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
for attr_type in attrs.values(): for attr_type in attrs.values():
assert not isinstance(attr_type, AssembleOptions), \ assert not isinstance(attr_type, AssembleOptions),\
f"Options for {name} should be type hinted on the class, not assigned" f"Options for {name} should be type hinted on the class, not assigned"
return super().__new__(mcs, name, bases, attrs) return super().__new__(mcs, name, bases, attrs)
@@ -1127,11 +1110,6 @@ class PerGameCommonOptions(CommonOptions):
item_links: ItemLinks item_links: ItemLinks
@dataclass
class DeathLinkMixin:
death_link: DeathLink
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
import os import os

View File

@@ -8,7 +8,7 @@ if __name__ == "__main__":
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
from worlds.Files import AutoPatchRegister, APAutoPatchInterface from worlds.Files import AutoPatchRegister, APDeltaPatch
class RomMeta(TypedDict): class RomMeta(TypedDict):
@@ -20,7 +20,7 @@ class RomMeta(TypedDict):
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]: def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
auto_handler = AutoPatchRegister.get_handler(patch_file) auto_handler = AutoPatchRegister.get_handler(patch_file)
if auto_handler: if auto_handler:
handler: APAutoPatchInterface = auto_handler(patch_file) handler: APDeltaPatch = auto_handler(patch_file)
target = os.path.splitext(patch_file)[0]+handler.result_file_ending target = os.path.splitext(patch_file)[0]+handler.result_file_ending
handler.patch(target) handler.patch(target)
return {"server": handler.server, return {"server": handler.server,

View File

@@ -25,7 +25,7 @@ Currently, the following games are supported:
* Hollow Knight * Hollow Knight
* The Witness * The Witness
* Sonic Adventure 2: Battle * Sonic Adventure 2: Battle
* Starcraft 2 * Starcraft 2: Wings of Liberty
* Donkey Kong Country 3 * Donkey Kong Country 3
* Dark Souls 3 * Dark Souls 3
* Super Mario World * Super Mario World
@@ -59,12 +59,6 @@ Currently, the following games are supported:
* Landstalker: The Treasures of King Nole * Landstalker: The Treasures of King Nole
* Final Fantasy Mystic Quest * Final Fantasy Mystic Quest
* TUNIC * TUNIC
* Kirby's Dream Land 3
* Celeste 64
* Zork Grand Inquisitor
* Castlevania 64
* A Short Hike
* Yoshi's Island
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
@@ -86,9 +80,9 @@ We recognize that there is a strong community of incredibly smart people that ha
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project. Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
## Running Archipelago ## Running Archipelago
For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems. For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md). If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
## Related Repositories ## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present. This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
from worlds.sc2.Client import launch from worlds.sc2wol.Client import launch
import Utils import Utils
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -19,13 +19,14 @@ import warnings
from argparse import Namespace from argparse import Namespace
from settings import Settings, get_settings from settings import Settings, get_settings
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard from yaml import load, load_all, dump, SafeLoader
from yaml import load, load_all, dump
try: try:
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper from yaml import CLoader as UnsafeLoader
from yaml import CDumper as Dumper
except ImportError: except ImportError:
from yaml import Loader as UnsafeLoader, SafeLoader, Dumper from yaml import Loader as UnsafeLoader
from yaml import Dumper
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import tkinter import tkinter
@@ -46,7 +47,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.4.5" __version__ = "0.4.4"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
@@ -225,9 +226,6 @@ class UniqueKeyLoader(SafeLoader):
if key in mapping: if key in mapping:
logging.error(f"YAML duplicates sanity check failed{key_node.start_mark}") logging.error(f"YAML duplicates sanity check failed{key_node.start_mark}")
raise KeyError(f"Duplicate key {key} found in YAML. Already found keys: {mapping}.") raise KeyError(f"Duplicate key {key} found in YAML. Already found keys: {mapping}.")
if (str(key).startswith("+") and (str(key)[1:] in mapping)) or (f"+{key}" in mapping):
logging.error(f"YAML merge duplicates sanity check failed{key_node.start_mark}")
raise KeyError(f"Equivalent key {key} found in YAML. Already found keys: {mapping}.")
mapping.add(key) mapping.add(key)
return super().construct_mapping(node, deep) return super().construct_mapping(node, deep)
@@ -716,7 +714,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
import ctypes import ctypes
style = 0x10 if error else 0x0 style = 0x10 if error else 0x0
return ctypes.windll.user32.MessageBoxW(0, text, title, style) return ctypes.windll.user32.MessageBoxW(0, text, title, style)
# fall back to tk # fall back to tk
try: try:
import tkinter import tkinter
@@ -873,8 +871,8 @@ def visualize_regions(root_region: Region, file_name: str, *,
Example usage in Main code: Example usage in Main code:
from Utils import visualize_regions from Utils import visualize_regions
for player in multiworld.player_ids: for player in world.player_ids:
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml") visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
""" """
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled" assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
@@ -970,10 +968,3 @@ class RepeatableChain:
def __len__(self): def __len__(self):
return sum(len(iterable) for iterable in self.iterable) return sum(len(iterable) for iterable in self.iterable)
def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]]:
""" `str` is `Iterable`, but that's not what we want """
if isinstance(obj, str):
return False
return isinstance(obj, typing.Iterable)

View File

@@ -28,7 +28,7 @@ def check():
results, _ = roll_options(options) results, _ = roll_options(options)
if len(options) > 1: if len(options) > 1:
# offer combined file back # offer combined file back
combined_yaml = "\n---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}" combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
for file_name, file_content in options.items()) for file_name, file_content in options.items())
combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode() combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
else: else:

View File

@@ -25,16 +25,16 @@ window.addEventListener('load', () => {
// Collapsible advancement sections // Collapsible advancement sections
const categories = document.getElementsByClassName("location-category"); const categories = document.getElementsByClassName("location-category");
for (let category of categories) { for (let i = 0; i < categories.length; i++) {
let hide_id = category.id.split('_')[0]; let hide_id = categories[i].id.split('-')[0];
if (hide_id === 'Total') { if (hide_id == 'Total') {
continue; continue;
} }
category.addEventListener('click', function() { categories[i].addEventListener('click', function() {
// Toggle the advancement list // Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide"); document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header // Change text of the header
const tab_header = document.getElementById(hide_id+'_header').children[0]; const tab_header = document.getElementById(hide_id+'-header').children[0];
const orig_text = tab_header.innerHTML; const orig_text = tab_header.innerHTML;
let new_text; let new_text;
if (orig_text.includes("▼")) { if (orig_text.includes("▼")) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,160 +0,0 @@
#player-tracker-wrapper{
margin: 0;
}
#tracker-table td {
vertical-align: top;
}
.inventory-table-area{
border: 2px solid #000000;
border-radius: 4px;
padding: 3px 10px 3px 10px;
}
.inventory-table-area:has(.inventory-table-terran) {
width: 690px;
background-color: #525494;
}
.inventory-table-area:has(.inventory-table-zerg) {
width: 360px;
background-color: #9d60d2;
}
.inventory-table-area:has(.inventory-table-protoss) {
width: 400px;
background-color: #d2b260;
}
#tracker-table .inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
.inventory-table td.title{
padding-top: 10px;
height: 20px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
}
.inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
background-color: black;
}
.inventory-table img.acquired{
filter: none;
background-color: black;
}
.inventory-table .tint-terran img.acquired {
filter: sepia(100%) saturate(300%) brightness(130%) hue-rotate(120deg)
}
.inventory-table .tint-protoss img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(180deg)
}
.inventory-table .tint-level-1 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg)
}
.inventory-table .tint-level-2 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(120deg)
}
.inventory-table .tint-level-3 img.acquired {
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(240deg)
}
.inventory-table div.counted-item {
position: relative;
}
.inventory-table div.item-count {
width: 160px;
text-align: left;
color: black;
font-family: "JuraBook", monospace;
font-weight: bold;
}
#location-table{
border: 2px solid #000000;
border-radius: 4px;
background-color: #87b678;
padding: 10px 3px 3px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
cursor: default;
}
#location-table table{
width: 100%;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 16px;
}
#location-table td.location-name {
padding-left: 16px;
}
#location-table td:has(.location-column) {
vertical-align: top;
}
#location-table .location-column {
width: 100%;
height: 100%;
}
#location-table .location-column .spacer {
min-height: 24px;
}
.hide {
display: none;
}

View File

@@ -0,0 +1,112 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 710px;
background-color: #525494;
}
#inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
#inventory-table td.title{
padding-top: 10px;
height: 20px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
background-color: black;
}
#inventory-table img.acquired{
filter: none;
background-color: black;
}
#inventory-table div.counted-item {
position: relative;
}
#inventory-table div.item-count {
text-align: left;
color: black;
font-family: "JuraBook", monospace;
font-weight: bold;
}
#location-table{
width: 710px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #525494;
padding: 10px 3px 3px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
cursor: default;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 16px;
}
#location-table td.location-name {
padding-left: 16px;
}
.hide {
display: none;
}

View File

@@ -22,7 +22,7 @@
{% for patch in room.seed.slots|list|sort(attribute="player_id") %} {% for patch in room.seed.slots|list|sort(attribute="player_id") %}
<tr> <tr>
<td>{{ patch.player_id }}</td> <td>{{ patch.player_id }}</td>
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td> <td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
<td>{{ patch.game }}</td> <td>{{ patch.game }}</td>
<td> <td>
{% if patch.data %} {% if patch.data %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/sc2wolTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/sc2wolTracker.js') }}"></script>
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/jura" type="text/css"/>
</head>
<body>
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
<div style="margin-bottom: 0.5rem">
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
</div>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td colspan="15" class="title">
Starting Resources
</td>
</tr>
<tr>
<td><img src="{{ icons['Starting Minerals'] }}" class="{{ 'acquired' if '+15 Starting Minerals' in acquired_items }}" title="Starting Minerals" /></td>
<td colspan="2"><div class="item-count">+{{ minerals_count }}</div></td>
<td><img src="{{ icons['Starting Vespene'] }}" class="{{ 'acquired' if '+15 Starting Vespene' in acquired_items }}" title="Starting Vespene" /></td>
<td colspan="2"><div class="item-count">+{{ vespene_count }}</div></td>
<!--
<td><img src="{{ icons['Starting Supply'] }}" class="{{ 'acquired' if '+2 Starting Supply' in acquired_items }}" title="Starting Supply" /></td>
<td colspan="2"><div class="item-count">+{{ supply_count }}</div></td>
-->
</tr>
<tr>
<td colspan="15" class="title">
Weapon & Armor Upgrades
</td>
</tr>
<tr>
<td><img src="{{ infantry_weapon_url }}" class="{{ 'acquired' if 'Progressive Infantry Weapon' in acquired_items }}" title="Progressive Infantry Weapons{% if infantry_weapon_level > 0 %} (Level {{ infantry_weapon_level }}){% endif %}" /></td>
<td><img src="{{ infantry_armor_url }}" class="{{ 'acquired' if 'Progressive Infantry Armor' in acquired_items }}" title="Progressive Infantry Armor{% if infantry_armor_level > 0 %} (Level {{ infantry_armor_level }}){% endif %}" /></td>
<td><img src="{{ vehicle_weapon_url }}" class="{{ 'acquired' if 'Progressive Vehicle Weapon' in acquired_items }}" title="Progressive Vehicle Weapons{% if vehicle_weapon_level > 0 %} (Level {{ vehicle_weapon_level }}){% endif %}" /></td>
<td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td>
<td><img src="{{ ship_weapon_url }}" class="{{ 'acquired' if 'Progressive Ship Weapon' in acquired_items }}" title="Progressive Ship Weapons{% if ship_weapon_level > 0 %} (Level {{ ship_weapon_level }}){% endif %}" /></td>
<td><img src="{{ ship_armor_url }}" class="{{ 'acquired' if 'Progressive Ship Armor' in acquired_items }}" title="Progressive Ship Armor{% if ship_armor_level > 0 %} (Level {{ ship_armor_level }}){% endif %}" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Base
</td>
</tr>
<tr>
<td><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
<td><img src="{{ icons['Projectile Accelerator (Bunker)'] }}" class="{{ 'acquired' if 'Projectile Accelerator (Bunker)' in acquired_items }}" title="Projectile Accelerator (Bunker)" /></td>
<td><img src="{{ icons['Neosteel Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Neosteel Bunker (Bunker)' in acquired_items }}" title="Neosteel Bunker (Bunker)" /></td>
<td><img src="{{ icons['Shrike Turret (Bunker)'] }}" class="{{ 'acquired' if 'Shrike Turret (Bunker)' in acquired_items }}" title="Shrike Turret (Bunker)" /></td>
<td><img src="{{ icons['Fortified Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Fortified Bunker (Bunker)' in acquired_items }}" title="Fortified Bunker (Bunker)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
<td><img src="{{ icons['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td>
<td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
<td></td>
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
<td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td>
<td></td>
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
<td></td>
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
<td></td>
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
</tr>
<tr>
<td><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
<td></td>
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
<td></td>
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
<td></td>
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
</tr>
<tr>
<td colspan="7" class="title">
Infantry
</td>
<td></td>
<td colspan="7" class="title">
Vehicles
</td>
</tr>
<tr>
<td><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
<td><img src="{{ stimpack_marine_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marine)' in acquired_items }}" title="{{ stimpack_marine_name }}" /></td>
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td>
<td><img src="{{ icons['Laser Targeting System (Marine)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marine)' in acquired_items }}" title="Laser Targeting System (Marine)" /></td>
<td><img src="{{ icons['Magrail Munitions (Marine)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marine)' in acquired_items }}" title="Magrail Munitions (Marine)" /></td>
<td><img src="{{ icons['Optimized Logistics (Marine)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Marine)' in acquired_items }}" title="Optimized Logistics (Marine)" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
<td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td>
<td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td>
<td><img src="{{ icons['Hellbat Aspect (Hellion)'] }}" class="{{ 'acquired' if 'Hellbat Aspect (Hellion)' in acquired_items }}" title="Hellbat Aspect (Hellion)" /></td>
<td><img src="{{ icons['Smart Servos (Hellion)'] }}" class="{{ 'acquired' if 'Smart Servos (Hellion)' in acquired_items }}" title="Smart Servos (Hellion)" /></td>
<td><img src="{{ icons['Optimized Logistics (Hellion)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Hellion)' in acquired_items }}" title="Optimized Logistics (Hellion)" /></td>
<td><img src="{{ icons['Jump Jets (Hellion)'] }}" class="{{ 'acquired' if 'Jump Jets (Hellion)' in acquired_items }}" title="Jump Jets (Hellion)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
<td><img src="{{ icons['Restoration (Medic)'] }}" class="{{ 'acquired' if 'Restoration (Medic)' in acquired_items }}" title="Restoration (Medic)" /></td>
<td><img src="{{ icons['Optical Flare (Medic)'] }}" class="{{ 'acquired' if 'Optical Flare (Medic)' in acquired_items }}" title="Optical Flare (Medic)" /></td>
<td><img src="{{ icons['Optimized Logistics (Medic)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Medic)' in acquired_items }}" title="Optimized Logistics (Medic)" /></td>
<td colspan="2"></td>
<td></td>
<td><img src="{{ stimpack_hellion_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Hellion)' in acquired_items }}" title="{{ stimpack_hellion_name }}" /></td>
</tr>
<tr>
<td><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
<td><img src="{{ stimpack_firebat_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Firebat)' in acquired_items }}" title="{{ stimpack_firebat_name }}" /></td>
<td><img src="{{ icons['Optimized Logistics (Firebat)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Firebat)' in acquired_items }}" title="Optimized Logistics (Firebat)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
<td><img src="{{ icons['Ion Thrusters (Vulture)'] }}" class="{{ 'acquired' if 'Ion Thrusters (Vulture)' in acquired_items }}" title="Ion Thrusters (Vulture)" /></td>
<td><img src="{{ icons['Auto Launchers (Vulture)'] }}" class="{{ 'acquired' if 'Auto Launchers (Vulture)' in acquired_items }}" title="Auto Launchers (Vulture)" /></td>
<td></td>
<td><img src="{{ icons['Cerberus Mine (Spider Mine)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Spider Mine)' in acquired_items }}" title="Cerberus Mine (Spider Mine)" /></td>
<td><img src="{{ icons['High Explosive Munition (Spider Mine)'] }}" class="{{ 'acquired' if 'High Explosive Munition (Spider Mine)' in acquired_items }}" title="High Explosive Munition (Spider Mine)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
<td><img src="{{ stimpack_marauder_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marauder)' in acquired_items }}" title="{{ stimpack_marauder_name }}" /></td>
<td><img src="{{ icons['Laser Targeting System (Marauder)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marauder)' in acquired_items }}" title="Laser Targeting System (Marauder)" /></td>
<td><img src="{{ icons['Magrail Munitions (Marauder)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marauder)' in acquired_items }}" title="Magrail Munitions (Marauder)" /></td>
<td><img src="{{ icons['Internal Tech Module (Marauder)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Marauder)' in acquired_items }}" title="Internal Tech Module (Marauder)" /></td>
<td></td>
<td><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
<td><img src="{{ icons['Jump Jets (Goliath)'] }}" class="{{ 'acquired' if 'Jump Jets (Goliath)' in acquired_items }}" title="Jump Jets (Goliath)" /></td>
<td><img src="{{ icons['Optimized Logistics (Goliath)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Goliath)' in acquired_items }}" title="Optimized Logistics (Goliath)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
<td><img src="{{ stimpack_reaper_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Reaper)' in acquired_items }}" title="{{ stimpack_reaper_name }}" /></td>
<td><img src="{{ icons['Laser Targeting System (Reaper)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Reaper)' in acquired_items }}" title="Laser Targeting System (Reaper)" /></td>
<td><img src="{{ icons['Advanced Cloaking Field (Reaper)'] }}" class="{{ 'acquired' if 'Advanced Cloaking Field (Reaper)' in acquired_items }}" title="Advanced Cloaking Field (Reaper)" /></td>
<td><img src="{{ icons['Spider Mines (Reaper)'] }}" class="{{ 'acquired' if 'Spider Mines (Reaper)' in acquired_items }}" title="Spider Mines (Reaper)" /></td>
<td></td>
<td><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
<td><img src="{{ icons['Hyperfluxor (Diamondback)'] }}" class="{{ 'acquired' if 'Hyperfluxor (Diamondback)' in acquired_items }}" title="Hyperfluxor (Diamondback)" /></td>
<td><img src="{{ icons['Burst Capacitors (Diamondback)'] }}" class="{{ 'acquired' if 'Burst Capacitors (Diamondback)' in acquired_items }}" title="Burst Capacitors (Diamondback)" /></td>
<td><img src="{{ icons['Optimized Logistics (Diamondback)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Diamondback)' in acquired_items }}" title="Optimized Logistics (Diamondback)" /></td>
</tr>
<tr>
<td></td>
<td><img src="{{ icons['Combat Drugs (Reaper)'] }}" class="{{ 'acquired' if 'Combat Drugs (Reaper)' in acquired_items }}" title="Combat Drugs (Reaper)" /></td>
<td colspan="6"></td>
<td><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
<td><img src="{{ icons['Jump Jets (Siege Tank)'] }}" class="{{ 'acquired' if 'Jump Jets (Siege Tank)' in acquired_items }}" title="Jump Jets (Siege Tank)" /></td>
<td><img src="{{ icons['Spider Mines (Siege Tank)'] }}" class="{{ 'acquired' if 'Spider Mines (Siege Tank)' in acquired_items }}" title="Spider Mines (Siege Tank)" /></td>
<td><img src="{{ icons['Smart Servos (Siege Tank)'] }}" class="{{ 'acquired' if 'Smart Servos (Siege Tank)' in acquired_items }}" title="Smart Servos (Siege Tank)" /></td>
<td><img src="{{ icons['Graduating Range (Siege Tank)'] }}" class="{{ 'acquired' if 'Graduating Range (Siege Tank)' in acquired_items }}" title="Graduating Range (Siege Tank)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
<td><img src="{{ icons['EMP Rounds (Ghost)'] }}" class="{{ 'acquired' if 'EMP Rounds (Ghost)' in acquired_items }}" title="EMP Rounds (Ghost)" /></td>
<td><img src="{{ icons['Lockdown (Ghost)'] }}" class="{{ 'acquired' if 'Lockdown (Ghost)' in acquired_items }}" title="Lockdown (Ghost)" /></td>
<td colspan="3"></td>
<td></td>
<td><img src="{{ icons['Laser Targeting System (Siege Tank)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Siege Tank)' in acquired_items }}" title="Laser Targeting System (Siege Tank)" /></td>
<td><img src="{{ icons['Advanced Siege Tech (Siege Tank)'] }}" class="{{ 'acquired' if 'Advanced Siege Tech (Siege Tank)' in acquired_items }}" title="Advanced Siege Tech (Siege Tank)" /></td>
<td><img src="{{ icons['Internal Tech Module (Siege Tank)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Siege Tank)' in acquired_items }}" title="Internal Tech Module (Siege Tank)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
<td><img src="{{ icons['Impaler Rounds (Spectre)'] }}" class="{{ 'acquired' if 'Impaler Rounds (Spectre)' in acquired_items }}" title="Impaler Rounds (Spectre)" /></td>
<td colspan="4"></td>
<td><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
<td><img src="{{ high_impact_payload_thor_url }}" class="{{ 'acquired' if 'Progressive High Impact Payload (Thor)' in acquired_items }}" title="{{ high_impact_payload_thor_name }}" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
<td><img src="{{ icons['Optimized Logistics (Predator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Predator)' in acquired_items }}" title="Optimized Logistics (Predator)" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Widow Mine'] }}" class="{{ 'acquired' if 'Widow Mine' in acquired_items }}" title="Widow Mine" /></td>
<td><img src="{{ icons['Drilling Claws (Widow Mine)'] }}" class="{{ 'acquired' if 'Drilling Claws (Widow Mine)' in acquired_items }}" title="Drilling Claws (Widow Mine)" /></td>
<td><img src="{{ icons['Concealment (Widow Mine)'] }}" class="{{ 'acquired' if 'Concealment (Widow Mine)' in acquired_items }}" title="Concealment (Widow Mine)" /></td>
<td><img src="{{ icons['Black Market Launchers (Widow Mine)'] }}" class="{{ 'acquired' if 'Black Market Launchers (Widow Mine)' in acquired_items }}" title="Black Market Launchers (Widow Mine)" /></td>
<td><img src="{{ icons['Executioner Missiles (Widow Mine)'] }}" class="{{ 'acquired' if 'Executioner Missiles (Widow Mine)' in acquired_items }}" title="Executioner Missiles (Widow Mine)" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Cyclone'] }}" class="{{ 'acquired' if 'Cyclone' in acquired_items }}" title="Cyclone" /></td>
<td><img src="{{ icons['Mag-Field Accelerators (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Accelerators (Cyclone)' in acquired_items }}" title="Mag-Field Accelerators (Cyclone)" /></td>
<td><img src="{{ icons['Mag-Field Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Launchers (Cyclone)' in acquired_items }}" title="Mag-Field Launchers (Cyclone)" /></td>
<td><img src="{{ icons['Targeting Optics (Cyclone)'] }}" class="{{ 'acquired' if 'Targeting Optics (Cyclone)' in acquired_items }}" title="Targeting Optics (Cyclone)" /></td>
<td><img src="{{ icons['Rapid Fire Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Rapid Fire Launchers (Cyclone)' in acquired_items }}" title="Rapid Fire Launchers (Cyclone)" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Starships
</td>
</tr>
<tr>
<td><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
<td><img src="{{ icons['Rapid Deployment Tube (Medivac)'] }}" class="{{ 'acquired' if 'Rapid Deployment Tube (Medivac)' in acquired_items }}" title="Rapid Deployment Tube (Medivac)" /></td>
<td><img src="{{ icons['Advanced Healing AI (Medivac)'] }}" class="{{ 'acquired' if 'Advanced Healing AI (Medivac)' in acquired_items }}" title="Advanced Healing AI (Medivac)" /></td>
<td><img src="{{ icons['Expanded Hull (Medivac)'] }}" class="{{ 'acquired' if 'Expanded Hull (Medivac)' in acquired_items }}" title="Expanded Hull (Medivac)" /></td>
<td><img src="{{ icons['Afterburners (Medivac)'] }}" class="{{ 'acquired' if 'Afterburners (Medivac)' in acquired_items }}" title="Afterburners (Medivac)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
<td><img src="{{ icons['Bio Mechanical Repair Drone (Raven)'] }}" class="{{ 'acquired' if 'Bio Mechanical Repair Drone (Raven)' in acquired_items }}" title="Bio Mechanical Repair Drone (Raven)" /></td>
<td><img src="{{ icons['Spider Mines (Raven)'] }}" class="{{ 'acquired' if 'Spider Mines (Raven)' in acquired_items }}" title="Spider Mines (Raven)" /></td>
<td><img src="{{ icons['Railgun Turret (Raven)'] }}" class="{{ 'acquired' if 'Railgun Turret (Raven)' in acquired_items }}" title="Railgun Turret (Raven)" /></td>
<td><img src="{{ icons['Hunter-Seeker Weapon (Raven)'] }}" class="{{ 'acquired' if 'Hunter-Seeker Weapon (Raven)' in acquired_items }}" title="Hunter-Seeker Weapon (Raven)" /></td>
<td><img src="{{ icons['Interference Matrix (Raven)'] }}" class="{{ 'acquired' if 'Interference Matrix (Raven)' in acquired_items }}" title="Interference Matrix (Raven)" /></td>
<td><img src="{{ icons['Anti-Armor Missile (Raven)'] }}" class="{{ 'acquired' if 'Anti-Armor Missile (Raven)' in acquired_items }}" title="Anti-Armor Missile (Raven)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
<td><img src="{{ icons['Tomahawk Power Cells (Wraith)'] }}" class="{{ 'acquired' if 'Tomahawk Power Cells (Wraith)' in acquired_items }}" title="Tomahawk Power Cells (Wraith)" /></td>
<td><img src="{{ icons['Displacement Field (Wraith)'] }}" class="{{ 'acquired' if 'Displacement Field (Wraith)' in acquired_items }}" title="Displacement Field (Wraith)" /></td>
<td><img src="{{ icons['Advanced Laser Technology (Wraith)'] }}" class="{{ 'acquired' if 'Advanced Laser Technology (Wraith)' in acquired_items }}" title="Advanced Laser Technology (Wraith)" /></td>
<td colspan="4"></td>
<td></td>
<td><img src="{{ icons['Internal Tech Module (Raven)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Raven)' in acquired_items }}" title="Internal Tech Module (Raven)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
<td><img src="{{ icons['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td>
<td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td>
<td><img src="{{ icons['Smart Servos (Viking)'] }}" class="{{ 'acquired' if 'Smart Servos (Viking)' in acquired_items }}" title="Smart Servos (Viking)" /></td>
<td><img src="{{ icons['Magrail Munitions (Viking)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Viking)' in acquired_items }}" title="Magrail Munitions (Viking)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
<td><img src="{{ icons['EMP Shockwave (Science Vessel)'] }}" class="{{ 'acquired' if 'EMP Shockwave (Science Vessel)' in acquired_items }}" title="EMP Shockwave (Science Vessel)" /></td>
<td><img src="{{ icons['Defensive Matrix (Science Vessel)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Science Vessel)' in acquired_items }}" title="Defensive Matrix (Science Vessel)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
<td><img src="{{ crossspectrum_dampeners_banshee_url }}" class="{{ 'acquired' if 'Progressive Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="{{ crossspectrum_dampeners_banshee_name }}" /></td>
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td>
<td><img src="{{ icons['Hyperflight Rotors (Banshee)'] }}" class="{{ 'acquired' if 'Hyperflight Rotors (Banshee)' in acquired_items }}" title="Hyperflight Rotors (Banshee)" /></td>
<td><img src="{{ icons['Laser Targeting System (Banshee)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Banshee)' in acquired_items }}" title="Laser Targeting System (Banshee)" /></td>
<td><img src="{{ icons['Internal Tech Module (Banshee)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Banshee)' in acquired_items }}" title="Internal Tech Module (Banshee)" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
</tr>
<tr>
<td><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
<td><img src="{{ icons['Missile Pods (Battlecruiser)'] }}" class="{{ 'acquired' if 'Missile Pods (Battlecruiser)' in acquired_items }}" title="Missile Pods (Battlecruiser)" /></td>
<td><img src="{{ icons['Defensive Matrix (Battlecruiser)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Battlecruiser)' in acquired_items }}" title="Defensive Matrix (Battlecruiser)" /></td>
<td><img src="{{ icons['Tactical Jump (Battlecruiser)'] }}" class="{{ 'acquired' if 'Tactical Jump (Battlecruiser)' in acquired_items }}" title="Tactical Jump (Battlecruiser)" /></td>
<td><img src="{{ icons['Cloak (Battlecruiser)'] }}" class="{{ 'acquired' if 'Cloak (Battlecruiser)' in acquired_items }}" title="Cloak (Battlecruiser)" /></td>
<td><img src="{{ icons['ATX Laser Battery (Battlecruiser)'] }}" class="{{ 'acquired' if 'ATX Laser Battery (Battlecruiser)' in acquired_items }}" title="ATX Laser Battery (Battlecruiser)" /></td>
<td><img src="{{ icons['Optimized Logistics (Battlecruiser)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Battlecruiser)' in acquired_items }}" title="Optimized Logistics (Battlecruiser)" /></td>
<td></td>
<td><img src="{{ icons['Liberator'] }}" class="{{ 'acquired' if 'Liberator' in acquired_items }}" title="Liberator" /></td>
<td><img src="{{ icons['Advanced Ballistics (Liberator)'] }}" class="{{ 'acquired' if 'Advanced Ballistics (Liberator)' in acquired_items }}" title="Advanced Ballistics (Liberator)" /></td>
<td><img src="{{ icons['Raid Artillery (Liberator)'] }}" class="{{ 'acquired' if 'Raid Artillery (Liberator)' in acquired_items }}" title="Raid Artillery (Liberator)" /></td>
<td><img src="{{ icons['Cloak (Liberator)'] }}" class="{{ 'acquired' if 'Cloak (Liberator)' in acquired_items }}" title="Cloak (Liberator)" /></td>
<td><img src="{{ icons['Laser Targeting System (Liberator)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Liberator)' in acquired_items }}" title="Laser Targeting System (Liberator)" /></td>
<td><img src="{{ icons['Optimized Logistics (Liberator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Liberator)' in acquired_items }}" title="Optimized Logistics (Liberator)" /></td>
</tr>
<tr>
<td></td>
<td><img src="{{ icons['Internal Tech Module (Battlecruiser)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Battlecruiser)' in acquired_items }}" title="Internal Tech Module (Battlecruiser)" /></td>
<td colspan="6"></td>
<td><img src="{{ icons['Valkyrie'] }}" class="{{ 'acquired' if 'Valkyrie' in acquired_items }}" title="Valkyrie" /></td>
<td><img src="{{ icons['Enhanced Cluster Launchers (Valkyrie)'] }}" class="{{ 'acquired' if 'Enhanced Cluster Launchers (Valkyrie)' in acquired_items }}" title="Enhanced Cluster Launchers (Valkyrie)" /></td>
<td><img src="{{ icons['Shaped Hull (Valkyrie)'] }}" class="{{ 'acquired' if 'Shaped Hull (Valkyrie)' in acquired_items }}" title="Shaped Hull (Valkyrie)" /></td>
<td><img src="{{ icons['Burst Lasers (Valkyrie)'] }}" class="{{ 'acquired' if 'Burst Lasers (Valkyrie)' in acquired_items }}" title="Burst Lasers (Valkyrie)" /></td>
<td><img src="{{ icons['Afterburners (Valkyrie)'] }}" class="{{ 'acquired' if 'Afterburners (Valkyrie)' in acquired_items }}" title="Afterburners (Valkyrie)" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Mercenaries
</td>
</tr>
<tr>
<td><img src="{{ icons['War Pigs'] }}" class="{{ 'acquired' if 'War Pigs' in acquired_items }}" title="War Pigs" /></td>
<td><img src="{{ icons['Devil Dogs'] }}" class="{{ 'acquired' if 'Devil Dogs' in acquired_items }}" title="Devil Dogs" /></td>
<td><img src="{{ icons['Hammer Securities'] }}" class="{{ 'acquired' if 'Hammer Securities' in acquired_items }}" title="Hammer Securities" /></td>
<td><img src="{{ icons['Spartan Company'] }}" class="{{ 'acquired' if 'Spartan Company' in acquired_items }}" title="Spartan Company" /></td>
<td><img src="{{ icons['Siege Breakers'] }}" class="{{ 'acquired' if 'Siege Breakers' in acquired_items }}" title="Siege Breakers" /></td>
<td><img src="{{ icons['Hel\'s Angel'] }}" class="{{ 'acquired' if 'Hel\'s Angel' in acquired_items }}" title="Hel's Angel" /></td>
<td><img src="{{ icons['Dusk Wings'] }}" class="{{ 'acquired' if 'Dusk Wings' in acquired_items }}" title="Dusk Wings" /></td>
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
</tr>
<tr>
<td colspan="15" class="title">
General Upgrades
</td>
</tr>
<tr>
<td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
<td><img src="{{ regenerative_biosteel_url }}" class="{{ 'acquired' if 'Progressive Regenerative Bio-Steel' in acquired_items }}" title="Progressive Regenerative Bio-Steel{% if regenerative_biosteel_level > 0 %} (Level {{ regenerative_biosteel_level }}){% endif %}" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Protoss Units
</td>
</tr>
<tr>
<td><img src="{{ icons['Zealot'] }}" class="{{ 'acquired' if 'Zealot' in acquired_items }}" title="Zealot" /></td>
<td><img src="{{ icons['Stalker'] }}" class="{{ 'acquired' if 'Stalker' in acquired_items }}" title="Stalker" /></td>
<td><img src="{{ icons['High Templar'] }}" class="{{ 'acquired' if 'High Templar' in acquired_items }}" title="High Templar" /></td>
<td><img src="{{ icons['Dark Templar'] }}" class="{{ 'acquired' if 'Dark Templar' in acquired_items }}" title="Dark Templar" /></td>
<td><img src="{{ icons['Immortal'] }}" class="{{ 'acquired' if 'Immortal' in acquired_items }}" title="Immortal" /></td>
<td><img src="{{ icons['Colossus'] }}" class="{{ 'acquired' if 'Colossus' in acquired_items }}" title="Colossus" /></td>
<td><img src="{{ icons['Phoenix'] }}" class="{{ 'acquired' if 'Phoenix' in acquired_items }}" title="Phoenix" /></td>
<td><img src="{{ icons['Void Ray'] }}" class="{{ 'acquired' if 'Void Ray' in acquired_items }}" title="Void Ray" /></td>
<td><img src="{{ icons['Carrier'] }}" class="{{ 'acquired' if 'Carrier' in acquired_items }}" title="Carrier" /></td>
</tr>
</table>
<table id="location-table">
{% for area in checks_in_area %}
{% if checks_in_area[area] > 0 %}
<tr class="location-category" id="{{area}}-header">
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
</tr>
<tbody class="locations hide" id="{{area}}">
{% for location in location_info[area] %}
<tr>
<td class="location-name">{{ location }}</td>
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endif %}
{% endfor %}
</table>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -48,7 +48,6 @@ cdef struct IndexEntry:
size_t count size_t count
@cython.auto_pickle(False)
cdef class LocationStore: cdef class LocationStore:
"""Compact store for locations and their items in a MultiServer""" """Compact store for locations and their items in a MultiServer"""
# The original implementation uses Dict[int, Dict[int, Tuple(int, int, int]] # The original implementation uses Dict[int, Dict[int, Tuple(int, int, int]]
@@ -79,6 +78,18 @@ cdef class LocationStore:
size += sizeof(self._raw_proxies[0]) * self.sender_index_size size += sizeof(self._raw_proxies[0]) * self.sender_index_size
return size return size
def __cinit__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
self._mem = None
self._keys = None
self._items = None
self._proxies = None
self._len = 0
self.entries = NULL
self.entry_count = 0
self.sender_index = NULL
self.sender_index_size = 0
self._raw_proxies = NULL
def __init__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None: def __init__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
self._mem = Pool() self._mem = Pool()
cdef object key cdef object key
@@ -270,7 +281,6 @@ cdef class LocationStore:
entry.location not in checked]) entry.location not in checked])
@cython.auto_pickle(False)
@cython.internal # unsafe. disable direct import @cython.internal # unsafe. disable direct import
cdef class PlayerLocationProxy: cdef class PlayerLocationProxy:
cdef LocationStore _store cdef LocationStore _store

Binary file not shown.

View File

@@ -61,42 +61,36 @@
found_text: "Found?" found_text: "Found?"
TooltipLabel: TooltipLabel:
id: receiving id: receiving
sort_key: 'receiving'
text: root.receiving_text text: root.receiving_text
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'
pos_hint: {"center_y": 0.5} pos_hint: {"center_y": 0.5}
TooltipLabel: TooltipLabel:
id: item id: item
sort_key: 'item'
text: root.item_text text: root.item_text
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'
pos_hint: {"center_y": 0.5} pos_hint: {"center_y": 0.5}
TooltipLabel: TooltipLabel:
id: finding id: finding
sort_key: 'finding'
text: root.finding_text text: root.finding_text
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'
pos_hint: {"center_y": 0.5} pos_hint: {"center_y": 0.5}
TooltipLabel: TooltipLabel:
id: location id: location
sort_key: 'location'
text: root.location_text text: root.location_text
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'
pos_hint: {"center_y": 0.5} pos_hint: {"center_y": 0.5}
TooltipLabel: TooltipLabel:
id: entrance id: entrance
sort_key: 'entrance'
text: root.entrance_text text: root.entrance_text
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'
pos_hint: {"center_y": 0.5} pos_hint: {"center_y": 0.5}
TooltipLabel: TooltipLabel:
id: found id: found
sort_key: 'found'
text: root.found_text text: root.found_text
halign: 'center' halign: 'center'
valign: 'center' valign: 'center'

View File

@@ -22,10 +22,6 @@ SOFTWARE.
local SCRIPT_VERSION = 1 local SCRIPT_VERSION = 1
-- Set to log incoming requests
-- Will cause lag due to large console output
local DEBUG = false
--[[ --[[
This script expects to receive JSON and will send JSON back. A message should This script expects to receive JSON and will send JSON back. A message should
be a list of 1 or more requests which will be executed in order. Each request be a list of 1 or more requests which will be executed in order. Each request
@@ -275,6 +271,10 @@ local base64 = require("base64")
local socket = require("socket") local socket = require("socket")
local json = require("json") local json = require("json")
-- Set to log incoming requests
-- Will cause lag due to large console output
local DEBUG = false
local SOCKET_PORT_FIRST = 43055 local SOCKET_PORT_FIRST = 43055
local SOCKET_PORT_RANGE_SIZE = 5 local SOCKET_PORT_RANGE_SIZE = 5
local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE
@@ -330,28 +330,18 @@ function unlock ()
client_socket:settimeout(0) client_socket:settimeout(0)
end end
request_handlers = { function process_request (req)
["PING"] = function (req) local res = {}
local res = {}
if req["type"] == "PING" then
res["type"] = "PONG" res["type"] = "PONG"
return res elseif req["type"] == "SYSTEM" then
end,
["SYSTEM"] = function (req)
local res = {}
res["type"] = "SYSTEM_RESPONSE" res["type"] = "SYSTEM_RESPONSE"
res["value"] = emu.getsystemid() res["value"] = emu.getsystemid()
return res elseif req["type"] == "PREFERRED_CORES" then
end,
["PREFERRED_CORES"] = function (req)
local res = {}
local preferred_cores = client.getconfig().PreferredCores local preferred_cores = client.getconfig().PreferredCores
res["type"] = "PREFERRED_CORES_RESPONSE" res["type"] = "PREFERRED_CORES_RESPONSE"
res["value"] = {} res["value"] = {}
res["value"]["NES"] = preferred_cores.NES res["value"]["NES"] = preferred_cores.NES
@@ -364,21 +354,14 @@ request_handlers = {
res["value"]["PCECD"] = preferred_cores.PCECD res["value"]["PCECD"] = preferred_cores.PCECD
res["value"]["SGX"] = preferred_cores.SGX res["value"]["SGX"] = preferred_cores.SGX
return res elseif req["type"] == "HASH" then
end,
["HASH"] = function (req)
local res = {}
res["type"] = "HASH_RESPONSE" res["type"] = "HASH_RESPONSE"
res["value"] = rom_hash res["value"] = rom_hash
return res elseif req["type"] == "GUARD" then
end, res["type"] = "GUARD_RESPONSE"
["GUARD"] = function (req)
local res = {}
local expected_data = base64.decode(req["expected_data"]) local expected_data = base64.decode(req["expected_data"])
local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"]) local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"])
local data_is_validated = true local data_is_validated = true
@@ -389,83 +372,39 @@ request_handlers = {
end end
end end
res["type"] = "GUARD_RESPONSE"
res["value"] = data_is_validated res["value"] = data_is_validated
res["address"] = req["address"] res["address"] = req["address"]
return res elseif req["type"] == "LOCK" then
end,
["LOCK"] = function (req)
local res = {}
res["type"] = "LOCKED" res["type"] = "LOCKED"
lock() lock()
return res elseif req["type"] == "UNLOCK" then
end,
["UNLOCK"] = function (req)
local res = {}
res["type"] = "UNLOCKED" res["type"] = "UNLOCKED"
unlock() unlock()
return res elseif req["type"] == "READ" then
end,
["READ"] = function (req)
local res = {}
res["type"] = "READ_RESPONSE" res["type"] = "READ_RESPONSE"
res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"])) res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"]))
return res elseif req["type"] == "WRITE" then
end,
["WRITE"] = function (req)
local res = {}
res["type"] = "WRITE_RESPONSE" res["type"] = "WRITE_RESPONSE"
memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"]) memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"])
return res elseif req["type"] == "DISPLAY_MESSAGE" then
end,
["DISPLAY_MESSAGE"] = function (req)
local res = {}
res["type"] = "DISPLAY_MESSAGE_RESPONSE" res["type"] = "DISPLAY_MESSAGE_RESPONSE"
message_queue:push(req["message"]) message_queue:push(req["message"])
return res elseif req["type"] == "SET_MESSAGE_INTERVAL" then
end,
["SET_MESSAGE_INTERVAL"] = function (req)
local res = {}
res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE" res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE"
message_interval = req["value"] message_interval = req["value"]
return res else
end,
["default"] = function (req)
local res = {}
res["type"] = "ERROR" res["type"] = "ERROR"
res["err"] = "Unknown command: "..req["type"] res["err"] = "Unknown command: "..req["type"]
return res
end,
}
function process_request (req)
if request_handlers[req["type"]] then
return request_handlers[req["type"]](req)
else
return request_handlers["default"](req)
end end
return res
end end
-- Receive data from AP client and send message back -- Receive data from AP client and send message back

View File

@@ -322,7 +322,7 @@ function processBlock(block)
end end
end end
end end
if #itemsBlock > itemIndex then if #itemsBlock ~= itemIndex then
wU8(ITEM_INDEX, #itemsBlock) wU8(ITEM_INDEX, #itemsBlock)
end end

View File

@@ -17,10 +17,10 @@
# A. This is a .yaml file. You are allowed to use most characters. # A. This is a .yaml file. You are allowed to use most characters.
# To test if your yaml is valid or not, you can use this website: # To test if your yaml is valid or not, you can use this website:
# http://www.yamllint.com/ # http://www.yamllint.com/
# You can also verify that your Archipelago options are valid at this site: # You can also verify your Archipelago settings are valid at this site:
# https://archipelago.gg/check # https://archipelago.gg/check
# Your name in-game, limited to 16 characters. # Your name in-game. Spaces will be replaced with underscores and there is a 16-character limit.
# {player} will be replaced with the player's slot number. # {player} will be replaced with the player's slot number.
# {PLAYER} will be replaced with the player's slot number, if that slot number is greater than 1. # {PLAYER} will be replaced with the player's slot number, if that slot number is greater than 1.
# {number} will be replaced with the counter value of the name. # {number} will be replaced with the counter value of the name.

View File

@@ -28,12 +28,6 @@
# Bumper Stickers # Bumper Stickers
/worlds/bumpstik/ @FelicitusNeko /worlds/bumpstik/ @FelicitusNeko
# Castlevania 64
/worlds/cv64/ @LiquidCat64
# Celeste 64
/worlds/celeste64/ @PoryGone
# ChecksFinder # ChecksFinder
/worlds/checksfinder/ @jonloveslegos /worlds/checksfinder/ @jonloveslegos
@@ -73,9 +67,6 @@
# Hylics 2 # Hylics 2
/worlds/hylics2/ @TRPG0 /worlds/hylics2/ @TRPG0
# Kirby's Dream Land 3
/worlds/kdl3/ @Silvris
# Kingdom Hearts 2 # Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike /worlds/kh2/ @JaredWeakStrike
@@ -134,14 +125,11 @@
# Shivers # Shivers
/worlds/shivers/ @GodlFire /worlds/shivers/ @GodlFire
# A Short Hike
/worlds/shorthike/ @chandler05
# Sonic Adventure 2 Battle # Sonic Adventure 2 Battle
/worlds/sa2b/ @PoryGone @RaspberrySpace /worlds/sa2b/ @PoryGone @RaspberrySpace
# Starcraft 2 # Starcraft 2 Wings of Liberty
/worlds/sc2/ @Ziktofel /worlds/sc2wol/ @Ziktofel
# Super Metroid # Super Metroid
/worlds/sm/ @lordlou /worlds/sm/ @lordlou
@@ -177,7 +165,7 @@
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt /worlds/tloz/ @Rosalie-A @t3hf1gm3nt
# TUNIC # TUNIC
/worlds/tunic/ @silent-destroyer @ScipioWright /worlds/tunic/ @silent-destroyer
# Undertale # Undertale
/worlds/undertale/ @jonloveslegos /worlds/undertale/ @jonloveslegos
@@ -191,15 +179,9 @@
# The Witness # The Witness
/worlds/witness/ @NewSoupVi @blastron /worlds/witness/ @NewSoupVi @blastron
# Yoshi's Island
/worlds/yoshisisland/ @PinkSwitch
# Zillion # Zillion
/worlds/zillion/ @beauxq /worlds/zillion/ @beauxq
# Zork Grand Inquisitor
/worlds/zork_grand_inquisitor/ @nbrochu
################################## ##################################
## Disabled Unmaintained Worlds ## ## Disabled Unmaintained Worlds ##
################################## ##################################

View File

@@ -1,78 +1,269 @@
# Adding Games # How do I add a game to Archipelago?
Adding a new game to Archipelago has two major parts: This guide is going to try and be a broad summary of how you can do just that.
There are two key steps to incorporating a game into Archipelago:
* Game Modification to communicate with Archipelago server (hereafter referred to as "client") - Game Modification
* Archipelago Generation and Server integration plugin (hereafter referred to as "world") - Archipelago Server Integration
This document will attempt to illustrate the bare minimum requirements and expectations of both parts of a new world Refer to the following documents as well:
integration. As game modification wildly varies by system and engine, and has no bearing on the Archipelago protocol,
it will not be detailed here.
## Client - [network protocol.md](/docs/network%20protocol.md) for network communication between client and server.
- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package.
The client is an intermediary program between the game and the Archipelago server. This can either be a direct # Game Modification
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
to behave as expected are:
* Handle both secure and unsecure websocket connections One half of the work required to integrate a game into Archipelago is the development of the game client. This is
* Detect and react when a location has been "checked" by the player by sending a network packet to the server typically done through a modding API or other modification process, described further down.
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
demand
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
normally expect from features such as starting inventory, item link replacement, or item cheating
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
a player or location attributed to them
* Be able to change the port for saved connection info
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
privilege can be lost, requiring the room to be moved to a new port
* Reconnect if the connection is unstable and lost while playing
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
order.
* Receive items that were sent to the player while they were not connected to the server
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
strictly required
* Send a status update packet alerting the server that the player has completed their goal
Libraries for most modern languages and the spec for various packets can be found in the As an example, modifications to a game typically include (more on this later):
[network protocol](/docs/network%20protocol.md) API reference document.
## World - Hooking into when a 'location check' is completed.
- Networking with the Archipelago server.
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
The world is your game integration for the Archipelago generator, webhost, and multiworld server. It contains all the In order to determine how to modify a game, refer to the following sections.
information necessary for creating the items and locations to be randomized, the logic for item placement, the
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
following requirements:
* A folder within `/worlds/` that contains an `__init__.py` ## Engine Identification
* A `World` subclass where you create your world and define all of its rules
* A unique game name
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
definition
* The game_info doc must follow the format `{language_code}_{game_name}.md`
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
`item_name_to_id` and `location_name_to_id`, respectively.
* Create an item when `create_item` is called both by your code and externally
* An `options_dataclass` defining the options players have available to them
* A `Region` for your player with the name "Menu" to start from
* Create a non-zero number of locations and add them to your regions
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
Notable caveats: This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
* The "Menu" region will always be considered the "start" for the player critical. The first step is to look at a game's files. Let's go over what some game files might look like. Its
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
start of the game from anywhere Examples are provided below.
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
`append`, `extend`, or `+=`. **Do not use `=`**
* Regions are simply containers for locations that share similar access rules. They do not have to map to
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during ### Creepy Castle
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
regarding the API can be found in the [world api doc](/docs/world%20api.md). ![Creepy Castle Root Directory in Windows Explorer](/docs/img/creepy-castle-directory.png)
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. Its also your worst-case
scenario as a modder. All thats present here is an executable file and some meta-information that Steam uses. You have
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty
nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Lets look at some other
examples of game releases.
### Heavy Bullets
![Heavy Bullets Root Directory in Window's Explorer](/docs/img/heavy-bullets-directory.png)
Heres the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
information, credits, and general info about the game. You usually wont find anything too helpful here, but it never
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
“steam_api.dll” is a file you can safely ignore, its just some code used to interface with Steam.
The directory “HEAVY_BULLETS_Data”, however, has some good news.
![Heavy Bullets Data Directory in Window's Explorer](/docs/img/heavy-bullets-data-directory.png)
Jackpot! It might not be obvious what youre looking at here, but I can instantly tell from this folders contents that
what we have is a game made in the Unity Engine. If you look in the sub-folders, youll seem some .dll files which
affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered,
extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools
and information to help you on your journey can be found at this
[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking)
### Stardew Valley
![Stardew Valley Root Directory in Window's Explorer](/docs/img/stardew-valley-directory.png)
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good
news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx
and MonoMod.
### Gato Roboto
![Gato Roboto Root Directory in Window's Explorer](/docs/img/gato-roboto-directory.png)
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For
modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful.
This isn't all you'll ever see looking at game files, but it's a good place to start.
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
advantage!
## Open or Leaked Source Games
As a side note, many games have either been made open source, or have had source files leaked at some point.
This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for
"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time.
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
## Modifying Release Versions of Games
However, for now we'll assume you haven't been so lucky, and have to work with only whats sitting in your install
directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
but these are often not geared to the kind of work you'll be doing and may not help much.
As a general rule, any modding tool that lets you write actual code is something worth using.
### Research
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
it's possible other motivated parties have concocted useful tools for your game already.
Always be sure to search the Internet for the efforts of other modders.
### Other helpful tools
Depending on the games underlying engine, there may be some tools you can use either in lieu of or in addition to
existing game tools.
#### [CheatEngine](https://cheatengine.org/)
CheatEngine is a tool with a very long and storied history.
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
malware (because this behavior is most commonly found in malware and rarely used by other programs).
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
including binary data formats, addressing, and assembly language programming.
The tool itself is highly complex and even I have not yet charted its expanses.
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
modifying the actual game itself.
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
anything with it.
### What Modifications You Should Make to the Game
We talked about this briefly in [Game Modification](#game-modification) section.
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
- Know when the player has checked a location, and react accordingly
- Be able to receive items from the server on the fly
- Keep an index for items received in order to resync from disconnections
- Add interface for connecting to the Archipelago server with passwords and sessions
- Add commands for manually rewarding, re-syncing, releasing, and other actions
Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's
servers.
## But my Game is a console game. Can I still add it?
That depends what console?
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console
games.
### My Game isnt that old, its for the Wii/PS2/360/etc
This is very complex, but doable.
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
### My Game is a classic for the SNES/Sega Genesis/etc
Thats a lot more feasible.
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
community will have figured out the bulk of the consoles secrets.
Look for debugging tools, but be ready to learn assembly.
Old consoles usually have their own unique dialects of ASM youll need to get used to.
Also make sure theres a good way to interface with a running emulator, since thats the only way you can connect these
older consoles to the Internet.
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a
computer, but these will require the same sort of interface software to be written in order to work properly; from your
perspective the two won't really look any different.
### My Game is an exclusive for the Super Baby Magic Dream Boy. Its this console from the Soviet Union that-
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
Obscurity is your enemy there will likely be little to no emulator or modding information, and youd essentially be
working from scratch.
## How to Distribute Game Modifications
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
This is a good way to get any project you're working on sued out from under you.
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
to copy them wholesale, is as patches.
There are many patch formats, which I'll cover in brief. The common theme is that you cant distribute anything that
wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
the issue of distributing someone elses original work.
Users who have a copy of the game just need to apply the patch, and those who dont are unable to play.
### Patches
#### IPS
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
fine.
#### UPS, BPS, VCDIFF (xdelta), bsdiff
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
compression, so this format is used by APBP.
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
#### APBP Archipelago Binary Patch
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
bsdiff between the original and the randomized ROM.
To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`.
### Mod files
Games which support modding will usually just let you drag and drop the mods files into a folder somewhere.
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data`
so that the users don't have to move files around in order to play.
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
integration into the Webhost by inheriting from `worlds.Files.APContainer`.
## Archipelago Integration
In order for your game to communicate with the Archipelago server and generate the necessary randomized information,
you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations
and show the basics of a world. More in depth documentation on the available API can be read in
the [world api doc.](/docs/world%20api.md)
For setting up your working environment with Archipelago refer
to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md).
### Requirements
A world implementation requires a few key things from its implementation
- A folder within `worlds` that contains an `__init__.py`
- This is what defines it as a Python package and how it's able to be imported
into Archipelago's generation system. During generation time only code that is
defined within this file will be run. It's suggested to split up your information
into more files to improve readability, but all of that information can be
imported at its base level within your world.
- A `World` subclass where you create your world and define all of its rules
and the following requirements:
- Your items and locations need a `item_name_to_id` and `location_name_to_id`,
respectively, mapping.
- An `option_definitions` mapping of your game options with the format
`{name: Class}`, where `name` uses Python snake_case.
- You must define your world's `create_item` method, because this may be called
by the generator in certain circumstances
- When creating your world you submit items and regions to the Multiworld.
- These are lists of said objects which you can access at
`self.multiworld.itempool` and `self.multiworld.regions`. Best practice for
adding to these lists is with either `append` or `extend`, where `append` is a
single object and `extend` is a list.
- Do not use `=` as this will delete other worlds' items and regions.
- Regions are containers for holding your world's Locations.
- Locations are where players will "check" for items and must exist within
a region. It's also important for your world's submitted items to be the same as
its submitted locations count.
- You must always have a "Menu" Region from which the generation algorithm
uses to enter the game and access locations.
- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing.

View File

@@ -17,17 +17,7 @@ It is recommended that automated github actions are turned on in your fork to ha
You can turn them on here: You can turn them on here:
![Github actions example](./img/github-actions-example.png) ![Github actions example](./img/github-actions-example.png)
* **When reviewing PRs, please leave a message about what was done.** Other than these requests, we tend to judge code on a case by case basis.
We don't have full test coverage, so manual testing can help.
For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing
or checking if all code paths are covered by automated tests is desired. The original author may not have been able
to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to
state which games or settings were rolled, if any.
Please also tell us if you looked at code, just did functional testing, did both, or did neither.
If testing the PR depends on other PRs, please state what you merged into what for testing.
We cannot determine what "LGTM" means without additional context, so that should not be the norm.
Other than these requests, we tend to judge code on a case-by-case basis.
For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md). For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md).

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -27,13 +27,8 @@ There are also a number of community-supported libraries available that implemen
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | | | Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
| Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | | | Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | |
| Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | | | Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | |
| Game Maker + Studio 1.x | [gm-apclientpp](https://github.com/black-sliver/gm-apclientpp) | For GM7, GM8 and GMS1.x, maybe older |
| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | |
## Synchronizing Items ## Synchronizing Items
After a client connects, it will receive all previously collected items for its associated slot in a [ReceivedItems](#ReceivedItems) packet. This will include items the client may have already processed in a previous play session.
To ensure the client is able to reject those items if it needs to, each item in the packet has an associated `index` argument. You will need to find a way to save the "last processed item index" to the player's local savegame, a local file, or something to that effect. Before connecting, you should load that "last processed item index" value and compare against it in your received items handling.
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption. Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption.
@@ -330,11 +325,7 @@ Sent to server to inform it of locations that the client has checked. Used to in
| locations | list\[int\] | The ids of the locations checked by the client. May contain any number of checks, even ones sent before; duplicates do not cause issues with the Archipelago server. | | locations | list\[int\] | The ids of the locations checked by the client. May contain any number of checks, even ones sent before; duplicates do not cause issues with the Archipelago server. |
### LocationScouts ### LocationScouts
Sent to the server to retrieve the items that are on a specified list of locations. The server will respond with a [LocationInfo](#LocationInfo) packet containing the items located in the scouted locations. Sent to the server to inform it of locations the client has seen, but not checked. Useful in cases in which the item may appear in the game world, such as 'ledge items' in A Link to the Past. The server will always respond with a [LocationInfo](#LocationInfo) packet with the items located in the scouted location.
Fully remote clients without a patch file may use this to "place" items onto their in-game locations, most commonly to display their names or item classifications before/upon pickup.
LocationScouts can also be used to inform the server of locations the client has seen, but not checked. This creates a hint as if the player had run `!hint_location` on a location, but without deducting hint points.
This is useful in cases where an item appears in the game world, such as 'ledge items' in _A Link to the Past_. To do this, set the `create_as_hint` parameter to a non-zero value.
#### Arguments #### Arguments
| Name | Type | Notes | | Name | Type | Notes |
@@ -348,7 +339,7 @@ Sent to the server to update on the sender's status. Examples include readiness
#### Arguments #### Arguments
| Name | Type | Notes | | Name | Type | Notes |
| ---- | ---- | ----- | | ---- | ---- | ----- |
| status | ClientStatus\[int\] | One of [Client States](#ClientStatus). Send as int. Follow the link for more information. | | status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
### Say ### Say
Basic chat command which sends text to the server to be distributed to other clients. Basic chat command which sends text to the server to be distributed to other clients.

View File

@@ -10,9 +10,10 @@ Archipelago will be abbreviated as "AP" from now on.
## Option Definitions ## Option Definitions
Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you
need to create: need to create:
- A new option class, with a docstring detailing what the option does, to be exposed to the user. - A new option class with a docstring detailing what the option will do to your user.
- A new entry in the `options_dataclass` definition for your World. - A `display_name` to be displayed on the webhost.
By style and convention, the dataclass attributes should be `snake_case`. - A new entry in the `option_definitions` dict for your World.
By style and convention, the internal names should be snake_case.
### Option Creation ### Option Creation
- If the option supports having multiple sub_options, such as Choice options, these can be defined with - If the option supports having multiple sub_options, such as Choice options, these can be defined with
@@ -23,55 +24,31 @@ display as `Value1` on the webhost.
(i.e. `alias_value_1 = option_value1`) which will allow users to use either `value_1` or `value1` in their yaml (i.e. `alias_value_1 = option_value1`) which will allow users to use either `value_1` or `value1` in their yaml
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
Choice, and defining `alias_true = option_full`. Choice, and defining `alias_true = option_full`.
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or - All options support `random` as a generic option. `random` chooses from any of the available values for that option,
`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
implement it for additional option types.
As an example, suppose we want an option that lets the user start their game with a sword in their inventory, an option As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
to let the player choose the difficulty, and an option to choose how much health the final boss has. Let's create our create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass:
option classes (with a docstring), give them a `display_name`, and add them to our game's options dataclass:
```python ```python
# options.py # options.py
from dataclasses import dataclass from dataclasses import dataclass
from Options import Toggle, Range, Choice, PerGameCommonOptions from Options import Toggle, PerGameCommonOptions
class StartingSword(Toggle): class StartingSword(Toggle):
"""Adds a sword to your starting inventory.""" """Adds a sword to your starting inventory."""
display_name = "Start With Sword" # this is the option name as it's displayed to the user on the webhost and in the spoiler log display_name = "Start With Sword"
class Difficulty(Choice):
"""Sets overall game difficulty."""
display_name = "Difficulty"
option_easy = 0
option_normal = 1
option_hard = 2
alias_beginner = 0 # same as easy but allows the player to use beginner as an alternative for easy in the result in their options
alias_expert = 2 # same as hard
default = 1 # default to normal
class FinalBossHP(Range):
"""Sets the HP of the final boss"""
display_name = "Final Boss HP"
range_start = 100
range_end = 10000
default = 2000
@dataclass @dataclass
class ExampleGameOptions(PerGameCommonOptions): class ExampleGameOptions(PerGameCommonOptions):
starting_sword: StartingSword starting_sword: StartingSword
difficulty: Difficulty
final_boss_health: FinalBossHP
``` ```
To then submit this to the multiworld, we add it to our world's `__init__.py`: This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
to our world's `__init__.py`:
```python ```python
from worlds.AutoWorld import World from worlds.AutoWorld import World
@@ -203,7 +180,7 @@ For example:
```python ```python
range_start = 1 range_start = 1
range_end = 99 range_end = 99
special_range_names = { special_range_names: {
"normal": 20, "normal": 20,
"extreme": 99, "extreme": 99,
"unlimited": -1, "unlimited": -1,

Some files were not shown because too many files have changed in this diff Show More