Compare commits
318 Commits
core_optim
...
0.4.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aeeeb077a | ||
|
|
35458380e6 | ||
|
|
4ac1866689 | ||
|
|
4aa03da66e | ||
|
|
24a03bc8b6 | ||
|
|
f813a7005f | ||
|
|
2a0b7e0def | ||
|
|
03d47e460e | ||
|
|
e546c0f7ff | ||
|
|
2ec93ba82a | ||
|
|
4e3d396394 | ||
|
|
72c53513f8 | ||
|
|
b7ac6a4cbd | ||
|
|
5f0112e783 | ||
|
|
bb481256de | ||
|
|
301d9de975 | ||
|
|
9dc708978b | ||
|
|
4391d1f4c1 | ||
|
|
5d9d4ed9f1 | ||
|
|
c97215e0e7 | ||
|
|
eb66886a90 | ||
|
|
de860623d1 | ||
|
|
74b2bf5161 | ||
|
|
74ac66b032 | ||
|
|
80d7ac4164 | ||
|
|
77311719fa | ||
|
|
cfc1541be9 | ||
|
|
4d954afd9b | ||
|
|
17748a4bf1 | ||
|
|
9182fe563f | ||
|
|
bcf223081f | ||
|
|
fa93488f3f | ||
|
|
db15dd4bde | ||
|
|
01cdb0d761 | ||
|
|
d0ac2b744e | ||
|
|
14f5f0127e | ||
|
|
cf133dde72 | ||
|
|
ca18121811 | ||
|
|
1d4512590e | ||
|
|
f7b415dab0 | ||
|
|
702f006c84 | ||
|
|
98ce8f8844 | ||
|
|
ea47b90367 | ||
|
|
bf3856866c | ||
|
|
c0368ae0d4 | ||
|
|
36c83073ad | ||
|
|
2b24539ea5 | ||
|
|
7e904a1c78 | ||
|
|
bdd498db23 | ||
|
|
355223b8f0 | ||
|
|
aaa3472d5d | ||
|
|
96d93c1ae3 | ||
|
|
ca549df20a | ||
|
|
44988d430d | ||
|
|
11b32f17ab | ||
|
|
218cd45844 | ||
|
|
4196bde597 | ||
|
|
40f843f54d | ||
|
|
da333fbb0c | ||
|
|
43084da23c | ||
|
|
14816743fc | ||
|
|
30a0aa2c85 | ||
|
|
f4b7c28a33 | ||
|
|
12864f7b24 | ||
|
|
db02e9d2aa | ||
|
|
32315776ac | ||
|
|
e9620bea77 | ||
|
|
183ca35bba | ||
|
|
fcaaa197a1 | ||
|
|
8f7b63a787 | ||
|
|
6f64bb9869 | ||
|
|
d0a9d0e2d1 | ||
|
|
94650a02de | ||
|
|
8a8263fa61 | ||
|
|
9efc7bae40 | ||
|
|
e6198585c8 | ||
|
|
2a8784ef72 | ||
|
|
e0e9fdd86a | ||
|
|
ed9cbfecb9 | ||
|
|
d1ef198566 | ||
|
|
1324bccc89 | ||
|
|
cafacfe754 | ||
|
|
e1038b4431 | ||
|
|
2e8a84121e | ||
|
|
0342c1e09d | ||
|
|
f330e5835c | ||
|
|
329a2f9175 | ||
|
|
31249ec6e2 | ||
|
|
52e65e208e | ||
|
|
f7da833572 | ||
|
|
afe7aea536 | ||
|
|
e534abeab0 | ||
|
|
fa233b2583 | ||
|
|
3e3965272d | ||
|
|
72e6383cc7 | ||
|
|
67ed0fdca5 | ||
|
|
c4ec8682d5 | ||
|
|
1705213353 | ||
|
|
d953927b3a | ||
|
|
ecd84fd1ca | ||
|
|
fb9ef19c15 | ||
|
|
03d403ff51 | ||
|
|
f8d5fe0e1e | ||
|
|
b6b88070be | ||
|
|
a6e1ea8c48 | ||
|
|
2692604c09 | ||
|
|
4bf676e588 | ||
|
|
ae6c16bde1 | ||
|
|
c795c72471 | ||
|
|
30ad2aa4a8 | ||
|
|
51243abea1 | ||
|
|
cb2c00f644 | ||
|
|
6d3f7865ff | ||
|
|
a7384b4b63 | ||
|
|
3d56f3c096 | ||
|
|
a3125cb06e | ||
|
|
d3019421de | ||
|
|
d20d09e682 | ||
|
|
078d793073 | ||
|
|
5fecb7f043 | ||
|
|
9c920fbc53 | ||
|
|
fed3d04c8d | ||
|
|
6badc75237 | ||
|
|
c7e735da15 | ||
|
|
2e1a5b0e3b | ||
|
|
b8c24def8d | ||
|
|
3602ed45a4 | ||
|
|
37add8ee59 | ||
|
|
5a4d88d554 | ||
|
|
a4f89396d9 | ||
|
|
8c11c385f3 | ||
|
|
939a5ec959 | ||
|
|
4ce58c0240 | ||
|
|
be802b4723 | ||
|
|
3c4ebb2114 | ||
|
|
3e3b4c6732 | ||
|
|
3e0ff3f72d | ||
|
|
b4bb88fcf7 | ||
|
|
862d77820d | ||
|
|
c6b1039e0f | ||
|
|
ac791f2999 | ||
|
|
e33ea0147b | ||
|
|
db30a0116e | ||
|
|
a5a1494a96 | ||
|
|
bfa9e7da00 | ||
|
|
938beb34df | ||
|
|
4ddfb7ce8b | ||
|
|
b147c5bf8a | ||
|
|
bf60e905ec | ||
|
|
6926f38414 | ||
|
|
644f75978d | ||
|
|
af4172f32f | ||
|
|
45a15004a4 | ||
|
|
ce43c52589 | ||
|
|
7384bbdf23 | ||
|
|
bcbb06d78d | ||
|
|
26ee9fe05c | ||
|
|
12cc930825 | ||
|
|
b9d561ae25 | ||
|
|
ecec931e9f | ||
|
|
a70b94fd62 | ||
|
|
37a871eab1 | ||
|
|
113c54f9be | ||
|
|
4e31e51d7a | ||
|
|
519dffdb73 | ||
|
|
d124df72e4 | ||
|
|
57d1fe6d79 | ||
|
|
ef37ee81f9 | ||
|
|
526eb09089 | ||
|
|
b2f30d5fd0 | ||
|
|
b8bf67a166 | ||
|
|
2c5b2e0759 | ||
|
|
b65a3b7464 | ||
|
|
01cf60f48d | ||
|
|
ad3ffde785 | ||
|
|
f17ff15669 | ||
|
|
983da12a03 | ||
|
|
564ec8c32e | ||
|
|
7a85ee7ed1 | ||
|
|
e60a2636cd | ||
|
|
184dedfa69 | ||
|
|
5e06a75bf2 | ||
|
|
3bc2c44ac3 | ||
|
|
7ebd5d3891 | ||
|
|
a659036e95 | ||
|
|
36cee91a2c | ||
|
|
59a6e4a1b5 | ||
|
|
c126418f35 | ||
|
|
738a9ebb7d | ||
|
|
5c05ab1527 | ||
|
|
46fc8df36e | ||
|
|
8f7b0ee489 | ||
|
|
86a7ac466e | ||
|
|
57fcd57a85 | ||
|
|
6bf4a94537 | ||
|
|
96163c6408 | ||
|
|
afa5ce4afe | ||
|
|
b18641091f | ||
|
|
f8981a4638 | ||
|
|
9f0d736aed | ||
|
|
ffdcb91a13 | ||
|
|
17c73916b7 | ||
|
|
7fc159c881 | ||
|
|
38cc90efd0 | ||
|
|
7a86285807 | ||
|
|
933e5bacff | ||
|
|
818b0a49e1 | ||
|
|
523c7dbfad | ||
|
|
e8249d1f72 | ||
|
|
04b02f5a4a | ||
|
|
687af30d14 | ||
|
|
539307cf0b | ||
|
|
4d9202537c | ||
|
|
3869a25944 | ||
|
|
9805bf92e4 | ||
|
|
057e372325 | ||
|
|
2c38b9fd51 | ||
|
|
475e803500 | ||
|
|
f178d438b8 | ||
|
|
e5980ac5f5 | ||
|
|
2167db5a88 | ||
|
|
2165253961 | ||
|
|
57fcdf4fbe | ||
|
|
74e79bff06 | ||
|
|
3ca3417172 | ||
|
|
55455914e6 | ||
|
|
0c8f726393 | ||
|
|
6f3bc3a7ad | ||
|
|
151e2c3ac2 | ||
|
|
a6deffb9f2 | ||
|
|
77c326cb81 | ||
|
|
1a675821cf | ||
|
|
03c3ef4e72 | ||
|
|
4a703c5aba | ||
|
|
59ef010842 | ||
|
|
4032cfb9ea | ||
|
|
281fe01c25 | ||
|
|
6c19bc42bb | ||
|
|
33237bd5c0 | ||
|
|
57cb971177 | ||
|
|
140f802564 | ||
|
|
3a51c035ac | ||
|
|
016c1e9bb4 | ||
|
|
697deb98b4 | ||
|
|
144769a141 | ||
|
|
dc49d50c2c | ||
|
|
5663c21f39 | ||
|
|
1b188bab3c | ||
|
|
69c80501c4 | ||
|
|
0bc9966d6f | ||
|
|
b4212d1c3e | ||
|
|
aa72f671bc | ||
|
|
5f9ce2b7b6 | ||
|
|
1307754f02 | ||
|
|
ac7b707e3e | ||
|
|
ec440b7785 | ||
|
|
4c901dcfc0 | ||
|
|
834b6e35b4 | ||
|
|
602c2966fc | ||
|
|
49ecd4b9c1 | ||
|
|
de8fe21d4a | ||
|
|
4fdeec4f70 | ||
|
|
71a3e2230d | ||
|
|
325a510ba7 | ||
|
|
5dcaa6ca20 | ||
|
|
e15873e861 | ||
|
|
5c7bae7940 | ||
|
|
e6f7ed5060 | ||
|
|
1c2dcb7b01 | ||
|
|
5df7a8f686 | ||
|
|
3a588099bd | ||
|
|
d000b52ae0 | ||
|
|
fe3bc8d6be | ||
|
|
7affb885ba | ||
|
|
d390d2eff8 | ||
|
|
0efc13fc8a | ||
|
|
c6896c6af9 | ||
|
|
adad7b532d | ||
|
|
d756960a0b | ||
|
|
30ec080449 | ||
|
|
79e2f7e357 | ||
|
|
b4077a0717 | ||
|
|
518b04c08e | ||
|
|
d10f8f66c7 | ||
|
|
6d393fe42b | ||
|
|
5b93db121f | ||
|
|
ad074490bc | ||
|
|
6ac3d5c651 | ||
|
|
ed6b7b2670 | ||
|
|
6904bd5885 | ||
|
|
962b9b28f0 | ||
|
|
37b03807fd | ||
|
|
73e41cb701 | ||
|
|
cfd758168c | ||
|
|
01fb44c186 | ||
|
|
2725c0258f | ||
|
|
0c0adb0745 | ||
|
|
4a85f21c25 | ||
|
|
3933fd3929 | ||
|
|
b241644e54 | ||
|
|
e00b5a7d17 | ||
|
|
47dd36456e | ||
|
|
4ce8a7ec4d | ||
|
|
a99c1e15ad | ||
|
|
44de140add | ||
|
|
ac2387e17c | ||
|
|
2760deb5b6 | ||
|
|
f530895c33 | ||
|
|
b6f3ccb8c5 | ||
|
|
388413fcdd | ||
|
|
4045c6a9cf | ||
|
|
e082c83dc7 | ||
|
|
82410fd554 | ||
|
|
570ba28bee | ||
|
|
b0638b993d | ||
|
|
89f211f31e | ||
|
|
70fdd6b90d | ||
|
|
f22daca74e |
5
.coveragerc
Normal file
@@ -0,0 +1,5 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
if typing.TYPE_CHECKING:
|
||||
31
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
'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@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Determine modified files (pull_request)"
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
run: |
|
||||
echo "diff=." >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
if: env.diff != ''
|
||||
with:
|
||||
python-version: 3.8
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
if: env.diff != '' && matrix.task == 'flake8'
|
||||
run: |
|
||||
flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||
flake8 --count --max-complexity=14 --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,11 +8,13 @@ on:
|
||||
- '.github/workflows/build.yml'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
- '*.iss'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/build.yml'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
- '*.iss'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -25,9 +27,9 @@ jobs:
|
||||
build-win-py38: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Download run-time dependencies
|
||||
@@ -46,25 +48,42 @@ jobs:
|
||||
cd build
|
||||
Rename-Item "exe.$NAME" Archipelago
|
||||
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
|
||||
Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name
|
||||
- name: Store 7z
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ZIP_NAME }}
|
||||
path: dist/${{ env.ZIP_NAME }}
|
||||
retention-days: 7 # keep for 7 days, should be enough
|
||||
- name: Build Setup
|
||||
run: |
|
||||
& "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL
|
||||
if ( $? -eq $false ) {
|
||||
Write-Error "Building setup failed!"
|
||||
exit 1
|
||||
}
|
||||
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
|
||||
$SETUP_NAME=$contents[0].Name
|
||||
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
||||
- name: Store Setup
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.SETUP_NAME }}
|
||||
path: setups/${{ env.SETUP_NAME }}
|
||||
retention-days: 7 # keep for 7 days, should be enough
|
||||
|
||||
build-ubuntu2004:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install build-time dependencies
|
||||
@@ -100,13 +119,13 @@ jobs:
|
||||
source venv/bin/activate
|
||||
python setup.py build_exe --yes
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APPIMAGE_NAME }}
|
||||
path: dist/${{ env.APPIMAGE_NAME }}
|
||||
retention-days: 7
|
||||
- name: Store .tar.gz
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.TAR_NAME }}
|
||||
path: dist/${{ env.TAR_NAME }}
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
46
.github/workflows/label-pull-requests.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
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@b7e450da2a4b4cb4bfbae528f788167786cfcedf
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # don't publish right away, especially since windows build is added by hand
|
||||
prerelease: false
|
||||
@@ -35,14 +35,14 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
# - code below copied from build.yml -
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install build-time dependencies
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - code above copied from build.yml -
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
|
||||
65
.github/workflows/scan-build.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
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@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python.version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
|
||||
18
.run/Archipelago Unittests.run.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
@@ -18,11 +18,14 @@ import NetUtils
|
||||
import Options
|
||||
import Utils
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class Group(TypedDict, total=False):
|
||||
name: str
|
||||
game: str
|
||||
world: auto_world
|
||||
world: "AutoWorld.World"
|
||||
players: Set[int]
|
||||
item_pool: Set[str]
|
||||
replacement_items: Dict[int, Optional[str]]
|
||||
@@ -55,7 +58,7 @@ class MultiWorld():
|
||||
plando_texts: List[Dict[str, str]]
|
||||
plando_items: List[List[Dict[str, Any]]]
|
||||
plando_connections: List
|
||||
worlds: Dict[int, auto_world]
|
||||
worlds: Dict[int, "AutoWorld.World"]
|
||||
groups: Dict[int, Group]
|
||||
regions: RegionManager
|
||||
itempool: List[Item]
|
||||
@@ -82,7 +85,7 @@ class MultiWorld():
|
||||
game: Dict[int, str]
|
||||
|
||||
random: random.Random
|
||||
per_slot_randoms: Dict[int, random.Random]
|
||||
per_slot_randoms: Utils.DeprecateDict[int, random.Random]
|
||||
"""Deprecated. Please use `self.random` instead."""
|
||||
|
||||
class AttributeProxy():
|
||||
@@ -107,10 +110,14 @@ 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):
|
||||
@@ -156,11 +163,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', 'dungeonssimple'])
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
self.fix_palaceofdarkness_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
self.fix_trock_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
@@ -210,7 +217,8 @@ class MultiWorld():
|
||||
set_player_attr('game', "A Link to the Past")
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.worlds = {}
|
||||
self.per_slot_randoms = {}
|
||||
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||
"world's random object instead (usually self.random)")
|
||||
self.plando_options = PlandoOptions.none
|
||||
|
||||
def get_all_ids(self) -> Tuple[int, ...]:
|
||||
@@ -219,6 +227,8 @@ 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
|
||||
@@ -242,17 +252,18 @@ 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:
|
||||
@@ -264,12 +275,13 @@ class MultiWorld():
|
||||
for player in self.player_ids:
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
self.worlds[player] = world_type(self, player)
|
||||
self.worlds[player].random = self.per_slot_randoms[player]
|
||||
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
|
||||
for option_key in options_dataclass.type_hints})
|
||||
|
||||
def set_item_links(self):
|
||||
from worlds import AutoWorld
|
||||
|
||||
item_links = {}
|
||||
replacement_prio = [False, True, None]
|
||||
for player in self.player_ids:
|
||||
@@ -572,9 +584,10 @@ 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["minimal"]:
|
||||
return False
|
||||
return True
|
||||
if location.player in players["locations"] or (location.item and location.item.player not in
|
||||
players["minimal"]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def location_relevant(location: Location):
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
@@ -703,14 +716,23 @@ class CollectionState():
|
||||
assert isinstance(player, int), "can_reach: player is required if spot is str"
|
||||
# try to resolve a name
|
||||
if resolution_hint == 'Location':
|
||||
spot = self.multiworld.get_location(spot, player)
|
||||
return self.can_reach_location(spot, player)
|
||||
elif resolution_hint == 'Entrance':
|
||||
spot = self.multiworld.get_entrance(spot, player)
|
||||
return self.can_reach_entrance(spot, player)
|
||||
else:
|
||||
# default to Region
|
||||
spot = self.multiworld.get_region(spot, player)
|
||||
return self.can_reach_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()
|
||||
@@ -823,8 +845,8 @@ class Entrance:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
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})'
|
||||
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})'
|
||||
|
||||
|
||||
class Region:
|
||||
@@ -867,6 +889,8 @@ 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
|
||||
|
||||
@@ -877,6 +901,8 @@ 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
|
||||
|
||||
@@ -1006,7 +1032,7 @@ class Location:
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
always_allow = staticmethod(lambda state, item: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
@@ -1040,8 +1066,8 @@ class Location:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
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})'
|
||||
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})'
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
@@ -1056,9 +1082,6 @@ 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("-", " ")
|
||||
|
||||
|
||||
@@ -1178,7 +1201,7 @@ class Spoiler:
|
||||
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
|
||||
|
||||
def create_playthrough(self, create_paths: bool = True) -> None:
|
||||
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||
"""Destructive to the multiworld while it is run, damage gets repaired afterwards."""
|
||||
from itertools import chain
|
||||
# get locations containing progress items
|
||||
multiworld = self.multiworld
|
||||
@@ -1265,12 +1288,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}')
|
||||
|
||||
@@ -1329,6 +1352,8 @@ 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)
|
||||
@@ -1452,8 +1477,3 @@ def get_seed(seed: Optional[int] = None) -> int:
|
||||
random.seed(None)
|
||||
return random.randint(0, pow(10, seeddigits) - 1)
|
||||
return seed
|
||||
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
auto_world = AutoWorld.World
|
||||
|
||||
@@ -20,8 +20,8 @@ if __name__ == "__main__":
|
||||
Utils.init_logging("TextClient", exception_logger="Client")
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
||||
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
|
||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes)
|
||||
from Utils import Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -72,9 +72,16 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_received(self) -> bool:
|
||||
"""List all received items"""
|
||||
self.output(f'{len(self.ctx.items_received)} received items:')
|
||||
item: NetworkItem
|
||||
self.output(f'{len(self.ctx.items_received)} received items, sorted by time:')
|
||||
for index, item in enumerate(self.ctx.items_received, 1):
|
||||
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
|
||||
parts = []
|
||||
add_json_item(parts, item.item, self.ctx.slot, item.flags)
|
||||
add_json_text(parts, " from ")
|
||||
add_json_location(parts, item.location, item.player)
|
||||
add_json_text(parts, " by ")
|
||||
add_json_text(parts, item.player, type=JSONTypes.player_id)
|
||||
self.ctx.on_print_json({"data": parts, "cmd": "PrintJSON"})
|
||||
return True
|
||||
|
||||
def _cmd_missing(self, filter_text = "") -> bool:
|
||||
@@ -115,6 +122,15 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
|
||||
self.output(item_name)
|
||||
|
||||
def _cmd_item_groups(self):
|
||||
"""List all item group names for the currently running game."""
|
||||
if not self.ctx.game:
|
||||
self.output("No game set, cannot determine existing item groups.")
|
||||
return False
|
||||
self.output(f"Item Group Names for {self.ctx.game}")
|
||||
for group_name in AutoWorldRegister.world_types[self.ctx.game].item_name_groups:
|
||||
self.output(group_name)
|
||||
|
||||
def _cmd_locations(self):
|
||||
"""List all location names for the currently running game."""
|
||||
if not self.ctx.game:
|
||||
@@ -124,6 +140,15 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
|
||||
self.output(location_name)
|
||||
|
||||
def _cmd_location_groups(self):
|
||||
"""List all location group names for the currently running game."""
|
||||
if not self.ctx.game:
|
||||
self.output("No game set, cannot determine existing location groups.")
|
||||
return False
|
||||
self.output(f"Location Group Names for {self.ctx.game}")
|
||||
for group_name in AutoWorldRegister.world_types[self.ctx.game].location_name_groups:
|
||||
self.output(group_name)
|
||||
|
||||
def _cmd_ready(self):
|
||||
"""Send ready status to server."""
|
||||
self.ctx.ready = not self.ctx.ready
|
||||
@@ -460,7 +485,7 @@ class CommonContext:
|
||||
else:
|
||||
self.update_game(cached_game)
|
||||
if needed_updates:
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
|
||||
|
||||
def update_game(self, game_package: dict):
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
@@ -477,6 +502,7 @@ 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)
|
||||
|
||||
@@ -617,13 +643,13 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
ctx.username = server_url.username
|
||||
if server_url.password:
|
||||
ctx.password = server_url.password
|
||||
port = server_url.port or 38281
|
||||
|
||||
def reconnect_hint() -> str:
|
||||
return ", type /connect to reconnect" if ctx.server_address else ""
|
||||
|
||||
logger.info(f'Connecting to Archipelago server at {address}')
|
||||
try:
|
||||
port = server_url.port or 38281 # raises ValueError if invalid
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None)
|
||||
if ctx.ui is not None:
|
||||
@@ -727,14 +753,15 @@ 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. '
|
||||
@@ -941,4 +968,5 @@ 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()
|
||||
|
||||
226
Fill.py
@@ -27,12 +27,12 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
return new_state
|
||||
|
||||
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
def fill_restrictive(multiworld: 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 world: Multiworld to be filled.
|
||||
:param multiworld: 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(world: MultiWorld, base_state: CollectionState, locations:
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, item_pool + unplaced_items)
|
||||
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
has_beaten_game = multiworld.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(world: MultiWorld, base_state: CollectionState, locations:
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
|
||||
# if minimal accessibility, only check whether location is reachable if game not beatable
|
||||
if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
|
||||
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not multiworld.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(world: MultiWorld, base_state: CollectionState, locations:
|
||||
# Verify placing this item won't reduce available locations, which would be a useless swap.
|
||||
prev_state = swap_state.copy()
|
||||
prev_loc_count = len(
|
||||
world.get_reachable_locations(prev_state))
|
||||
multiworld.get_reachable_locations(prev_state))
|
||||
|
||||
swap_state.collect(item_to_place, True)
|
||||
new_loc_count = len(
|
||||
world.get_reachable_locations(swap_state))
|
||||
multiworld.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(world: MultiWorld, base_state: CollectionState, locations:
|
||||
else:
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
multiworld.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(world: MultiWorld, base_state: CollectionState, locations:
|
||||
# validate all placements and remove invalid ones
|
||||
state = sweep_from_pool(base_state, [])
|
||||
for placement in placements:
|
||||
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
|
||||
if multiworld.worlds[placement.item.player].options.accessibility != "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(world: MultiWorld, base_state: CollectionState, locations:
|
||||
if excluded_locations:
|
||||
for location in excluded_locations:
|
||||
location.progress_type = location.progress_type.DEFAULT
|
||||
fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
|
||||
fill_restrictive(multiworld, 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,19 +196,26 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
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 world.can_beat_game():
|
||||
if multiworld.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
|
||||
f"Not all items placed. Game beatable anyway.\nCould not place:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}")
|
||||
else:
|
||||
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)}')
|
||||
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)}")
|
||||
|
||||
item_pool.extend(unplaced_items)
|
||||
|
||||
|
||||
def remaining_fill(world: MultiWorld,
|
||||
def remaining_fill(multiworld: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> None:
|
||||
itempool: typing.List[Item],
|
||||
name: str = "Remaining") -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
@@ -261,36 +268,41 @@ def remaining_fill(world: MultiWorld,
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
multiworld.push_item(spot_to_fill, item_to_place, False)
|
||||
placements.append(spot_to_fill)
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
_log_fill_progress(name, placed, total)
|
||||
|
||||
if total > 1000:
|
||||
_log_fill_progress("Remaining", placed, total)
|
||||
_log_fill_progress(name, placed, total)
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
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)}')
|
||||
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)}")
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld,
|
||||
def fast_fill(multiworld: 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):
|
||||
world.push_item(location, item, False)
|
||||
multiworld.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||
maximum_exploration_state = sweep_from_pool(state, pool)
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -304,36 +316,36 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
fill_restrictive(world, state, locations, pool, name="Accessibility Corrections")
|
||||
fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections")
|
||||
|
||||
|
||||
def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
|
||||
def inaccessible_location_rules(multiworld: 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 world.worlds[item.player].options.accessibility != 'minimal')
|
||||
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal')
|
||||
|
||||
for location in unreachable_locations:
|
||||
add_item_rule(location, forbid_important_item_rule)
|
||||
|
||||
|
||||
def distribute_early_items(world: MultiWorld,
|
||||
def distribute_early_items(multiworld: 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 world.player_ids:
|
||||
items = itertools.chain(world.early_items[player], world.local_early_items[player])
|
||||
for player in multiworld.player_ids:
|
||||
items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player])
|
||||
for item in items:
|
||||
early_items_count[item, player] = [world.early_items[player].get(item, 0),
|
||||
world.local_early_items[player].get(item, 0)]
|
||||
early_items_count[item, player] = [multiworld.early_items[player].get(item, 0),
|
||||
multiworld.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 = world.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
|
||||
base_state = multiworld.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in multiworld.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:
|
||||
@@ -345,8 +357,8 @@ def distribute_early_items(world: 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 world.player_ids}
|
||||
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
|
||||
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}
|
||||
item_indexes_to_remove: typing.Set[int] = set()
|
||||
for i, item in enumerate(itempool):
|
||||
if (item.name, item.player) in early_items_count:
|
||||
@@ -370,28 +382,28 @@ def distribute_early_items(world: 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 world.player_ids:
|
||||
for player in multiworld.player_ids:
|
||||
player_local = early_local_rest_items[player]
|
||||
fill_restrictive(world, base_state,
|
||||
fill_restrictive(multiworld, 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(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
|
||||
fill_restrictive(multiworld, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
|
||||
name="Early Items")
|
||||
early_locations += early_priority_locations
|
||||
for player in world.player_ids:
|
||||
for player in multiworld.player_ids:
|
||||
player_local = early_local_prog_items[player]
|
||||
fill_restrictive(world, base_state,
|
||||
fill_restrictive(multiworld, 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(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
|
||||
fill_restrictive(multiworld, 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:
|
||||
@@ -400,18 +412,18 @@ def distribute_early_items(world: MultiWorld,
|
||||
itempool += unplaced_early_items
|
||||
|
||||
fill_locations.extend(early_locations)
|
||||
world.random.shuffle(fill_locations)
|
||||
multiworld.random.shuffle(fill_locations)
|
||||
return fill_locations, itempool
|
||||
|
||||
|
||||
def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
fill_locations = sorted(world.get_unfilled_locations())
|
||||
world.random.shuffle(fill_locations)
|
||||
def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
fill_locations = sorted(multiworld.get_unfilled_locations())
|
||||
multiworld.random.shuffle(fill_locations)
|
||||
# get items to distribute
|
||||
itempool = sorted(world.itempool)
|
||||
world.random.shuffle(itempool)
|
||||
itempool = sorted(multiworld.itempool)
|
||||
multiworld.random.shuffle(itempool)
|
||||
|
||||
fill_locations, itempool = distribute_early_items(world, fill_locations, itempool)
|
||||
fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool)
|
||||
|
||||
progitempool: typing.List[Item] = []
|
||||
usefulitempool: typing.List[Item] = []
|
||||
@@ -425,7 +437,7 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
else:
|
||||
filleritempool.append(item)
|
||||
|
||||
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
|
||||
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
|
||||
loc_type: [] for loc_type in LocationProgressType}
|
||||
@@ -446,75 +458,79 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
name="Priority")
|
||||
accessibility_corrections(world, world.state, prioritylocations, progitempool)
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
if progitempool:
|
||||
# "advancement/progression fill"
|
||||
fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
|
||||
accessibility_corrections(world, world.state, defaultlocations)
|
||||
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)
|
||||
|
||||
for location in lock_later:
|
||||
if location.item:
|
||||
location.locked = True
|
||||
del mark_for_locking, lock_later
|
||||
|
||||
inaccessible_location_rules(world, world.state, defaultlocations)
|
||||
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
remaining_fill(world, excludedlocations, filleritempool)
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
f"Not enough filler items for excluded locations. "
|
||||
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
|
||||
)
|
||||
|
||||
restitempool = filleritempool + usefulitempool
|
||||
|
||||
remaining_fill(world, defaultlocations, restitempool)
|
||||
remaining_fill(multiworld, 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 world.get_locations() if location.item)
|
||||
locations_counter = Counter(location.player for location in world.get_locations())
|
||||
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())
|
||||
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(world: MultiWorld) -> None:
|
||||
def flood_items(multiworld: MultiWorld) -> None:
|
||||
# get items to distribute
|
||||
world.random.shuffle(world.itempool)
|
||||
itempool = world.itempool
|
||||
multiworld.random.shuffle(multiworld.itempool)
|
||||
itempool = multiworld.itempool
|
||||
progress_done = False
|
||||
|
||||
# sweep once to pick up preplaced items
|
||||
world.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_events()
|
||||
|
||||
# fill world from top of itempool while we can
|
||||
# fill multiworld from top of itempool while we can
|
||||
while not progress_done:
|
||||
location_list = world.get_unfilled_locations()
|
||||
world.random.shuffle(location_list)
|
||||
location_list = multiworld.get_unfilled_locations()
|
||||
multiworld.random.shuffle(location_list)
|
||||
spot_to_fill = None
|
||||
for location in location_list:
|
||||
if location.can_fill(world.state, itempool[0]):
|
||||
if location.can_fill(multiworld.state, itempool[0]):
|
||||
spot_to_fill = location
|
||||
break
|
||||
|
||||
if spot_to_fill:
|
||||
item = itempool.pop(0)
|
||||
world.push_item(spot_to_fill, item, True)
|
||||
multiworld.push_item(spot_to_fill, item, True)
|
||||
continue
|
||||
|
||||
# ran out of spots, check if we need to step in and correct things
|
||||
if len(world.get_reachable_locations()) == len(world.get_locations()):
|
||||
if len(multiworld.get_reachable_locations()) == len(multiworld.get_locations()):
|
||||
progress_done = True
|
||||
continue
|
||||
|
||||
@@ -524,7 +540,7 @@ def flood_items(world: MultiWorld) -> None:
|
||||
for item in itempool:
|
||||
if item.advancement:
|
||||
candidate_item_to_place = item
|
||||
if world.unlocks_new_location(item):
|
||||
if multiworld.unlocks_new_location(item):
|
||||
item_to_place = item
|
||||
break
|
||||
|
||||
@@ -537,15 +553,15 @@ def flood_items(world: MultiWorld) -> None:
|
||||
raise FillError('No more progress items left to place.')
|
||||
|
||||
# find item to replace with progress item
|
||||
location_list = world.get_reachable_locations()
|
||||
world.random.shuffle(location_list)
|
||||
location_list = multiworld.get_reachable_locations()
|
||||
multiworld.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)
|
||||
world.push_item(location, item_to_place, True)
|
||||
multiworld.push_item(location, item_to_place, True)
|
||||
itempool.remove(item_to_place)
|
||||
break
|
||||
|
||||
@@ -755,7 +771,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(world: MultiWorld) -> None:
|
||||
def distribute_planned(multiworld: 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}')
|
||||
@@ -768,24 +784,24 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
else:
|
||||
warn(warning, force)
|
||||
|
||||
swept_state = world.state.copy()
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state.sweep_for_events()
|
||||
reachable = frozenset(world.get_reachable_locations(swept_state))
|
||||
reachable = frozenset(multiworld.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 world.get_unfilled_locations():
|
||||
for loc in multiworld.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 = world.world_name_lookup
|
||||
world_name_lookup = multiworld.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(world.player_ids)
|
||||
player_ids = set(multiworld.player_ids)
|
||||
for player in player_ids:
|
||||
for block in world.plando_items[player]:
|
||||
for block in multiworld.plando_items[player]:
|
||||
block['player'] = player
|
||||
if 'force' not in block:
|
||||
block['force'] = 'silent'
|
||||
@@ -799,12 +815,12 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
else:
|
||||
target_world = block['world']
|
||||
|
||||
if target_world is False or world.players == 1: # target own world
|
||||
if target_world is False or multiworld.players == 1: # target own world
|
||||
worlds: typing.Set[int] = {player}
|
||||
elif target_world is True: # target any worlds besides own
|
||||
worlds = set(world.player_ids) - {player}
|
||||
worlds = set(multiworld.player_ids) - {player}
|
||||
elif target_world is None: # target all worlds
|
||||
worlds = set(world.player_ids)
|
||||
worlds = set(multiworld.player_ids)
|
||||
elif type(target_world) == list: # list of target worlds
|
||||
worlds = set()
|
||||
for listed_world in target_world:
|
||||
@@ -814,9 +830,9 @@ def distribute_planned(world: 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, world.players + 1):
|
||||
if target_world not in range(1, multiworld.players + 1):
|
||||
failed(
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
|
||||
block['force'])
|
||||
continue
|
||||
worlds = {target_world}
|
||||
@@ -844,7 +860,7 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
item_list: typing.List[str] = []
|
||||
for key, value in items.items():
|
||||
if value is True:
|
||||
value = world.itempool.count(world.worlds[player].create_item(key))
|
||||
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
if isinstance(items, str):
|
||||
@@ -894,17 +910,17 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
count = block['count']
|
||||
failed(f"Plando count {count} greater than locations specified", block['force'])
|
||||
block['count'] = len(block['locations'])
|
||||
block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max'])
|
||||
block['count']['target'] = multiworld.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
|
||||
world.random.shuffle(plando_blocks)
|
||||
multiworld.random.shuffle(plando_blocks)
|
||||
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
|
||||
if len(block['locations']) > 0
|
||||
else len(world.get_unfilled_locations(player)) - block['count']['target']))
|
||||
else len(multiworld.get_unfilled_locations(player)) - block['count']['target']))
|
||||
|
||||
for placement in plando_blocks:
|
||||
player = placement['player']
|
||||
@@ -915,19 +931,19 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
maxcount = placement['count']['target']
|
||||
from_pool = placement['from_pool']
|
||||
|
||||
candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
world.random.shuffle(candidates)
|
||||
world.random.shuffle(items)
|
||||
candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
multiworld.random.shuffle(candidates)
|
||||
multiworld.random.shuffle(items)
|
||||
count = 0
|
||||
err: typing.List[str] = []
|
||||
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
|
||||
for item_name in items:
|
||||
item = world.worlds[player].create_item(item_name)
|
||||
item = multiworld.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(world.state, item, False):
|
||||
if location.can_fill(multiworld.state, item, False):
|
||||
successful_pairs.append((item, location))
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
@@ -945,21 +961,21 @@ def distribute_planned(world: 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 {world.player_name[player]}, error(s): {' '.join(err)}",
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
|
||||
placement['force'])
|
||||
for (item, location) in successful_pairs:
|
||||
world.push_item(location, item, collect=False)
|
||||
multiworld.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:
|
||||
world.itempool.remove(item)
|
||||
multiworld.itempool.remove(item)
|
||||
except ValueError:
|
||||
warn(
|
||||
f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
|
||||
f"Could not remove {item} from pool for {multiworld.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} ({world.player_name[player]})") from e
|
||||
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e
|
||||
|
||||
192
Generate.py
@@ -26,6 +26,7 @@ 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():
|
||||
@@ -302,7 +303,9 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
NUMBER=(number if number > 1 else ''),
|
||||
player=player,
|
||||
PLAYER=(player if player > 1 else '')))
|
||||
new_name = new_name.strip()[:16]
|
||||
# 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()
|
||||
if new_name == "Archipelago":
|
||||
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||
return new_name
|
||||
@@ -315,33 +318,35 @@ 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, type: str, name: str) -> dict:
|
||||
def update_weights(weights: dict, new_weights: dict, update_type: str, name: str) -> dict:
|
||||
logging.debug(f'Applying {new_weights}')
|
||||
new_options = set(new_weights) - set(weights)
|
||||
weights.update(new_weights)
|
||||
cleaned_weights = {}
|
||||
for option in new_weights:
|
||||
option_name = option.lstrip("+")
|
||||
if option.startswith("+") and option_name in weights:
|
||||
cleaned_value = weights[option_name]
|
||||
new_value = new_weights[option]
|
||||
if isinstance(new_value, (set, dict)):
|
||||
cleaned_value.update(new_value)
|
||||
elif isinstance(new_value, list):
|
||||
cleaned_value.extend(new_value)
|
||||
else:
|
||||
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
||||
f" received {type(new_value).__name__}.")
|
||||
cleaned_weights[option_name] = cleaned_value
|
||||
else:
|
||||
cleaned_weights[option_name] = new_weights[option]
|
||||
new_options = set(cleaned_weights) - set(weights)
|
||||
weights.update(cleaned_weights)
|
||||
if new_options:
|
||||
for new_option in new_options:
|
||||
logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
|
||||
logging.warning(f'{update_type} Suboption "{new_option}" of "{name}" did not '
|
||||
f'overwrite a root option. '
|
||||
f'This is probably in error.')
|
||||
return weights
|
||||
@@ -357,15 +362,6 @@ 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}.")
|
||||
|
||||
|
||||
@@ -463,7 +459,11 @@ 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, AutoWorldRegister.world_types, limit=1)[0]
|
||||
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.")
|
||||
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,6 +473,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
world_type = AutoWorldRegister.world_types[ret.game]
|
||||
game_weights = weights[ret.game]
|
||||
|
||||
if any(weight.startswith("+") for weight in game_weights) or \
|
||||
any(weight.startswith("+") for weight in weights):
|
||||
raise Exception(f"Merge tag cannot be used outside of trigger contexts.")
|
||||
|
||||
if "triggers" in game_weights:
|
||||
weights = roll_triggers(weights, game_weights["triggers"])
|
||||
game_weights = weights[ret.game]
|
||||
@@ -485,120 +489,23 @@ 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 == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
# bad hardcoded behavior to make this work for now
|
||||
ret.plando_connections = []
|
||||
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":
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights, plando_options)
|
||||
if PlandoOptions.connections in plando_options:
|
||||
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")
|
||||
))
|
||||
|
||||
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:
|
||||
@@ -612,17 +519,6 @@ 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 Settings", func=generate_yamls),
|
||||
Component("Generate Template Options", 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
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
|
||||
from kivy.uix.image import AsyncImage
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
@@ -185,11 +185,16 @@ def run_gui():
|
||||
self.container = ContainerLayout()
|
||||
self.grid = GridLayout(cols=2)
|
||||
self.container.add_widget(self.grid)
|
||||
self.grid.add_widget(Label(text="General"))
|
||||
self.grid.add_widget(Label(text="Clients"))
|
||||
button_layout = self.grid # make buttons fill the window
|
||||
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)
|
||||
|
||||
def build_button(component: Component):
|
||||
def build_button(component: Component) -> Widget:
|
||||
"""
|
||||
Builds a button widget for a given component.
|
||||
|
||||
@@ -200,31 +205,26 @@ def run_gui():
|
||||
None. The button is added to the parent grid layout.
|
||||
|
||||
"""
|
||||
button = Button(text=component.display_name)
|
||||
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
||||
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()
|
||||
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
||||
box_layout.add_widget(button)
|
||||
box_layout.add_widget(image)
|
||||
button_layout.add_widget(box_layout)
|
||||
else:
|
||||
button_layout.add_widget(button)
|
||||
return box_layout
|
||||
return 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:
|
||||
build_button(tool[1])
|
||||
else:
|
||||
button_layout.add_widget(Label())
|
||||
tool_layout.layout.add_widget(build_button(tool[1]))
|
||||
# column 2
|
||||
if client:
|
||||
build_button(client[1])
|
||||
else:
|
||||
button_layout.add_widget(Label())
|
||||
client_layout.layout.add_widget(build_button(client[1]))
|
||||
|
||||
return self.container
|
||||
|
||||
|
||||
@@ -348,7 +348,8 @@ class LinksAwakeningClient():
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
self.stop_bizhawk_spam = False
|
||||
logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
|
||||
logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} "
|
||||
f"running {rom_name.decode('ascii', errors='replace')}")
|
||||
return
|
||||
except (BlockingIOError, TimeoutError, ConnectionResetError):
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
286
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 world
|
||||
world = MultiWorld(args.multi)
|
||||
# initialize the multiworld
|
||||
multiworld = MultiWorld(args.multi)
|
||||
|
||||
logger = logging.getLogger()
|
||||
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
world.plando_options = args.plando_options
|
||||
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
multiworld.plando_options = args.plando_options
|
||||
|
||||
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.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.set_options(args)
|
||||
world.set_item_links()
|
||||
world.state = CollectionState(world)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
multiworld.set_options(args)
|
||||
multiworld.set_item_links()
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.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,87 +103,93 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||
if not args.skip_output:
|
||||
AutoWorld.call_stage(world, "assert_generate")
|
||||
AutoWorld.call_stage(multiworld, "assert_generate")
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
AutoWorld.call_all(multiworld, "generate_early")
|
||||
|
||||
logger.info('')
|
||||
|
||||
for player in world.player_ids:
|
||||
for item_name, count in world.worlds[player].options.start_inventory.value.items():
|
||||
for player in multiworld.player_ids:
|
||||
for item_name, count in multiworld.worlds[player].options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
multiworld.push_precollected(multiworld.create_item(item_name, player))
|
||||
|
||||
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
|
||||
for item_name, count in getattr(multiworld.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.items():
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
multiworld.push_precollected(multiworld.create_item(item_name, player))
|
||||
# remove from_pool items also from early items handling, as starting is plenty early.
|
||||
early = world.early_items[player].get(item_name, 0)
|
||||
early = multiworld.early_items[player].get(item_name, 0)
|
||||
if early:
|
||||
world.early_items[player][item_name] = max(0, early-count)
|
||||
multiworld.early_items[player][item_name] = max(0, early-count)
|
||||
remaining_count = count-early
|
||||
if remaining_count > 0:
|
||||
local_early = world.early_local_items[player].get(item_name, 0)
|
||||
local_early = multiworld.early_local_items[player].get(item_name, 0)
|
||||
if local_early:
|
||||
world.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||
del local_early
|
||||
del early
|
||||
|
||||
logger.info('Creating World.')
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
logger.info('Creating MultiWorld.')
|
||||
AutoWorld.call_all(multiworld, "create_regions")
|
||||
|
||||
logger.info('Creating Items.')
|
||||
AutoWorld.call_all(world, "create_items")
|
||||
AutoWorld.call_all(multiworld, "create_items")
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
for player in world.player_ids:
|
||||
for player in multiworld.player_ids:
|
||||
# items can't be both local and non-local, prefer local
|
||||
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])
|
||||
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])
|
||||
|
||||
AutoWorld.call_all(world, "set_rules")
|
||||
AutoWorld.call_all(multiworld, "set_rules")
|
||||
|
||||
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:
|
||||
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:
|
||||
try:
|
||||
location = world.get_location(location_name, player)
|
||||
location = multiworld.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 world.worlds[player].location_name_to_id:
|
||||
if location_name not in multiworld.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 world.players > 1:
|
||||
locality_rules(world)
|
||||
if multiworld.players > 1:
|
||||
locality_rules(multiworld)
|
||||
else:
|
||||
world.worlds[1].options.non_local_items.value = set()
|
||||
world.worlds[1].options.local_items.value = set()
|
||||
multiworld.worlds[1].options.non_local_items.value = set()
|
||||
multiworld.worlds[1].options.local_items.value = set()
|
||||
|
||||
AutoWorld.call_all(world, "generate_basic")
|
||||
AutoWorld.call_all(multiworld, "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(world.start_inventory_from_pool[player].value for player in world.player_ids):
|
||||
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
|
||||
new_items: List[Item] = []
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
|
||||
player: getattr(multiworld.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.copy()
|
||||
for player in multiworld.player_ids
|
||||
}
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = world.worlds[player]
|
||||
player_world: AutoWorld.World = multiworld.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(world.itempool):
|
||||
for i, item in enumerate(multiworld.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(world.itempool[i+1:])
|
||||
new_items.extend(multiworld.itempool[i+1:])
|
||||
break
|
||||
else:
|
||||
new_items.append(item)
|
||||
@@ -193,19 +199,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"{world.get_player_name(player)}"
|
||||
raise Exception(f"{multiworld.get_player_name(player)}"
|
||||
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||
assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||
world.itempool[:] = new_items
|
||||
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||
multiworld.itempool[:] = new_items
|
||||
|
||||
# temporary home for item links, should be moved out of Main
|
||||
for group_id, group in world.groups.items():
|
||||
for group_id, group in multiworld.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 world.itempool:
|
||||
for item in multiworld.itempool:
|
||||
if item.player in counters and item.name in shared_pool:
|
||||
counters[item.player][item.name] += 1
|
||||
classifications[item.name] |= item.classification
|
||||
@@ -240,13 +246,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, world, "ItemLink")
|
||||
world.regions.append(region)
|
||||
region = Region("Menu", group_id, multiworld, "ItemLink")
|
||||
multiworld.regions.append(region)
|
||||
locations = region.locations
|
||||
for item in world.itempool:
|
||||
for item in multiworld.itempool:
|
||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||
if count:
|
||||
loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
|
||||
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.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_)
|
||||
@@ -257,10 +263,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
new_itempool.append(item)
|
||||
|
||||
itemcount = len(world.itempool)
|
||||
world.itempool = new_itempool
|
||||
itemcount = len(multiworld.itempool)
|
||||
multiworld.itempool = new_itempool
|
||||
|
||||
while itemcount > len(world.itempool):
|
||||
while itemcount > len(multiworld.itempool):
|
||||
items_to_add = []
|
||||
for player in group["players"]:
|
||||
if group["link_replacement"]:
|
||||
@@ -268,64 +274,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(world, "create_item", item_player,
|
||||
items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player,
|
||||
group["replacement_items"][player]))
|
||||
else:
|
||||
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)])
|
||||
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)])
|
||||
|
||||
if any(world.item_links.values()):
|
||||
world._all_state = None
|
||||
if any(multiworld.item_links.values()):
|
||||
multiworld._all_state = None
|
||||
|
||||
logger.info("Running Item Plando.")
|
||||
|
||||
distribute_planned(world)
|
||||
distribute_planned(multiworld)
|
||||
|
||||
logger.info('Running Pre Main Fill.')
|
||||
|
||||
AutoWorld.call_all(world, "pre_fill")
|
||||
AutoWorld.call_all(multiworld, "pre_fill")
|
||||
|
||||
logger.info(f'Filling the world with {len(world.itempool)} items.')
|
||||
logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.')
|
||||
|
||||
if world.algorithm == 'flood':
|
||||
flood_items(world) # different algo, biased towards early game progress items
|
||||
elif world.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world)
|
||||
if multiworld.algorithm == 'flood':
|
||||
flood_items(multiworld) # different algo, biased towards early game progress items
|
||||
elif multiworld.algorithm == 'balanced':
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
AutoWorld.call_all(world, 'post_fill')
|
||||
AutoWorld.call_all(multiworld, 'post_fill')
|
||||
|
||||
if world.players > 1 and not args.skip_prog_balancing:
|
||||
balance_multiworld_progression(world)
|
||||
if multiworld.players > 1 and not args.skip_prog_balancing:
|
||||
balance_multiworld_progression(multiworld)
|
||||
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
|
||||
world.random.passthrough = False
|
||||
multiworld.random.passthrough = False
|
||||
|
||||
if args.skip_output:
|
||||
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
|
||||
return world
|
||||
return multiworld
|
||||
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
outfilebase = 'AP_' + multiworld.seed_name
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
|
||||
is not world.worlds[player].generate_output.__code__]
|
||||
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
||||
is not multiworld.worlds[player].generate_output.__code__]
|
||||
with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
check_accessibility_task = pool.submit(multiworld.fulfills_accessibility)
|
||||
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, multiworld, "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, world, "generate_output", player, temp_dir))
|
||||
pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir))
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
|
||||
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
@@ -334,38 +340,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(world.player_name.items())]]
|
||||
for slot in world.player_ids:
|
||||
player_world: AutoWorld.World = world.worlds[slot]
|
||||
names = [[name for player, name in sorted(multiworld.player_name.items())]]
|
||||
for slot in multiworld.player_ids:
|
||||
player_world: AutoWorld.World = multiworld.worlds[slot]
|
||||
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
|
||||
client_versions[slot] = player_world.required_client_version
|
||||
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],
|
||||
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],
|
||||
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 world.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
|
||||
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 slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
for slot in multiworld.player_ids:
|
||||
slot_data[slot] = multiworld.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 world.groups:
|
||||
if location.item.player not in multiworld.groups:
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
else:
|
||||
for player in world.groups[location.item.player]["players"]:
|
||||
for player in multiworld.groups[location.item.player]["players"]:
|
||||
precollected_hints[player].add(hint)
|
||||
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids}
|
||||
for location in multiworld.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: " \
|
||||
@@ -375,18 +381,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 world.worlds[location.player].options.start_location_hints:
|
||||
if location.name in multiworld.worlds[location.player].options.start_location_hints:
|
||||
precollect_hint(location)
|
||||
elif location.item.name in world.worlds[location.item.player].options.start_hints:
|
||||
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
|
||||
precollect_hint(location)
|
||||
elif any([location.item.name in world.worlds[player].options.start_hints
|
||||
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
||||
elif any([location.item.name in multiworld.worlds[player].options.start_hints
|
||||
for player in multiworld.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 world.worlds.values()
|
||||
for game_world in multiworld.worlds.values()
|
||||
}
|
||||
|
||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||
@@ -394,7 +400,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 world.player_name.items()},
|
||||
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": baked_server_options,
|
||||
@@ -404,10 +410,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": world.seed_name,
|
||||
"seed_name": multiworld.seed_name,
|
||||
"datapackage": data_package,
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
@@ -417,7 +423,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 world.can_beat_game():
|
||||
if not multiworld.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
@@ -430,12 +436,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
if args.spoiler > 1:
|
||||
logger.info('Calculating playthrough.')
|
||||
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
|
||||
if args.spoiler:
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
zipfilename = output_path(f"AP_{multiworld.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:
|
||||
@@ -443,4 +449,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 world
|
||||
return multiworld
|
||||
|
||||
@@ -4,14 +4,29 @@ 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)
|
||||
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
|
||||
_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'),))
|
||||
|
||||
if not update_ran:
|
||||
for entry in os.scandir(os.path.join(local_dir, "worlds")):
|
||||
|
||||
@@ -656,7 +656,8 @@ class Context:
|
||||
else:
|
||||
return self.player_names[team, slot]
|
||||
|
||||
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
||||
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
|
||||
recipients: typing.Sequence[int] = None):
|
||||
"""Send and remember hints."""
|
||||
if only_new:
|
||||
hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]]
|
||||
@@ -685,12 +686,13 @@ class Context:
|
||||
for slot in new_hint_events:
|
||||
self.on_new_hint(team, slot)
|
||||
for slot, hint_data in concerns.items():
|
||||
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))
|
||||
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))
|
||||
|
||||
# "events"
|
||||
|
||||
@@ -705,15 +707,18 @@ class Context:
|
||||
self.save() # save goal completion flag
|
||||
|
||||
def on_new_hint(self, team: int, slot: int):
|
||||
key: str = f"_read_hints_{team}_{slot}"
|
||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||
if targets:
|
||||
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
|
||||
self.on_changed_hints(team, slot)
|
||||
self.broadcast(self.clients[team][slot], [{
|
||||
"cmd": "RoomUpdate",
|
||||
"hint_points": get_slot_points(self, team, slot)
|
||||
}])
|
||||
|
||||
def on_changed_hints(self, team: int, slot: int):
|
||||
key: str = f"_read_hints_{team}_{slot}"
|
||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||
if targets:
|
||||
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
|
||||
|
||||
def on_client_status_change(self, team: int, slot: int):
|
||||
key: str = f"_read_client_status_{team}_{slot}"
|
||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||
@@ -973,7 +978,10 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
"hint_points": get_slot_points(ctx, team, slot),
|
||||
"checked_locations": new_locations, # send back new checks only
|
||||
}])
|
||||
|
||||
old_hints = ctx.hints[team, slot].copy()
|
||||
ctx.recheck_hints(team, slot)
|
||||
if old_hints != ctx.hints[team, slot]:
|
||||
ctx.on_changed_hints(team, slot)
|
||||
ctx.save()
|
||||
|
||||
|
||||
@@ -1050,17 +1058,19 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
|
||||
if picks[0][1] == 100:
|
||||
return picks[0][0], True, "Perfect Match"
|
||||
elif picks[0][1] < 75:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches, " \
|
||||
f"did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
elif dif > 5:
|
||||
return picks[0][0], True, "Close Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Too many close matches, did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Too many close matches for '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
else:
|
||||
if picks[0][1] > 90:
|
||||
return picks[0][0], True, "Only Option Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Did you mean {picks[0][0]}? ({picks[0][1]}% sure)"
|
||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||
|
||||
|
||||
class CommandMeta(type):
|
||||
@@ -1429,9 +1439,13 @@ 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))
|
||||
self.ctx.notify_hints(self.client.team, list(hints), recipients=(self.client.slot,))
|
||||
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():
|
||||
@@ -1958,7 +1972,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forbid_release(self, player_name: str) -> bool:
|
||||
""""Disallow the specified player from using the !release command."""
|
||||
"""Disallow the specified player from using the !release command."""
|
||||
player = self.resolve_player(player_name)
|
||||
if player:
|
||||
team, slot, name = player
|
||||
@@ -2210,25 +2224,24 @@ 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():
|
||||
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.")
|
||||
inactivity_shutdown()
|
||||
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:
|
||||
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.")
|
||||
inactivity_shutdown()
|
||||
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, item_id: int, player: int = 0, **kwargs) -> None:
|
||||
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
||||
def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) -> None:
|
||||
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
||||
|
||||
|
||||
class Hint(typing.NamedTuple):
|
||||
|
||||
@@ -195,10 +195,10 @@ def set_icon(window):
|
||||
window.tk.call('wm', 'iconphoto', window._w, logo)
|
||||
|
||||
def adjust(args):
|
||||
# Create a fake world and OOTWorld to use as a base
|
||||
world = MultiWorld(1)
|
||||
world.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(world, 1)
|
||||
# Create a fake multiworld and OOTWorld to use as a base
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(multiworld, 1)
|
||||
# Set options in the fake OOTWorld
|
||||
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
||||
result = getattr(args, name, None)
|
||||
|
||||
58
Options.py
@@ -1,19 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
import functools
|
||||
import logging
|
||||
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
|
||||
from Utils import get_fuzzy_results, is_iterable_except_str
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from BaseClasses import PlandoOptions
|
||||
@@ -42,6 +41,11 @@ class AssembleOptions(abc.ABCMeta):
|
||||
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||
name.startswith("alias_")}
|
||||
|
||||
assert (
|
||||
name in {"Option", "VerifyKeys"} or # base abstract classes don't need default
|
||||
"default" in attrs or
|
||||
any(hasattr(base, "default") for base in bases)
|
||||
), f"Option class {name} needs default value"
|
||||
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
|
||||
|
||||
# auto-alias Off and On being parsed as True and False
|
||||
@@ -59,6 +63,7 @@ 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"
|
||||
@@ -96,7 +101,7 @@ T = typing.TypeVar('T')
|
||||
|
||||
class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
value: T
|
||||
default = 0
|
||||
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
|
||||
|
||||
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
|
||||
# Handled in get_option_name()
|
||||
@@ -106,8 +111,9 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
supports_weighting = True
|
||||
|
||||
# filled by AssembleOptions:
|
||||
name_lookup: typing.Dict[T, str]
|
||||
options: typing.Dict[str, int]
|
||||
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
|
||||
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
|
||||
options: typing.ClassVar[typing.Dict[str, int]]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.current_option_name})"
|
||||
@@ -160,6 +166,8 @@ class FreeText(Option[str]):
|
||||
"""Text option that allows users to enter strings.
|
||||
Needs to be validated by the world or option definition."""
|
||||
|
||||
default = ""
|
||||
|
||||
def __init__(self, value: str):
|
||||
assert isinstance(value, str), "value of FreeText must be a string"
|
||||
self.value = value
|
||||
@@ -180,9 +188,18 @@ 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)
|
||||
@@ -598,7 +615,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]
|
||||
@@ -727,7 +744,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, value)
|
||||
return super().__new__(cls)
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
@@ -765,7 +782,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
value: typing.Any
|
||||
|
||||
@classmethod
|
||||
def verify_keys(cls, data: typing.List[str]):
|
||||
def verify_keys(cls, data: typing.Iterable[str]) -> None:
|
||||
if cls.valid_keys:
|
||||
data = set(data)
|
||||
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
||||
@@ -802,7 +819,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
|
||||
|
||||
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
||||
default: typing.Dict[str, typing.Any] = {}
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
@@ -843,11 +860,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: typing.List[typing.Any] = []
|
||||
default = ()
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.List[typing.Any]):
|
||||
self.value = deepcopy(value)
|
||||
def __init__(self, value: typing.Iterable[typing.Any]):
|
||||
self.value = list(deepcopy(value))
|
||||
super(OptionList, self).__init__()
|
||||
|
||||
@classmethod
|
||||
@@ -856,7 +873,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if type(data) == list:
|
||||
if is_iterable_except_str(data):
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
@@ -869,7 +886,7 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
|
||||
|
||||
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
@@ -882,7 +899,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if isinstance(data, (list, set, frozenset)):
|
||||
if is_iterable_except_str(data):
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
@@ -932,7 +949,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)
|
||||
|
||||
@@ -1110,6 +1127,11 @@ 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, APDeltaPatch
|
||||
from worlds.Files import AutoPatchRegister, APAutoPatchInterface
|
||||
|
||||
|
||||
class RomMeta(TypedDict):
|
||||
@@ -20,7 +20,7 @@ class RomMeta(TypedDict):
|
||||
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
|
||||
auto_handler = AutoPatchRegister.get_handler(patch_file)
|
||||
if auto_handler:
|
||||
handler: APDeltaPatch = auto_handler(patch_file)
|
||||
handler: APAutoPatchInterface = auto_handler(patch_file)
|
||||
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
|
||||
handler.patch(target)
|
||||
return {"server": handler.server,
|
||||
|
||||
13
README.md
@@ -25,7 +25,7 @@ Currently, the following games are supported:
|
||||
* Hollow Knight
|
||||
* The Witness
|
||||
* Sonic Adventure 2: Battle
|
||||
* Starcraft 2: Wings of Liberty
|
||||
* Starcraft 2
|
||||
* Donkey Kong Country 3
|
||||
* Dark Souls 3
|
||||
* Super Mario World
|
||||
@@ -58,6 +58,13 @@ 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
|
||||
@@ -79,9 +86,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. The installers function on Windows only.
|
||||
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.
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
## 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.sc2wol.Client import launch
|
||||
from worlds.sc2.Client import launch
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
27
Utils.py
@@ -19,14 +19,13 @@ import warnings
|
||||
from argparse import Namespace
|
||||
from settings import Settings, get_settings
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||
from yaml import load, load_all, dump, SafeLoader
|
||||
from typing_extensions import TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
|
||||
try:
|
||||
from yaml import CLoader as UnsafeLoader
|
||||
from yaml import CDumper as Dumper
|
||||
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
||||
except ImportError:
|
||||
from yaml import Loader as UnsafeLoader
|
||||
from yaml import Dumper
|
||||
from yaml import Loader as UnsafeLoader, SafeLoader, Dumper
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import tkinter
|
||||
@@ -47,7 +46,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.4"
|
||||
__version__ = "0.4.5"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -226,6 +225,9 @@ class UniqueKeyLoader(SafeLoader):
|
||||
if key in mapping:
|
||||
logging.error(f"YAML duplicates sanity check failed{key_node.start_mark}")
|
||||
raise KeyError(f"Duplicate key {key} found in YAML. Already found keys: {mapping}.")
|
||||
if (str(key).startswith("+") and (str(key)[1:] in mapping)) or (f"+{key}" in mapping):
|
||||
logging.error(f"YAML merge duplicates sanity check failed{key_node.start_mark}")
|
||||
raise KeyError(f"Equivalent key {key} found in YAML. Already found keys: {mapping}.")
|
||||
mapping.add(key)
|
||||
return super().construct_mapping(node, deep)
|
||||
|
||||
@@ -714,7 +716,7 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
import ctypes
|
||||
style = 0x10 if error else 0x0
|
||||
return ctypes.windll.user32.MessageBoxW(0, text, title, style)
|
||||
|
||||
|
||||
# fall back to tk
|
||||
try:
|
||||
import tkinter
|
||||
@@ -871,8 +873,8 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
|
||||
Example usage in Main code:
|
||||
from Utils import visualize_regions
|
||||
for player in world.player_ids:
|
||||
visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
|
||||
for player in multiworld.player_ids:
|
||||
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.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
|
||||
@@ -968,3 +970,10 @@ 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:
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
files = request.files.getlist('file')
|
||||
options = get_yaml_data(files)
|
||||
if isinstance(options, Markup):
|
||||
return {"text": options.striptags()}, 400
|
||||
if isinstance(options, str):
|
||||
|
||||
@@ -28,7 +28,7 @@ def check():
|
||||
results, _ = roll_options(options)
|
||||
if len(options) > 1:
|
||||
# offer combined file back
|
||||
combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
|
||||
combined_yaml = "\n---\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:
|
||||
|
||||
@@ -25,16 +25,16 @@ window.addEventListener('load', () => {
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
for (let category of categories) {
|
||||
let hide_id = category.id.split('_')[0];
|
||||
if (hide_id === 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
category.addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const tab_header = document.getElementById(hide_id+'_header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
160
WebHostLib/static/styles/sc2Tracker.css
Normal file
@@ -0,0 +1,160 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#tracker-table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.inventory-table-area{
|
||||
border: 2px solid #000000;
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px 3px 10px;
|
||||
}
|
||||
|
||||
.inventory-table-area:has(.inventory-table-terran) {
|
||||
width: 690px;
|
||||
background-color: #525494;
|
||||
}
|
||||
|
||||
.inventory-table-area:has(.inventory-table-zerg) {
|
||||
width: 360px;
|
||||
background-color: #9d60d2;
|
||||
}
|
||||
|
||||
.inventory-table-area:has(.inventory-table-protoss) {
|
||||
width: 400px;
|
||||
background-color: #d2b260;
|
||||
}
|
||||
|
||||
#tracker-table .inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.inventory-table td.title{
|
||||
padding-top: 10px;
|
||||
height: 20px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
border: 1px solid #000000;
|
||||
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.inventory-table img.acquired{
|
||||
filter: none;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.inventory-table .tint-terran img.acquired {
|
||||
filter: sepia(100%) saturate(300%) brightness(130%) hue-rotate(120deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-protoss img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(180deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-level-1 img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-level-2 img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(120deg)
|
||||
}
|
||||
|
||||
.inventory-table .tint-level-3 img.acquired {
|
||||
filter: sepia(100%) saturate(1000%) brightness(110%) hue-rotate(60deg) hue-rotate(240deg)
|
||||
}
|
||||
|
||||
.inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inventory-table div.item-count {
|
||||
width: 160px;
|
||||
text-align: left;
|
||||
color: black;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
border: 2px solid #000000;
|
||||
border-radius: 4px;
|
||||
background-color: #87b678;
|
||||
padding: 10px 3px 3px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
#location-table td:has(.location-column) {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#location-table .location-column {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#location-table .location-column .spacer {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 710px;
|
||||
background-color: #525494;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table td.title{
|
||||
padding-top: 10px;
|
||||
height: 20px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
border: 1px solid #000000;
|
||||
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
text-align: left;
|
||||
color: black;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 710px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #525494;
|
||||
padding: 10px 3px 3px;
|
||||
font-family: "JuraBook", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -69,8 +69,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<select name="collect_mode" id="collect_mode">
|
||||
<option value="goal">Allow !collect after goal completion</option>
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !collect after 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>
|
||||
|
||||
|
||||
@@ -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}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></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>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
|
||||
1092
WebHostLib/templates/tracker__Starcraft2.html
Normal file
@@ -1,366 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/sc2wolTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/sc2wolTracker.js') }}"></script>
|
||||
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/jura" type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td colspan="15" class="title">
|
||||
Starting Resources
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Starting Minerals'] }}" class="{{ 'acquired' if '+15 Starting Minerals' in acquired_items }}" title="Starting Minerals" /></td>
|
||||
<td colspan="2"><div class="item-count">+{{ minerals_count }}</div></td>
|
||||
<td><img src="{{ icons['Starting Vespene'] }}" class="{{ 'acquired' if '+15 Starting Vespene' in acquired_items }}" title="Starting Vespene" /></td>
|
||||
<td colspan="2"><div class="item-count">+{{ vespene_count }}</div></td>
|
||||
<!--
|
||||
<td><img src="{{ icons['Starting Supply'] }}" class="{{ 'acquired' if '+2 Starting Supply' in acquired_items }}" title="Starting Supply" /></td>
|
||||
<td colspan="2"><div class="item-count">+{{ supply_count }}</div></td>
|
||||
-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="15" class="title">
|
||||
Weapon & Armor Upgrades
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ infantry_weapon_url }}" class="{{ 'acquired' if 'Progressive Infantry Weapon' in acquired_items }}" title="Progressive Infantry Weapons{% if infantry_weapon_level > 0 %} (Level {{ infantry_weapon_level }}){% endif %}" /></td>
|
||||
<td><img src="{{ infantry_armor_url }}" class="{{ 'acquired' if 'Progressive Infantry Armor' in acquired_items }}" title="Progressive Infantry Armor{% if infantry_armor_level > 0 %} (Level {{ infantry_armor_level }}){% endif %}" /></td>
|
||||
<td><img src="{{ vehicle_weapon_url }}" class="{{ 'acquired' if 'Progressive Vehicle Weapon' in acquired_items }}" title="Progressive Vehicle Weapons{% if vehicle_weapon_level > 0 %} (Level {{ vehicle_weapon_level }}){% endif %}" /></td>
|
||||
<td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td>
|
||||
<td><img src="{{ ship_weapon_url }}" class="{{ 'acquired' if 'Progressive Ship Weapon' in acquired_items }}" title="Progressive Ship Weapons{% if ship_weapon_level > 0 %} (Level {{ ship_weapon_level }}){% endif %}" /></td>
|
||||
<td><img src="{{ ship_armor_url }}" class="{{ 'acquired' if 'Progressive Ship Armor' in acquired_items }}" title="Progressive Ship Armor{% if ship_armor_level > 0 %} (Level {{ ship_armor_level }}){% endif %}" /></td>
|
||||
<td colspan="2"></td>
|
||||
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
|
||||
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="15" class="title">
|
||||
Base
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
|
||||
<td><img src="{{ icons['Projectile Accelerator (Bunker)'] }}" class="{{ 'acquired' if 'Projectile Accelerator (Bunker)' in acquired_items }}" title="Projectile Accelerator (Bunker)" /></td>
|
||||
<td><img src="{{ icons['Neosteel Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Neosteel Bunker (Bunker)' in acquired_items }}" title="Neosteel Bunker (Bunker)" /></td>
|
||||
<td><img src="{{ icons['Shrike Turret (Bunker)'] }}" class="{{ 'acquired' if 'Shrike Turret (Bunker)' in acquired_items }}" title="Shrike Turret (Bunker)" /></td>
|
||||
<td><img src="{{ icons['Fortified Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Fortified Bunker (Bunker)' in acquired_items }}" title="Fortified Bunker (Bunker)" /></td>
|
||||
<td colspan="3"></td>
|
||||
<td><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
|
||||
<td><img src="{{ icons['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td>
|
||||
<td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
|
||||
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
|
||||
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
|
||||
<td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
|
||||
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="7" class="title">
|
||||
Infantry
|
||||
</td>
|
||||
<td></td>
|
||||
<td colspan="7" class="title">
|
||||
Vehicles
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
|
||||
<td><img src="{{ stimpack_marine_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marine)' in acquired_items }}" title="{{ stimpack_marine_name }}" /></td>
|
||||
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td>
|
||||
<td><img src="{{ icons['Laser Targeting System (Marine)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marine)' in acquired_items }}" title="Laser Targeting System (Marine)" /></td>
|
||||
<td><img src="{{ icons['Magrail Munitions (Marine)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marine)' in acquired_items }}" title="Magrail Munitions (Marine)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Marine)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Marine)' in acquired_items }}" title="Optimized Logistics (Marine)" /></td>
|
||||
<td colspan="2"></td>
|
||||
<td><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
|
||||
<td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td>
|
||||
<td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td>
|
||||
<td><img src="{{ icons['Hellbat Aspect (Hellion)'] }}" class="{{ 'acquired' if 'Hellbat Aspect (Hellion)' in acquired_items }}" title="Hellbat Aspect (Hellion)" /></td>
|
||||
<td><img src="{{ icons['Smart Servos (Hellion)'] }}" class="{{ 'acquired' if 'Smart Servos (Hellion)' in acquired_items }}" title="Smart Servos (Hellion)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Hellion)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Hellion)' in acquired_items }}" title="Optimized Logistics (Hellion)" /></td>
|
||||
<td><img src="{{ icons['Jump Jets (Hellion)'] }}" class="{{ 'acquired' if 'Jump Jets (Hellion)' in acquired_items }}" title="Jump Jets (Hellion)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
|
||||
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
|
||||
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
|
||||
<td><img src="{{ icons['Restoration (Medic)'] }}" class="{{ 'acquired' if 'Restoration (Medic)' in acquired_items }}" title="Restoration (Medic)" /></td>
|
||||
<td><img src="{{ icons['Optical Flare (Medic)'] }}" class="{{ 'acquired' if 'Optical Flare (Medic)' in acquired_items }}" title="Optical Flare (Medic)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Medic)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Medic)' in acquired_items }}" title="Optimized Logistics (Medic)" /></td>
|
||||
<td colspan="2"></td>
|
||||
<td></td>
|
||||
<td><img src="{{ stimpack_hellion_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Hellion)' in acquired_items }}" title="{{ stimpack_hellion_name }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
|
||||
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
|
||||
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
|
||||
<td><img src="{{ stimpack_firebat_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Firebat)' in acquired_items }}" title="{{ stimpack_firebat_name }}" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Firebat)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Firebat)' in acquired_items }}" title="Optimized Logistics (Firebat)" /></td>
|
||||
<td colspan="3"></td>
|
||||
<td><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
|
||||
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
|
||||
<td><img src="{{ icons['Ion Thrusters (Vulture)'] }}" class="{{ 'acquired' if 'Ion Thrusters (Vulture)' in acquired_items }}" title="Ion Thrusters (Vulture)" /></td>
|
||||
<td><img src="{{ icons['Auto Launchers (Vulture)'] }}" class="{{ 'acquired' if 'Auto Launchers (Vulture)' in acquired_items }}" title="Auto Launchers (Vulture)" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Cerberus Mine (Spider Mine)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Spider Mine)' in acquired_items }}" title="Cerberus Mine (Spider Mine)" /></td>
|
||||
<td><img src="{{ icons['High Explosive Munition (Spider Mine)'] }}" class="{{ 'acquired' if 'High Explosive Munition (Spider Mine)' in acquired_items }}" title="High Explosive Munition (Spider Mine)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
|
||||
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
|
||||
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
|
||||
<td><img src="{{ stimpack_marauder_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marauder)' in acquired_items }}" title="{{ stimpack_marauder_name }}" /></td>
|
||||
<td><img src="{{ icons['Laser Targeting System (Marauder)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marauder)' in acquired_items }}" title="Laser Targeting System (Marauder)" /></td>
|
||||
<td><img src="{{ icons['Magrail Munitions (Marauder)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marauder)' in acquired_items }}" title="Magrail Munitions (Marauder)" /></td>
|
||||
<td><img src="{{ icons['Internal Tech Module (Marauder)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Marauder)' in acquired_items }}" title="Internal Tech Module (Marauder)" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
|
||||
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
|
||||
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
|
||||
<td><img src="{{ icons['Jump Jets (Goliath)'] }}" class="{{ 'acquired' if 'Jump Jets (Goliath)' in acquired_items }}" title="Jump Jets (Goliath)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Goliath)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Goliath)' in acquired_items }}" title="Optimized Logistics (Goliath)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
|
||||
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
|
||||
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
|
||||
<td><img src="{{ stimpack_reaper_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Reaper)' in acquired_items }}" title="{{ stimpack_reaper_name }}" /></td>
|
||||
<td><img src="{{ icons['Laser Targeting System (Reaper)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Reaper)' in acquired_items }}" title="Laser Targeting System (Reaper)" /></td>
|
||||
<td><img src="{{ icons['Advanced Cloaking Field (Reaper)'] }}" class="{{ 'acquired' if 'Advanced Cloaking Field (Reaper)' in acquired_items }}" title="Advanced Cloaking Field (Reaper)" /></td>
|
||||
<td><img src="{{ icons['Spider Mines (Reaper)'] }}" class="{{ 'acquired' if 'Spider Mines (Reaper)' in acquired_items }}" title="Spider Mines (Reaper)" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
|
||||
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
|
||||
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
|
||||
<td><img src="{{ icons['Hyperfluxor (Diamondback)'] }}" class="{{ 'acquired' if 'Hyperfluxor (Diamondback)' in acquired_items }}" title="Hyperfluxor (Diamondback)" /></td>
|
||||
<td><img src="{{ icons['Burst Capacitors (Diamondback)'] }}" class="{{ 'acquired' if 'Burst Capacitors (Diamondback)' in acquired_items }}" title="Burst Capacitors (Diamondback)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Diamondback)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Diamondback)' in acquired_items }}" title="Optimized Logistics (Diamondback)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Combat Drugs (Reaper)'] }}" class="{{ 'acquired' if 'Combat Drugs (Reaper)' in acquired_items }}" title="Combat Drugs (Reaper)" /></td>
|
||||
<td colspan="6"></td>
|
||||
<td><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
|
||||
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
|
||||
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
|
||||
<td><img src="{{ icons['Jump Jets (Siege Tank)'] }}" class="{{ 'acquired' if 'Jump Jets (Siege Tank)' in acquired_items }}" title="Jump Jets (Siege Tank)" /></td>
|
||||
<td><img src="{{ icons['Spider Mines (Siege Tank)'] }}" class="{{ 'acquired' if 'Spider Mines (Siege Tank)' in acquired_items }}" title="Spider Mines (Siege Tank)" /></td>
|
||||
<td><img src="{{ icons['Smart Servos (Siege Tank)'] }}" class="{{ 'acquired' if 'Smart Servos (Siege Tank)' in acquired_items }}" title="Smart Servos (Siege Tank)" /></td>
|
||||
<td><img src="{{ icons['Graduating Range (Siege Tank)'] }}" class="{{ 'acquired' if 'Graduating Range (Siege Tank)' in acquired_items }}" title="Graduating Range (Siege Tank)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
|
||||
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
|
||||
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
|
||||
<td><img src="{{ icons['EMP Rounds (Ghost)'] }}" class="{{ 'acquired' if 'EMP Rounds (Ghost)' in acquired_items }}" title="EMP Rounds (Ghost)" /></td>
|
||||
<td><img src="{{ icons['Lockdown (Ghost)'] }}" class="{{ 'acquired' if 'Lockdown (Ghost)' in acquired_items }}" title="Lockdown (Ghost)" /></td>
|
||||
<td colspan="3"></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Laser Targeting System (Siege Tank)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Siege Tank)' in acquired_items }}" title="Laser Targeting System (Siege Tank)" /></td>
|
||||
<td><img src="{{ icons['Advanced Siege Tech (Siege Tank)'] }}" class="{{ 'acquired' if 'Advanced Siege Tech (Siege Tank)' in acquired_items }}" title="Advanced Siege Tech (Siege Tank)" /></td>
|
||||
<td><img src="{{ icons['Internal Tech Module (Siege Tank)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Siege Tank)' in acquired_items }}" title="Internal Tech Module (Siege Tank)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
|
||||
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
|
||||
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
|
||||
<td><img src="{{ icons['Impaler Rounds (Spectre)'] }}" class="{{ 'acquired' if 'Impaler Rounds (Spectre)' in acquired_items }}" title="Impaler Rounds (Spectre)" /></td>
|
||||
<td colspan="4"></td>
|
||||
<td><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
|
||||
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
|
||||
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
|
||||
<td><img src="{{ high_impact_payload_thor_url }}" class="{{ 'acquired' if 'Progressive High Impact Payload (Thor)' in acquired_items }}" title="{{ high_impact_payload_thor_name }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="8"></td>
|
||||
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Predator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Predator)' in acquired_items }}" title="Optimized Logistics (Predator)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="8"></td>
|
||||
<td><img src="{{ icons['Widow Mine'] }}" class="{{ 'acquired' if 'Widow Mine' in acquired_items }}" title="Widow Mine" /></td>
|
||||
<td><img src="{{ icons['Drilling Claws (Widow Mine)'] }}" class="{{ 'acquired' if 'Drilling Claws (Widow Mine)' in acquired_items }}" title="Drilling Claws (Widow Mine)" /></td>
|
||||
<td><img src="{{ icons['Concealment (Widow Mine)'] }}" class="{{ 'acquired' if 'Concealment (Widow Mine)' in acquired_items }}" title="Concealment (Widow Mine)" /></td>
|
||||
<td><img src="{{ icons['Black Market Launchers (Widow Mine)'] }}" class="{{ 'acquired' if 'Black Market Launchers (Widow Mine)' in acquired_items }}" title="Black Market Launchers (Widow Mine)" /></td>
|
||||
<td><img src="{{ icons['Executioner Missiles (Widow Mine)'] }}" class="{{ 'acquired' if 'Executioner Missiles (Widow Mine)' in acquired_items }}" title="Executioner Missiles (Widow Mine)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="8"></td>
|
||||
<td><img src="{{ icons['Cyclone'] }}" class="{{ 'acquired' if 'Cyclone' in acquired_items }}" title="Cyclone" /></td>
|
||||
<td><img src="{{ icons['Mag-Field Accelerators (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Accelerators (Cyclone)' in acquired_items }}" title="Mag-Field Accelerators (Cyclone)" /></td>
|
||||
<td><img src="{{ icons['Mag-Field Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Launchers (Cyclone)' in acquired_items }}" title="Mag-Field Launchers (Cyclone)" /></td>
|
||||
<td><img src="{{ icons['Targeting Optics (Cyclone)'] }}" class="{{ 'acquired' if 'Targeting Optics (Cyclone)' in acquired_items }}" title="Targeting Optics (Cyclone)" /></td>
|
||||
<td><img src="{{ icons['Rapid Fire Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Rapid Fire Launchers (Cyclone)' in acquired_items }}" title="Rapid Fire Launchers (Cyclone)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="15" class="title">
|
||||
Starships
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
|
||||
<td><img src="{{ icons['Rapid Deployment Tube (Medivac)'] }}" class="{{ 'acquired' if 'Rapid Deployment Tube (Medivac)' in acquired_items }}" title="Rapid Deployment Tube (Medivac)" /></td>
|
||||
<td><img src="{{ icons['Advanced Healing AI (Medivac)'] }}" class="{{ 'acquired' if 'Advanced Healing AI (Medivac)' in acquired_items }}" title="Advanced Healing AI (Medivac)" /></td>
|
||||
<td><img src="{{ icons['Expanded Hull (Medivac)'] }}" class="{{ 'acquired' if 'Expanded Hull (Medivac)' in acquired_items }}" title="Expanded Hull (Medivac)" /></td>
|
||||
<td><img src="{{ icons['Afterburners (Medivac)'] }}" class="{{ 'acquired' if 'Afterburners (Medivac)' in acquired_items }}" title="Afterburners (Medivac)" /></td>
|
||||
<td colspan="3"></td>
|
||||
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
|
||||
<td><img src="{{ icons['Bio Mechanical Repair Drone (Raven)'] }}" class="{{ 'acquired' if 'Bio Mechanical Repair Drone (Raven)' in acquired_items }}" title="Bio Mechanical Repair Drone (Raven)" /></td>
|
||||
<td><img src="{{ icons['Spider Mines (Raven)'] }}" class="{{ 'acquired' if 'Spider Mines (Raven)' in acquired_items }}" title="Spider Mines (Raven)" /></td>
|
||||
<td><img src="{{ icons['Railgun Turret (Raven)'] }}" class="{{ 'acquired' if 'Railgun Turret (Raven)' in acquired_items }}" title="Railgun Turret (Raven)" /></td>
|
||||
<td><img src="{{ icons['Hunter-Seeker Weapon (Raven)'] }}" class="{{ 'acquired' if 'Hunter-Seeker Weapon (Raven)' in acquired_items }}" title="Hunter-Seeker Weapon (Raven)" /></td>
|
||||
<td><img src="{{ icons['Interference Matrix (Raven)'] }}" class="{{ 'acquired' if 'Interference Matrix (Raven)' in acquired_items }}" title="Interference Matrix (Raven)" /></td>
|
||||
<td><img src="{{ icons['Anti-Armor Missile (Raven)'] }}" class="{{ 'acquired' if 'Anti-Armor Missile (Raven)' in acquired_items }}" title="Anti-Armor Missile (Raven)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
|
||||
<td><img src="{{ icons['Tomahawk Power Cells (Wraith)'] }}" class="{{ 'acquired' if 'Tomahawk Power Cells (Wraith)' in acquired_items }}" title="Tomahawk Power Cells (Wraith)" /></td>
|
||||
<td><img src="{{ icons['Displacement Field (Wraith)'] }}" class="{{ 'acquired' if 'Displacement Field (Wraith)' in acquired_items }}" title="Displacement Field (Wraith)" /></td>
|
||||
<td><img src="{{ icons['Advanced Laser Technology (Wraith)'] }}" class="{{ 'acquired' if 'Advanced Laser Technology (Wraith)' in acquired_items }}" title="Advanced Laser Technology (Wraith)" /></td>
|
||||
<td colspan="4"></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Internal Tech Module (Raven)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Raven)' in acquired_items }}" title="Internal Tech Module (Raven)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
|
||||
<td><img src="{{ icons['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td>
|
||||
<td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td>
|
||||
<td><img src="{{ icons['Smart Servos (Viking)'] }}" class="{{ 'acquired' if 'Smart Servos (Viking)' in acquired_items }}" title="Smart Servos (Viking)" /></td>
|
||||
<td><img src="{{ icons['Magrail Munitions (Viking)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Viking)' in acquired_items }}" title="Magrail Munitions (Viking)" /></td>
|
||||
<td colspan="3"></td>
|
||||
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
|
||||
<td><img src="{{ icons['EMP Shockwave (Science Vessel)'] }}" class="{{ 'acquired' if 'EMP Shockwave (Science Vessel)' in acquired_items }}" title="EMP Shockwave (Science Vessel)" /></td>
|
||||
<td><img src="{{ icons['Defensive Matrix (Science Vessel)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Science Vessel)' in acquired_items }}" title="Defensive Matrix (Science Vessel)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
|
||||
<td><img src="{{ crossspectrum_dampeners_banshee_url }}" class="{{ 'acquired' if 'Progressive Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="{{ crossspectrum_dampeners_banshee_name }}" /></td>
|
||||
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td>
|
||||
<td><img src="{{ icons['Hyperflight Rotors (Banshee)'] }}" class="{{ 'acquired' if 'Hyperflight Rotors (Banshee)' in acquired_items }}" title="Hyperflight Rotors (Banshee)" /></td>
|
||||
<td><img src="{{ icons['Laser Targeting System (Banshee)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Banshee)' in acquired_items }}" title="Laser Targeting System (Banshee)" /></td>
|
||||
<td><img src="{{ icons['Internal Tech Module (Banshee)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Banshee)' in acquired_items }}" title="Internal Tech Module (Banshee)" /></td>
|
||||
<td colspan="2"></td>
|
||||
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
|
||||
<td><img src="{{ icons['Missile Pods (Battlecruiser)'] }}" class="{{ 'acquired' if 'Missile Pods (Battlecruiser)' in acquired_items }}" title="Missile Pods (Battlecruiser)" /></td>
|
||||
<td><img src="{{ icons['Defensive Matrix (Battlecruiser)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Battlecruiser)' in acquired_items }}" title="Defensive Matrix (Battlecruiser)" /></td>
|
||||
<td><img src="{{ icons['Tactical Jump (Battlecruiser)'] }}" class="{{ 'acquired' if 'Tactical Jump (Battlecruiser)' in acquired_items }}" title="Tactical Jump (Battlecruiser)" /></td>
|
||||
<td><img src="{{ icons['Cloak (Battlecruiser)'] }}" class="{{ 'acquired' if 'Cloak (Battlecruiser)' in acquired_items }}" title="Cloak (Battlecruiser)" /></td>
|
||||
<td><img src="{{ icons['ATX Laser Battery (Battlecruiser)'] }}" class="{{ 'acquired' if 'ATX Laser Battery (Battlecruiser)' in acquired_items }}" title="ATX Laser Battery (Battlecruiser)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Battlecruiser)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Battlecruiser)' in acquired_items }}" title="Optimized Logistics (Battlecruiser)" /></td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Liberator'] }}" class="{{ 'acquired' if 'Liberator' in acquired_items }}" title="Liberator" /></td>
|
||||
<td><img src="{{ icons['Advanced Ballistics (Liberator)'] }}" class="{{ 'acquired' if 'Advanced Ballistics (Liberator)' in acquired_items }}" title="Advanced Ballistics (Liberator)" /></td>
|
||||
<td><img src="{{ icons['Raid Artillery (Liberator)'] }}" class="{{ 'acquired' if 'Raid Artillery (Liberator)' in acquired_items }}" title="Raid Artillery (Liberator)" /></td>
|
||||
<td><img src="{{ icons['Cloak (Liberator)'] }}" class="{{ 'acquired' if 'Cloak (Liberator)' in acquired_items }}" title="Cloak (Liberator)" /></td>
|
||||
<td><img src="{{ icons['Laser Targeting System (Liberator)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Liberator)' in acquired_items }}" title="Laser Targeting System (Liberator)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Liberator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Liberator)' in acquired_items }}" title="Optimized Logistics (Liberator)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Internal Tech Module (Battlecruiser)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Battlecruiser)' in acquired_items }}" title="Internal Tech Module (Battlecruiser)" /></td>
|
||||
<td colspan="6"></td>
|
||||
<td><img src="{{ icons['Valkyrie'] }}" class="{{ 'acquired' if 'Valkyrie' in acquired_items }}" title="Valkyrie" /></td>
|
||||
<td><img src="{{ icons['Enhanced Cluster Launchers (Valkyrie)'] }}" class="{{ 'acquired' if 'Enhanced Cluster Launchers (Valkyrie)' in acquired_items }}" title="Enhanced Cluster Launchers (Valkyrie)" /></td>
|
||||
<td><img src="{{ icons['Shaped Hull (Valkyrie)'] }}" class="{{ 'acquired' if 'Shaped Hull (Valkyrie)' in acquired_items }}" title="Shaped Hull (Valkyrie)" /></td>
|
||||
<td><img src="{{ icons['Burst Lasers (Valkyrie)'] }}" class="{{ 'acquired' if 'Burst Lasers (Valkyrie)' in acquired_items }}" title="Burst Lasers (Valkyrie)" /></td>
|
||||
<td><img src="{{ icons['Afterburners (Valkyrie)'] }}" class="{{ 'acquired' if 'Afterburners (Valkyrie)' in acquired_items }}" title="Afterburners (Valkyrie)" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="15" class="title">
|
||||
Mercenaries
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['War Pigs'] }}" class="{{ 'acquired' if 'War Pigs' in acquired_items }}" title="War Pigs" /></td>
|
||||
<td><img src="{{ icons['Devil Dogs'] }}" class="{{ 'acquired' if 'Devil Dogs' in acquired_items }}" title="Devil Dogs" /></td>
|
||||
<td><img src="{{ icons['Hammer Securities'] }}" class="{{ 'acquired' if 'Hammer Securities' in acquired_items }}" title="Hammer Securities" /></td>
|
||||
<td><img src="{{ icons['Spartan Company'] }}" class="{{ 'acquired' if 'Spartan Company' in acquired_items }}" title="Spartan Company" /></td>
|
||||
<td><img src="{{ icons['Siege Breakers'] }}" class="{{ 'acquired' if 'Siege Breakers' in acquired_items }}" title="Siege Breakers" /></td>
|
||||
<td><img src="{{ icons['Hel\'s Angel'] }}" class="{{ 'acquired' if 'Hel\'s Angel' in acquired_items }}" title="Hel's Angel" /></td>
|
||||
<td><img src="{{ icons['Dusk Wings'] }}" class="{{ 'acquired' if 'Dusk Wings' in acquired_items }}" title="Dusk Wings" /></td>
|
||||
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="15" class="title">
|
||||
General Upgrades
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
|
||||
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
|
||||
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
|
||||
<td><img src="{{ regenerative_biosteel_url }}" class="{{ 'acquired' if 'Progressive Regenerative Bio-Steel' in acquired_items }}" title="Progressive Regenerative Bio-Steel{% if regenerative_biosteel_level > 0 %} (Level {{ regenerative_biosteel_level }}){% endif %}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="15" class="title">
|
||||
Protoss Units
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Zealot'] }}" class="{{ 'acquired' if 'Zealot' in acquired_items }}" title="Zealot" /></td>
|
||||
<td><img src="{{ icons['Stalker'] }}" class="{{ 'acquired' if 'Stalker' in acquired_items }}" title="Stalker" /></td>
|
||||
<td><img src="{{ icons['High Templar'] }}" class="{{ 'acquired' if 'High Templar' in acquired_items }}" title="High Templar" /></td>
|
||||
<td><img src="{{ icons['Dark Templar'] }}" class="{{ 'acquired' if 'Dark Templar' in acquired_items }}" title="Dark Templar" /></td>
|
||||
<td><img src="{{ icons['Immortal'] }}" class="{{ 'acquired' if 'Immortal' in acquired_items }}" title="Immortal" /></td>
|
||||
<td><img src="{{ icons['Colossus'] }}" class="{{ 'acquired' if 'Colossus' in acquired_items }}" title="Colossus" /></td>
|
||||
<td><img src="{{ icons['Phoenix'] }}" class="{{ 'acquired' if 'Phoenix' in acquired_items }}" title="Phoenix" /></td>
|
||||
<td><img src="{{ icons['Void Ray'] }}" class="{{ 'acquired' if 'Void Ray' in acquired_items }}" title="Void Ray" /></td>
|
||||
<td><img src="{{ icons['Carrier'] }}" class="{{ 'acquired' if 'Carrier' in acquired_items }}" title="Carrier" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
{% for area in checks_in_area %}
|
||||
{% if checks_in_area[area] > 0 %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,6 +48,7 @@ cdef struct IndexEntry:
|
||||
size_t count
|
||||
|
||||
|
||||
@cython.auto_pickle(False)
|
||||
cdef class LocationStore:
|
||||
"""Compact store for locations and their items in a MultiServer"""
|
||||
# The original implementation uses Dict[int, Dict[int, Tuple(int, int, int]]
|
||||
@@ -78,18 +79,6 @@ cdef class LocationStore:
|
||||
size += sizeof(self._raw_proxies[0]) * self.sender_index_size
|
||||
return size
|
||||
|
||||
def __cinit__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
|
||||
self._mem = None
|
||||
self._keys = None
|
||||
self._items = None
|
||||
self._proxies = None
|
||||
self._len = 0
|
||||
self.entries = NULL
|
||||
self.entry_count = 0
|
||||
self.sender_index = NULL
|
||||
self.sender_index_size = 0
|
||||
self._raw_proxies = NULL
|
||||
|
||||
def __init__(self, locations_dict: Dict[int, Dict[int, Sequence[int]]]) -> None:
|
||||
self._mem = Pool()
|
||||
cdef object key
|
||||
@@ -281,6 +270,7 @@ cdef class LocationStore:
|
||||
entry.location not in checked])
|
||||
|
||||
|
||||
@cython.auto_pickle(False)
|
||||
@cython.internal # unsafe. disable direct import
|
||||
cdef class PlayerLocationProxy:
|
||||
cdef LocationStore _store
|
||||
|
||||
@@ -61,36 +61,42 @@
|
||||
found_text: "Found?"
|
||||
TooltipLabel:
|
||||
id: receiving
|
||||
sort_key: 'receiving'
|
||||
text: root.receiving_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: item
|
||||
sort_key: 'item'
|
||||
text: root.item_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: finding
|
||||
sort_key: 'finding'
|
||||
text: root.finding_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: location
|
||||
sort_key: 'location'
|
||||
text: root.location_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: entrance
|
||||
sort_key: 'entrance'
|
||||
text: root.entrance_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: found
|
||||
sort_key: 'found'
|
||||
text: root.found_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
|
||||
@@ -22,6 +22,10 @@ SOFTWARE.
|
||||
|
||||
local SCRIPT_VERSION = 1
|
||||
|
||||
-- Set to log incoming requests
|
||||
-- Will cause lag due to large console output
|
||||
local DEBUG = false
|
||||
|
||||
--[[
|
||||
This script expects to receive JSON and will send JSON back. A message should
|
||||
be a list of 1 or more requests which will be executed in order. Each request
|
||||
@@ -271,10 +275,6 @@ local base64 = require("base64")
|
||||
local socket = require("socket")
|
||||
local json = require("json")
|
||||
|
||||
-- Set to log incoming requests
|
||||
-- Will cause lag due to large console output
|
||||
local DEBUG = false
|
||||
|
||||
local SOCKET_PORT_FIRST = 43055
|
||||
local SOCKET_PORT_RANGE_SIZE = 5
|
||||
local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE
|
||||
@@ -330,18 +330,28 @@ function unlock ()
|
||||
client_socket:settimeout(0)
|
||||
end
|
||||
|
||||
function process_request (req)
|
||||
local res = {}
|
||||
request_handlers = {
|
||||
["PING"] = function (req)
|
||||
local res = {}
|
||||
|
||||
if req["type"] == "PING" then
|
||||
res["type"] = "PONG"
|
||||
|
||||
elseif req["type"] == "SYSTEM" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["SYSTEM"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "SYSTEM_RESPONSE"
|
||||
res["value"] = emu.getsystemid()
|
||||
|
||||
elseif req["type"] == "PREFERRED_CORES" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["PREFERRED_CORES"] = function (req)
|
||||
local res = {}
|
||||
local preferred_cores = client.getconfig().PreferredCores
|
||||
|
||||
res["type"] = "PREFERRED_CORES_RESPONSE"
|
||||
res["value"] = {}
|
||||
res["value"]["NES"] = preferred_cores.NES
|
||||
@@ -354,14 +364,21 @@ function process_request (req)
|
||||
res["value"]["PCECD"] = preferred_cores.PCECD
|
||||
res["value"]["SGX"] = preferred_cores.SGX
|
||||
|
||||
elseif req["type"] == "HASH" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["HASH"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "HASH_RESPONSE"
|
||||
res["value"] = rom_hash
|
||||
|
||||
elseif req["type"] == "GUARD" then
|
||||
res["type"] = "GUARD_RESPONSE"
|
||||
local expected_data = base64.decode(req["expected_data"])
|
||||
return res
|
||||
end,
|
||||
|
||||
["GUARD"] = function (req)
|
||||
local res = {}
|
||||
local expected_data = base64.decode(req["expected_data"])
|
||||
local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"])
|
||||
|
||||
local data_is_validated = true
|
||||
@@ -372,39 +389,83 @@ function process_request (req)
|
||||
end
|
||||
end
|
||||
|
||||
res["type"] = "GUARD_RESPONSE"
|
||||
res["value"] = data_is_validated
|
||||
res["address"] = req["address"]
|
||||
|
||||
elseif req["type"] == "LOCK" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["LOCK"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "LOCKED"
|
||||
lock()
|
||||
|
||||
elseif req["type"] == "UNLOCK" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["UNLOCK"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "UNLOCKED"
|
||||
unlock()
|
||||
|
||||
elseif req["type"] == "READ" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["READ"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "READ_RESPONSE"
|
||||
res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"]))
|
||||
|
||||
elseif req["type"] == "WRITE" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["WRITE"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "WRITE_RESPONSE"
|
||||
memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"])
|
||||
|
||||
elseif req["type"] == "DISPLAY_MESSAGE" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["DISPLAY_MESSAGE"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "DISPLAY_MESSAGE_RESPONSE"
|
||||
message_queue:push(req["message"])
|
||||
|
||||
elseif req["type"] == "SET_MESSAGE_INTERVAL" then
|
||||
return res
|
||||
end,
|
||||
|
||||
["SET_MESSAGE_INTERVAL"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE"
|
||||
message_interval = req["value"]
|
||||
|
||||
else
|
||||
return res
|
||||
end,
|
||||
|
||||
["default"] = function (req)
|
||||
local res = {}
|
||||
|
||||
res["type"] = "ERROR"
|
||||
res["err"] = "Unknown command: "..req["type"]
|
||||
end
|
||||
|
||||
return res
|
||||
return res
|
||||
end,
|
||||
}
|
||||
|
||||
function process_request (req)
|
||||
if request_handlers[req["type"]] then
|
||||
return request_handlers[req["type"]](req)
|
||||
else
|
||||
return request_handlers["default"](req)
|
||||
end
|
||||
end
|
||||
|
||||
-- Receive data from AP client and send message back
|
||||
|
||||
@@ -322,7 +322,7 @@ function processBlock(block)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #itemsBlock ~= itemIndex then
|
||||
if #itemsBlock > itemIndex then
|
||||
wU8(ITEM_INDEX, #itemsBlock)
|
||||
end
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
# A. This is a .yaml file. You are allowed to use most characters.
|
||||
# To test if your yaml is valid or not, you can use this website:
|
||||
# http://www.yamllint.com/
|
||||
# You can also verify your Archipelago settings are valid at this site:
|
||||
# You can also verify that your Archipelago options are valid at this site:
|
||||
# https://archipelago.gg/check
|
||||
|
||||
# Your name in-game. Spaces will be replaced with underscores and there is a 16-character limit.
|
||||
# Your name in-game, limited to 16 characters.
|
||||
# {player} will be replaced with the player's slot number.
|
||||
# {PLAYER} will be replaced with the player's slot number, if that slot number is greater than 1.
|
||||
# {number} will be replaced with the counter value of the name.
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
# Bumper Stickers
|
||||
/worlds/bumpstik/ @FelicitusNeko
|
||||
|
||||
# Castlevania 64
|
||||
/worlds/cv64/ @LiquidCat64
|
||||
|
||||
# Celeste 64
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
# ChecksFinder
|
||||
/worlds/checksfinder/ @jonloveslegos
|
||||
|
||||
@@ -67,6 +73,9 @@
|
||||
# Hylics 2
|
||||
/worlds/hylics2/ @TRPG0
|
||||
|
||||
# Kirby's Dream Land 3
|
||||
/worlds/kdl3/ @Silvris
|
||||
|
||||
# Kingdom Hearts 2
|
||||
/worlds/kh2/ @JaredWeakStrike
|
||||
|
||||
@@ -125,11 +134,14 @@
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
|
||||
# A Short Hike
|
||||
/worlds/shorthike/ @chandler05
|
||||
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
# Starcraft 2 Wings of Liberty
|
||||
/worlds/sc2wol/ @Ziktofel
|
||||
# Starcraft 2
|
||||
/worlds/sc2/ @Ziktofel
|
||||
|
||||
# Super Metroid
|
||||
/worlds/sm/ @lordlou
|
||||
@@ -164,6 +176,9 @@
|
||||
# The Legend of Zelda (1)
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
|
||||
# TUNIC
|
||||
/worlds/tunic/ @silent-destroyer @ScipioWright
|
||||
|
||||
# Undertale
|
||||
/worlds/undertale/ @jonloveslegos
|
||||
|
||||
@@ -176,9 +191,15 @@
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
# Yoshi's Island
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
|
||||
# Zork Grand Inquisitor
|
||||
/worlds/zork_grand_inquisitor/ @nbrochu
|
||||
|
||||
##################################
|
||||
## Disabled Unmaintained Worlds ##
|
||||
##################################
|
||||
|
||||
@@ -1,269 +1,78 @@
|
||||
# How do I add a game to Archipelago?
|
||||
|
||||
This guide is going to try and be a broad summary of how you can do just that.
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
Refer to the following documents as well:
|
||||
|
||||
- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server.
|
||||
- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package.
|
||||
|
||||
# Game Modification
|
||||
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, described further down.
|
||||
|
||||
As an example, modifications to a game typically include (more on this later):
|
||||
|
||||
- Hooking into when a 'location check' is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
In order to determine how to modify a game, refer to the following sections.
|
||||
|
||||
## Engine Identification
|
||||
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
|
||||
critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s
|
||||
important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
Examples are provided below.
|
||||
|
||||
### Creepy Castle
|
||||
|
||||

|
||||
|
||||
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case
|
||||
scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
|
||||
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty
|
||||
nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other
|
||||
examples of game releases.
|
||||
|
||||
### Heavy Bullets
|
||||
|
||||

|
||||
|
||||
Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
|
||||
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
|
||||
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
|
||||
information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
|
||||
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
|
||||
“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
|
||||
The directory “HEAVY_BULLETS_Data”, however, has some good news.
|
||||
|
||||

|
||||
|
||||
Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
|
||||
what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which
|
||||
affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered,
|
||||
extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools
|
||||
and information to help you on your journey can be found at this
|
||||
[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking)
|
||||
|
||||
### Stardew Valley
|
||||
|
||||

|
||||
|
||||
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
|
||||
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good
|
||||
news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx
|
||||
and MonoMod.
|
||||
|
||||
### Gato Roboto
|
||||
|
||||

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

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