Merge branch 'main' into rework_accessibility
# Conflicts: # worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py # worlds/alttp/test/inverted_owg/TestInvertedOWG.py
4
.github/workflows/analyze-modified-files.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Determine modified files (pull_request)"
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
run: |
|
||||
echo "diff=." >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
if: env.diff != ''
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
33
.github/workflows/build.yml
vendored
@@ -8,11 +8,13 @@ on:
|
||||
- '.github/workflows/build.yml'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
- '*.iss'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/build.yml'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
- '*.iss'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -25,9 +27,9 @@ jobs:
|
||||
build-win-py38: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Download run-time dependencies
|
||||
@@ -46,25 +48,42 @@ jobs:
|
||||
cd build
|
||||
Rename-Item "exe.$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
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ZIP_NAME }}
|
||||
path: dist/${{ env.ZIP_NAME }}
|
||||
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:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
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
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install build-time dependencies
|
||||
@@ -100,13 +119,13 @@ jobs:
|
||||
source venv/bin/activate
|
||||
python setup.py build_exe --yes
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APPIMAGE_NAME }}
|
||||
path: dist/${{ env.APPIMAGE_NAME }}
|
||||
retention-days: 7
|
||||
- name: Store .tar.gz
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.TAR_NAME }}
|
||||
path: dist/${{ env.TAR_NAME }}
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
4
.github/workflows/label-pull-requests.yml
vendored
@@ -15,9 +15,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: true
|
||||
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
|
||||
@@ -30,6 +31,7 @@ jobs:
|
||||
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:
|
||||
|
||||
8
.github/workflows/release.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # don't publish right away, especially since windows build is added by hand
|
||||
prerelease: false
|
||||
@@ -35,14 +35,14 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
# - code below copied from build.yml -
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
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
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install build-time dependencies
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - code above copied from build.yml -
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
|
||||
4
.github/workflows/unittests.yml
vendored
@@ -46,9 +46,9 @@ jobs:
|
||||
os: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python.version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -84,7 +84,7 @@ class MultiWorld():
|
||||
game: Dict[int, str]
|
||||
|
||||
random: random.Random
|
||||
per_slot_randoms: Dict[int, random.Random]
|
||||
per_slot_randoms: Utils.DeprecateDict[int, random.Random]
|
||||
"""Deprecated. Please use `self.random` instead."""
|
||||
|
||||
class AttributeProxy():
|
||||
@@ -216,7 +216,8 @@ class MultiWorld():
|
||||
set_player_attr('game', "A Link to the Past")
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.worlds = {}
|
||||
self.per_slot_randoms = {}
|
||||
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||
"world's random object instead (usually self.random)")
|
||||
self.plando_options = PlandoOptions.none
|
||||
|
||||
def get_all_ids(self) -> Tuple[int, ...]:
|
||||
@@ -250,14 +251,13 @@ class MultiWorld():
|
||||
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):
|
||||
assert not self.worlds, "seed needs to be initialized before Worlds"
|
||||
self.seed = get_seed(seed)
|
||||
if secure:
|
||||
self.secure()
|
||||
else:
|
||||
self.random.seed(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:
|
||||
# TODO - remove this section once all worlds use options dataclasses
|
||||
@@ -274,7 +274,6 @@ class MultiWorld():
|
||||
for player in self.player_ids:
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[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
|
||||
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
|
||||
for option_key in options_dataclass.type_hints})
|
||||
@@ -1027,7 +1026,7 @@ class Location:
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
always_allow = staticmethod(lambda state, item: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
|
||||
@@ -20,8 +20,8 @@ if __name__ == "__main__":
|
||||
Utils.init_logging("TextClient", exception_logger="Client")
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
||||
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
|
||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes)
|
||||
from Utils import Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -72,9 +72,16 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_received(self) -> bool:
|
||||
"""List all received items"""
|
||||
self.output(f'{len(self.ctx.items_received)} received items:')
|
||||
item: NetworkItem
|
||||
self.output(f'{len(self.ctx.items_received)} received items, sorted by time:')
|
||||
for index, item in enumerate(self.ctx.items_received, 1):
|
||||
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
|
||||
parts = []
|
||||
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
|
||||
|
||||
def _cmd_missing(self, filter_text = "") -> bool:
|
||||
@@ -115,6 +122,15 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
|
||||
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):
|
||||
"""List all location names for the currently running game."""
|
||||
if not self.ctx.game:
|
||||
@@ -124,6 +140,15 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
|
||||
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):
|
||||
"""Send ready status to server."""
|
||||
self.ctx.ready = not self.ctx.ready
|
||||
@@ -618,13 +643,13 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
ctx.username = server_url.username
|
||||
if server_url.password:
|
||||
ctx.password = server_url.password
|
||||
port = server_url.port or 38281
|
||||
|
||||
def reconnect_hint() -> str:
|
||||
return ", type /connect to reconnect" if ctx.server_address else ""
|
||||
|
||||
logger.info(f'Connecting to Archipelago server at {address}')
|
||||
try:
|
||||
port = server_url.port or 38281 # raises ValueError if invalid
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None)
|
||||
if ctx.ui is not None:
|
||||
@@ -733,8 +758,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
elif cmd == 'ConnectionRefused':
|
||||
errors = args["errors"]
|
||||
if 'InvalidSlot' in errors:
|
||||
ctx.disconnected_intentionally = True
|
||||
ctx.event_invalid_slot()
|
||||
elif 'InvalidGame' in errors:
|
||||
ctx.disconnected_intentionally = True
|
||||
ctx.event_invalid_game()
|
||||
elif 'IncompatibleVersion' in errors:
|
||||
raise Exception('Server reported your client version as incompatible. '
|
||||
|
||||
9
Fill.py
@@ -208,7 +208,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
|
||||
def remaining_fill(multiworld: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> None:
|
||||
itempool: typing.List[Item],
|
||||
name: str = "Remaining") -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
@@ -265,10 +266,10 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
placements.append(spot_to_fill)
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
_log_fill_progress(name, placed, total)
|
||||
|
||||
if total > 1000:
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
_log_fill_progress(name, placed, total)
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
@@ -466,7 +467,7 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
|
||||
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool)
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
|
||||
28
Generate.py
@@ -323,13 +323,29 @@ def roll_percentage(percentage: Union[int, float]) -> bool:
|
||||
return random.random() < (float(percentage) / 100)
|
||||
|
||||
|
||||
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
|
||||
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict:
|
||||
logging.debug(f'Applying {new_weights}')
|
||||
new_options = set(new_weights) - set(weights)
|
||||
weights.update(new_weights)
|
||||
cleaned_weights = {}
|
||||
for option in 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:
|
||||
for new_option in new_options:
|
||||
logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
|
||||
logging.warning(f'{update_type} Suboption "{new_option}" of "{name}" did not '
|
||||
f'overwrite a root option. '
|
||||
f'This is probably in error.')
|
||||
return weights
|
||||
@@ -452,6 +468,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
world_type = AutoWorldRegister.world_types[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:
|
||||
weights = roll_triggers(weights, game_weights["triggers"])
|
||||
game_weights = weights[ret.game]
|
||||
|
||||
@@ -707,15 +707,18 @@ class Context:
|
||||
self.save() # save goal completion flag
|
||||
|
||||
def on_new_hint(self, team: int, slot: int):
|
||||
key: str = f"_read_hints_{team}_{slot}"
|
||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||
if targets:
|
||||
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
|
||||
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}"
|
||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||
if targets:
|
||||
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
|
||||
|
||||
def on_client_status_change(self, team: int, slot: int):
|
||||
key: str = f"_read_client_status_{team}_{slot}"
|
||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||
@@ -975,7 +978,10 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
"hint_points": get_slot_points(ctx, team, slot),
|
||||
"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()
|
||||
|
||||
|
||||
@@ -1052,17 +1058,19 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
|
||||
if picks[0][1] == 100:
|
||||
return picks[0][0], True, "Perfect Match"
|
||||
elif picks[0][1] < 75:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches, " \
|
||||
f"did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
elif dif > 5:
|
||||
return picks[0][0], True, "Close Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Too many close matches, did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Too many close matches for '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
else:
|
||||
if picks[0][1] > 90:
|
||||
return picks[0][0], True, "Only Option Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
|
||||
|
||||
class CommandMeta(type):
|
||||
@@ -1964,7 +1972,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
||||
@mark_raw
|
||||
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)
|
||||
if player:
|
||||
team, slot, name = player
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
def add_json_location(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
|
||||
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
||||
def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) -> None:
|
||||
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
||||
|
||||
|
||||
class Hint(typing.NamedTuple):
|
||||
|
||||
36
Options.py
@@ -12,7 +12,7 @@ from dataclasses import dataclass
|
||||
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
from Utils import get_fuzzy_results, is_iterable_of_str
|
||||
from Utils import get_fuzzy_results, is_iterable_except_str
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from BaseClasses import PlandoOptions
|
||||
@@ -41,6 +41,11 @@ class AssembleOptions(abc.ABCMeta):
|
||||
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||
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."
|
||||
|
||||
# auto-alias Off and On being parsed as True and False
|
||||
@@ -96,7 +101,7 @@ T = typing.TypeVar('T')
|
||||
|
||||
class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
value: T
|
||||
default = 0
|
||||
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
|
||||
|
||||
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
|
||||
# Handled in get_option_name()
|
||||
@@ -106,8 +111,9 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
supports_weighting = True
|
||||
|
||||
# filled by AssembleOptions:
|
||||
name_lookup: typing.Dict[T, str]
|
||||
options: typing.Dict[str, int]
|
||||
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
|
||||
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
|
||||
options: typing.ClassVar[typing.Dict[str, int]]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.current_option_name})"
|
||||
@@ -160,6 +166,8 @@ class FreeText(Option[str]):
|
||||
"""Text option that allows users to enter strings.
|
||||
Needs to be validated by the world or option definition."""
|
||||
|
||||
default = ""
|
||||
|
||||
def __init__(self, value: str):
|
||||
assert isinstance(value, str), "value of FreeText must be a string"
|
||||
self.value = value
|
||||
@@ -180,6 +188,14 @@ class FreeText(Option[str]):
|
||||
def get_option_name(cls, value: str) -> str:
|
||||
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):
|
||||
default = 0
|
||||
@@ -803,7 +819,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
|
||||
|
||||
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
||||
default: typing.Dict[str, typing.Any] = {}
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
@@ -844,10 +860,10 @@ 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.
|
||||
# Not a docstring so it doesn't get grabbed by the options system.
|
||||
|
||||
default: typing.Union[typing.List[typing.Any], typing.Tuple[typing.Any, ...]] = ()
|
||||
default = ()
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
def __init__(self, value: typing.Iterable[typing.Any]):
|
||||
self.value = list(deepcopy(value))
|
||||
super(OptionList, self).__init__()
|
||||
|
||||
@@ -857,7 +873,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if is_iterable_of_str(data):
|
||||
if is_iterable_except_str(data):
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
@@ -870,7 +886,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
|
||||
|
||||
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
@@ -883,7 +899,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if is_iterable_of_str(data):
|
||||
if is_iterable_except_str(data):
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
4
Patch.py
@@ -8,7 +8,7 @@ if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from worlds.Files import AutoPatchRegister, APPatch
|
||||
from worlds.Files import AutoPatchRegister, APAutoPatchInterface
|
||||
|
||||
|
||||
class RomMeta(TypedDict):
|
||||
@@ -20,7 +20,7 @@ class RomMeta(TypedDict):
|
||||
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
|
||||
auto_handler = AutoPatchRegister.get_handler(patch_file)
|
||||
if auto_handler:
|
||||
handler: APPatch = auto_handler(patch_file)
|
||||
handler: APAutoPatchInterface = auto_handler(patch_file)
|
||||
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
|
||||
handler.patch(target)
|
||||
return {"server": handler.server,
|
||||
|
||||
@@ -25,7 +25,7 @@ Currently, the following games are supported:
|
||||
* Hollow Knight
|
||||
* The Witness
|
||||
* Sonic Adventure 2: Battle
|
||||
* Starcraft 2: Wings of Liberty
|
||||
* Starcraft 2
|
||||
* Donkey Kong Country 3
|
||||
* Dark Souls 3
|
||||
* Super Mario World
|
||||
@@ -61,6 +61,10 @@ Currently, the following games are supported:
|
||||
* 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/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from worlds.sc2wol.Client import launch
|
||||
from worlds.sc2.Client import launch
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
16
Utils.py
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.4"
|
||||
__version__ = "0.4.5"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -225,6 +225,9 @@ class UniqueKeyLoader(SafeLoader):
|
||||
if key in mapping:
|
||||
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}.")
|
||||
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)
|
||||
return super().construct_mapping(node, deep)
|
||||
|
||||
@@ -713,7 +716,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
import ctypes
|
||||
style = 0x10 if error else 0x0
|
||||
return ctypes.windll.user32.MessageBoxW(0, text, title, style)
|
||||
|
||||
|
||||
# fall back to tk
|
||||
try:
|
||||
import tkinter
|
||||
@@ -969,11 +972,8 @@ class RepeatableChain:
|
||||
return sum(len(iterable) for iterable in self.iterable)
|
||||
|
||||
|
||||
def is_iterable_of_str(obj: object) -> TypeGuard[typing.Iterable[str]]:
|
||||
""" but not a `str` (because technically, `str` is `Iterable[str]`) """
|
||||
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
|
||||
if not isinstance(obj, typing.Iterable):
|
||||
return False
|
||||
obj_it: typing.Iterable[object] = obj
|
||||
return all(isinstance(v, str) for v in obj_it)
|
||||
return isinstance(obj, typing.Iterable)
|
||||
|
||||
@@ -25,16 +25,16 @@ window.addEventListener('load', () => {
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
for (let category of categories) {
|
||||
let hide_id = category.id.split('_')[0];
|
||||
if (hide_id === 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
category.addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// 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;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
160
WebHostLib/static/styles/sc2Tracker.css
Normal file
@@ -0,0 +1,160 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
1090
WebHostLib/templates/tracker__Starcraft2.html
Normal file
@@ -1,366 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'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>
|
||||
@@ -61,36 +61,42 @@
|
||||
found_text: "Found?"
|
||||
TooltipLabel:
|
||||
id: receiving
|
||||
sort_key: 'receiving'
|
||||
text: root.receiving_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: item
|
||||
sort_key: 'item'
|
||||
text: root.item_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: finding
|
||||
sort_key: 'finding'
|
||||
text: root.finding_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: location
|
||||
sort_key: 'location'
|
||||
text: root.location_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: entrance
|
||||
sort_key: 'entrance'
|
||||
text: root.entrance_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: found
|
||||
sort_key: 'found'
|
||||
text: root.found_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
|
||||
@@ -322,7 +322,7 @@ function processBlock(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #itemsBlock ~= itemIndex then
|
||||
if #itemsBlock > itemIndex then
|
||||
wU8(ITEM_INDEX, #itemsBlock)
|
||||
end
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
# Bumper Stickers
|
||||
/worlds/bumpstik/ @FelicitusNeko
|
||||
|
||||
# Castlevania 64
|
||||
/worlds/cv64/ @LiquidCat64
|
||||
|
||||
# Celeste 64
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
@@ -131,11 +134,14 @@
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
|
||||
# A Short Hike
|
||||
/worlds/shorthike/ @chandler05
|
||||
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
# Starcraft 2 Wings of Liberty
|
||||
/worlds/sc2wol/ @Ziktofel
|
||||
# Starcraft 2
|
||||
/worlds/sc2/ @Ziktofel
|
||||
|
||||
# Super Metroid
|
||||
/worlds/sm/ @lordlou
|
||||
@@ -171,7 +177,7 @@
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
|
||||
# TUNIC
|
||||
/worlds/tunic/ @silent-destroyer
|
||||
/worlds/tunic/ @silent-destroyer @ScipioWright
|
||||
|
||||
# Undertale
|
||||
/worlds/undertale/ @jonloveslegos
|
||||
@@ -185,9 +191,15 @@
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
# Yoshi's Island
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
|
||||
# Zork Grand Inquisitor
|
||||
/worlds/zork_grand_inquisitor/ @nbrochu
|
||||
|
||||
##################################
|
||||
## Disabled Unmaintained Worlds ##
|
||||
##################################
|
||||
|
||||
@@ -204,7 +204,7 @@ For example:
|
||||
```python
|
||||
range_start = 1
|
||||
range_end = 99
|
||||
special_range_names: {
|
||||
special_range_names = {
|
||||
"normal": 20,
|
||||
"extreme": 99,
|
||||
"unlimited": -1,
|
||||
|
||||
@@ -31,8 +31,11 @@ ArchitecturesAllowed=x64 arm64
|
||||
AllowNoIcons=yes
|
||||
SetupIconFile={#MyAppIcon}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
; you will likely have to remove the following signtool line when testing/debugging locally. Don't include that change in PRs.
|
||||
#ifndef NO_SIGNTOOL
|
||||
; You will likely have to remove the SignTool= line when testing/debugging locally or run with iscc.exe /DNO_SIGNTOOL.
|
||||
; Don't include that change in PRs.
|
||||
SignTool= signtool
|
||||
#endif
|
||||
LicenseFile= LICENSE
|
||||
WizardStyle= modern
|
||||
SetupLogging=yes
|
||||
@@ -131,10 +134,10 @@ Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apkdl3"; ValueData: "{#MyAppName}kdl3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Archipelago Kirby's Dream Land 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||
Root: HKCR; Subkey: ".apkdl3"; ValueData: "{#MyAppName}kdl3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Archipelago Kirby's Dream Land 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
@@ -166,6 +169,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Ar
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
@@ -181,6 +189,11 @@ Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apyi"; ValueData: "{#MyAppName}yipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archipelago Yoshi's Island Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
71
kvui.py
@@ -2,6 +2,7 @@ import os
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
import re
|
||||
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
@@ -72,6 +73,8 @@ if typing.TYPE_CHECKING:
|
||||
else:
|
||||
context_type = object
|
||||
|
||||
remove_between_brackets = re.compile(r"\[.*?]")
|
||||
|
||||
|
||||
# I was surprised to find this didn't already exist in kivy :(
|
||||
class HoverBehavior(object):
|
||||
@@ -129,6 +132,8 @@ class ScrollBox(ScrollView):
|
||||
self.layout.bind(minimum_height=self.layout.setter("height"))
|
||||
self.add_widget(self.layout)
|
||||
self.effect_cls = ScrollEffect
|
||||
self.bar_width = dp(12)
|
||||
self.scroll_type = ["content", "bars"]
|
||||
|
||||
|
||||
class HovererableLabel(HoverBehavior, Label):
|
||||
@@ -303,7 +308,6 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
index = None
|
||||
no_select = []
|
||||
|
||||
def __init__(self):
|
||||
super(HintLabel, self).__init__()
|
||||
@@ -321,9 +325,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
self.index = index
|
||||
if "select" in data and not data["select"] and index not in self.no_select:
|
||||
self.no_select.append(index)
|
||||
self.striped = data["striped"]
|
||||
self.striped = data.get("striped", False)
|
||||
self.receiving_text = data["receiving"]["text"]
|
||||
self.item_text = data["item"]["text"]
|
||||
self.finding_text = data["finding"]["text"]
|
||||
@@ -337,24 +339,44 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
""" Add selection on touch down """
|
||||
if super(HintLabel, self).on_touch_down(touch):
|
||||
return True
|
||||
if self.index not in self.no_select:
|
||||
if self.index: # skip header
|
||||
if self.collide_point(*touch.pos):
|
||||
if self.selected:
|
||||
self.parent.clear_selection()
|
||||
else:
|
||||
text = "".join([self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
|
||||
text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
|
||||
self.finding_text, "\'s World", (" at " + self.entrance_text)
|
||||
if self.entrance_text != "Vanilla"
|
||||
else "", ". (", self.found_text.lower(), ")"])
|
||||
else "", ". (", self.found_text.lower(), ")"))
|
||||
temp = MarkupLabel(text).markup
|
||||
text = "".join(
|
||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
else:
|
||||
parent = self.parent
|
||||
parent.clear_selection()
|
||||
parent: HintLog = parent.parent
|
||||
# find correct column
|
||||
for child in self.children:
|
||||
if child.collide_point(*touch.pos):
|
||||
key = child.sort_key
|
||||
parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
|
||||
if key == parent.sort_key:
|
||||
# second click reverses order
|
||||
parent.reversed = not parent.reversed
|
||||
else:
|
||||
parent.sort_key = key
|
||||
parent.reversed = False
|
||||
break
|
||||
else:
|
||||
logging.warning("Did not find clicked header for sorting.")
|
||||
|
||||
App.get_running_app().update_hints()
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
""" Respond to the selection of items in the view. """
|
||||
if self.index not in self.no_select:
|
||||
if self.index:
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
@@ -646,20 +668,20 @@ class HintLog(RecycleView):
|
||||
"entrance": {"text": "[u]Entrance[/u]"},
|
||||
"found": {"text": "[u]Status[/u]"},
|
||||
"striped": True,
|
||||
"select": False,
|
||||
}
|
||||
|
||||
sort_key: str = ""
|
||||
reversed: bool = False
|
||||
|
||||
def __init__(self, parser):
|
||||
super(HintLog, self).__init__()
|
||||
self.data = [self.header]
|
||||
self.parser = parser
|
||||
|
||||
def refresh_hints(self, hints):
|
||||
self.data = [self.header]
|
||||
striped = False
|
||||
data = []
|
||||
for hint in hints:
|
||||
self.data.append({
|
||||
"striped": striped,
|
||||
data.append({
|
||||
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
||||
"item": {"text": self.parser.handle_node(
|
||||
{"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})},
|
||||
@@ -672,7 +694,22 @@ class HintLog(RecycleView):
|
||||
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
|
||||
"text": "Found" if hint["found"] else "Not Found"})},
|
||||
})
|
||||
striped = not striped
|
||||
|
||||
data.sort(key=self.hint_sorter, reverse=self.reversed)
|
||||
for i in range(0, len(data), 2):
|
||||
data[i]["striped"] = True
|
||||
data.insert(0, self.header)
|
||||
self.data = data
|
||||
|
||||
@staticmethod
|
||||
def hint_sorter(element: dict) -> str:
|
||||
return ""
|
||||
|
||||
def fix_heights(self):
|
||||
"""Workaround fix for divergent texture and layout heights"""
|
||||
for element in self.children[0].children:
|
||||
max_height = max(child.texture_size[1] for child in element.children)
|
||||
element.height = max_height
|
||||
|
||||
|
||||
class E(ExceptionHandler):
|
||||
@@ -721,8 +758,10 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
text = f"Game: {slot_info.game}<br>" \
|
||||
f"Type: {SlotType(slot_info.type).name}"
|
||||
if slot_info.group_members:
|
||||
text += f"<br>Members:<br> " + \
|
||||
"<br> ".join(self.ctx.player_names[player] for player in slot_info.group_members)
|
||||
text += f"<br>Members:<br> " + "<br> ".join(
|
||||
escape_markup(self.ctx.player_names[player])
|
||||
for player in slot_info.group_members
|
||||
)
|
||||
node.setdefault("refs", []).append(text)
|
||||
return super(KivyJSONtoTextParser, self)._handle_player_id(node)
|
||||
|
||||
|
||||
2
setup.py
@@ -68,7 +68,6 @@ non_apworlds: set = {
|
||||
"Archipelago",
|
||||
"ChecksFinder",
|
||||
"Clique",
|
||||
"DLCQuest",
|
||||
"Final Fantasy",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
@@ -85,7 +84,6 @@ non_apworlds: set = {
|
||||
# LogicMixin is broken before 3.10 import revamp
|
||||
if sys.version_info < (3,10):
|
||||
non_apworlds.add("Hollow Knight")
|
||||
non_apworlds.add("Starcraft 2 Wings of Liberty")
|
||||
|
||||
def download_SNI():
|
||||
print("Updating SNI")
|
||||
|
||||
@@ -10,7 +10,7 @@ from worlds import AutoWorld
|
||||
from worlds.AutoWorld import World, call_all
|
||||
|
||||
from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Items import item_factory
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
@@ -91,15 +91,15 @@ class TestBase(unittest.TestCase):
|
||||
items = self.multiworld.itempool[:]
|
||||
items = [item for item in items if
|
||||
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
|
||||
items.extend(ItemFactory(item_pool[0], 1))
|
||||
items.extend(item_factory(item_pool[0], self.multiworld.worlds[1]))
|
||||
else:
|
||||
items = ItemFactory(item_pool[0], 1)
|
||||
items = item_factory(item_pool[0], self.multiworld.worlds[1])
|
||||
return self.get_state(items)
|
||||
|
||||
def _get_items_partial(self, item_pool, missing_item):
|
||||
new_items = item_pool[0].copy()
|
||||
new_items.remove(missing_item)
|
||||
items = ItemFactory(new_items, 1)
|
||||
items = item_factory(new_items, self.multiworld.worlds[1])
|
||||
return self.get_state(items)
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,50 @@
|
||||
from argparse import Namespace
|
||||
from typing import Type, Tuple
|
||||
from typing import List, Optional, Tuple, Type, Union
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from worlds.AutoWorld import call_all, World
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from worlds.AutoWorld import World, call_all
|
||||
|
||||
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
|
||||
|
||||
def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
|
||||
def setup_solo_multiworld(
|
||||
world_type: Type[World], steps: Tuple[str, ...] = gen_steps, seed: Optional[int] = None
|
||||
) -> MultiWorld:
|
||||
"""
|
||||
Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps.
|
||||
|
||||
:param world_type: Type of the world to generate a multiworld for
|
||||
:param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
|
||||
steps through pre_fill
|
||||
:param seed: The seed to be used when creating this multiworld
|
||||
"""
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.game[1] = world_type.game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
multiworld.set_seed()
|
||||
return setup_multiworld(world_type, steps, seed)
|
||||
|
||||
|
||||
def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple[str, ...] = gen_steps,
|
||||
seed: Optional[int] = None) -> MultiWorld:
|
||||
"""
|
||||
Creates a multiworld with a player for each provided world type, allowing duplicates, setting default options, and
|
||||
calling the provided gen steps.
|
||||
|
||||
:param worlds: type/s of worlds to generate a multiworld for
|
||||
:param steps: gen steps that should be called before returning. Default calls through pre_fill
|
||||
:param seed: The seed to be used when creating this multiworld
|
||||
"""
|
||||
if not isinstance(worlds, list):
|
||||
worlds = [worlds]
|
||||
players = len(worlds)
|
||||
multiworld = MultiWorld(players)
|
||||
multiworld.game = {player: world_type.game for player, world_type in enumerate(worlds, 1)}
|
||||
multiworld.player_name = {player: f"Tester{player}" for player in multiworld.player_ids}
|
||||
multiworld.set_seed(seed)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
args = Namespace()
|
||||
for name, option in world_type.options_dataclass.type_hints.items():
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
for player, world_type in enumerate(worlds, 1):
|
||||
for key, option in world_type.options_dataclass.type_hints.items():
|
||||
updated_options = getattr(args, key, {})
|
||||
updated_options[player] = option.from_any(option.default)
|
||||
setattr(args, key, updated_options)
|
||||
multiworld.set_options(args)
|
||||
for step in steps:
|
||||
call_all(multiworld, step)
|
||||
|
||||
@@ -13,6 +13,7 @@ from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules,
|
||||
|
||||
def generate_multiworld(players: int = 1) -> MultiWorld:
|
||||
multiworld = MultiWorld(players)
|
||||
multiworld.set_seed(0)
|
||||
multiworld.player_name = {}
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
for i in range(players):
|
||||
@@ -32,8 +33,6 @@ def generate_multiworld(players: int = 1) -> MultiWorld:
|
||||
world.options = world.options_dataclass(**{option_key: getattr(multiworld, option_key)[player_id]
|
||||
for option_key in world.options_dataclass.type_hints})
|
||||
|
||||
multiworld.set_seed(0)
|
||||
|
||||
return multiworld
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class TestBase(unittest.TestCase):
|
||||
def test_create_item(self):
|
||||
"""Test that a world can successfully create all items in its datapackage"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds
|
||||
proxy_world = setup_solo_multiworld(world_type, ()).worlds[1]
|
||||
for item_name in world_type.item_name_to_id:
|
||||
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
|
||||
item = proxy_world.create_item(item_name)
|
||||
@@ -23,8 +23,8 @@ class TestBase(unittest.TestCase):
|
||||
{"Pendants", "Crystals"},
|
||||
"Ocarina of Time":
|
||||
{"medallions", "stones", "rewards", "logic_bottles"},
|
||||
"Starcraft 2 Wings of Liberty":
|
||||
{"Missions"},
|
||||
"Starcraft 2":
|
||||
{"Missions", "WoL Missions"},
|
||||
}
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game_name, game_name=game_name):
|
||||
|
||||
39
test/general/test_player_options.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import unittest
|
||||
import Generate
|
||||
|
||||
|
||||
class TestPlayerOptions(unittest.TestCase):
|
||||
|
||||
def test_update_weights(self):
|
||||
original_weights = {
|
||||
"scalar_1": 50,
|
||||
"scalar_2": 25,
|
||||
"list_1": ["string"],
|
||||
"dict_1": {"option_a": 50, "option_b": 50},
|
||||
"dict_2": {"option_f": 50},
|
||||
"set_1": {"option_c"}
|
||||
}
|
||||
|
||||
# test that we don't allow +merge syntax on scalar variables
|
||||
with self.assertRaises(BaseException):
|
||||
Generate.update_weights(original_weights, {"+scalar_1": 0}, "Tested", "")
|
||||
|
||||
new_weights = Generate.update_weights(original_weights, {"scalar_2": 0,
|
||||
"+list_1": ["string_2"],
|
||||
"+dict_1": {"option_b": 0, "option_c": 50},
|
||||
"+set_1": {"option_c", "option_d"},
|
||||
"dict_2": {"option_g": 50},
|
||||
"+list_2": ["string_3"]},
|
||||
"Tested", "")
|
||||
|
||||
self.assertEqual(new_weights["scalar_1"], 50)
|
||||
self.assertEqual(new_weights["scalar_2"], 0)
|
||||
self.assertEqual(new_weights["list_2"], ["string_3"])
|
||||
self.assertEqual(new_weights["list_1"], ["string", "string_2"])
|
||||
self.assertEqual(new_weights["dict_1"]["option_a"], 50)
|
||||
self.assertEqual(new_weights["dict_1"]["option_b"], 0)
|
||||
self.assertEqual(new_weights["dict_1"]["option_c"], 50)
|
||||
self.assertNotIn("option_f", new_weights["dict_2"])
|
||||
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
||||
self.assertEqual(len(new_weights["set_1"]), 2)
|
||||
self.assertIn("option_d", new_weights["set_1"])
|
||||
77
test/multiworld/test_multiworlds.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import unittest
|
||||
from typing import List, Tuple
|
||||
from unittest import TestCase
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld
|
||||
from Fill import distribute_items_restrictive
|
||||
from Options import Accessibility
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
||||
from ..general import gen_steps, setup_multiworld
|
||||
|
||||
|
||||
class MultiworldTestBase(TestCase):
|
||||
multiworld: MultiWorld
|
||||
|
||||
# similar to the implementation in WorldTestBase.test_fill
|
||||
# but for multiple players and doesn't allow minimal accessibility
|
||||
def fulfills_accessibility(self) -> bool:
|
||||
"""
|
||||
Checks that the multiworld satisfies locations accessibility requirements, failing if all locations are cleared
|
||||
but not beatable, or some locations are unreachable.
|
||||
"""
|
||||
locations = [loc for loc in self.multiworld.get_locations()]
|
||||
state = CollectionState(self.multiworld)
|
||||
while locations:
|
||||
sphere: List[Location] = []
|
||||
for n in range(len(locations) - 1, -1, -1):
|
||||
if locations[n].can_reach(state):
|
||||
sphere.append(locations.pop(n))
|
||||
self.assertTrue(sphere, f"Unreachable locations: {locations}")
|
||||
if not sphere:
|
||||
return False
|
||||
for location in sphere:
|
||||
if location.item:
|
||||
state.collect(location.item, True, location)
|
||||
return self.multiworld.has_beaten_game(state, 1)
|
||||
|
||||
def assertSteps(self, steps: Tuple[str, ...]) -> None:
|
||||
"""Calls each step individually, continuing if a step for a specific world step fails."""
|
||||
world_types = {world.__class__ for world in self.multiworld.worlds.values()}
|
||||
for step in steps:
|
||||
for player, world in self.multiworld.worlds.items():
|
||||
with self.subTest(game=world.game, step=step):
|
||||
call_single(self.multiworld, step, player)
|
||||
for world_type in sorted(world_types, key=lambda world: world.__name__):
|
||||
with self.subTest(game=world_type.game, step=f"stage_{step}"):
|
||||
stage_callable = getattr(world_type, f"stage_{step}", None)
|
||||
if stage_callable:
|
||||
stage_callable(self.multiworld)
|
||||
|
||||
|
||||
@unittest.skip("too slow for main")
|
||||
class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
def test_fills(self) -> None:
|
||||
"""Tests that a multiworld with one of every registered game world can generate."""
|
||||
all_worlds = list(AutoWorldRegister.world_types.values())
|
||||
self.multiworld = setup_multiworld(all_worlds, ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_locations
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
|
||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||
def test_two_player_single_game_fills(self) -> None:
|
||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||
for world in AutoWorldRegister.world_types.values():
|
||||
self.multiworld = setup_multiworld([world, world], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_locations
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
@@ -1,11 +1,35 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Tuple, Any, Optional
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
|
||||
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"))
|
||||
components.append(component)
|
||||
|
||||
|
||||
def valid_patch_suffix(obj: object) -> TypeGuard[Union[str, Iterable[str]]]:
|
||||
""" make sure this is a valid value for the class variable `patch_suffix` """
|
||||
|
||||
def valid_individual(one: object) -> TypeGuard[str]:
|
||||
""" check an individual suffix """
|
||||
# TODO: decide: len(one) > 3 and one.startswith(".ap") ?
|
||||
# or keep it more general?
|
||||
return isinstance(one, str) and len(one) > 1 and one.startswith(".")
|
||||
|
||||
if isinstance(obj, str):
|
||||
return valid_individual(obj)
|
||||
if not isinstance(obj, Iterable):
|
||||
return False
|
||||
obj_it: Iterable[object] = obj
|
||||
return all(valid_individual(each) for each in obj_it)
|
||||
|
||||
|
||||
class AutoSNIClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[str, SNIClient]] = {}
|
||||
@@ -15,6 +39,22 @@ class AutoSNIClientRegister(abc.ABCMeta):
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
if "game" in dct:
|
||||
AutoSNIClientRegister.game_handlers[dct["game"]] = new_class()
|
||||
|
||||
if "patch_suffix" in dct:
|
||||
patch_suffix = dct["patch_suffix"]
|
||||
assert valid_patch_suffix(patch_suffix), f"class {name} defining invalid {patch_suffix=}"
|
||||
|
||||
existing_identifier = component.file_identifier
|
||||
assert isinstance(existing_identifier, SuffixIdentifier), f"{existing_identifier=}"
|
||||
new_suffixes = [*existing_identifier.suffixes]
|
||||
|
||||
if isinstance(patch_suffix, str):
|
||||
new_suffixes.append(patch_suffix)
|
||||
else:
|
||||
new_suffixes.extend(patch_suffix)
|
||||
|
||||
component.file_identifier = SuffixIdentifier(*new_suffixes)
|
||||
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
@@ -27,6 +67,9 @@ class AutoSNIClientRegister(abc.ABCMeta):
|
||||
|
||||
class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
||||
|
||||
patch_suffix: ClassVar[Union[str, Iterable[str]]] = ()
|
||||
"""The file extension(s) this client is meant to open and patch (e.g. ".aplttp")"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: SNIContext) -> bool:
|
||||
""" TODO: interface documentation here """
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@@ -296,8 +297,11 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""path it was loaded from"""
|
||||
|
||||
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||
assert multiworld is not None
|
||||
self.multiworld = multiworld
|
||||
self.player = player
|
||||
self.random = random.Random(multiworld.random.getrandbits(64))
|
||||
multiworld.per_slot_randoms[player] = self.random
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
if item == "settings":
|
||||
@@ -306,13 +310,15 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
# overridable methods that get called by Main.py, sorted by execution order
|
||||
# can also be implemented as a classmethod and called "stage_<original_name>",
|
||||
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
|
||||
# in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld.
|
||||
# An example of this can be found in alttp as stage_pre_fill
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: "MultiWorld") -> None:
|
||||
"""Checks that a game is capable of generating, usually checks for some base file like a ROM.
|
||||
This gets called once per present world type. Not run for unittests since they don't produce output"""
|
||||
"""
|
||||
Checks that a game is capable of generating, such as checking for some base file like a ROM.
|
||||
This gets called once per present world type. Not run for unittests since they don't produce output.
|
||||
"""
|
||||
pass
|
||||
|
||||
def generate_early(self) -> None:
|
||||
@@ -357,16 +363,21 @@ class World(metaclass=AutoWorldRegister):
|
||||
pass
|
||||
|
||||
def post_fill(self) -> None:
|
||||
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
|
||||
This happens before progression balancing, so the items may not be in their final locations yet."""
|
||||
"""
|
||||
Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
|
||||
This happens before progression balancing, so the items may not be in their final locations yet.
|
||||
"""
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""This method gets called from a threadpool, do not use multiworld.random here.
|
||||
If you need any last-second randomization, use self.random instead."""
|
||||
"""
|
||||
This method gets called from a threadpool, do not use multiworld.random here.
|
||||
If you need any last-second randomization, use self.random instead.
|
||||
"""
|
||||
pass
|
||||
|
||||
def fill_slot_data(self) -> Mapping[str, Any]: # json of WebHostLib.models.Slot
|
||||
"""What is returned from this function will be in the `slot_data` field
|
||||
"""
|
||||
What is returned from this function will be in the `slot_data` field
|
||||
in the `Connected` network package.
|
||||
It should be a `dict` with `str` keys, and should be serializable with json.
|
||||
|
||||
@@ -374,15 +385,18 @@ class World(metaclass=AutoWorldRegister):
|
||||
The client will receive this as JSON in the `Connected` response.
|
||||
|
||||
The generation does not wait for `generate_output` to complete before calling this.
|
||||
`threading.Event` can be used if you need to wait for something from `generate_output`."""
|
||||
`threading.Event` can be used if you need to wait for something from `generate_output`.
|
||||
"""
|
||||
# The reason for the `Mapping` type annotation, rather than `dict`
|
||||
# is so that type checkers won't worry about the mutability of `dict`,
|
||||
# so you can have more specific typing in your world implementation.
|
||||
return {}
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||
"""Fill in additional entrance information text into locations, which is displayed when hinted.
|
||||
structure is {player_id: {location_id: text}} You will need to insert your own player_id."""
|
||||
"""
|
||||
Fill in additional entrance information text into locations, which is displayed when hinted.
|
||||
structure is {player_id: {location_id: text}} You will need to insert your own player_id.
|
||||
"""
|
||||
pass
|
||||
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
|
||||
@@ -391,13 +405,17 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
# Spoiler writing is optional, these may not get called.
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
||||
"""Write to the spoiler header. If individual it's right at the end of that player's options,
|
||||
if as stage it's right under the common header before per-player options."""
|
||||
"""
|
||||
Write to the spoiler header. If individual it's right at the end of that player's options,
|
||||
if as stage it's right under the common header before per-player options.
|
||||
"""
|
||||
pass
|
||||
|
||||
def write_spoiler(self, spoiler_handle: TextIO) -> None:
|
||||
"""Write to the spoiler "middle", this is after the per-player options and before locations,
|
||||
meant for useful or interesting info."""
|
||||
"""
|
||||
Write to the spoiler "middle", this is after the per-player options and before locations,
|
||||
meant for useful or interesting info.
|
||||
"""
|
||||
pass
|
||||
|
||||
def write_spoiler_end(self, spoiler_handle: TextIO) -> None:
|
||||
@@ -407,8 +425,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def create_item(self, name: str) -> "Item":
|
||||
"""Create an item for this world type and player.
|
||||
Warning: this may be called with self.world = None, for example by MultiServer"""
|
||||
"""
|
||||
Create an item for this world type and player.
|
||||
Warning: this may be called with self.world = None, for example by MultiServer
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
@@ -418,34 +438,42 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
@classmethod
|
||||
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
|
||||
"""Creates a group, which is an instance of World that is responsible for multiple others.
|
||||
An example case is ItemLinks creating these."""
|
||||
"""
|
||||
Creates a group, which is an instance of World that is responsible for multiple others.
|
||||
An example case is ItemLinks creating these.
|
||||
"""
|
||||
# TODO remove loop when worlds use options dataclass
|
||||
for option_key, option in cls.options_dataclass.type_hints.items():
|
||||
getattr(multiworld, option_key)[new_player_id] = option(option.default)
|
||||
getattr(multiworld, option_key)[new_player_id] = option.from_any(option.default)
|
||||
group = cls(multiworld, new_player_id)
|
||||
group.options = cls.options_dataclass(**{option_key: option(option.default)
|
||||
group.options = cls.options_dataclass(**{option_key: option.from_any(option.default)
|
||||
for option_key, option in cls.options_dataclass.type_hints.items()})
|
||||
|
||||
return group
|
||||
|
||||
# decent place to implement progressive items, in most cases can stay as-is
|
||||
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
|
||||
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
||||
"""
|
||||
Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
||||
Collect None to skip item.
|
||||
:param state: CollectionState to collect into
|
||||
:param item: Item to decide on if it should be collected into state
|
||||
:param remove: indicate if this is meant to remove from state instead of adding."""
|
||||
:param remove: indicate if this is meant to remove from state instead of adding.
|
||||
"""
|
||||
if item.advancement:
|
||||
return item.name
|
||||
return None
|
||||
|
||||
# called to create all_state, return Items that are created during pre_fill
|
||||
def get_pre_fill_items(self) -> List["Item"]:
|
||||
"""
|
||||
Used to return items that need to be collected when creating a fresh all_state, but don't exist in the
|
||||
multiworld itempool.
|
||||
"""
|
||||
return []
|
||||
|
||||
# these two methods can be extended for pseudo-items on state
|
||||
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||
"""Called when an item is collected in to state. Useful for things such as progressive items or currency."""
|
||||
name = self.collect_item(state, item)
|
||||
if name:
|
||||
state.prog_items[self.player][name] += 1
|
||||
@@ -453,6 +481,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
return False
|
||||
|
||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||
"""Called when an item is removed from to state. Useful for things such as progressive items or currency."""
|
||||
name = self.collect_item(state, item, True)
|
||||
if name:
|
||||
state.prog_items[self.player][name] -= 1
|
||||
@@ -461,6 +490,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
return True
|
||||
return False
|
||||
|
||||
# following methods should not need to be overridden.
|
||||
def create_filler(self) -> "Item":
|
||||
return self.create_item(self.get_filler_item_name())
|
||||
|
||||
@@ -509,7 +539,8 @@ def data_package_checksum(data: "GamesPackage") -> str:
|
||||
|
||||
|
||||
def _normalize_description(description):
|
||||
"""Normalizes a description in item_descriptions or location_descriptions.
|
||||
"""
|
||||
Normalizes a description in item_descriptions or location_descriptions.
|
||||
|
||||
This allows authors to write descritions with nice indentation and line lengths in their world
|
||||
definitions without having it affect the rendered format.
|
||||
|
||||
315
worlds/Files.py
@@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
import abc
|
||||
import json
|
||||
import zipfile
|
||||
from enum import IntEnum
|
||||
import os
|
||||
import threading
|
||||
|
||||
from typing import ClassVar, Dict, Tuple, Any, Optional, Union, BinaryIO
|
||||
from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload, Sequence
|
||||
|
||||
import bsdiff4
|
||||
|
||||
@@ -38,7 +39,35 @@ class AutoPatchRegister(abc.ABCMeta):
|
||||
return None
|
||||
|
||||
|
||||
current_patch_version: int = 5
|
||||
class AutoPatchExtensionRegister(abc.ABCMeta):
|
||||
extension_types: ClassVar[Dict[str, AutoPatchExtensionRegister]] = {}
|
||||
required_extensions: Tuple[str, ...] = ()
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchExtensionRegister:
|
||||
# construct class
|
||||
new_class = super().__new__(mcs, name, bases, dct)
|
||||
if "game" in dct:
|
||||
AutoPatchExtensionRegister.extension_types[dct["game"]] = new_class
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
def get_handler(game: Optional[str]) -> Union[AutoPatchExtensionRegister, List[AutoPatchExtensionRegister]]:
|
||||
if not game:
|
||||
return APPatchExtension
|
||||
handler = AutoPatchExtensionRegister.extension_types.get(game, APPatchExtension)
|
||||
if handler.required_extensions:
|
||||
handlers = [handler]
|
||||
for required in handler.required_extensions:
|
||||
ext = AutoPatchExtensionRegister.extension_types.get(required)
|
||||
if not ext:
|
||||
raise NotImplementedError(f"No handler for {required}.")
|
||||
handlers.append(ext)
|
||||
return handlers
|
||||
else:
|
||||
return handler
|
||||
|
||||
|
||||
container_version: int = 6
|
||||
|
||||
|
||||
class InvalidDataError(Exception):
|
||||
@@ -50,7 +79,7 @@ class InvalidDataError(Exception):
|
||||
|
||||
class APContainer:
|
||||
"""A zipfile containing at least archipelago.json"""
|
||||
version: int = current_patch_version
|
||||
version: int = container_version
|
||||
compression_level: int = 9
|
||||
compression_method: int = zipfile.ZIP_DEFLATED
|
||||
game: Optional[str] = None
|
||||
@@ -124,14 +153,31 @@ class APContainer:
|
||||
"game": self.game,
|
||||
# minimum version of patch system expected for patching to be successful
|
||||
"compatible_version": 5,
|
||||
"version": current_patch_version,
|
||||
"version": container_version,
|
||||
}
|
||||
|
||||
|
||||
class APPatch(APContainer, abc.ABC, metaclass=AutoPatchRegister):
|
||||
class APPatch(APContainer):
|
||||
"""
|
||||
An abstract `APContainer` that defines the requirements for an object
|
||||
to be used by the `Patch.create_rom_file` function.
|
||||
An `APContainer` that represents a patch file.
|
||||
It includes the `procedure` key in the manifest to indicate that it is a patch.
|
||||
|
||||
Your implementation should inherit from this if your output file
|
||||
represents a patch file, but will not be applied with AP's `Patch.py`
|
||||
"""
|
||||
procedure: Union[Literal["custom"], List[Tuple[str, List[Any]]]] = "custom"
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
manifest = super(APPatch, self).get_manifest()
|
||||
manifest["procedure"] = self.procedure
|
||||
manifest["compatible_version"] = 6
|
||||
return manifest
|
||||
|
||||
|
||||
class APAutoPatchInterface(APPatch, abc.ABC, metaclass=AutoPatchRegister):
|
||||
"""
|
||||
An abstract `APPatch` that defines the requirements for a patch
|
||||
to be applied with AP's `Patch.py`
|
||||
"""
|
||||
result_file_ending: str = ".sfc"
|
||||
|
||||
@@ -140,25 +186,14 @@ class APPatch(APContainer, abc.ABC, metaclass=AutoPatchRegister):
|
||||
""" create the output file with the file name `target` """
|
||||
|
||||
|
||||
class APDeltaPatch(APPatch):
|
||||
"""An APPatch that additionally has delta.bsdiff4
|
||||
containing a delta patch to get the desired file, often a rom."""
|
||||
|
||||
class APProcedurePatch(APAutoPatchInterface):
|
||||
"""
|
||||
An APPatch that defines a procedure to produce the desired file.
|
||||
"""
|
||||
hash: Optional[str] # base checksum of source file
|
||||
patch_file_ending: str = ""
|
||||
delta: Optional[bytes] = None
|
||||
source_data: bytes
|
||||
|
||||
def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
|
||||
self.patched_path = patched_path
|
||||
super(APDeltaPatch, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
manifest = super(APDeltaPatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
return manifest
|
||||
patch_file_ending: str = ""
|
||||
files: Dict[str, bytes]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
@@ -171,21 +206,223 @@ class APDeltaPatch(APPatch):
|
||||
cls.source_data = cls.get_source_data()
|
||||
return cls.source_data
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).write_contents(opened_zipfile)
|
||||
# write Delta
|
||||
opened_zipfile.writestr("delta.bsdiff4",
|
||||
bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()),
|
||||
compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super(APProcedurePatch, self).__init__(*args, **kwargs)
|
||||
self.files = {}
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.delta = opened_zipfile.read("delta.bsdiff4")
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
manifest = super(APProcedurePatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
manifest["procedure"] = self.procedure
|
||||
if self.procedure == APDeltaPatch.procedure:
|
||||
manifest["compatible_version"] = 5
|
||||
return manifest
|
||||
|
||||
def patch(self, target: str):
|
||||
"""Base + Delta -> Patched"""
|
||||
if not self.delta:
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
super(APProcedurePatch, self).read_contents(opened_zipfile)
|
||||
with opened_zipfile.open("archipelago.json", "r") as f:
|
||||
manifest = json.load(f)
|
||||
if "procedure" not in manifest:
|
||||
# support patching files made before moving to procedures
|
||||
self.procedure = [("apply_bsdiff4", ["delta.bsdiff4"])]
|
||||
else:
|
||||
self.procedure = manifest["procedure"]
|
||||
for file in opened_zipfile.namelist():
|
||||
if file not in ["archipelago.json"]:
|
||||
self.files[file] = opened_zipfile.read(file)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
super(APProcedurePatch, self).write_contents(opened_zipfile)
|
||||
for file in self.files:
|
||||
opened_zipfile.writestr(file, self.files[file],
|
||||
compress_type=zipfile.ZIP_STORED if file.endswith(".bsdiff4") else None)
|
||||
|
||||
def get_file(self, file: str) -> bytes:
|
||||
""" Retrieves a file from the patch container."""
|
||||
if file not in self.files:
|
||||
self.read()
|
||||
result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta)
|
||||
with open(target, "wb") as f:
|
||||
f.write(result)
|
||||
return self.files[file]
|
||||
|
||||
def write_file(self, file_name: str, file: bytes) -> None:
|
||||
""" Writes a file to the patch container, to be retrieved upon patching. """
|
||||
self.files[file_name] = file
|
||||
|
||||
def patch(self, target: str) -> None:
|
||||
self.read()
|
||||
base_data = self.get_source_data_with_cache()
|
||||
patch_extender = AutoPatchExtensionRegister.get_handler(self.game)
|
||||
assert not isinstance(self.procedure, str), f"{type(self)} must define procedures"
|
||||
for step, args in self.procedure:
|
||||
if isinstance(patch_extender, list):
|
||||
extension = next((item for item in [getattr(extender, step, None) for extender in patch_extender]
|
||||
if item is not None), None)
|
||||
else:
|
||||
extension = getattr(patch_extender, step, None)
|
||||
if extension is not None:
|
||||
base_data = extension(self, base_data, *args)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown procedure {step} for {self.game}.")
|
||||
with open(target, 'wb') as f:
|
||||
f.write(base_data)
|
||||
|
||||
|
||||
class APDeltaPatch(APProcedurePatch):
|
||||
"""An APProcedurePatch that additionally has delta.bsdiff4
|
||||
containing a delta patch to get the desired file, often a rom."""
|
||||
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["delta.bsdiff4"])
|
||||
]
|
||||
|
||||
def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
|
||||
super(APDeltaPatch, self).__init__(*args, **kwargs)
|
||||
self.patched_path = patched_path
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
self.write_file("delta.bsdiff4",
|
||||
bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()))
|
||||
super(APDeltaPatch, self).write_contents(opened_zipfile)
|
||||
|
||||
|
||||
class APTokenTypes(IntEnum):
|
||||
WRITE = 0
|
||||
COPY = 1
|
||||
RLE = 2
|
||||
AND_8 = 3
|
||||
OR_8 = 4
|
||||
XOR_8 = 5
|
||||
|
||||
|
||||
class APTokenMixin:
|
||||
"""
|
||||
A class that defines functions for generating a token binary, for use in patches.
|
||||
"""
|
||||
_tokens: Sequence[
|
||||
Tuple[APTokenTypes, int, Union[
|
||||
bytes, # WRITE
|
||||
Tuple[int, int], # COPY, RLE
|
||||
int # AND_8, OR_8, XOR_8
|
||||
]]] = ()
|
||||
|
||||
def get_token_binary(self) -> bytes:
|
||||
"""
|
||||
Returns the token binary created from stored tokens.
|
||||
:return: A bytes object representing the token data.
|
||||
"""
|
||||
data = bytearray()
|
||||
data.extend(len(self._tokens).to_bytes(4, "little"))
|
||||
for token_type, offset, args in self._tokens:
|
||||
data.append(token_type)
|
||||
data.extend(offset.to_bytes(4, "little"))
|
||||
if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]:
|
||||
assert isinstance(args, int), f"Arguments to AND/OR/XOR must be of type int, not {type(args)}"
|
||||
data.extend(int.to_bytes(1, 4, "little"))
|
||||
data.append(args)
|
||||
elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]:
|
||||
assert isinstance(args, tuple), f"Arguments to COPY/RLE must be of type tuple, not {type(args)}"
|
||||
data.extend(int.to_bytes(8, 4, "little"))
|
||||
data.extend(args[0].to_bytes(4, "little"))
|
||||
data.extend(args[1].to_bytes(4, "little"))
|
||||
elif token_type == APTokenTypes.WRITE:
|
||||
assert isinstance(args, bytes), f"Arguments to WRITE must be of type bytes, not {type(args)}"
|
||||
data.extend(len(args).to_bytes(4, "little"))
|
||||
data.extend(args)
|
||||
else:
|
||||
raise ValueError(f"Unknown token type {token_type}")
|
||||
return bytes(data)
|
||||
|
||||
@overload
|
||||
def write_token(self,
|
||||
token_type: Literal[APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8],
|
||||
offset: int,
|
||||
data: int) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def write_token(self,
|
||||
token_type: Literal[APTokenTypes.COPY, APTokenTypes.RLE],
|
||||
offset: int,
|
||||
data: Tuple[int, int]) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def write_token(self,
|
||||
token_type: Literal[APTokenTypes.WRITE],
|
||||
offset: int,
|
||||
data: bytes) -> None:
|
||||
...
|
||||
|
||||
def write_token(self, token_type: APTokenTypes, offset: int, data: Union[bytes, Tuple[int, int], int]) -> None:
|
||||
"""
|
||||
Stores a token to be used by patching.
|
||||
"""
|
||||
if not isinstance(self._tokens, list):
|
||||
assert len(self._tokens) == 0, f"{type(self)}._tokens was tampered with."
|
||||
self._tokens = []
|
||||
self._tokens.append((token_type, offset, data))
|
||||
|
||||
|
||||
class APPatchExtension(metaclass=AutoPatchExtensionRegister):
|
||||
"""Class that defines patch extension functions for a given game.
|
||||
Patch extension functions must have the following two arguments in the following order:
|
||||
|
||||
caller: APProcedurePatch (used to retrieve files from the patch container)
|
||||
|
||||
rom: bytes (the data to patch)
|
||||
|
||||
Further arguments are passed in from the procedure as defined.
|
||||
|
||||
Patch extension functions must return the changed bytes.
|
||||
"""
|
||||
game: str
|
||||
required_extensions: ClassVar[Tuple[str, ...]] = ()
|
||||
|
||||
@staticmethod
|
||||
def apply_bsdiff4(caller: APProcedurePatch, rom: bytes, patch: str) -> bytes:
|
||||
"""Applies the given bsdiff4 from the patch onto the current file."""
|
||||
return bsdiff4.patch(rom, caller.get_file(patch))
|
||||
|
||||
@staticmethod
|
||||
def apply_tokens(caller: APProcedurePatch, rom: bytes, token_file: str) -> bytes:
|
||||
"""Applies the given token file from the patch onto the current file."""
|
||||
token_data = caller.get_file(token_file)
|
||||
rom_data = bytearray(rom)
|
||||
token_count = int.from_bytes(token_data[0:4], "little")
|
||||
bpr = 4
|
||||
for _ in range(token_count):
|
||||
token_type = token_data[bpr:bpr + 1][0]
|
||||
offset = int.from_bytes(token_data[bpr + 1:bpr + 5], "little")
|
||||
size = int.from_bytes(token_data[bpr + 5:bpr + 9], "little")
|
||||
data = token_data[bpr + 9:bpr + 9 + size]
|
||||
if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]:
|
||||
arg = data[0]
|
||||
if token_type == APTokenTypes.AND_8:
|
||||
rom_data[offset] = rom_data[offset] & arg
|
||||
elif token_type == APTokenTypes.OR_8:
|
||||
rom_data[offset] = rom_data[offset] | arg
|
||||
else:
|
||||
rom_data[offset] = rom_data[offset] ^ arg
|
||||
elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]:
|
||||
length = int.from_bytes(data[:4], "little")
|
||||
value = int.from_bytes(data[4:], "little")
|
||||
if token_type == APTokenTypes.COPY:
|
||||
rom_data[offset: offset + length] = rom_data[value: value + length]
|
||||
else:
|
||||
rom_data[offset: offset + length] = bytes([value] * length)
|
||||
else:
|
||||
rom_data[offset:offset + len(data)] = data
|
||||
bpr += 9 + size
|
||||
return bytes(rom_data)
|
||||
|
||||
@staticmethod
|
||||
def calc_snes_crc(caller: APProcedurePatch, rom: bytes) -> bytes:
|
||||
"""Calculates and applies a valid CRC for the SNES rom header."""
|
||||
rom_data = bytearray(rom)
|
||||
if len(rom) < 0x8000:
|
||||
raise Exception("Tried to calculate SNES CRC on file too small to be a SNES ROM.")
|
||||
crc = (sum(rom_data[:0x7FDC] + rom_data[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||
inv = crc ^ 0xFFFF
|
||||
rom_data[0x7FDC:0x7FE0] = [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]
|
||||
return bytes(rom_data)
|
||||
|
||||
@@ -85,10 +85,6 @@ components: List[Component] = [
|
||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
||||
Component('Generate', 'Generate', cli=True),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient),
|
||||
# SNI
|
||||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac', '.apkdl3')),
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
|
||||
@@ -7,7 +7,6 @@ checking or launching the client, otherwise it will probably cause circular impo
|
||||
import asyncio
|
||||
import enum
|
||||
import subprocess
|
||||
import traceback
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled
|
||||
@@ -260,7 +259,7 @@ def launch() -> None:
|
||||
try:
|
||||
await watcher_task
|
||||
except Exception as e:
|
||||
logger.error("".join(traceback.format_exception(e)))
|
||||
logger.exception(e)
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
|
||||
@@ -12,6 +12,8 @@ from .position import Point2
|
||||
from .unit import Unit
|
||||
from .units import Units
|
||||
|
||||
from worlds._sc2common.bot import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .game_info import Ramp
|
||||
|
||||
@@ -310,6 +312,7 @@ class BotAI(BotAIInternal):
|
||||
:param message:
|
||||
:param team_only:"""
|
||||
assert isinstance(message, str), f"{message} is not a string"
|
||||
logger.debug("Sending message: " + message)
|
||||
await self.client.chat_send(message, team_only)
|
||||
|
||||
def in_map_bounds(self, pos: Union[Point2, tuple, list]) -> bool:
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Optional, Any
|
||||
import Utils
|
||||
from .Locations import AdventureLocation, LocationData
|
||||
from settings import get_settings
|
||||
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
|
||||
from worlds.Files import APPatch, AutoPatchRegister
|
||||
|
||||
import bsdiff4
|
||||
|
||||
@@ -78,7 +78,7 @@ class BatNoTouchLocation:
|
||||
return ret_dict
|
||||
|
||||
|
||||
class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
|
||||
hash = ADVENTUREHASH
|
||||
game = "Adventure"
|
||||
patch_file_ending = ".apadvn"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Adventure
|
||||
|
||||
## Where is the settings page?
|
||||
The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file.
|
||||
## Where is the options page?
|
||||
The [player options page for Adventure](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All
|
||||
Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized,
|
||||
Adventure items are added to the multiworld item pool. Depending on the `dragon_rando_type` value, dragon locations may be randomized,
|
||||
slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates'
|
||||
can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist
|
||||
to reduce their speeds.
|
||||
@@ -15,7 +15,7 @@ Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle
|
||||
|
||||
## Which items can be in another player's world?
|
||||
All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on
|
||||
settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
|
||||
options, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
|
||||
|
||||
## What is considered a location check in Adventure?
|
||||
Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item.
|
||||
@@ -41,7 +41,7 @@ A message is shown in the client log. While empty handed, the player can press
|
||||
order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to
|
||||
return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions.
|
||||
|
||||
## What are recommended settings to tweak for beginners to the rando?
|
||||
## What are recommended options to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
@@ -41,7 +41,7 @@ an experience customized for their taste, and different players in the same mult
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings)
|
||||
You can generate a yaml or download a template by visiting the [Adventure Options Page](/games/Adventure/player-options)
|
||||
|
||||
### What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
|
||||
@@ -471,6 +471,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
||||
|
||||
class ALTTPSNIClient(SNIClient):
|
||||
game = "A Link to the Past"
|
||||
patch_suffix = [".aplttp", ".apz3"]
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
|
||||
|
||||