Compare commits
1 Commits
0.4.5
...
factorio_a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3738399348 |
@@ -1,5 +0,0 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
if typing.TYPE_CHECKING:
|
||||
31
.github/labeler.yml
vendored
@@ -1,31 +0,0 @@
|
||||
'is: documentation':
|
||||
- changed-files:
|
||||
- all-globs-to-all-files: '{**/docs/**,**/README.md}'
|
||||
|
||||
'affects: webhost':
|
||||
- changed-files:
|
||||
- all-globs-to-any-file: 'WebHost.py'
|
||||
- all-globs-to-any-file: 'WebHostLib/**/*'
|
||||
|
||||
'affects: core':
|
||||
- changed-files:
|
||||
- all-globs-to-any-file:
|
||||
- '!*Client.py'
|
||||
- '!README.md'
|
||||
- '!LICENSE'
|
||||
- '!*.yml'
|
||||
- '!.gitignore'
|
||||
- '!**/docs/**'
|
||||
- '!typings/kivy/**'
|
||||
- '!test/**'
|
||||
- '!data/**'
|
||||
- '!.run/**'
|
||||
- '!.github/**'
|
||||
- '!worlds_disabled/**'
|
||||
- '!worlds/**'
|
||||
- '!WebHost.py'
|
||||
- '!WebHostLib/**'
|
||||
- any-glob-to-any-file: # exceptions to the above rules of "stuff that isn't core"
|
||||
- 'worlds/generic/**/*.py'
|
||||
- 'worlds/*.py'
|
||||
- 'CommonClient.py'
|
||||
6
.github/workflows/analyze-modified-files.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- 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@v5
|
||||
- uses: actions/setup-python@v4
|
||||
if: env.diff != ''
|
||||
with:
|
||||
python-version: 3.8
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
if: env.diff != '' && matrix.task == 'flake8'
|
||||
run: |
|
||||
flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||
flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||
|
||||
- name: "mypy: Type check modified files"
|
||||
continue-on-error: true
|
||||
|
||||
33
.github/workflows/build.yml
vendored
@@ -8,13 +8,11 @@ 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:
|
||||
@@ -27,9 +25,9 @@ jobs:
|
||||
build-win-py38: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Download run-time dependencies
|
||||
@@ -48,42 +46,25 @@ 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@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
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@v4
|
||||
- uses: actions/checkout@v3
|
||||
- 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@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install build-time dependencies
|
||||
@@ -119,13 +100,13 @@ jobs:
|
||||
source venv/bin/activate
|
||||
python setup.py build_exe --yes
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.APPIMAGE_NAME }}
|
||||
path: dist/${{ env.APPIMAGE_NAME }}
|
||||
retention-days: 7
|
||||
- name: Store .tar.gz
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
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@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
46
.github/workflows/label-pull-requests.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Label Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: ['opened', 'reopened', 'synchronize', 'ready_for_review', 'converted_to_draft', 'closed']
|
||||
branches: ['main']
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
name: 'Apply content-based labels'
|
||||
if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: false
|
||||
peer_review:
|
||||
name: 'Apply peer review label'
|
||||
needs: labeler
|
||||
if: >-
|
||||
(github.event.action == 'opened' || github.event.action == 'reopened' ||
|
||||
github.event.action == 'ready_for_review') && !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Add label'
|
||||
run: "gh pr edit \"$PR_URL\" --add-label 'waiting-on: peer-review'"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
unblock_draft_prs:
|
||||
name: 'Remove waiting-on labels'
|
||||
needs: labeler
|
||||
if: github.event.action == 'converted_to_draft' || github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Remove labels'
|
||||
run: |-
|
||||
gh pr edit "$PR_URL" --remove-label 'waiting-on: peer-review' \
|
||||
--remove-label 'waiting-on: core-review' \
|
||||
--remove-label 'waiting-on: world-maintainer' \
|
||||
--remove-label 'waiting-on: author'
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
|
||||
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@v4
|
||||
- uses: actions/checkout@v3
|
||||
- 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@v5
|
||||
uses: actions/setup-python@v4
|
||||
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@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
|
||||
65
.github/workflows/scan-build.yml
vendored
@@ -1,65 +0,0 @@
|
||||
name: Native Code Static Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.c'
|
||||
- '**.cc'
|
||||
- '**.cpp'
|
||||
- '**.cxx'
|
||||
- '**.h'
|
||||
- '**.hh'
|
||||
- '**.hpp'
|
||||
- '**.pyx'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
- '.github/workflows/scan-build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.c'
|
||||
- '**.cc'
|
||||
- '**.cpp'
|
||||
- '**.cxx'
|
||||
- '**.h'
|
||||
- '**.hh'
|
||||
- '**.hpp'
|
||||
- '**.pyx'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
- '.github/workflows/scan-build.yml'
|
||||
|
||||
jobs:
|
||||
scan-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install newer Clang
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x ./llvm.sh
|
||||
sudo ./llvm.sh 17
|
||||
- name: Install scan-build command
|
||||
run: |
|
||||
sudo apt install clang-tools-17
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
python -m pip install --upgrade pip -r requirements.txt
|
||||
- name: scan-build
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
|
||||
- name: Store report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: scan-build-reports
|
||||
path: scan-build-reports
|
||||
4
.github/workflows/unittests.yml
vendored
@@ -46,9 +46,9 @@ jobs:
|
||||
os: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python.version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Archipelago Unittests" type="tests" factoryName="Unittests">
|
||||
<module name="Archipelago" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="_new_pattern" value="""" />
|
||||
<option name="_new_additionalArguments" value="""" />
|
||||
<option name="_new_target" value=""$PROJECT_DIR$/test"" />
|
||||
<option name="_new_targetType" value=""PATH"" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -115,12 +115,11 @@ class AdventureContext(CommonContext):
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "Retrieved":
|
||||
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
|
||||
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
elif cmd == "SetReply":
|
||||
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
|
||||
self.freeincarnates_used = args["value"]
|
||||
|
||||
138
BaseClasses.py
@@ -18,14 +18,11 @@ import NetUtils
|
||||
import Options
|
||||
import Utils
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class Group(TypedDict, total=False):
|
||||
name: str
|
||||
game: str
|
||||
world: "AutoWorld.World"
|
||||
world: auto_world
|
||||
players: Set[int]
|
||||
item_pool: Set[str]
|
||||
replacement_items: Dict[int, Optional[str]]
|
||||
@@ -58,7 +55,7 @@ class MultiWorld():
|
||||
plando_texts: List[Dict[str, str]]
|
||||
plando_items: List[List[Dict[str, Any]]]
|
||||
plando_connections: List
|
||||
worlds: Dict[int, "AutoWorld.World"]
|
||||
worlds: Dict[int, auto_world]
|
||||
groups: Dict[int, Group]
|
||||
regions: RegionManager
|
||||
itempool: List[Item]
|
||||
@@ -85,7 +82,7 @@ class MultiWorld():
|
||||
game: Dict[int, str]
|
||||
|
||||
random: random.Random
|
||||
per_slot_randoms: Utils.DeprecateDict[int, random.Random]
|
||||
per_slot_randoms: Dict[int, random.Random]
|
||||
"""Deprecated. Please use `self.random` instead."""
|
||||
|
||||
class AttributeProxy():
|
||||
@@ -110,14 +107,10 @@ class MultiWorld():
|
||||
return self
|
||||
|
||||
def append(self, region: Region):
|
||||
assert region.name not in self.region_cache[region.player], \
|
||||
f"{region.name} already exists in region cache."
|
||||
self.region_cache[region.player][region.name] = region
|
||||
|
||||
def extend(self, regions: Iterable[Region]):
|
||||
for region in regions:
|
||||
assert region.name not in self.region_cache[region.player], \
|
||||
f"{region.name} already exists in region cache."
|
||||
self.region_cache[region.player][region.name] = region
|
||||
|
||||
def add_group(self, new_id: int):
|
||||
@@ -163,11 +156,11 @@ class MultiWorld():
|
||||
self.fix_trock_doors = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||
self.fix_skullwoods_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
self.fix_palaceofdarkness_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
self.fix_trock_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
@@ -217,8 +210,7 @@ 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 = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||
"world's random object instead (usually self.random)")
|
||||
self.per_slot_randoms = {}
|
||||
self.plando_options = PlandoOptions.none
|
||||
|
||||
def get_all_ids(self) -> Tuple[int, ...]:
|
||||
@@ -227,8 +219,6 @@ class MultiWorld():
|
||||
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
||||
"""Create a group with name and return the assigned player ID and group.
|
||||
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
||||
from worlds import AutoWorld
|
||||
|
||||
for group_id, group in self.groups.items():
|
||||
if group["name"] == name:
|
||||
group["players"] |= players
|
||||
@@ -252,36 +242,29 @@ 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
|
||||
from worlds import AutoWorld
|
||||
|
||||
all_keys: Set[str] = {key for player in self.player_ids for key in
|
||||
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
|
||||
for option_key in all_keys:
|
||||
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
|
||||
f"Please use `self.options.{option_key}` instead.")
|
||||
option.update(getattr(args, option_key, {}))
|
||||
setattr(self, option_key, option)
|
||||
|
||||
for player in self.player_ids:
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
self.worlds[player] = world_type(self, player)
|
||||
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
self.worlds[player].random = self.per_slot_randoms[player]
|
||||
for option_key in world_type.options_dataclass.type_hints:
|
||||
option_values = getattr(args, option_key, {})
|
||||
setattr(self, option_key, option_values)
|
||||
# TODO - remove this loop once all worlds use options dataclasses
|
||||
options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
|
||||
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
|
||||
for option_key in options_dataclass.type_hints})
|
||||
|
||||
def set_item_links(self):
|
||||
from worlds import AutoWorld
|
||||
|
||||
item_links = {}
|
||||
replacement_prio = [False, True, None]
|
||||
for player in self.player_ids:
|
||||
@@ -508,7 +491,7 @@ class MultiWorld():
|
||||
else:
|
||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||
|
||||
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
|
||||
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
|
||||
if starting_state:
|
||||
if self.has_beaten_game(starting_state):
|
||||
return True
|
||||
@@ -521,7 +504,7 @@ class MultiWorld():
|
||||
and location.item.advancement and location not in state.locations_checked}
|
||||
|
||||
while prog_locations:
|
||||
sphere: Set[Location] = set()
|
||||
sphere = set()
|
||||
# build up spheres of collection radius.
|
||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
for location in prog_locations:
|
||||
@@ -541,19 +524,12 @@ class MultiWorld():
|
||||
|
||||
return False
|
||||
|
||||
def get_spheres(self) -> Iterator[Set[Location]]:
|
||||
"""
|
||||
yields a set of locations for each logical sphere
|
||||
|
||||
If there are unreachable locations, the last sphere of reachable
|
||||
locations is followed by an empty set, and then a set of all of the
|
||||
unreachable locations.
|
||||
"""
|
||||
def get_spheres(self):
|
||||
state = CollectionState(self)
|
||||
locations = set(self.get_filled_locations())
|
||||
|
||||
while locations:
|
||||
sphere: Set[Location] = set()
|
||||
sphere = set()
|
||||
|
||||
for location in locations:
|
||||
if location.can_reach(state):
|
||||
@@ -584,10 +560,9 @@ class MultiWorld():
|
||||
|
||||
def location_condition(location: Location):
|
||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||
if location.player in players["locations"] or (location.item and location.item.player not in
|
||||
players["minimal"]):
|
||||
return True
|
||||
return False
|
||||
if location.player in players["minimal"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def location_relevant(location: Location):
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
@@ -664,34 +639,34 @@ class CollectionState():
|
||||
|
||||
def update_reachable_regions(self, player: int):
|
||||
self.stale[player] = False
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
rrp = self.reachable_regions[player]
|
||||
bc = self.blocked_connections[player]
|
||||
queue = deque(self.blocked_connections[player])
|
||||
start = self.multiworld.get_region("Menu", player)
|
||||
start = self.multiworld.get_region('Menu', player)
|
||||
|
||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||
if start not in reachable_regions:
|
||||
reachable_regions.add(start)
|
||||
blocked_connections.update(start.exits)
|
||||
if start not in rrp:
|
||||
rrp.add(start)
|
||||
bc.update(start.exits)
|
||||
queue.extend(start.exits)
|
||||
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region in reachable_regions:
|
||||
blocked_connections.remove(connection)
|
||||
if new_region in rrp:
|
||||
bc.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
||||
reachable_regions.add(new_region)
|
||||
blocked_connections.remove(connection)
|
||||
blocked_connections.update(new_region.exits)
|
||||
rrp.add(new_region)
|
||||
bc.remove(connection)
|
||||
bc.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
|
||||
# Retry connections if the new region can unblock them
|
||||
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
|
||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||
if new_entrance in bc and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
|
||||
def copy(self) -> CollectionState:
|
||||
@@ -716,23 +691,14 @@ class CollectionState():
|
||||
assert isinstance(player, int), "can_reach: player is required if spot is str"
|
||||
# try to resolve a name
|
||||
if resolution_hint == 'Location':
|
||||
return self.can_reach_location(spot, player)
|
||||
spot = self.multiworld.get_location(spot, player)
|
||||
elif resolution_hint == 'Entrance':
|
||||
return self.can_reach_entrance(spot, player)
|
||||
spot = self.multiworld.get_entrance(spot, player)
|
||||
else:
|
||||
# default to Region
|
||||
return self.can_reach_region(spot, player)
|
||||
spot = self.multiworld.get_region(spot, player)
|
||||
return spot.can_reach(self)
|
||||
|
||||
def can_reach_location(self, spot: str, player: int) -> bool:
|
||||
return self.multiworld.get_location(spot, player).can_reach(self)
|
||||
|
||||
def can_reach_entrance(self, spot: str, player: int) -> bool:
|
||||
return self.multiworld.get_entrance(spot, player).can_reach(self)
|
||||
|
||||
def can_reach_region(self, spot: str, player: int) -> bool:
|
||||
return self.multiworld.get_region(spot, player).can_reach(self)
|
||||
|
||||
def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
|
||||
if locations is None:
|
||||
locations = self.multiworld.get_filled_locations()
|
||||
@@ -845,8 +811,8 @@ class Entrance:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
world = self.parent_region.multiworld if self.parent_region else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
class Region:
|
||||
@@ -889,8 +855,6 @@ class Region:
|
||||
del(self.region_manager.location_cache[location.player][location.name])
|
||||
|
||||
def insert(self, index: int, value: Location) -> None:
|
||||
assert value.name not in self.region_manager.location_cache[value.player], \
|
||||
f"{value.name} already exists in the location cache."
|
||||
self._list.insert(index, value)
|
||||
self.region_manager.location_cache[value.player][value.name] = value
|
||||
|
||||
@@ -901,8 +865,6 @@ class Region:
|
||||
del(self.region_manager.entrance_cache[entrance.player][entrance.name])
|
||||
|
||||
def insert(self, index: int, value: Entrance) -> None:
|
||||
assert value.name not in self.region_manager.entrance_cache[value.player], \
|
||||
f"{value.name} already exists in the entrance cache."
|
||||
self._list.insert(index, value)
|
||||
self.region_manager.entrance_cache[value.player][value.name] = value
|
||||
|
||||
@@ -1032,7 +994,7 @@ class Location:
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda state, item: False)
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
@@ -1066,8 +1028,8 @@ class Location:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
world = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
@@ -1082,6 +1044,9 @@ class Location:
|
||||
|
||||
@property
|
||||
def hint_text(self) -> str:
|
||||
hint_text = getattr(self, "_hint_text", None)
|
||||
if hint_text:
|
||||
return hint_text
|
||||
return "at " + self.name.replace("_", " ").replace("-", " ")
|
||||
|
||||
|
||||
@@ -1201,7 +1166,7 @@ class Spoiler:
|
||||
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
|
||||
|
||||
def create_playthrough(self, create_paths: bool = True) -> None:
|
||||
"""Destructive to the multiworld while it is run, damage gets repaired afterwards."""
|
||||
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||
from itertools import chain
|
||||
# get locations containing progress items
|
||||
multiworld = self.multiworld
|
||||
@@ -1288,12 +1253,12 @@ class Spoiler:
|
||||
for location in sphere:
|
||||
state.collect(location.item, True, location)
|
||||
|
||||
required_locations -= sphere
|
||||
|
||||
collection_spheres.append(sphere)
|
||||
|
||||
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
|
||||
len(sphere), len(required_locations))
|
||||
|
||||
required_locations -= sphere
|
||||
if not sphere:
|
||||
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
||||
|
||||
@@ -1352,8 +1317,6 @@ class Spoiler:
|
||||
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
def to_file(self, filename: str) -> None:
|
||||
from worlds import AutoWorld
|
||||
|
||||
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
|
||||
res = getattr(self.multiworld.worlds[player].options, option_key)
|
||||
display_name = getattr(option_obj, "display_name", option_key)
|
||||
@@ -1477,3 +1440,8 @@ def get_seed(seed: Optional[int] = None) -> int:
|
||||
random.seed(None)
|
||||
return random.randint(0, pow(10, seeddigits) - 1)
|
||||
return seed
|
||||
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
auto_world = AutoWorld.World
|
||||
|
||||
@@ -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, add_json_text, add_json_location, add_json_item, JSONTypes)
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
||||
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
|
||||
from Utils import Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -72,16 +72,9 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_received(self) -> bool:
|
||||
"""List all received items"""
|
||||
item: NetworkItem
|
||||
self.output(f'{len(self.ctx.items_received)} received items, sorted by time:')
|
||||
self.output(f'{len(self.ctx.items_received)} received items:')
|
||||
for index, item in enumerate(self.ctx.items_received, 1):
|
||||
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"})
|
||||
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
|
||||
return True
|
||||
|
||||
def _cmd_missing(self, filter_text = "") -> bool:
|
||||
@@ -122,15 +115,6 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
|
||||
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:
|
||||
@@ -140,15 +124,6 @@ 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
|
||||
@@ -485,7 +460,7 @@ class CommonContext:
|
||||
else:
|
||||
self.update_game(cached_game)
|
||||
if needed_updates:
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
|
||||
|
||||
def update_game(self, game_package: dict):
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
@@ -502,7 +477,6 @@ class CommonContext:
|
||||
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
current_cache.update(data_package["games"])
|
||||
Utils.persistent_store("datapackage", "games", current_cache)
|
||||
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
|
||||
for game, game_data in data_package["games"].items():
|
||||
Utils.store_data_package_for_checksum(game, game_data)
|
||||
|
||||
@@ -643,13 +617,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:
|
||||
@@ -753,15 +727,14 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
await ctx.server_auth(args['password'])
|
||||
|
||||
elif cmd == 'DataPackage':
|
||||
logger.info("Got new ID/Name DataPackage")
|
||||
ctx.consume_network_data_package(args['data'])
|
||||
|
||||
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. '
|
||||
@@ -968,5 +941,4 @@ def run_as_textclient():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING
|
||||
run_as_textclient()
|
||||
|
||||
267
Fill.py
@@ -27,12 +27,12 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
return new_state
|
||||
|
||||
|
||||
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
|
||||
"""
|
||||
:param multiworld: Multiworld to be filled.
|
||||
:param world: Multiworld to be filled.
|
||||
:param base_state: State assumed before fill.
|
||||
:param locations: Locations to be filled with item_pool
|
||||
:param item_pool: Items to fill into the locations
|
||||
@@ -68,7 +68,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, item_pool + unplaced_items)
|
||||
|
||||
has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
while items_to_place:
|
||||
# if we have run out of locations to fill,break out of this loop
|
||||
@@ -80,8 +80,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
|
||||
# if minimal accessibility, only check whether location is reachable if game not beatable
|
||||
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
|
||||
if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) \
|
||||
if single_player_placement else not has_beaten_game
|
||||
else:
|
||||
@@ -122,11 +122,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
# Verify placing this item won't reduce available locations, which would be a useless swap.
|
||||
prev_state = swap_state.copy()
|
||||
prev_loc_count = len(
|
||||
multiworld.get_reachable_locations(prev_state))
|
||||
world.get_reachable_locations(prev_state))
|
||||
|
||||
swap_state.collect(item_to_place, True)
|
||||
new_loc_count = len(
|
||||
multiworld.get_reachable_locations(swap_state))
|
||||
world.get_reachable_locations(swap_state))
|
||||
|
||||
if new_loc_count >= prev_loc_count:
|
||||
# Add this item to the existing placement, and
|
||||
@@ -156,7 +156,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
else:
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
multiworld.push_item(spot_to_fill, item_to_place, False)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
spot_to_fill.locked = lock
|
||||
placements.append(spot_to_fill)
|
||||
spot_to_fill.event = item_to_place.advancement
|
||||
@@ -173,7 +173,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
# validate all placements and remove invalid ones
|
||||
state = sweep_from_pool(base_state, [])
|
||||
for placement in placements:
|
||||
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
|
||||
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
|
||||
placement.item.location = None
|
||||
unplaced_items.append(placement.item)
|
||||
placement.item = None
|
||||
@@ -188,7 +188,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
if excluded_locations:
|
||||
for location in excluded_locations:
|
||||
location.progress_type = location.progress_type.DEFAULT
|
||||
fill_restrictive(multiworld, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
|
||||
fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
|
||||
swap, on_place, allow_partial, False)
|
||||
for location in excluded_locations:
|
||||
if not location.item:
|
||||
@@ -196,26 +196,19 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
|
||||
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
if multiworld.can_beat_game():
|
||||
if world.can_beat_game():
|
||||
logging.warning(
|
||||
f"Not all items placed. Game beatable anyway.\nCould not place:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}")
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
|
||||
else:
|
||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||
f"Unplaced items:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
item_pool.extend(unplaced_items)
|
||||
|
||||
|
||||
def remaining_fill(multiworld: MultiWorld,
|
||||
def remaining_fill(world: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item],
|
||||
name: str = "Remaining") -> None:
|
||||
itempool: typing.List[Item]) -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
@@ -268,41 +261,36 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
|
||||
multiworld.push_item(spot_to_fill, item_to_place, False)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
placements.append(spot_to_fill)
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
_log_fill_progress(name, placed, total)
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
|
||||
if total > 1000:
|
||||
_log_fill_progress(name, placed, total)
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||
f"Unplaced items:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def fast_fill(multiworld: MultiWorld,
|
||||
def fast_fill(world: MultiWorld,
|
||||
item_pool: typing.List[Item],
|
||||
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
multiworld.push_item(location, item, False)
|
||||
world.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||
def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||
maximum_exploration_state = sweep_from_pool(state, pool)
|
||||
minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and
|
||||
minimal_players = {player for player in world.player_ids if world.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
|
||||
not location.can_reach(maximum_exploration_state)]
|
||||
for location in unreachable_locations:
|
||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||
@@ -316,36 +304,36 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections")
|
||||
fill_restrictive(world, state, locations, pool, name="Accessibility Corrections")
|
||||
|
||||
|
||||
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations):
|
||||
def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
|
||||
maximum_exploration_state = sweep_from_pool(state)
|
||||
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
|
||||
if unreachable_locations:
|
||||
def forbid_important_item_rule(item: Item):
|
||||
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal')
|
||||
return not ((item.classification & 0b0011) and world.worlds[item.player].options.accessibility != 'minimal')
|
||||
|
||||
for location in unreachable_locations:
|
||||
add_item_rule(location, forbid_important_item_rule)
|
||||
|
||||
|
||||
def distribute_early_items(multiworld: MultiWorld,
|
||||
def distribute_early_items(world: MultiWorld,
|
||||
fill_locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
|
||||
""" returns new fill_locations and itempool """
|
||||
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
|
||||
for player in multiworld.player_ids:
|
||||
items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player])
|
||||
for player in world.player_ids:
|
||||
items = itertools.chain(world.early_items[player], world.local_early_items[player])
|
||||
for item in items:
|
||||
early_items_count[item, player] = [multiworld.early_items[player].get(item, 0),
|
||||
multiworld.local_early_items[player].get(item, 0)]
|
||||
early_items_count[item, player] = [world.early_items[player].get(item, 0),
|
||||
world.local_early_items[player].get(item, 0)]
|
||||
if early_items_count:
|
||||
early_locations: typing.List[Location] = []
|
||||
early_priority_locations: typing.List[Location] = []
|
||||
loc_indexes_to_remove: typing.Set[int] = set()
|
||||
base_state = multiworld.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||
base_state = world.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
|
||||
for i, loc in enumerate(fill_locations):
|
||||
if loc.can_reach(base_state):
|
||||
if loc.progress_type == LocationProgressType.PRIORITY:
|
||||
@@ -357,8 +345,8 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
|
||||
early_prog_items: typing.List[Item] = []
|
||||
early_rest_items: typing.List[Item] = []
|
||||
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
|
||||
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
|
||||
item_indexes_to_remove: typing.Set[int] = set()
|
||||
for i, item in enumerate(itempool):
|
||||
if (item.name, item.player) in early_items_count:
|
||||
@@ -382,28 +370,28 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
if len(early_items_count) == 0:
|
||||
break
|
||||
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
|
||||
for player in multiworld.player_ids:
|
||||
for player in world.player_ids:
|
||||
player_local = early_local_rest_items[player]
|
||||
fill_restrictive(multiworld, base_state,
|
||||
fill_restrictive(world, base_state,
|
||||
[loc for loc in early_locations if loc.player == player],
|
||||
player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}")
|
||||
if player_local:
|
||||
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||
early_rest_items.extend(early_local_rest_items[player])
|
||||
early_locations = [loc for loc in early_locations if not loc.item]
|
||||
fill_restrictive(multiworld, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
|
||||
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
|
||||
name="Early Items")
|
||||
early_locations += early_priority_locations
|
||||
for player in multiworld.player_ids:
|
||||
for player in world.player_ids:
|
||||
player_local = early_local_prog_items[player]
|
||||
fill_restrictive(multiworld, base_state,
|
||||
fill_restrictive(world, base_state,
|
||||
[loc for loc in early_locations if loc.player == player],
|
||||
player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}")
|
||||
if player_local:
|
||||
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||
early_prog_items.extend(player_local)
|
||||
early_locations = [loc for loc in early_locations if not loc.item]
|
||||
fill_restrictive(multiworld, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
|
||||
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
|
||||
name="Early Progression")
|
||||
unplaced_early_items = early_rest_items + early_prog_items
|
||||
if unplaced_early_items:
|
||||
@@ -412,18 +400,18 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
itempool += unplaced_early_items
|
||||
|
||||
fill_locations.extend(early_locations)
|
||||
multiworld.random.shuffle(fill_locations)
|
||||
world.random.shuffle(fill_locations)
|
||||
return fill_locations, itempool
|
||||
|
||||
|
||||
def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
fill_locations = sorted(multiworld.get_unfilled_locations())
|
||||
multiworld.random.shuffle(fill_locations)
|
||||
def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
fill_locations = sorted(world.get_unfilled_locations())
|
||||
world.random.shuffle(fill_locations)
|
||||
# get items to distribute
|
||||
itempool = sorted(multiworld.itempool)
|
||||
multiworld.random.shuffle(itempool)
|
||||
itempool = sorted(world.itempool)
|
||||
world.random.shuffle(itempool)
|
||||
|
||||
fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool)
|
||||
fill_locations, itempool = distribute_early_items(world, fill_locations, itempool)
|
||||
|
||||
progitempool: typing.List[Item] = []
|
||||
usefulitempool: typing.List[Item] = []
|
||||
@@ -437,7 +425,7 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
else:
|
||||
filleritempool.append(item)
|
||||
|
||||
call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
|
||||
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
|
||||
loc_type: [] for loc_type in LocationProgressType}
|
||||
@@ -458,79 +446,75 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
name="Priority")
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
accessibility_corrections(world, world.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
if progitempool:
|
||||
# "advancement/progression fill"
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
|
||||
fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f"Not enough locations for progression items. "
|
||||
f"There are {len(progitempool)} more progression items than there are available locations."
|
||||
)
|
||||
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
||||
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
|
||||
accessibility_corrections(world, world.state, defaultlocations)
|
||||
|
||||
for location in lock_later:
|
||||
if location.item:
|
||||
location.locked = True
|
||||
del mark_for_locking, lock_later
|
||||
|
||||
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
|
||||
inaccessible_location_rules(world, world.state, defaultlocations)
|
||||
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
|
||||
remaining_fill(world, excludedlocations, filleritempool)
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. "
|
||||
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
|
||||
)
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
|
||||
restitempool = filleritempool + usefulitempool
|
||||
|
||||
remaining_fill(multiworld, defaultlocations, restitempool)
|
||||
remaining_fill(world, defaultlocations, restitempool)
|
||||
|
||||
unplaced = restitempool
|
||||
unfilled = defaultlocations
|
||||
|
||||
if unplaced or unfilled:
|
||||
logging.warning(
|
||||
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}")
|
||||
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
|
||||
locations_counter = Counter(location.player for location in multiworld.get_locations())
|
||||
f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
items_counter = Counter(location.item.player for location in world.get_locations() if location.item)
|
||||
locations_counter = Counter(location.player for location in world.get_locations())
|
||||
items_counter.update(item.player for item in unplaced)
|
||||
locations_counter.update(location.player for location in unfilled)
|
||||
print_data = {"items": items_counter, "locations": locations_counter}
|
||||
logging.info(f"Per-Player counts: {print_data})")
|
||||
logging.info(f'Per-Player counts: {print_data})')
|
||||
|
||||
|
||||
def flood_items(multiworld: MultiWorld) -> None:
|
||||
def flood_items(world: MultiWorld) -> None:
|
||||
# get items to distribute
|
||||
multiworld.random.shuffle(multiworld.itempool)
|
||||
itempool = multiworld.itempool
|
||||
world.random.shuffle(world.itempool)
|
||||
itempool = world.itempool
|
||||
progress_done = False
|
||||
|
||||
# sweep once to pick up preplaced items
|
||||
multiworld.state.sweep_for_events()
|
||||
world.state.sweep_for_events()
|
||||
|
||||
# fill multiworld from top of itempool while we can
|
||||
# fill world from top of itempool while we can
|
||||
while not progress_done:
|
||||
location_list = multiworld.get_unfilled_locations()
|
||||
multiworld.random.shuffle(location_list)
|
||||
location_list = world.get_unfilled_locations()
|
||||
world.random.shuffle(location_list)
|
||||
spot_to_fill = None
|
||||
for location in location_list:
|
||||
if location.can_fill(multiworld.state, itempool[0]):
|
||||
if location.can_fill(world.state, itempool[0]):
|
||||
spot_to_fill = location
|
||||
break
|
||||
|
||||
if spot_to_fill:
|
||||
item = itempool.pop(0)
|
||||
multiworld.push_item(spot_to_fill, item, True)
|
||||
world.push_item(spot_to_fill, item, True)
|
||||
continue
|
||||
|
||||
# ran out of spots, check if we need to step in and correct things
|
||||
if len(multiworld.get_reachable_locations()) == len(multiworld.get_locations()):
|
||||
if len(world.get_reachable_locations()) == len(world.get_locations()):
|
||||
progress_done = True
|
||||
continue
|
||||
|
||||
@@ -540,7 +524,7 @@ def flood_items(multiworld: MultiWorld) -> None:
|
||||
for item in itempool:
|
||||
if item.advancement:
|
||||
candidate_item_to_place = item
|
||||
if multiworld.unlocks_new_location(item):
|
||||
if world.unlocks_new_location(item):
|
||||
item_to_place = item
|
||||
break
|
||||
|
||||
@@ -553,20 +537,20 @@ def flood_items(multiworld: MultiWorld) -> None:
|
||||
raise FillError('No more progress items left to place.')
|
||||
|
||||
# find item to replace with progress item
|
||||
location_list = multiworld.get_reachable_locations()
|
||||
multiworld.random.shuffle(location_list)
|
||||
location_list = world.get_reachable_locations()
|
||||
world.random.shuffle(location_list)
|
||||
for location in location_list:
|
||||
if location.item is not None and not location.item.advancement:
|
||||
# safe to replace
|
||||
replace_item = location.item
|
||||
replace_item.location = None
|
||||
itempool.append(replace_item)
|
||||
multiworld.push_item(location, item_to_place, True)
|
||||
world.push_item(location, item_to_place, True)
|
||||
itempool.remove(item_to_place)
|
||||
break
|
||||
|
||||
|
||||
def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
def balance_multiworld_progression(world: MultiWorld) -> None:
|
||||
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
|
||||
# Overall progression balancing algorithm:
|
||||
# Gather up all locations in a sphere.
|
||||
@@ -574,28 +558,28 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
|
||||
# which gives more locations available by this sphere.
|
||||
balanceable_players: typing.Dict[int, float] = {
|
||||
player: multiworld.worlds[player].options.progression_balancing / 100
|
||||
for player in multiworld.player_ids
|
||||
if multiworld.worlds[player].options.progression_balancing > 0
|
||||
player: world.worlds[player].options.progression_balancing / 100
|
||||
for player in world.player_ids
|
||||
if world.worlds[player].options.progression_balancing > 0
|
||||
}
|
||||
if not balanceable_players:
|
||||
logging.info('Skipping multiworld progression balancing.')
|
||||
else:
|
||||
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
|
||||
logging.debug(balanceable_players)
|
||||
state: CollectionState = CollectionState(multiworld)
|
||||
state: CollectionState = CollectionState(world)
|
||||
checked_locations: typing.Set[Location] = set()
|
||||
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
|
||||
unchecked_locations: typing.Set[Location] = set(world.get_locations())
|
||||
|
||||
total_locations_count: typing.Counter[int] = Counter(
|
||||
location.player
|
||||
for location in multiworld.get_locations()
|
||||
for location in world.get_locations()
|
||||
if not location.locked
|
||||
)
|
||||
reachable_locations_count: typing.Dict[int, int] = {
|
||||
player: 0
|
||||
for player in multiworld.player_ids
|
||||
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
|
||||
for player in world.player_ids
|
||||
if total_locations_count[player] and len(world.get_filled_locations(player)) != 0
|
||||
}
|
||||
balanceable_players = {
|
||||
player: balanceable_players[player]
|
||||
@@ -674,7 +658,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
balancing_unchecked_locations.remove(location)
|
||||
if not location.locked:
|
||||
balancing_reachables[location.player] += 1
|
||||
if multiworld.has_beaten_game(balancing_state) or all(
|
||||
if world.has_beaten_game(balancing_state) or all(
|
||||
item_percentage(player, reachables) >= threshold_percentages[player]
|
||||
for player, reachables in balancing_reachables.items()
|
||||
if player in threshold_percentages):
|
||||
@@ -691,7 +675,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
locations_to_test = unlocked_locations[player]
|
||||
items_to_test = list(candidate_items[player])
|
||||
items_to_test.sort()
|
||||
multiworld.random.shuffle(items_to_test)
|
||||
world.random.shuffle(items_to_test)
|
||||
while items_to_test:
|
||||
testing = items_to_test.pop()
|
||||
reducing_state = state.copy()
|
||||
@@ -703,8 +687,8 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
|
||||
if multiworld.has_beaten_game(balancing_state):
|
||||
if not multiworld.has_beaten_game(reducing_state):
|
||||
if world.has_beaten_game(balancing_state):
|
||||
if not world.has_beaten_game(reducing_state):
|
||||
items_to_replace.append(testing)
|
||||
else:
|
||||
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
|
||||
@@ -712,32 +696,33 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
if p < threshold_percentages[player]:
|
||||
items_to_replace.append(testing)
|
||||
|
||||
old_moved_item_count = moved_item_count
|
||||
replaced_items = False
|
||||
|
||||
# sort then shuffle to maintain deterministic behaviour,
|
||||
# while allowing use of set for better algorithm growth behaviour elsewhere
|
||||
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
|
||||
multiworld.random.shuffle(replacement_locations)
|
||||
world.random.shuffle(replacement_locations)
|
||||
items_to_replace.sort()
|
||||
multiworld.random.shuffle(items_to_replace)
|
||||
world.random.shuffle(items_to_replace)
|
||||
|
||||
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
|
||||
while replacement_locations and items_to_replace:
|
||||
old_location = items_to_replace.pop()
|
||||
for i, new_location in enumerate(replacement_locations):
|
||||
for new_location in replacement_locations:
|
||||
if new_location.can_fill(state, old_location.item, False) and \
|
||||
old_location.can_fill(state, new_location.item, False):
|
||||
replacement_locations.pop(i)
|
||||
replacement_locations.remove(new_location)
|
||||
swap_location_item(old_location, new_location)
|
||||
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
||||
f"displacing {old_location.item} into {old_location}")
|
||||
moved_item_count += 1
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not Progression Balance {old_location.item}")
|
||||
|
||||
if old_moved_item_count < moved_item_count:
|
||||
if replaced_items:
|
||||
logging.debug(f"Moved {moved_item_count} items so far\n")
|
||||
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
|
||||
for location in get_sphere_locations(state, unlocked):
|
||||
@@ -751,7 +736,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations |= sphere_locations
|
||||
|
||||
if multiworld.has_beaten_game(state):
|
||||
if world.has_beaten_game(state):
|
||||
break
|
||||
elif not sphere_locations:
|
||||
logging.warning("Progression Balancing ran out of paths.")
|
||||
@@ -771,7 +756,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
|
||||
location_1.event, location_2.event = location_2.event, location_1.event
|
||||
|
||||
|
||||
def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
def distribute_planned(world: MultiWorld) -> None:
|
||||
def warn(warning: str, force: typing.Union[bool, str]) -> None:
|
||||
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
|
||||
logging.warning(f'{warning}')
|
||||
@@ -784,24 +769,24 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
else:
|
||||
warn(warning, force)
|
||||
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state = world.state.copy()
|
||||
swept_state.sweep_for_events()
|
||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||
reachable = frozenset(world.get_reachable_locations(swept_state))
|
||||
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
for loc in multiworld.get_unfilled_locations():
|
||||
for loc in world.get_unfilled_locations():
|
||||
if loc in reachable:
|
||||
early_locations[loc.player].append(loc.name)
|
||||
else: # not reachable with swept state
|
||||
non_early_locations[loc.player].append(loc.name)
|
||||
|
||||
world_name_lookup = multiworld.world_name_lookup
|
||||
world_name_lookup = world.world_name_lookup
|
||||
|
||||
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
||||
plando_blocks: typing.List[typing.Dict[str, typing.Any]] = []
|
||||
player_ids = set(multiworld.player_ids)
|
||||
player_ids = set(world.player_ids)
|
||||
for player in player_ids:
|
||||
for block in multiworld.plando_items[player]:
|
||||
for block in world.plando_items[player]:
|
||||
block['player'] = player
|
||||
if 'force' not in block:
|
||||
block['force'] = 'silent'
|
||||
@@ -815,12 +800,12 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
else:
|
||||
target_world = block['world']
|
||||
|
||||
if target_world is False or multiworld.players == 1: # target own world
|
||||
if target_world is False or world.players == 1: # target own world
|
||||
worlds: typing.Set[int] = {player}
|
||||
elif target_world is True: # target any worlds besides own
|
||||
worlds = set(multiworld.player_ids) - {player}
|
||||
worlds = set(world.player_ids) - {player}
|
||||
elif target_world is None: # target all worlds
|
||||
worlds = set(multiworld.player_ids)
|
||||
worlds = set(world.player_ids)
|
||||
elif type(target_world) == list: # list of target worlds
|
||||
worlds = set()
|
||||
for listed_world in target_world:
|
||||
@@ -830,9 +815,9 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
continue
|
||||
worlds.add(world_name_lookup[listed_world])
|
||||
elif type(target_world) == int: # target world by slot number
|
||||
if target_world not in range(1, multiworld.players + 1):
|
||||
if target_world not in range(1, world.players + 1):
|
||||
failed(
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
|
||||
block['force'])
|
||||
continue
|
||||
worlds = {target_world}
|
||||
@@ -860,7 +845,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
item_list: typing.List[str] = []
|
||||
for key, value in items.items():
|
||||
if value is True:
|
||||
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
|
||||
value = world.itempool.count(world.worlds[player].create_item(key))
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
if isinstance(items, str):
|
||||
@@ -910,17 +895,17 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
count = block['count']
|
||||
failed(f"Plando count {count} greater than locations specified", block['force'])
|
||||
block['count'] = len(block['locations'])
|
||||
block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max'])
|
||||
block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max'])
|
||||
|
||||
if block['count']['target'] > 0:
|
||||
plando_blocks.append(block)
|
||||
|
||||
# shuffle, but then sort blocks by number of locations minus number of items,
|
||||
# so less-flexible blocks get priority
|
||||
multiworld.random.shuffle(plando_blocks)
|
||||
world.random.shuffle(plando_blocks)
|
||||
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
|
||||
if len(block['locations']) > 0
|
||||
else len(multiworld.get_unfilled_locations(player)) - block['count']['target']))
|
||||
else len(world.get_unfilled_locations(player)) - block['count']['target']))
|
||||
|
||||
for placement in plando_blocks:
|
||||
player = placement['player']
|
||||
@@ -931,19 +916,19 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
maxcount = placement['count']['target']
|
||||
from_pool = placement['from_pool']
|
||||
|
||||
candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
multiworld.random.shuffle(candidates)
|
||||
multiworld.random.shuffle(items)
|
||||
candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
world.random.shuffle(candidates)
|
||||
world.random.shuffle(items)
|
||||
count = 0
|
||||
err: typing.List[str] = []
|
||||
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
|
||||
for item_name in items:
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
item = world.worlds[player].create_item(item_name)
|
||||
for location in reversed(candidates):
|
||||
if (location.address is None) == (item.code is None): # either both None or both not None
|
||||
if not location.item:
|
||||
if location.item_rule(item):
|
||||
if location.can_fill(multiworld.state, item, False):
|
||||
if location.can_fill(world.state, item, False):
|
||||
successful_pairs.append((item, location))
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
@@ -961,21 +946,21 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
if count < placement['count']['min']:
|
||||
m = placement['count']['min']
|
||||
failed(
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {' '.join(err)}",
|
||||
placement['force'])
|
||||
for (item, location) in successful_pairs:
|
||||
multiworld.push_item(location, item, collect=False)
|
||||
world.push_item(location, item, collect=False)
|
||||
location.event = True # flag location to be checked during fill
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
if from_pool:
|
||||
try:
|
||||
multiworld.itempool.remove(item)
|
||||
world.itempool.remove(item)
|
||||
except ValueError:
|
||||
warn(
|
||||
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
||||
f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
|
||||
placement['force'])
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e
|
||||
f"Error running plando for player {player} ({world.player_name[player]})") from e
|
||||
|
||||
192
Generate.py
@@ -26,7 +26,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.generic import PlandoConnection
|
||||
from worlds import failed_world_loads
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
@@ -303,9 +302,7 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
NUMBER=(number if number > 1 else ''),
|
||||
player=player,
|
||||
PLAYER=(player if player > 1 else '')))
|
||||
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
|
||||
# Could cause issues for some clients that cannot handle the additional whitespace.
|
||||
new_name = new_name.strip()[:16].strip()
|
||||
new_name = new_name.strip()[:16]
|
||||
if new_name == "Archipelago":
|
||||
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||
return new_name
|
||||
@@ -318,35 +315,33 @@ def prefer_int(input_data: str) -> Union[str, int]:
|
||||
return input_data
|
||||
|
||||
|
||||
goals = {
|
||||
'ganon': 'ganon',
|
||||
'crystals': 'crystals',
|
||||
'bosses': 'bosses',
|
||||
'pedestal': 'pedestal',
|
||||
'ganon_pedestal': 'ganonpedestal',
|
||||
'triforce_hunt': 'triforcehunt',
|
||||
'local_triforce_hunt': 'localtriforcehunt',
|
||||
'ganon_triforce_hunt': 'ganontriforcehunt',
|
||||
'local_ganon_triforce_hunt': 'localganontriforcehunt',
|
||||
'ice_rod_hunt': 'icerodhunt',
|
||||
}
|
||||
|
||||
|
||||
def roll_percentage(percentage: Union[int, float]) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
return random.random() < (float(percentage) / 100)
|
||||
|
||||
|
||||
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict:
|
||||
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
|
||||
logging.debug(f'Applying {new_weights}')
|
||||
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)
|
||||
new_options = set(new_weights) - set(weights)
|
||||
weights.update(new_weights)
|
||||
if new_options:
|
||||
for new_option in new_options:
|
||||
logging.warning(f'{update_type} Suboption "{new_option}" of "{name}" did not '
|
||||
logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
|
||||
f'overwrite a root option. '
|
||||
f'This is probably in error.')
|
||||
return weights
|
||||
@@ -362,6 +357,15 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
|
||||
if options[option_key].supports_weighting:
|
||||
return get_choice(option_key, category_dict)
|
||||
return category_dict[option_key]
|
||||
if game == "A Link to the Past": # TODO wow i hate this
|
||||
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
|
||||
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
|
||||
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
|
||||
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
|
||||
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
|
||||
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
|
||||
"random_sprite_on_event"}:
|
||||
return get_choice(option_key, category_dict)
|
||||
raise Exception(f"Error generating meta option {option_key} for {game}.")
|
||||
|
||||
|
||||
@@ -459,11 +463,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
if ret.game not in AutoWorldRegister.world_types:
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
||||
if picks[0] in failed_world_loads:
|
||||
raise Exception(f"No functional world found to handle game {ret.game}. "
|
||||
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||
f"If so, it appears the world failed to initialize correctly.")
|
||||
picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
|
||||
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||
f"Check your spelling or installation of that world.")
|
||||
|
||||
@@ -473,10 +473,6 @@ 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]
|
||||
@@ -489,23 +485,120 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
if PlandoOptions.connections in plando_options:
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement, "both")
|
||||
))
|
||||
if PlandoOptions.connections in plando_options:
|
||||
options = game_weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice("entrance", placement),
|
||||
get_choice("exit", placement),
|
||||
get_choice("direction", placement)
|
||||
))
|
||||
elif ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
|
||||
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
|
||||
glitches_required = get_choice_legacy('glitches_required', weights)
|
||||
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
|
||||
logging.warning("Only NMG, OWG, HMG and No Logic supported")
|
||||
glitches_required = 'none'
|
||||
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
|
||||
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
|
||||
glitches_required]
|
||||
|
||||
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
|
||||
if not ret.dark_room_logic: # None/False
|
||||
ret.dark_room_logic = "none"
|
||||
if ret.dark_room_logic == "sconces":
|
||||
ret.dark_room_logic = "torches"
|
||||
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
|
||||
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
|
||||
|
||||
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
|
||||
if entrance_shuffle.startswith('none-'):
|
||||
ret.shuffle = 'vanilla'
|
||||
else:
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goal = get_choice_legacy('goals', weights, 'ganon')
|
||||
|
||||
ret.goal = goals[goal]
|
||||
|
||||
|
||||
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
|
||||
|
||||
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
|
||||
|
||||
# sum a percentage to required
|
||||
if extra_pieces == 'percentage':
|
||||
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
|
||||
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
|
||||
# vanilla mode (specify how many pieces are)
|
||||
elif extra_pieces == 'available':
|
||||
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
|
||||
get_choice_legacy('triforce_pieces_available', weights, 30))
|
||||
# required pieces + fixed extra
|
||||
elif extra_pieces == 'extra':
|
||||
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
|
||||
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
|
||||
|
||||
# change minimum to required pieces to avoid problems
|
||||
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
|
||||
|
||||
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
|
||||
if not ret.shop_shuffle:
|
||||
ret.shop_shuffle = ''
|
||||
|
||||
ret.mode = get_choice_legacy("mode", weights)
|
||||
|
||||
ret.difficulty = get_choice_legacy('item_pool', weights)
|
||||
|
||||
ret.item_functionality = get_choice_legacy('item_functionality', weights)
|
||||
|
||||
|
||||
ret.enemy_damage = {None: 'default',
|
||||
'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos', # to be removed
|
||||
'chaos': 'chaos',
|
||||
}[get_choice_legacy('enemy_damage', weights)]
|
||||
|
||||
ret.enemy_health = get_choice_legacy('enemy_health', weights)
|
||||
|
||||
ret.timer = {'none': False,
|
||||
None: False,
|
||||
False: False,
|
||||
'timed': 'timed',
|
||||
'timed_ohko': 'timed-ohko',
|
||||
'ohko': 'ohko',
|
||||
'timed_countdown': 'timed-countdown',
|
||||
'display': 'display'}[get_choice_legacy('timer', weights, False)]
|
||||
|
||||
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
|
||||
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
|
||||
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
|
||||
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
|
||||
|
||||
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
|
||||
|
||||
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
|
||||
|
||||
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
|
||||
get_choice_legacy("turtle_rock_medallion", weights, "random")]
|
||||
|
||||
for index, medallion in enumerate(ret.required_medallions):
|
||||
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
|
||||
.get(medallion.lower(), None)
|
||||
if not ret.required_medallions[index]:
|
||||
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
|
||||
|
||||
ret.plando_texts = {}
|
||||
if PlandoOptions.texts in plando_options:
|
||||
@@ -519,6 +612,17 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
raise Exception(f"No text target \"{at}\" found.")
|
||||
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
|
||||
|
||||
ret.plando_connections = []
|
||||
if PlandoOptions.connections in plando_options:
|
||||
options = weights.get("plando_connections", [])
|
||||
for placement in options:
|
||||
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
|
||||
ret.plando_connections.append(PlandoConnection(
|
||||
get_choice_legacy("entrance", placement),
|
||||
get_choice_legacy("exit", placement),
|
||||
get_choice_legacy("direction", placement, "both")
|
||||
))
|
||||
|
||||
ret.sprite_pool = weights.get('sprite_pool', [])
|
||||
ret.sprite = get_choice_legacy('sprite', weights, "Link")
|
||||
if 'random_sprite_on_event' in weights:
|
||||
|
||||
34
Launcher.py
@@ -100,7 +100,7 @@ components.extend([
|
||||
# Functions
|
||||
Component("Open host.yaml", func=open_host_yaml),
|
||||
Component("Open Patch", func=open_patch),
|
||||
Component("Generate Template Options", func=generate_yamls),
|
||||
Component("Generate Template Settings", func=generate_yamls),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Browse Files", func=browse_files),
|
||||
@@ -161,7 +161,7 @@ def launch(exe, in_terminal=False):
|
||||
|
||||
|
||||
def run_gui():
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label
|
||||
from kivy.uix.image import AsyncImage
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
@@ -185,16 +185,11 @@ def run_gui():
|
||||
self.container = ContainerLayout()
|
||||
self.grid = GridLayout(cols=2)
|
||||
self.container.add_widget(self.grid)
|
||||
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
|
||||
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
|
||||
tool_layout = ScrollBox()
|
||||
tool_layout.layout.orientation = "vertical"
|
||||
self.grid.add_widget(tool_layout)
|
||||
client_layout = ScrollBox()
|
||||
client_layout.layout.orientation = "vertical"
|
||||
self.grid.add_widget(client_layout)
|
||||
self.grid.add_widget(Label(text="General"))
|
||||
self.grid.add_widget(Label(text="Clients"))
|
||||
button_layout = self.grid # make buttons fill the window
|
||||
|
||||
def build_button(component: Component) -> Widget:
|
||||
def build_button(component: Component):
|
||||
"""
|
||||
Builds a button widget for a given component.
|
||||
|
||||
@@ -205,26 +200,31 @@ def run_gui():
|
||||
None. The button is added to the parent grid layout.
|
||||
|
||||
"""
|
||||
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
||||
button = Button(text=component.display_name)
|
||||
button.component = component
|
||||
button.bind(on_release=self.component_action)
|
||||
if component.icon != "icon":
|
||||
image = AsyncImage(source=icon_paths[component.icon],
|
||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
||||
box_layout = RelativeLayout()
|
||||
box_layout.add_widget(button)
|
||||
box_layout.add_widget(image)
|
||||
return box_layout
|
||||
return button
|
||||
button_layout.add_widget(box_layout)
|
||||
else:
|
||||
button_layout.add_widget(button)
|
||||
|
||||
for (tool, client) in itertools.zip_longest(itertools.chain(
|
||||
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
|
||||
# column 1
|
||||
if tool:
|
||||
tool_layout.layout.add_widget(build_button(tool[1]))
|
||||
build_button(tool[1])
|
||||
else:
|
||||
button_layout.add_widget(Label())
|
||||
# column 2
|
||||
if client:
|
||||
client_layout.layout.add_widget(build_button(client[1]))
|
||||
build_button(client[1])
|
||||
else:
|
||||
button_layout.add_widget(Label())
|
||||
|
||||
return self.container
|
||||
|
||||
|
||||
@@ -348,8 +348,7 @@ class LinksAwakeningClient():
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
self.stop_bizhawk_spam = False
|
||||
logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} "
|
||||
f"running {rom_name.decode('ascii', errors='replace')}")
|
||||
logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
|
||||
return
|
||||
except (BlockingIOError, TimeoutError, ConnectionResetError):
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
289
Main.py
@@ -30,49 +30,49 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.perf_counter()
|
||||
# initialize the multiworld
|
||||
multiworld = MultiWorld(args.multi)
|
||||
# initialize the world
|
||||
world = MultiWorld(args.multi)
|
||||
|
||||
logger = logging.getLogger()
|
||||
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
multiworld.plando_options = args.plando_options
|
||||
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
world.plando_options = args.plando_options
|
||||
|
||||
multiworld.shuffle = args.shuffle.copy()
|
||||
multiworld.logic = args.logic.copy()
|
||||
multiworld.mode = args.mode.copy()
|
||||
multiworld.difficulty = args.difficulty.copy()
|
||||
multiworld.item_functionality = args.item_functionality.copy()
|
||||
multiworld.timer = args.timer.copy()
|
||||
multiworld.goal = args.goal.copy()
|
||||
multiworld.boss_shuffle = args.shufflebosses.copy()
|
||||
multiworld.enemy_health = args.enemy_health.copy()
|
||||
multiworld.enemy_damage = args.enemy_damage.copy()
|
||||
multiworld.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||
multiworld.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||
multiworld.countdown_start_time = args.countdown_start_time.copy()
|
||||
multiworld.red_clock_time = args.red_clock_time.copy()
|
||||
multiworld.blue_clock_time = args.blue_clock_time.copy()
|
||||
multiworld.green_clock_time = args.green_clock_time.copy()
|
||||
multiworld.dungeon_counters = args.dungeon_counters.copy()
|
||||
multiworld.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
multiworld.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
multiworld.shop_shuffle = args.shop_shuffle.copy()
|
||||
multiworld.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
multiworld.sprite_pool = args.sprite_pool.copy()
|
||||
multiworld.dark_room_logic = args.dark_room_logic.copy()
|
||||
multiworld.plando_items = args.plando_items.copy()
|
||||
multiworld.plando_texts = args.plando_texts.copy()
|
||||
multiworld.plando_connections = args.plando_connections.copy()
|
||||
multiworld.required_medallions = args.required_medallions.copy()
|
||||
multiworld.game = args.game.copy()
|
||||
multiworld.player_name = args.name.copy()
|
||||
multiworld.sprite = args.sprite.copy()
|
||||
multiworld.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
world.shuffle = args.shuffle.copy()
|
||||
world.logic = args.logic.copy()
|
||||
world.mode = args.mode.copy()
|
||||
world.difficulty = args.difficulty.copy()
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.goal = args.goal.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||
world.countdown_start_time = args.countdown_start_time.copy()
|
||||
world.red_clock_time = args.red_clock_time.copy()
|
||||
world.blue_clock_time = args.blue_clock_time.copy()
|
||||
world.green_clock_time = args.green_clock_time.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
world.shop_shuffle = args.shop_shuffle.copy()
|
||||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
world.sprite_pool = args.sprite_pool.copy()
|
||||
world.dark_room_logic = args.dark_room_logic.copy()
|
||||
world.plando_items = args.plando_items.copy()
|
||||
world.plando_texts = args.plando_texts.copy()
|
||||
world.plando_connections = args.plando_connections.copy()
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.player_name = args.name.copy()
|
||||
world.sprite = args.sprite.copy()
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
multiworld.set_options(args)
|
||||
multiworld.set_item_links()
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
|
||||
world.set_options(args)
|
||||
world.set_item_links()
|
||||
world.state = CollectionState(world)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
@@ -103,93 +103,76 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||
if not args.skip_output:
|
||||
AutoWorld.call_stage(multiworld, "assert_generate")
|
||||
AutoWorld.call_stage(world, "assert_generate")
|
||||
|
||||
AutoWorld.call_all(multiworld, "generate_early")
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
|
||||
logger.info('')
|
||||
|
||||
for player in multiworld.player_ids:
|
||||
for item_name, count in multiworld.worlds[player].options.start_inventory.value.items():
|
||||
for player in world.player_ids:
|
||||
for item_name, count in world.worlds[player].options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
multiworld.push_precollected(multiworld.create_item(item_name, player))
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
|
||||
for item_name, count in getattr(multiworld.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.items():
|
||||
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
|
||||
for _ in range(count):
|
||||
multiworld.push_precollected(multiworld.create_item(item_name, player))
|
||||
# remove from_pool items also from early items handling, as starting is plenty early.
|
||||
early = multiworld.early_items[player].get(item_name, 0)
|
||||
if early:
|
||||
multiworld.early_items[player][item_name] = max(0, early-count)
|
||||
remaining_count = count-early
|
||||
if remaining_count > 0:
|
||||
local_early = multiworld.early_local_items[player].get(item_name, 0)
|
||||
if local_early:
|
||||
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||
del local_early
|
||||
del early
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
|
||||
logger.info('Creating MultiWorld.')
|
||||
AutoWorld.call_all(multiworld, "create_regions")
|
||||
logger.info('Creating World.')
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
|
||||
logger.info('Creating Items.')
|
||||
AutoWorld.call_all(multiworld, "create_items")
|
||||
AutoWorld.call_all(world, "create_items")
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
for player in multiworld.player_ids:
|
||||
for player in world.player_ids:
|
||||
# items can't be both local and non-local, prefer local
|
||||
multiworld.worlds[player].options.non_local_items.value -= multiworld.worlds[player].options.local_items.value
|
||||
multiworld.worlds[player].options.non_local_items.value -= set(multiworld.local_early_items[player])
|
||||
world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value
|
||||
world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player])
|
||||
|
||||
AutoWorld.call_all(multiworld, "set_rules")
|
||||
AutoWorld.call_all(world, "set_rules")
|
||||
|
||||
for player in multiworld.player_ids:
|
||||
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
|
||||
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
|
||||
for location_name in multiworld.worlds[player].options.priority_locations.value:
|
||||
for player in world.player_ids:
|
||||
exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value)
|
||||
world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value
|
||||
for location_name in world.worlds[player].options.priority_locations.value:
|
||||
try:
|
||||
location = multiworld.get_location(location_name, player)
|
||||
location = world.get_location(location_name, player)
|
||||
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||
if location_name not in multiworld.worlds[player].location_name_to_id:
|
||||
if location_name not in world.worlds[player].location_name_to_id:
|
||||
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
|
||||
else:
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
# Set local and non-local item rules.
|
||||
if multiworld.players > 1:
|
||||
locality_rules(multiworld)
|
||||
if world.players > 1:
|
||||
locality_rules(world)
|
||||
else:
|
||||
multiworld.worlds[1].options.non_local_items.value = set()
|
||||
multiworld.worlds[1].options.local_items.value = set()
|
||||
world.worlds[1].options.non_local_items.value = set()
|
||||
world.worlds[1].options.local_items.value = set()
|
||||
|
||||
AutoWorld.call_all(multiworld, "generate_basic")
|
||||
AutoWorld.call_all(world, "generate_basic")
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
|
||||
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
|
||||
new_items: List[Item] = []
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: getattr(multiworld.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.copy()
|
||||
for player in multiworld.player_ids
|
||||
}
|
||||
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = multiworld.worlds[player]
|
||||
player_world: AutoWorld.World = world.worlds[player]
|
||||
for count in items.values():
|
||||
for _ in range(count):
|
||||
new_items.append(player_world.create_filler())
|
||||
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
||||
for i, item in enumerate(multiworld.itempool):
|
||||
for i, item in enumerate(world.itempool):
|
||||
if depletion_pool[item.player].get(item.name, 0):
|
||||
target -= 1
|
||||
depletion_pool[item.player][item.name] -= 1
|
||||
# quick abort if we have found all items
|
||||
if not target:
|
||||
new_items.extend(multiworld.itempool[i+1:])
|
||||
new_items.extend(world.itempool[i+1:])
|
||||
break
|
||||
else:
|
||||
new_items.append(item)
|
||||
@@ -199,19 +182,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for player, remaining_items in depletion_pool.items():
|
||||
remaining_items = {name: count for name, count in remaining_items.items() if count}
|
||||
if remaining_items:
|
||||
raise Exception(f"{multiworld.get_player_name(player)}"
|
||||
raise Exception(f"{world.get_player_name(player)}"
|
||||
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||
multiworld.itempool[:] = new_items
|
||||
assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||
world.itempool[:] = new_items
|
||||
|
||||
# temporary home for item links, should be moved out of Main
|
||||
for group_id, group in multiworld.groups.items():
|
||||
for group_id, group in world.groups.items():
|
||||
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
||||
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
|
||||
]:
|
||||
classifications: Dict[str, int] = collections.defaultdict(int)
|
||||
counters = {player: {name: 0 for name in shared_pool} for player in players}
|
||||
for item in multiworld.itempool:
|
||||
for item in world.itempool:
|
||||
if item.player in counters and item.name in shared_pool:
|
||||
counters[item.player][item.name] += 1
|
||||
classifications[item.name] |= item.classification
|
||||
@@ -246,13 +229,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
new_item.classification |= classifications[item_name]
|
||||
new_itempool.append(new_item)
|
||||
|
||||
region = Region("Menu", group_id, multiworld, "ItemLink")
|
||||
multiworld.regions.append(region)
|
||||
region = Region("Menu", group_id, world, "ItemLink")
|
||||
world.regions.append(region)
|
||||
locations = region.locations
|
||||
for item in multiworld.itempool:
|
||||
for item in world.itempool:
|
||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||
if count:
|
||||
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}",
|
||||
loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
|
||||
None, region)
|
||||
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
|
||||
state.has(item_name, group_id_, count_)
|
||||
@@ -263,10 +246,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
new_itempool.append(item)
|
||||
|
||||
itemcount = len(multiworld.itempool)
|
||||
multiworld.itempool = new_itempool
|
||||
itemcount = len(world.itempool)
|
||||
world.itempool = new_itempool
|
||||
|
||||
while itemcount > len(multiworld.itempool):
|
||||
while itemcount > len(world.itempool):
|
||||
items_to_add = []
|
||||
for player in group["players"]:
|
||||
if group["link_replacement"]:
|
||||
@@ -274,64 +257,64 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
item_player = player
|
||||
if group["replacement_items"][player]:
|
||||
items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player,
|
||||
items_to_add.append(AutoWorld.call_single(world, "create_item", item_player,
|
||||
group["replacement_items"][player]))
|
||||
else:
|
||||
items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player))
|
||||
multiworld.random.shuffle(items_to_add)
|
||||
multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)])
|
||||
items_to_add.append(AutoWorld.call_single(world, "create_filler", item_player))
|
||||
world.random.shuffle(items_to_add)
|
||||
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
|
||||
|
||||
if any(multiworld.item_links.values()):
|
||||
multiworld._all_state = None
|
||||
if any(world.item_links.values()):
|
||||
world._all_state = None
|
||||
|
||||
logger.info("Running Item Plando.")
|
||||
|
||||
distribute_planned(multiworld)
|
||||
distribute_planned(world)
|
||||
|
||||
logger.info('Running Pre Main Fill.')
|
||||
|
||||
AutoWorld.call_all(multiworld, "pre_fill")
|
||||
AutoWorld.call_all(world, "pre_fill")
|
||||
|
||||
logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.')
|
||||
logger.info(f'Filling the world with {len(world.itempool)} items.')
|
||||
|
||||
if multiworld.algorithm == 'flood':
|
||||
flood_items(multiworld) # different algo, biased towards early game progress items
|
||||
elif multiworld.algorithm == 'balanced':
|
||||
distribute_items_restrictive(multiworld)
|
||||
if world.algorithm == 'flood':
|
||||
flood_items(world) # different algo, biased towards early game progress items
|
||||
elif world.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world)
|
||||
|
||||
AutoWorld.call_all(multiworld, 'post_fill')
|
||||
AutoWorld.call_all(world, 'post_fill')
|
||||
|
||||
if multiworld.players > 1 and not args.skip_prog_balancing:
|
||||
balance_multiworld_progression(multiworld)
|
||||
if world.players > 1 and not args.skip_prog_balancing:
|
||||
balance_multiworld_progression(world)
|
||||
else:
|
||||
logger.info("Progression balancing skipped.")
|
||||
|
||||
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
||||
multiworld.random.passthrough = False
|
||||
world.random.passthrough = False
|
||||
|
||||
if args.skip_output:
|
||||
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
|
||||
return multiworld
|
||||
return world
|
||||
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + multiworld.seed_name
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
||||
is not multiworld.worlds[player].generate_output.__code__]
|
||||
output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
|
||||
is not world.worlds[player].generate_output.__code__]
|
||||
with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
|
||||
check_accessibility_task = pool.submit(multiworld.fulfills_accessibility)
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, multiworld, "generate_output", temp_dir)]
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||
for player in output_players:
|
||||
# skip starting a thread for methods that say "pass".
|
||||
output_file_futures.append(
|
||||
pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir))
|
||||
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
|
||||
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
@@ -340,38 +323,38 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
games = {}
|
||||
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
|
||||
slot_info = {}
|
||||
names = [[name for player, name in sorted(multiworld.player_name.items())]]
|
||||
for slot in multiworld.player_ids:
|
||||
player_world: AutoWorld.World = multiworld.worlds[slot]
|
||||
names = [[name for player, name in sorted(world.player_name.items())]]
|
||||
for slot in world.player_ids:
|
||||
player_world: AutoWorld.World = world.worlds[slot]
|
||||
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
|
||||
client_versions[slot] = player_world.required_client_version
|
||||
games[slot] = multiworld.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], multiworld.game[slot],
|
||||
multiworld.player_types[slot])
|
||||
for slot, group in multiworld.groups.items():
|
||||
games[slot] = multiworld.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(group["name"], multiworld.game[slot], multiworld.player_types[slot],
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
|
||||
world.player_types[slot])
|
||||
for slot, group in world.groups.items():
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
|
||||
group_members=sorted(group["players"]))
|
||||
precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
|
||||
for player, world_precollected in multiworld.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups))}
|
||||
for player, world_precollected in world.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
|
||||
|
||||
for slot in multiworld.player_ids:
|
||||
slot_data[slot] = multiworld.worlds[slot].fill_slot_data()
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
|
||||
def precollect_hint(location):
|
||||
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False, entrance, location.item.flags)
|
||||
precollected_hints[location.player].add(hint)
|
||||
if location.item.player not in multiworld.groups:
|
||||
if location.item.player not in world.groups:
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
else:
|
||||
for player in multiworld.groups[location.item.player]["players"]:
|
||||
for player in world.groups[location.item.player]["players"]:
|
||||
precollected_hints[player].add(hint)
|
||||
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids}
|
||||
for location in multiworld.get_filled_locations():
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
assert location.item.code is not None, "item code None should be event, " \
|
||||
"location.address should then also be None. Location: " \
|
||||
@@ -381,18 +364,18 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
f"{locations_data[location.player][location.address]}")
|
||||
locations_data[location.player][location.address] = \
|
||||
location.item.code, location.item.player, location.item.flags
|
||||
if location.name in multiworld.worlds[location.player].options.start_location_hints:
|
||||
if location.name in world.worlds[location.player].options.start_location_hints:
|
||||
precollect_hint(location)
|
||||
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
|
||||
elif location.item.name in world.worlds[location.item.player].options.start_hints:
|
||||
precollect_hint(location)
|
||||
elif any([location.item.name in multiworld.worlds[player].options.start_hints
|
||||
for player in multiworld.groups.get(location.item.player, {}).get("players", [])]):
|
||||
elif any([location.item.name in world.worlds[player].options.start_hints
|
||||
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
||||
precollect_hint(location)
|
||||
|
||||
# embedded data package
|
||||
data_package = {
|
||||
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||
for game_world in multiworld.worlds.values()
|
||||
for game_world in world.worlds.values()
|
||||
}
|
||||
|
||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||
@@ -400,7 +383,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"slot_info": slot_info,
|
||||
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()},
|
||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": baked_server_options,
|
||||
@@ -410,10 +393,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
"version": tuple(version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": multiworld.seed_name,
|
||||
"seed_name": world.seed_name,
|
||||
"datapackage": data_package,
|
||||
}
|
||||
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
@@ -423,7 +406,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
output_file_futures.append(pool.submit(write_multidata))
|
||||
if not check_accessibility_task.result():
|
||||
if not multiworld.can_beat_game():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
@@ -436,12 +419,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
if args.spoiler > 1:
|
||||
logger.info('Calculating playthrough.')
|
||||
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
|
||||
if args.spoiler:
|
||||
multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
zipfilename = output_path(f"AP_{multiworld.seed_name}.zip")
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
logger.info(f"Creating final archive at {zipfilename}")
|
||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
@@ -449,4 +432,4 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
zf.write(file.path, arcname=file.name)
|
||||
|
||||
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
||||
return multiworld
|
||||
return world
|
||||
|
||||
@@ -4,29 +4,14 @@ import subprocess
|
||||
import multiprocessing
|
||||
import warnings
|
||||
|
||||
local_dir = os.path.dirname(__file__)
|
||||
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
|
||||
|
||||
if sys.version_info < (3, 8, 6):
|
||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
||||
|
||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
|
||||
update_ran = _skip_update
|
||||
|
||||
|
||||
class RequirementsSet(set):
|
||||
def add(self, e):
|
||||
global update_ran
|
||||
update_ran &= _skip_update
|
||||
super().add(e)
|
||||
|
||||
def update(self, *s):
|
||||
global update_ran
|
||||
update_ran &= _skip_update
|
||||
super().update(*s)
|
||||
|
||||
|
||||
local_dir = os.path.dirname(__file__)
|
||||
requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
|
||||
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
|
||||
|
||||
if not update_ran:
|
||||
for entry in os.scandir(os.path.join(local_dir, "worlds")):
|
||||
|
||||
@@ -656,8 +656,7 @@ class Context:
|
||||
else:
|
||||
return self.player_names[team, slot]
|
||||
|
||||
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
|
||||
recipients: typing.Sequence[int] = None):
|
||||
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
||||
"""Send and remember hints."""
|
||||
if only_new:
|
||||
hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]]
|
||||
@@ -686,13 +685,12 @@ class Context:
|
||||
for slot in new_hint_events:
|
||||
self.on_new_hint(team, slot)
|
||||
for slot, hint_data in concerns.items():
|
||||
if recipients is None or slot in recipients:
|
||||
clients = self.clients[team].get(slot)
|
||||
if not clients:
|
||||
continue
|
||||
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
|
||||
for client in clients:
|
||||
async_start(self.send_msgs(client, client_hints))
|
||||
clients = self.clients[team].get(slot)
|
||||
if not clients:
|
||||
continue
|
||||
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
|
||||
for client in clients:
|
||||
async_start(self.send_msgs(client, client_hints))
|
||||
|
||||
# "events"
|
||||
|
||||
@@ -707,17 +705,14 @@ class Context:
|
||||
self.save() # save goal completion flag
|
||||
|
||||
def on_new_hint(self, team: int, slot: int):
|
||||
self.on_changed_hints(team, slot)
|
||||
self.broadcast(self.clients[team][slot], [{
|
||||
"cmd": "RoomUpdate",
|
||||
"hint_points": get_slot_points(self, team, slot)
|
||||
}])
|
||||
|
||||
def on_changed_hints(self, team: int, slot: int):
|
||||
key: str = f"_read_hints_{team}_{slot}"
|
||||
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.broadcast(self.clients[team][slot], [{
|
||||
"cmd": "RoomUpdate",
|
||||
"hint_points": get_slot_points(self, team, slot)
|
||||
}])
|
||||
|
||||
def on_client_status_change(self, team: int, slot: int):
|
||||
key: str = f"_read_client_status_{team}_{slot}"
|
||||
@@ -978,10 +973,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
"hint_points": get_slot_points(ctx, team, slot),
|
||||
"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()
|
||||
|
||||
|
||||
@@ -1058,19 +1050,17 @@ 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 '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Didn't find something that closely matches, " \
|
||||
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 for '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Too many close matches, 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"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
|
||||
|
||||
|
||||
class CommandMeta(type):
|
||||
@@ -1439,13 +1429,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||
self.ctx.hints[self.client.team, self.client.slot]}
|
||||
self.ctx.hints[self.client.team, self.client.slot] = hints
|
||||
self.ctx.notify_hints(self.client.team, list(hints), recipients=(self.client.slot,))
|
||||
self.ctx.notify_hints(self.client.team, list(hints))
|
||||
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
||||
f"You have {points_available} points.")
|
||||
if hints and Utils.version_tuple < (0, 5, 0):
|
||||
self.output("It was recently changed, so that the above hints are only shown to you. "
|
||||
"If you meant to alert another player of an above hint, "
|
||||
"please let them know of the content or to run !hint themselves.")
|
||||
return True
|
||||
|
||||
elif input_text.isnumeric():
|
||||
@@ -1972,7 +1958,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
|
||||
@@ -2224,24 +2210,25 @@ def parse_args() -> argparse.Namespace:
|
||||
|
||||
async def auto_shutdown(ctx, to_cancel=None):
|
||||
await asyncio.sleep(ctx.auto_shutdown)
|
||||
|
||||
def inactivity_shutdown():
|
||||
ctx.server.ws_server.close()
|
||||
ctx.exit_event.set()
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
|
||||
while not ctx.exit_event.is_set():
|
||||
if not ctx.client_activity_timers.values():
|
||||
inactivity_shutdown()
|
||||
ctx.server.ws_server.close()
|
||||
ctx.exit_event.set()
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
else:
|
||||
newest_activity = max(ctx.client_activity_timers.values())
|
||||
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
|
||||
seconds = ctx.auto_shutdown - delta.total_seconds()
|
||||
if seconds < 0:
|
||||
inactivity_shutdown()
|
||||
ctx.server.ws_server.close()
|
||||
ctx.exit_event.set()
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
else:
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
|
||||
@@ -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, location_id: int, player: int = 0, **kwargs) -> None:
|
||||
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_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})
|
||||
|
||||
|
||||
class Hint(typing.NamedTuple):
|
||||
|
||||
@@ -195,10 +195,10 @@ def set_icon(window):
|
||||
window.tk.call('wm', 'iconphoto', window._w, logo)
|
||||
|
||||
def adjust(args):
|
||||
# Create a fake multiworld and OOTWorld to use as a base
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(multiworld, 1)
|
||||
# Create a fake world and OOTWorld to use as a base
|
||||
world = MultiWorld(1)
|
||||
world.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(world, 1)
|
||||
# Set options in the fake OOTWorld
|
||||
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
||||
result = getattr(args, name, None)
|
||||
|
||||
63
Options.py
@@ -1,18 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import functools
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
import functools
|
||||
import math
|
||||
import numbers
|
||||
import random
|
||||
import typing
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
from Utils import get_fuzzy_results, is_iterable_except_str
|
||||
from Utils import get_fuzzy_results
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from BaseClasses import PlandoOptions
|
||||
@@ -41,11 +42,6 @@ class AssembleOptions(abc.ABCMeta):
|
||||
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||
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
|
||||
@@ -63,7 +59,6 @@ class AssembleOptions(abc.ABCMeta):
|
||||
def verify(self, *args, **kwargs) -> None:
|
||||
for f in verifiers:
|
||||
f(self, *args, **kwargs)
|
||||
|
||||
attrs["verify"] = verify
|
||||
else:
|
||||
assert verifiers, "class Option is supposed to implement def verify"
|
||||
@@ -101,7 +96,7 @@ T = typing.TypeVar('T')
|
||||
|
||||
class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
value: T
|
||||
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
|
||||
default = 0
|
||||
|
||||
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
|
||||
# Handled in get_option_name()
|
||||
@@ -111,9 +106,8 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
supports_weighting = True
|
||||
|
||||
# filled by AssembleOptions:
|
||||
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]]
|
||||
name_lookup: typing.Dict[T, str]
|
||||
options: typing.Dict[str, int]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.current_option_name})"
|
||||
@@ -166,8 +160,6 @@ class FreeText(Option[str]):
|
||||
"""Text option that allows users to enter strings.
|
||||
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
|
||||
@@ -188,18 +180,9 @@ 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
|
||||
|
||||
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
|
||||
# `int` is not a `numbers.Integral` according to the official typestubs
|
||||
# (even though isinstance(5, numbers.Integral) == True)
|
||||
@@ -615,7 +598,7 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
|
||||
if isinstance(self.value, int):
|
||||
return
|
||||
from BaseClasses import PlandoOptions
|
||||
if not (PlandoOptions.bosses & plando_options):
|
||||
if not(PlandoOptions.bosses & plando_options):
|
||||
# plando is disabled but plando options were given so pull the option and change it to an int
|
||||
option = self.value.split(";")[-1]
|
||||
self.value = self.options[option]
|
||||
@@ -744,7 +727,7 @@ class SpecialRange(NamedRange):
|
||||
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
|
||||
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
|
||||
"placed anywhere (below, inside, or above the regular range).")
|
||||
return super().__new__(cls)
|
||||
return super().__new__(cls, value)
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
@@ -782,7 +765,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
value: typing.Any
|
||||
|
||||
@classmethod
|
||||
def verify_keys(cls, data: typing.Iterable[str]) -> None:
|
||||
def verify_keys(cls, data: typing.List[str]):
|
||||
if cls.valid_keys:
|
||||
data = set(data)
|
||||
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
||||
@@ -819,7 +802,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
|
||||
|
||||
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
||||
default = {}
|
||||
default: typing.Dict[str, typing.Any] = {}
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
@@ -860,11 +843,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
|
||||
# Not a docstring so it doesn't get grabbed by the options system.
|
||||
|
||||
default = ()
|
||||
default: typing.List[typing.Any] = []
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Iterable[typing.Any]):
|
||||
self.value = list(deepcopy(value))
|
||||
def __init__(self, value: typing.List[typing.Any]):
|
||||
self.value = deepcopy(value)
|
||||
super(OptionList, self).__init__()
|
||||
|
||||
@classmethod
|
||||
@@ -873,7 +856,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if is_iterable_except_str(data):
|
||||
if type(data) == list:
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
@@ -886,7 +869,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
|
||||
|
||||
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
default = frozenset()
|
||||
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
@@ -899,7 +882,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if is_iterable_except_str(data):
|
||||
if isinstance(data, (list, set, frozenset)):
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
@@ -949,7 +932,7 @@ class OptionsMetaProperty(type):
|
||||
bases: typing.Tuple[type, ...],
|
||||
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
|
||||
for attr_type in attrs.values():
|
||||
assert not isinstance(attr_type, AssembleOptions), \
|
||||
assert not isinstance(attr_type, AssembleOptions),\
|
||||
f"Options for {name} should be type hinted on the class, not assigned"
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
@@ -1050,6 +1033,11 @@ class DeathLink(Toggle):
|
||||
display_name = "Death Link"
|
||||
|
||||
|
||||
class AllowCollect(DefaultOnToggle):
|
||||
"""Allows checks in your world to be automatically marked as collected when !collect is run."""
|
||||
display_name = "Allow Collect"
|
||||
|
||||
|
||||
class ItemLinks(OptionList):
|
||||
"""Share part of your item pool with other players."""
|
||||
display_name = "Item Links"
|
||||
@@ -1127,11 +1115,6 @@ class PerGameCommonOptions(CommonOptions):
|
||||
item_links: ItemLinks
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeathLinkMixin:
|
||||
death_link: DeathLink
|
||||
|
||||
|
||||
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
|
||||
import os
|
||||
|
||||
|
||||
4
Patch.py
@@ -8,7 +8,7 @@ if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from worlds.Files import AutoPatchRegister, APAutoPatchInterface
|
||||
from worlds.Files import AutoPatchRegister, APDeltaPatch
|
||||
|
||||
|
||||
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: APAutoPatchInterface = auto_handler(patch_file)
|
||||
handler: APDeltaPatch = auto_handler(patch_file)
|
||||
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
|
||||
handler.patch(target)
|
||||
return {"server": handler.server,
|
||||
|
||||
13
README.md
@@ -25,7 +25,7 @@ Currently, the following games are supported:
|
||||
* Hollow Knight
|
||||
* The Witness
|
||||
* Sonic Adventure 2: Battle
|
||||
* Starcraft 2
|
||||
* Starcraft 2: Wings of Liberty
|
||||
* Donkey Kong Country 3
|
||||
* Dark Souls 3
|
||||
* Super Mario World
|
||||
@@ -58,13 +58,6 @@ Currently, the following games are supported:
|
||||
* Heretic
|
||||
* Landstalker: The Treasures of King Nole
|
||||
* Final Fantasy Mystic Quest
|
||||
* 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
|
||||
@@ -86,9 +79,9 @@ We recognize that there is a strong community of incredibly smart people that ha
|
||||
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
|
||||
|
||||
## Running Archipelago
|
||||
For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems.
|
||||
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
|
||||
|
||||
If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
|
||||
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
|
||||
|
||||
## Related Repositories
|
||||
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from worlds.sc2.Client import launch
|
||||
from worlds.sc2wol.Client import launch
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
46
Utils.py
@@ -19,13 +19,14 @@ import warnings
|
||||
from argparse import Namespace
|
||||
from settings import Settings, get_settings
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||
from typing_extensions import TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
from yaml import load, load_all, dump, SafeLoader
|
||||
|
||||
try:
|
||||
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
||||
from yaml import CLoader as UnsafeLoader
|
||||
from yaml import CDumper as Dumper
|
||||
except ImportError:
|
||||
from yaml import Loader as UnsafeLoader, SafeLoader, Dumper
|
||||
from yaml import Loader as UnsafeLoader
|
||||
from yaml import Dumper
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import tkinter
|
||||
@@ -46,7 +47,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.5"
|
||||
__version__ = "0.4.4"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -225,9 +226,6 @@ 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)
|
||||
|
||||
@@ -716,7 +714,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
|
||||
@@ -781,25 +779,6 @@ def deprecate(message: str):
|
||||
import warnings
|
||||
warnings.warn(message)
|
||||
|
||||
|
||||
class DeprecateDict(dict):
|
||||
log_message: str
|
||||
should_error: bool
|
||||
|
||||
def __init__(self, message, error: bool = False) -> None:
|
||||
self.log_message = message
|
||||
self.should_error = error
|
||||
super().__init__()
|
||||
|
||||
def __getitem__(self, item: Any) -> Any:
|
||||
if self.should_error:
|
||||
deprecate(self.log_message)
|
||||
elif __debug__:
|
||||
import warnings
|
||||
warnings.warn(self.log_message)
|
||||
return super().__getitem__(item)
|
||||
|
||||
|
||||
def _extend_freeze_support() -> None:
|
||||
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
|
||||
# upstream issue: https://github.com/python/cpython/issues/76327
|
||||
@@ -873,8 +852,8 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
|
||||
Example usage in Main code:
|
||||
from Utils import visualize_regions
|
||||
for player in multiworld.player_ids:
|
||||
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
|
||||
for player in world.player_ids:
|
||||
visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
|
||||
"""
|
||||
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
|
||||
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
@@ -970,10 +949,3 @@ class RepeatableChain:
|
||||
|
||||
def __len__(self):
|
||||
return sum(len(iterable) for iterable in self.iterable)
|
||||
|
||||
|
||||
def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]]:
|
||||
""" `str` is `Iterable`, but that's not what we want """
|
||||
if isinstance(obj, str):
|
||||
return False
|
||||
return isinstance(obj, typing.Iterable)
|
||||
|
||||
@@ -20,8 +20,8 @@ def generate_api():
|
||||
race = False
|
||||
meta_options_source = {}
|
||||
if 'file' in request.files:
|
||||
files = request.files.getlist('file')
|
||||
options = get_yaml_data(files)
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
if isinstance(options, Markup):
|
||||
return {"text": options.striptags()}, 400
|
||||
if isinstance(options, str):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import zipfile
|
||||
import base64
|
||||
from typing import Union, Dict, Set, Tuple
|
||||
@@ -7,7 +6,13 @@ from flask import request, flash, redirect, url_for, render_template
|
||||
from markupsafe import Markup
|
||||
|
||||
from WebHostLib import app
|
||||
from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file
|
||||
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return filename.endswith(('.txt', ".yaml", ".zip"))
|
||||
|
||||
|
||||
from Generate import roll_settings, PlandoOptions
|
||||
from Utils import parse_yamls
|
||||
@@ -28,7 +33,7 @@ def check():
|
||||
results, _ = roll_options(options)
|
||||
if len(options) > 1:
|
||||
# offer combined file back
|
||||
combined_yaml = "\n---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
|
||||
combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
|
||||
for file_name, file_content in options.items())
|
||||
combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
|
||||
else:
|
||||
@@ -46,41 +51,33 @@ def mysterycheck():
|
||||
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
||||
options = {}
|
||||
for uploaded_file in files:
|
||||
if banned_file(uploaded_file.filename):
|
||||
return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. "
|
||||
"Your file was deleted.")
|
||||
# If the user does not select file, the browser will still submit an empty string without a file name.
|
||||
elif uploaded_file.filename == "":
|
||||
return "No selected file."
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if uploaded_file.filename == '':
|
||||
return 'No selected file'
|
||||
elif uploaded_file.filename in options:
|
||||
return f"Conflicting files named {uploaded_file.filename} submitted."
|
||||
elif uploaded_file and allowed_options(uploaded_file.filename):
|
||||
return f'Conflicting files named {uploaded_file.filename} submitted'
|
||||
elif uploaded_file and allowed_file(uploaded_file.filename):
|
||||
if uploaded_file.filename.endswith(".zip"):
|
||||
if not zipfile.is_zipfile(uploaded_file):
|
||||
return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened."
|
||||
|
||||
uploaded_file.seek(0) # offset from is_zipfile check
|
||||
with zipfile.ZipFile(uploaded_file, "r") as zfile:
|
||||
for file in zfile.infolist():
|
||||
# Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS).
|
||||
base_filename = os.path.basename(file.filename)
|
||||
with zipfile.ZipFile(uploaded_file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
|
||||
if base_filename.endswith(".archipelago"):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
elif base_filename.endswith(".zip"):
|
||||
return "Nested .zip files inside a .zip are not supported."
|
||||
elif banned_file(base_filename):
|
||||
return ("Uploaded data contained a rom file, which is likely to contain copyrighted "
|
||||
"material. Your file was deleted.")
|
||||
# Ignore dot-files.
|
||||
elif not base_filename.startswith(".") and allowed_options(base_filename):
|
||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return ("Uploaded data contained a rom file, "
|
||||
"which is likely to contain copyrighted material. "
|
||||
"Your file was deleted.")
|
||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options[uploaded_file.filename] = uploaded_file.read()
|
||||
|
||||
if not options:
|
||||
return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}"
|
||||
return "Did not find a .yaml file to process."
|
||||
return options
|
||||
|
||||
|
||||
|
||||
@@ -205,12 +205,6 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
await ctx.shutdown_task
|
||||
|
||||
# ensure auto launch is on the same page in regard to room activity.
|
||||
with db_session:
|
||||
room: Room = Room.get(id=ctx.room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
|
||||
|
||||
logging.info("Shutting down")
|
||||
|
||||
with Locker(room_id):
|
||||
|
||||
@@ -5,5 +5,5 @@ Flask-Caching>=2.1.0
|
||||
Flask-Compress>=1.14
|
||||
Flask-Limiter>=3.5.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.3.2; python_version >= '3.9'
|
||||
bokeh>=3.2.2; python_version >= '3.9'
|
||||
markupsafe>=2.1.3
|
||||
|
||||
@@ -369,7 +369,7 @@ const setPresets = (optionsData, presetName) => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'named_range': {
|
||||
case 'special_range': {
|
||||
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
@@ -25,16 +25,16 @@ window.addEventListener('load', () => {
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let category of categories) {
|
||||
let hide_id = category.id.split('_')[0];
|
||||
if (hide_id === 'Total') {
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
category.addEventListener('click', function() {
|
||||
categories[i].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("▼")) {
|
||||
@@ -576,7 +576,7 @@ class GameSettings {
|
||||
option = parseInt(option, 10);
|
||||
|
||||
let optionAcceptable = false;
|
||||
if ((option >= setting.min) && (option <= setting.max)) {
|
||||
if ((option > setting.min) && (option < setting.max)) {
|
||||
optionAcceptable = true;
|
||||
}
|
||||
if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){
|
||||
|
||||
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/advanceballistics.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/autoturretblackops.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/biomechanicaldrone.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/burstcapacitors.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclone.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/drillingclaws.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/emergencythrusters.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/hellionbattlemode.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperflightrotors.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperfluxor.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/impalerrounds.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedburstlaser.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedsiegemode.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/interferencematrix.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
WebHostLib/static/static/icons/sc2/internalizedtechmodule.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
WebHostLib/static/static/icons/sc2/jotunboosters.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/jumpjets.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
WebHostLib/static/static/icons/sc2/lasertargetingsystem.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/liberator.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/lockdown.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
WebHostLib/static/static/icons/sc2/magfieldaccelerator.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/magrailmunitions.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/opticalflare.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/optimizedlogistics.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/reapercombatdrugs.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/restoration.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/ripwavemissiles.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/shreddermissile.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetank-spidermines.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetankrange.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/specialordance.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/spidermine.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/staticempblast.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/superstimpack.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
WebHostLib/static/static/icons/sc2/targetingoptics.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-cloak-color.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-emp-color.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/thorsiegemode.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/transformationservos.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/valkyrie.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/warpjump.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-attackrange.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowminehidden.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,160 +0,0 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tracker-table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.inventory-table-area{
|
||||
border: 2px solid #000000;
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px 3px 10px;
|
||||
}
|
||||
|
||||
.inventory-table-area:has(.inventory-table-terran) {
|
||||
width: 690px;
|
||||
background-color: #525494;
|
||||
}
|
||||
|
||||
.inventory-table-area:has(.inventory-table-zerg) {
|
||||
width: 360px;
|
||||
background-color: #9d60d2;
|
||||
}
|
||||
|
||||
.inventory-table-area:has(.inventory-table-protoss) {
|
||||
width: 400px;
|
||||
background-color: #d2b260;
|
||||
}
|
||||
|
||||
#tracker-table .inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.inventory-table td.title{
|
||||
padding-top: 10px;
|
||||
height: 20px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
border: 1px solid #000000;
|
||||
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.inventory-table img.acquired{
|
||||
filter: none;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.inventory-table .tint-terran img.acquired {
|
||||
filter: sepia(100%) saturate(300%) brightness(130%) hue-rotate(120deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-protoss img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(180deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-level-1 img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-level-2 img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(120deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-level-3 img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(240deg)
|
||||
}
|
||||
|
||||
.inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inventory-table div.item-count {
|
||||
width: 160px;
|
||||
text-align: left;
|
||||
color: black;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
border: 2px solid #000000;
|
||||
border-radius: 4px;
|
||||
background-color: #87b678;
|
||||
padding: 10px 3px 3px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
#location-table td:has(.location-column) {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#location-table .location-column {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#location-table .location-column .spacer {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
112
WebHostLib/static/styles/sc2wolTracker.css
Normal file
@@ -0,0 +1,112 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 710px;
|
||||
background-color: #525494;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table td.title{
|
||||
padding-top: 10px;
|
||||
height: 20px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
border: 1px solid #000000;
|
||||
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
text-align: left;
|
||||
color: black;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 710px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #525494;
|
||||
padding: 10px 3px 3px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -69,8 +69,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<select name="collect_mode" id="collect_mode">
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !collect after goal completion</option>
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="auto-enabled">
|
||||
Automatic on goal completion and manual !collect
|
||||
</option>
|
||||
@@ -93,9 +93,9 @@
|
||||
{% if race -%}
|
||||
<option value="disabled">Disabled in Race mode</option>
|
||||
{%- else -%}
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="goal">Allow !remaining after goal completion</option>
|
||||
<option value="enabled">Manual !remaining</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
{%- endif -%}
|
||||
</select>
|
||||
</td>
|
||||
@@ -185,12 +185,12 @@ Warning: playthrough can take a significant amount of time for larger multiworld
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="plando_items" name="plando_items" value="items">
|
||||
<label for="plando_items">Items</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
|
||||
<label for="plando_bosses">Bosses</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
|
||||
<label for="plando_items">Items</label><br>
|
||||
|
||||
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
|
||||
<label for="plando_connections">Connections</label><br>
|
||||
|
||||
|
||||
@@ -3,16 +3,6 @@
|
||||
{% block head %}
|
||||
<title>Multiworld {{ room.id|suuid }}</title>
|
||||
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
|
||||
<meta name="og:site_name" content="Archipelago">
|
||||
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
|
||||
<meta property="og:type" content="website" />
|
||||
{% if room.seed.slots|length < 2 %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% else %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% endif %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% block footer %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2024 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2023 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="/sitemap">Site Map</a>
|
||||
-
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
<tr>
|
||||
<td>{{ patch.player_id }}</td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages | unique %}
|
||||
{% for message in messages %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{% endif %}
|
||||
{% if world.web.options_page is string %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ world.web.options_page }}">Options Page</a>
|
||||
<a href="{{ world.web.settings_page }}">Options Page</a>
|
||||
{% elif world.web.options_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
||||
|
||||
366
WebHostLib/templates/tracker__Starcraft2WingsOfLiberty.html
Normal file
@@ -0,0 +1,366 @@
|
||||
<!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>
|
||||
@@ -11,46 +11,17 @@ from flask import request, flash, redirect, url_for, session, render_template
|
||||
from markupsafe import Markup
|
||||
from pony.orm import commit, flush, select, rollback
|
||||
from pony.orm.core import TransactionIntegrityError
|
||||
import schema
|
||||
|
||||
import MultiServer
|
||||
from NetUtils import SlotType
|
||||
from Utils import VersionException, __version__
|
||||
from worlds import GamesPackage
|
||||
from worlds.Files import AutoPatchRegister
|
||||
from worlds.AutoWorld import data_package_checksum
|
||||
from . import app
|
||||
from .models import Seed, Room, Slot, GameDataPackage
|
||||
|
||||
banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba")
|
||||
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
|
||||
allowed_generation_extensions = (".archipelago", ".zip")
|
||||
|
||||
games_package_schema = schema.Schema({
|
||||
"item_name_groups": {str: [str]},
|
||||
"item_name_to_id": {str: int},
|
||||
"location_name_groups": {str: [str]},
|
||||
"location_name_to_id": {str: int},
|
||||
schema.Optional("checksum"): str,
|
||||
schema.Optional("version"): int,
|
||||
})
|
||||
|
||||
|
||||
def allowed_options(filename: str) -> bool:
|
||||
return filename.endswith(allowed_options_extensions)
|
||||
|
||||
|
||||
def allowed_generation(filename: str) -> bool:
|
||||
return filename.endswith(allowed_generation_extensions)
|
||||
|
||||
|
||||
def banned_file(filename: str) -> bool:
|
||||
return filename.endswith(banned_extensions)
|
||||
|
||||
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||
|
||||
def process_multidata(compressed_multidata, files={}):
|
||||
game_data: GamesPackage
|
||||
|
||||
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
|
||||
|
||||
slots: typing.Set[Slot] = set()
|
||||
@@ -59,19 +30,11 @@ def process_multidata(compressed_multidata, files={}):
|
||||
game_data_packages: typing.List[GameDataPackage] = []
|
||||
for game, game_data in decompressed_multidata["datapackage"].items():
|
||||
if game_data.get("checksum"):
|
||||
original_checksum = game_data.pop("checksum")
|
||||
game_data = games_package_schema.validate(game_data)
|
||||
game_data = {key: value for key, value in sorted(game_data.items())}
|
||||
game_data["checksum"] = data_package_checksum(game_data)
|
||||
game_data_package = GameDataPackage(checksum=game_data["checksum"],
|
||||
data=pickle.dumps(game_data))
|
||||
if original_checksum != game_data["checksum"]:
|
||||
raise Exception(f"Original checksum {original_checksum} != "
|
||||
f"calculated checksum {game_data['checksum']} "
|
||||
f"for game {game}.")
|
||||
decompressed_multidata["datapackage"][game] = {
|
||||
"version": game_data.get("version", 0),
|
||||
"checksum": game_data["checksum"],
|
||||
"checksum": game_data["checksum"]
|
||||
}
|
||||
try:
|
||||
commit() # commit game data package
|
||||
@@ -86,21 +49,20 @@ def process_multidata(compressed_multidata, files={}):
|
||||
if slot_info.type == SlotType.group:
|
||||
continue
|
||||
slots.add(Slot(data=files.get(slot, None),
|
||||
player_name=slot_info.name,
|
||||
player_id=slot,
|
||||
game=slot_info.game))
|
||||
player_name=slot_info.name,
|
||||
player_id=slot,
|
||||
game=slot_info.game))
|
||||
flush() # commit slots
|
||||
|
||||
compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
|
||||
return slots, compressed_multidata
|
||||
|
||||
|
||||
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
||||
if not owner:
|
||||
owner = session["_id"]
|
||||
infolist = zfile.infolist()
|
||||
if all(allowed_options(file.filename) or file.is_dir() for file in infolist):
|
||||
flash(Markup("Error: Your .zip file only contains options files. "
|
||||
if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
|
||||
flash(Markup("Error: Your .zip file only contains .yaml files. "
|
||||
'Did you mean to <a href="/generate">generate a game</a>?'))
|
||||
return
|
||||
|
||||
@@ -111,7 +73,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
# Load files.
|
||||
for file in infolist:
|
||||
handler = AutoPatchRegister.get_handler(file.filename)
|
||||
if banned_file(file.filename):
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
|
||||
@@ -174,34 +136,35 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
|
||||
|
||||
@app.route("/uploads", methods=["GET", "POST"])
|
||||
@app.route('/uploads', methods=['GET', 'POST'])
|
||||
def uploads():
|
||||
if request.method == "POST":
|
||||
# check if the POST request has a file part.
|
||||
if "file" not in request.files:
|
||||
flash("No file part in POST request.")
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
else:
|
||||
uploaded_file = request.files["file"]
|
||||
# If the user does not select file, the browser will still submit an empty string without a file name.
|
||||
if uploaded_file.filename == "":
|
||||
flash("No selected file.")
|
||||
elif uploaded_file and allowed_generation(uploaded_file.filename):
|
||||
if zipfile.is_zipfile(uploaded_file):
|
||||
with zipfile.ZipFile(uploaded_file, "r") as zfile:
|
||||
file = request.files['file']
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if zipfile.is_zipfile(file):
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
try:
|
||||
res = upload_zip_to_db(zfile)
|
||||
except VersionException:
|
||||
flash(f"Could not load multidata. Wrong Version detected.")
|
||||
else:
|
||||
if res is str:
|
||||
if type(res) == str:
|
||||
return res
|
||||
elif res:
|
||||
return redirect(url_for("view_seed", seed=res.id))
|
||||
else:
|
||||
uploaded_file.seek(0) # offset from is_zipfile check
|
||||
file.seek(0) # offset from is_zipfile check
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
multidata = uploaded_file.read()
|
||||
multidata = file.read()
|
||||
slots, multidata = process_multidata(multidata)
|
||||
except Exception as e:
|
||||
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
|
||||
@@ -219,3 +182,7 @@ def user_content():
|
||||
rooms = select(room for room in Room if room.owner == session["_id"])
|
||||
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
|
||||
return render_template("userContent.html", rooms=rooms, seeds=seeds)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return filename.endswith(('.archipelago', ".zip"))
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import List
|
||||
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
from worlds import lookup_any_location_id_to_name
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
@@ -152,7 +153,7 @@ def get_payload(ctx: ZeldaContext):
|
||||
|
||||
|
||||
def reconcile_shops(ctx: ZeldaContext):
|
||||
checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations]
|
||||
checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
|
||||
shops = [location for location in checked_location_names if "Shop" in location]
|
||||
left_slots = [shop for shop in shops if "Left" in shop]
|
||||
middle_slots = [shop for shop in shops if "Middle" in shop]
|
||||
@@ -190,7 +191,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone=
|
||||
locations_checked = []
|
||||
location = None
|
||||
for location in ctx.missing_locations:
|
||||
location_name = ctx.location_names[location]
|
||||
location_name = lookup_any_location_id_to_name[location]
|
||||
|
||||
if location_name in Locations.overworld_locations and zone == "overworld":
|
||||
status = locations_array[Locations.major_location_offsets[location_name]]
|
||||
|
||||