mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-21 03:03:33 -07:00
Compare commits
56 Commits
active/rc-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc911399fb | ||
|
|
0b38065123 | ||
|
|
a6c1347102 | ||
|
|
0a742b6c98 | ||
|
|
66712bbd87 | ||
|
|
5f9e38b783 | ||
|
|
bdde2140b3 | ||
|
|
42d24b4869 | ||
|
|
14aa6571de | ||
|
|
37d32a10b1 | ||
|
|
0ef2ed5b81 | ||
|
|
b7c4fcb4c6 | ||
|
|
bcd3f9a74c | ||
|
|
6262235161 | ||
|
|
030cb4b578 | ||
|
|
36bab6f52a | ||
|
|
e0cfef3407 | ||
|
|
bb2a775c05 | ||
|
|
427b147818 | ||
|
|
3f3c343fb3 | ||
|
|
debe4cf035 | ||
|
|
68f25f4642 | ||
|
|
3c4af8f432 | ||
|
|
5360b6bb37 | ||
|
|
2ee20a3ac4 | ||
|
|
c640d2fa24 | ||
|
|
58a6407040 | ||
|
|
ba7ca0bd23 | ||
|
|
bdbf72f148 | ||
|
|
2b46df90b4 | ||
|
|
88dc135960 | ||
|
|
95f696c04f | ||
|
|
96277fe9be | ||
|
|
a7a7879df4 | ||
|
|
773f3c4f08 | ||
|
|
139856a573 | ||
|
|
a1ed804267 | ||
|
|
2d58e7953c | ||
|
|
393ed51203 | ||
|
|
03c9d0717b | ||
|
|
5ca50cd8d3 | ||
|
|
36cf86f2e8 | ||
|
|
1705620c4f | ||
|
|
ffe4c6dd15 | ||
|
|
cf47cc67c0 | ||
|
|
645f25a94e | ||
|
|
74f41e3733 | ||
|
|
4276c6d6b0 | ||
|
|
116ab2286a | ||
|
|
fb45a2f87e | ||
|
|
2e5356ad05 | ||
|
|
8457ff3e4b | ||
|
|
70fc3e05fb | ||
|
|
d01c9577ab | ||
|
|
260bae359d | ||
|
|
3016379b85 |
2
.github/pyright-config.json
vendored
2
.github/pyright-config.json
vendored
@@ -3,6 +3,7 @@
|
|||||||
"../BizHawkClient.py",
|
"../BizHawkClient.py",
|
||||||
"../Patch.py",
|
"../Patch.py",
|
||||||
"../rule_builder/cached_world.py",
|
"../rule_builder/cached_world.py",
|
||||||
|
"../rule_builder/field_resolvers.py",
|
||||||
"../rule_builder/options.py",
|
"../rule_builder/options.py",
|
||||||
"../rule_builder/rules.py",
|
"../rule_builder/rules.py",
|
||||||
"../test/param.py",
|
"../test/param.py",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"../test/programs/test_multi_server.py",
|
"../test/programs/test_multi_server.py",
|
||||||
"../test/utils/__init__.py",
|
"../test/utils/__init__.py",
|
||||||
"../test/webhost/test_descriptions.py",
|
"../test/webhost/test_descriptions.py",
|
||||||
|
"../test/webhost/test_suuid.py",
|
||||||
"../worlds/AutoSNIClient.py",
|
"../worlds/AutoSNIClient.py",
|
||||||
"type_check.py"
|
"type_check.py"
|
||||||
],
|
],
|
||||||
|
|||||||
6
.github/workflows/analyze-modified-files.yml
vendored
6
.github/workflows/analyze-modified-files.yml
vendored
@@ -14,6 +14,8 @@ env:
|
|||||||
BEFORE: ${{ github.event.before }}
|
BEFORE: ${{ github.event.before }}
|
||||||
AFTER: ${{ github.event.after }}
|
AFTER: ${{ github.event.after }}
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
flake8-or-mypy:
|
flake8-or-mypy:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -25,7 +27,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
- name: "Determine modified files (pull_request)"
|
- name: "Determine modified files (pull_request)"
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
@@ -50,7 +52,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "diff=." >> $GITHUB_ENV
|
echo "diff=." >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6.2.0
|
||||||
if: env.diff != ''
|
if: env.diff != ''
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|||||||
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@@ -41,9 +41,9 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
# - copy code below to release.yml -
|
# - copy code below to release.yml -
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '~3.12.7'
|
python-version: '~3.12.7'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
# - copy code above to release.yml -
|
# - copy code above to release.yml -
|
||||||
- name: Attest Build
|
- name: Attest Build
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4.1.0
|
||||||
with:
|
with:
|
||||||
subject-path: |
|
subject-path: |
|
||||||
build/exe.*/ArchipelagoLauncher.exe
|
build/exe.*/ArchipelagoLauncher.exe
|
||||||
@@ -110,18 +110,17 @@ jobs:
|
|||||||
cp Players/Templates/VVVVVV.yaml Players/
|
cp Players/Templates/VVVVVV.yaml Players/
|
||||||
timeout 30 ./ArchipelagoGenerate
|
timeout 30 ./ArchipelagoGenerate
|
||||||
- name: Store 7z
|
- name: Store 7z
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ env.ZIP_NAME }}
|
|
||||||
path: dist/${{ env.ZIP_NAME }}
|
path: dist/${{ env.ZIP_NAME }}
|
||||||
compression-level: 0 # .7z is incompressible by zip
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7 # keep for 7 days, should be enough
|
retention-days: 7 # keep for 7 days, should be enough
|
||||||
- name: Store Setup
|
- name: Store Setup
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ env.SETUP_NAME }}
|
|
||||||
path: setups/${{ env.SETUP_NAME }}
|
path: setups/${{ env.SETUP_NAME }}
|
||||||
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7 # keep for 7 days, should be enough
|
retention-days: 7 # keep for 7 days, should be enough
|
||||||
|
|
||||||
@@ -129,14 +128,14 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
# - copy code below to release.yml -
|
# - copy code below to release.yml -
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
- name: Install base dependencies
|
- name: Install base dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '~3.12.7'
|
python-version: '~3.12.7'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -173,7 +172,7 @@ jobs:
|
|||||||
# - copy code above to release.yml -
|
# - copy code above to release.yml -
|
||||||
- name: Attest Build
|
- name: Attest Build
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4.1.0
|
||||||
with:
|
with:
|
||||||
subject-path: |
|
subject-path: |
|
||||||
build/exe.*/ArchipelagoLauncher
|
build/exe.*/ArchipelagoLauncher
|
||||||
@@ -204,17 +203,17 @@ jobs:
|
|||||||
cp Players/Templates/VVVVVV.yaml Players/
|
cp Players/Templates/VVVVVV.yaml Players/
|
||||||
timeout 30 ./ArchipelagoGenerate
|
timeout 30 ./ArchipelagoGenerate
|
||||||
- name: Store AppImage
|
- name: Store AppImage
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ env.APPIMAGE_NAME }}
|
|
||||||
path: dist/${{ env.APPIMAGE_NAME }}
|
path: dist/${{ env.APPIMAGE_NAME }}
|
||||||
|
archive: false
|
||||||
|
# TODO: decide if we want to also upload the zsync
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: Store .tar.gz
|
- name: Store .tar.gz
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7.0.0
|
||||||
with:
|
with:
|
||||||
name: ${{ env.TAR_NAME }}
|
|
||||||
path: dist/${{ env.TAR_NAME }}
|
path: dist/${{ env.TAR_NAME }}
|
||||||
compression-level: 0 # .gz is incompressible by zip
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|||||||
24
.github/workflows/codeql-analysis.yml
vendored
24
.github/workflows/codeql-analysis.yml
vendored
@@ -17,17 +17,26 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- '**.py'
|
- '**.py'
|
||||||
- '**.js'
|
- '**.js'
|
||||||
- '.github/workflows/codeql-analysis.yml'
|
- '.github/workflows/*.yml'
|
||||||
|
- '.github/workflows/*.yaml'
|
||||||
|
- '**/action.yml'
|
||||||
|
- '**/action.yaml'
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- '**.py'
|
- '**.py'
|
||||||
- '**.js'
|
- '**.js'
|
||||||
- '.github/workflows/codeql-analysis.yml'
|
- '.github/workflows/*.yml'
|
||||||
|
- '.github/workflows/*.yaml'
|
||||||
|
- '**/action.yml'
|
||||||
|
- '**/action.yaml'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '44 8 * * 1'
|
- cron: '44 8 * * 1'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
@@ -36,18 +45,17 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'javascript', 'python' ]
|
language: [ 'javascript', 'python', 'actions' ]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
|
||||||
# Learn more:
|
# Learn more:
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4.35.1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -58,7 +66,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4.35.1
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -72,4 +80,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4.35.1
|
||||||
|
|||||||
4
.github/workflows/ctest.yml
vendored
4
.github/workflows/ctest.yml
vendored
@@ -24,6 +24,8 @@ on:
|
|||||||
- '**/CMakeLists.txt'
|
- '**/CMakeLists.txt'
|
||||||
- '.github/workflows/ctest.yml'
|
- '.github/workflows/ctest.yml'
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ctest:
|
ctest:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -35,7 +37,7 @@ jobs:
|
|||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||||
if: startsWith(matrix.os,'windows')
|
if: startsWith(matrix.os,'windows')
|
||||||
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
|
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
|
||||||
|
|||||||
16
.github/workflows/docker.yml
vendored
16
.github/workflows/docker.yml
vendored
@@ -19,6 +19,8 @@ on:
|
|||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -29,7 +31,7 @@ jobs:
|
|||||||
package-name: ${{ steps.package.outputs.name }}
|
package-name: ${{ steps.package.outputs.name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
- name: Set lowercase image name
|
- name: Set lowercase image name
|
||||||
id: image
|
id: image
|
||||||
@@ -43,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6.0.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
|
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -92,13 +94,13 @@ jobs:
|
|||||||
cache-scope: arm64
|
cache-scope: arm64
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -115,7 +117,7 @@ jobs:
|
|||||||
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
|
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -135,7 +137,7 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
2
.github/workflows/label-pull-requests.yml
vendored
2
.github/workflows/label-pull-requests.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
name: 'Apply content-based labels'
|
name: 'Apply content-based labels'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v5
|
- uses: actions/labeler@v6.0.1
|
||||||
with:
|
with:
|
||||||
sync-labels: false
|
sync-labels: false
|
||||||
peer_review:
|
peer_review:
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -48,9 +48,9 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
# - code below copied from build.yml -
|
# - code below copied from build.yml -
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '~3.12.7'
|
python-version: '~3.12.7'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
||||||
# - code above copied from build.yml -
|
# - code above copied from build.yml -
|
||||||
- name: Attest Build
|
- name: Attest Build
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4.1.0
|
||||||
with:
|
with:
|
||||||
subject-path: |
|
subject-path: |
|
||||||
build/exe.*/ArchipelagoLauncher.exe
|
build/exe.*/ArchipelagoLauncher.exe
|
||||||
@@ -114,14 +114,14 @@ jobs:
|
|||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
# - code below copied from build.yml -
|
# - code below copied from build.yml -
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
- name: Install base dependencies
|
- name: Install base dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '~3.12.7'
|
python-version: '~3.12.7'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -157,7 +157,7 @@ jobs:
|
|||||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||||
# - code above copied from build.yml -
|
# - code above copied from build.yml -
|
||||||
- name: Attest Build
|
- name: Attest Build
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest@v4.1.0
|
||||||
with:
|
with:
|
||||||
subject-path: |
|
subject-path: |
|
||||||
build/exe.*/ArchipelagoLauncher
|
build/exe.*/ArchipelagoLauncher
|
||||||
|
|||||||
10
.github/workflows/scan-build.yml
vendored
10
.github/workflows/scan-build.yml
vendored
@@ -28,12 +28,14 @@ on:
|
|||||||
- 'requirements.txt'
|
- 'requirements.txt'
|
||||||
- '.github/workflows/scan-build.yml'
|
- '.github/workflows/scan-build.yml'
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scan-build:
|
scan-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Install newer Clang
|
- name: Install newer Clang
|
||||||
@@ -45,7 +47,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt install clang-tools-19
|
sudo apt install clang-tools-19
|
||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -59,7 +61,9 @@ jobs:
|
|||||||
scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
|
scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
|
||||||
- name: Store report
|
- name: Store report
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7.0.0
|
||||||
with:
|
with:
|
||||||
name: scan-build-reports
|
name: scan-build-reports
|
||||||
path: scan-build-reports
|
path: scan-build-reports
|
||||||
|
compression-level: 9 # highly compressible
|
||||||
|
if-no-files-found: error
|
||||||
|
|||||||
6
.github/workflows/strict-type-check.yml
vendored
6
.github/workflows/strict-type-check.yml
vendored
@@ -14,13 +14,15 @@ on:
|
|||||||
- ".github/workflows/strict-type-check.yml"
|
- ".github/workflows/strict-type-check.yml"
|
||||||
- "**.pyi"
|
- "**.pyi"
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pyright:
|
pyright:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/unittests.yml
vendored
10
.github/workflows/unittests.yml
vendored
@@ -29,6 +29,8 @@ on:
|
|||||||
- '!.github/workflows/**'
|
- '!.github/workflows/**'
|
||||||
- '.github/workflows/unittests.yml'
|
- '.github/workflows/unittests.yml'
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit:
|
unit:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -51,9 +53,9 @@ jobs:
|
|||||||
os: macos-latest
|
os: macos-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
- name: Set up Python ${{ matrix.python.version }}
|
- name: Set up Python ${{ matrix.python.version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python.version }}
|
python-version: ${{ matrix.python.version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -78,9 +80,9 @@ jobs:
|
|||||||
- {version: '3.13'} # current
|
- {version: '3.13'} # current
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6.0.2
|
||||||
- name: Set up Python ${{ matrix.python.version }}
|
- name: Set up Python ${{ matrix.python.version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python.version }}
|
python-version: ${{ matrix.python.version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,6 +45,7 @@ EnemizerCLI/
|
|||||||
/SNI/
|
/SNI/
|
||||||
/sni-*/
|
/sni-*/
|
||||||
/appimagetool*
|
/appimagetool*
|
||||||
|
/VC_redist.x64.exe
|
||||||
/host.yaml
|
/host.yaml
|
||||||
/options.yaml
|
/options.yaml
|
||||||
/config.yaml
|
/config.yaml
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ class CommonContext:
|
|||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
parts = title.split(', ', 1)
|
parts = title.split(', ', 1)
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
text = parts[1] + '\n\n' + text
|
text = f"{parts[1]}\n\n{text}" if text else parts[1]
|
||||||
title = parts[0]
|
title = parts[0]
|
||||||
# display error
|
# display error
|
||||||
self._messagebox = MessageBox(title, text, error=True)
|
self._messagebox = MessageBox(title, text, error=True)
|
||||||
@@ -896,6 +896,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
|||||||
"May not be running Archipelago on that address or port.")
|
"May not be running Archipelago on that address or port.")
|
||||||
except websockets.InvalidURI:
|
except websockets.InvalidURI:
|
||||||
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
|
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
ctx.handle_connection_loss("Failed to connect to the multiworld server. Connection timed out.")
|
||||||
except OSError:
|
except OSError:
|
||||||
ctx.handle_connection_loss("Failed to connect to the multiworld server")
|
ctx.handle_connection_loss("Failed to connect to the multiworld server")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
|||||||
|
|
||||||
seed = get_seed(args.seed)
|
seed = get_seed(args.seed)
|
||||||
|
|
||||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
|
if __name__ == "__main__":
|
||||||
|
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
|
||||||
random.seed(seed)
|
random.seed(seed)
|
||||||
seed_name = get_seed_name(random)
|
seed_name = get_seed_name(random)
|
||||||
|
|
||||||
|
|||||||
49
Launcher.py
49
Launcher.py
@@ -29,8 +29,8 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
import settings
|
import settings
|
||||||
import Utils
|
import Utils
|
||||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
from Utils import (env_cleared_lib_path, init_logging, is_frozen, is_linux, is_macos, is_windows, local_path,
|
||||||
user_path)
|
messagebox, open_filename, user_path)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init_logging('Launcher')
|
init_logging('Launcher')
|
||||||
@@ -52,10 +52,7 @@ def open_host_yaml():
|
|||||||
webbrowser.open(file)
|
webbrowser.open(file)
|
||||||
return
|
return
|
||||||
|
|
||||||
env = os.environ
|
env = env_cleared_lib_path()
|
||||||
if "LD_LIBRARY_PATH" in env:
|
|
||||||
env = env.copy()
|
|
||||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
|
||||||
subprocess.Popen([exe, file], env=env)
|
subprocess.Popen([exe, file], env=env)
|
||||||
|
|
||||||
def open_patch():
|
def open_patch():
|
||||||
@@ -106,10 +103,7 @@ def open_folder(folder_path):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if exe:
|
if exe:
|
||||||
env = os.environ
|
env = env_cleared_lib_path()
|
||||||
if "LD_LIBRARY_PATH" in env:
|
|
||||||
env = env.copy()
|
|
||||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
|
||||||
subprocess.Popen([exe, folder_path], env=env)
|
subprocess.Popen([exe, folder_path], env=env)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"No file browser available to open {folder_path}")
|
logging.warning(f"No file browser available to open {folder_path}")
|
||||||
@@ -202,22 +196,32 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
|
|||||||
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
|
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
|
||||||
|
|
||||||
|
|
||||||
def launch(exe, in_terminal=False):
|
def launch(exe: Sequence[str], in_terminal: bool = False) -> bool:
|
||||||
|
"""Runs the given command/args in `exe` in a new process.
|
||||||
|
|
||||||
|
If `in_terminal` is True, it will attempt to run in a terminal window,
|
||||||
|
and the return value will indicate whether one was found."""
|
||||||
if in_terminal:
|
if in_terminal:
|
||||||
if is_windows:
|
if is_windows:
|
||||||
# intentionally using a window title with a space so it gets quoted and treated as a title
|
# intentionally using a window title with a space so it gets quoted and treated as a title
|
||||||
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
|
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
|
||||||
return
|
return True
|
||||||
elif is_linux:
|
elif is_linux:
|
||||||
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
|
terminal = which("x-terminal-emulator") or which("konsole") or which("gnome-terminal") or which("xterm")
|
||||||
if terminal:
|
if terminal:
|
||||||
subprocess.Popen([terminal, '-e', shlex.join(exe)])
|
# Clear LD_LIB_PATH during terminal startup, but set it again when running command in case it's needed
|
||||||
return
|
ld_lib_path = os.environ.get("LD_LIBRARY_PATH")
|
||||||
|
lib_path_setter = f"env LD_LIBRARY_PATH={shlex.quote(ld_lib_path)} " if ld_lib_path else ""
|
||||||
|
env = env_cleared_lib_path()
|
||||||
|
|
||||||
|
subprocess.Popen([terminal, "-e", lib_path_setter + shlex.join(exe)], env=env)
|
||||||
|
return True
|
||||||
elif is_macos:
|
elif is_macos:
|
||||||
terminal = [which('open'), '-W', '-a', 'Terminal.app']
|
terminal = [which("open"), "-W", "-a", "Terminal.app"]
|
||||||
subprocess.Popen([*terminal, *exe])
|
subprocess.Popen([*terminal, *exe])
|
||||||
return
|
return True
|
||||||
subprocess.Popen(exe)
|
subprocess.Popen(exe)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_shortcut(button: Any, component: Component) -> None:
|
def create_shortcut(button: Any, component: Component) -> None:
|
||||||
@@ -406,12 +410,17 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def component_action(button):
|
def component_action(button):
|
||||||
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
|
open_text = "Opening in a new window..."
|
||||||
size_hint_x=0.5).open()
|
|
||||||
if button.component.func:
|
if button.component.func:
|
||||||
|
# Note: if we want to draw the Snackbar before running func, func needs to be wrapped in schedule_once
|
||||||
button.component.func()
|
button.component.func()
|
||||||
else:
|
else:
|
||||||
launch(get_exe(button.component), button.component.cli)
|
# if launch returns False, it started the process in background (not in a new terminal)
|
||||||
|
if not launch(get_exe(button.component), button.component.cli) and button.component.cli:
|
||||||
|
open_text = "Running in the background..."
|
||||||
|
|
||||||
|
MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5},
|
||||||
|
size_hint_x=0.5).open()
|
||||||
|
|
||||||
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
|
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
|
||||||
""" When a patch file is dropped into the window, run the associated component. """
|
""" When a patch file is dropped into the window, run the associated component. """
|
||||||
|
|||||||
183
MultiServer.py
183
MultiServer.py
@@ -44,9 +44,8 @@ import NetUtils
|
|||||||
import Utils
|
import Utils
|
||||||
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
|
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
|
||||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||||
SlotType, LocationStore, MultiData, Hint, HintStatus, GamesPackage
|
SlotType, LocationStore, MultiData, Hint, HintStatus
|
||||||
from BaseClasses import ItemClassification
|
from BaseClasses import ItemClassification
|
||||||
from apmw.multiserver.gamespackagecache import GamesPackageCache
|
|
||||||
|
|
||||||
|
|
||||||
min_client_version = Version(0, 5, 0)
|
min_client_version = Version(0, 5, 0)
|
||||||
@@ -242,38 +241,21 @@ class Context:
|
|||||||
slot_info: typing.Dict[int, NetworkSlot]
|
slot_info: typing.Dict[int, NetworkSlot]
|
||||||
generator_version = Version(0, 0, 0)
|
generator_version = Version(0, 0, 0)
|
||||||
checksums: typing.Dict[str, str]
|
checksums: typing.Dict[str, str]
|
||||||
played_games: set[str]
|
|
||||||
item_names: typing.Dict[str, typing.Dict[int, str]]
|
item_names: typing.Dict[str, typing.Dict[int, str]]
|
||||||
item_name_groups: typing.Dict[str, typing.Dict[str, list[str]]]
|
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||||
location_names: typing.Dict[str, typing.Dict[int, str]]
|
location_names: typing.Dict[str, typing.Dict[int, str]]
|
||||||
location_name_groups: typing.Dict[str, typing.Dict[str, list[str]]]
|
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||||
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
|
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||||
non_hintable_names: typing.Dict[str, typing.AbstractSet[str]]
|
non_hintable_names: typing.Dict[str, typing.AbstractSet[str]]
|
||||||
spheres: typing.List[typing.Dict[int, typing.Set[int]]]
|
spheres: typing.List[typing.Dict[int, typing.Set[int]]]
|
||||||
""" each sphere is { player: { location_id, ... } } """
|
""" each sphere is { player: { location_id, ... } } """
|
||||||
games_package_cache: GamesPackageCache
|
|
||||||
logger: logging.Logger
|
logger: logging.Logger
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||||
self,
|
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
|
||||||
host: str,
|
countdown_mode: str = "auto", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0,
|
||||||
port: int,
|
compatibility: int = 2, log_network: bool = False, logger: logging.Logger = logging.getLogger()):
|
||||||
server_password: str,
|
|
||||||
password: str,
|
|
||||||
location_check_points: int,
|
|
||||||
hint_cost: int,
|
|
||||||
item_cheat: bool,
|
|
||||||
release_mode: str = "disabled",
|
|
||||||
collect_mode="disabled",
|
|
||||||
countdown_mode: str = "auto",
|
|
||||||
remaining_mode: str = "disabled",
|
|
||||||
auto_shutdown: typing.SupportsFloat = 0,
|
|
||||||
compatibility: int = 2,
|
|
||||||
log_network: bool = False,
|
|
||||||
games_package_cache: GamesPackageCache | None = None,
|
|
||||||
logger: logging.Logger = logging.getLogger(),
|
|
||||||
) -> None:
|
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
super(Context, self).__init__()
|
super(Context, self).__init__()
|
||||||
self.slot_info = {}
|
self.slot_info = {}
|
||||||
@@ -324,7 +306,6 @@ class Context:
|
|||||||
self.save_dirty = False
|
self.save_dirty = False
|
||||||
self.tags = ['AP']
|
self.tags = ['AP']
|
||||||
self.games: typing.Dict[int, str] = {}
|
self.games: typing.Dict[int, str] = {}
|
||||||
self.played_games = set()
|
|
||||||
self.minimum_client_versions: typing.Dict[int, Version] = {}
|
self.minimum_client_versions: typing.Dict[int, Version] = {}
|
||||||
self.seed_name = ""
|
self.seed_name = ""
|
||||||
self.groups = {}
|
self.groups = {}
|
||||||
@@ -334,10 +315,9 @@ class Context:
|
|||||||
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
||||||
self.read_data = {}
|
self.read_data = {}
|
||||||
self.spheres = []
|
self.spheres = []
|
||||||
self.games_package_cache = games_package_cache or GamesPackageCache()
|
|
||||||
|
|
||||||
# init empty to satisfy linter, I suppose
|
# init empty to satisfy linter, I suppose
|
||||||
self.reduced_games_package = {}
|
self.gamespackage = {}
|
||||||
self.checksums = {}
|
self.checksums = {}
|
||||||
self.item_name_groups = {}
|
self.item_name_groups = {}
|
||||||
self.location_name_groups = {}
|
self.location_name_groups = {}
|
||||||
@@ -349,11 +329,50 @@ class Context:
|
|||||||
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))
|
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))
|
||||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||||
|
|
||||||
|
self._load_game_data()
|
||||||
|
|
||||||
|
# Data package retrieval
|
||||||
|
def _load_game_data(self):
|
||||||
|
import worlds
|
||||||
|
self.gamespackage = worlds.network_data_package["games"]
|
||||||
|
|
||||||
|
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
||||||
|
worlds.AutoWorldRegister.world_types.items()}
|
||||||
|
self.location_name_groups = {world_name: world.location_name_groups for world_name, world in
|
||||||
|
worlds.AutoWorldRegister.world_types.items()}
|
||||||
|
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||||
|
self.non_hintable_names[world_name] = world.hint_blacklist
|
||||||
|
|
||||||
|
for game_package in self.gamespackage.values():
|
||||||
|
# remove groups from data sent to clients
|
||||||
|
del game_package["item_name_groups"]
|
||||||
|
del game_package["location_name_groups"]
|
||||||
|
|
||||||
|
def _init_game_data(self):
|
||||||
|
for game_name, game_package in self.gamespackage.items():
|
||||||
|
if "checksum" in game_package:
|
||||||
|
self.checksums[game_name] = game_package["checksum"]
|
||||||
|
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||||
|
self.item_names[game_name][item_id] = item_name
|
||||||
|
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||||
|
self.location_names[game_name][location_id] = location_name
|
||||||
|
self.all_item_and_group_names[game_name] = \
|
||||||
|
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||||
|
self.all_location_and_group_names[game_name] = \
|
||||||
|
set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, []))
|
||||||
|
|
||||||
|
archipelago_item_names = self.item_names["Archipelago"]
|
||||||
|
archipelago_location_names = self.location_names["Archipelago"]
|
||||||
|
for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]:
|
||||||
|
# Add Archipelago items and locations to each data package.
|
||||||
|
self.item_names[game].update(archipelago_item_names)
|
||||||
|
self.location_names[game].update(archipelago_location_names)
|
||||||
|
|
||||||
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||||
return self.reduced_games_package[game]["item_name_to_id"] if game in self.reduced_games_package else None
|
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
|
||||||
|
|
||||||
def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||||
return self.reduced_games_package[game]["location_name_to_id"] if game in self.reduced_games_package else None
|
return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None
|
||||||
|
|
||||||
# General networking
|
# General networking
|
||||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||||
@@ -463,17 +482,19 @@ class Context:
|
|||||||
with open(multidatapath, 'rb') as f:
|
with open(multidatapath, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
||||||
self._load(self.decompress(data), use_embedded_server_options)
|
self._load(self.decompress(data), {}, use_embedded_server_options)
|
||||||
self.data_filename = multidatapath
|
self.data_filename = multidatapath
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decompress(data: bytes) -> typing.Any:
|
def decompress(data: bytes) -> dict:
|
||||||
format_version = data[0]
|
format_version = data[0]
|
||||||
if format_version > 3:
|
if format_version > 3:
|
||||||
raise Utils.VersionException("Incompatible multidata.")
|
raise Utils.VersionException("Incompatible multidata.")
|
||||||
return restricted_loads(zlib.decompress(data[1:]))
|
return restricted_loads(zlib.decompress(data[1:]))
|
||||||
|
|
||||||
def _load(self, decoded_obj: MultiData, use_embedded_server_options: bool) -> None:
|
def _load(self, decoded_obj: MultiData, game_data_packages: typing.Dict[str, typing.Any],
|
||||||
|
use_embedded_server_options: bool):
|
||||||
|
|
||||||
self.read_data = {}
|
self.read_data = {}
|
||||||
# there might be a better place to put this.
|
# there might be a better place to put this.
|
||||||
race_mode = decoded_obj.get("race_mode", 0)
|
race_mode = decoded_obj.get("race_mode", 0)
|
||||||
@@ -494,7 +515,6 @@ class Context:
|
|||||||
|
|
||||||
self.slot_info = decoded_obj["slot_info"]
|
self.slot_info = decoded_obj["slot_info"]
|
||||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||||
self.played_games = {"Archipelago"} | {self.games[x] for x in range(1, len(self.games) + 1)}
|
|
||||||
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
|
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
|
||||||
if slot_info.type == SlotType.group}
|
if slot_info.type == SlotType.group}
|
||||||
|
|
||||||
@@ -539,11 +559,18 @@ class Context:
|
|||||||
server_options = decoded_obj.get("server_options", {})
|
server_options = decoded_obj.get("server_options", {})
|
||||||
self._set_options(server_options)
|
self._set_options(server_options)
|
||||||
|
|
||||||
# load and apply world data and (embedded) data package
|
# embedded data package
|
||||||
self._load_world_data()
|
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
||||||
self._load_data_package(decoded_obj.get("datapackage", {}))
|
if game_name in game_data_packages:
|
||||||
|
data = game_data_packages[game_name]
|
||||||
|
self.logger.info(f"Loading embedded data package for game {game_name}")
|
||||||
|
self.gamespackage[game_name] = data
|
||||||
|
self.item_name_groups[game_name] = data["item_name_groups"]
|
||||||
|
if "location_name_groups" in data:
|
||||||
|
self.location_name_groups[game_name] = data["location_name_groups"]
|
||||||
|
del data["location_name_groups"]
|
||||||
|
del data["item_name_groups"] # remove from data package, but keep in self.item_name_groups
|
||||||
self._init_game_data()
|
self._init_game_data()
|
||||||
|
|
||||||
for game_name, data in self.item_name_groups.items():
|
for game_name, data in self.item_name_groups.items():
|
||||||
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
|
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
|
||||||
for game_name, data in self.location_name_groups.items():
|
for game_name, data in self.location_name_groups.items():
|
||||||
@@ -552,55 +579,6 @@ class Context:
|
|||||||
# sorted access spheres
|
# sorted access spheres
|
||||||
self.spheres = decoded_obj.get("spheres", [])
|
self.spheres = decoded_obj.get("spheres", [])
|
||||||
|
|
||||||
def _load_world_data(self) -> None:
|
|
||||||
import worlds
|
|
||||||
|
|
||||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
|
||||||
# TODO: move hint_blacklist into GamesPackage?
|
|
||||||
self.non_hintable_names[world_name] = world.hint_blacklist
|
|
||||||
|
|
||||||
def _load_data_package(self, data_package: dict[str, GamesPackage]) -> None:
|
|
||||||
"""Populates reduced_games_package, item_name_groups, location_name_groups from static data and data_package"""
|
|
||||||
# NOTE: for worlds loaded from db, only checksum is set in GamesPackage, but this is handled by cache
|
|
||||||
for game_name in sorted(self.played_games):
|
|
||||||
if game_name in data_package:
|
|
||||||
self.logger.info(f"Loading embedded data package for game {game_name}")
|
|
||||||
data = self.games_package_cache.get(game_name, data_package[game_name])
|
|
||||||
else:
|
|
||||||
# NOTE: we still allow uploading a game without datapackage. Once that is changed, we could drop this.
|
|
||||||
data = self.games_package_cache.get_static(game_name)
|
|
||||||
(
|
|
||||||
self.reduced_games_package[game_name],
|
|
||||||
self.item_name_groups[game_name],
|
|
||||||
self.location_name_groups[game_name],
|
|
||||||
) = data
|
|
||||||
|
|
||||||
del self.games_package_cache # Not used past this point. Free memory.
|
|
||||||
|
|
||||||
def _init_game_data(self) -> None:
|
|
||||||
"""Update internal values from previously loaded data packages"""
|
|
||||||
for game_name, game_package in self.reduced_games_package.items():
|
|
||||||
if game_name not in self.played_games:
|
|
||||||
continue
|
|
||||||
if "checksum" in game_package:
|
|
||||||
self.checksums[game_name] = game_package["checksum"]
|
|
||||||
# NOTE: we could save more memory by moving the stuff below to data package cache as well
|
|
||||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
|
||||||
self.item_names[game_name][item_id] = item_name
|
|
||||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
|
||||||
self.location_names[game_name][location_id] = location_name
|
|
||||||
self.all_item_and_group_names[game_name] = \
|
|
||||||
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
|
||||||
self.all_location_and_group_names[game_name] = \
|
|
||||||
set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, []))
|
|
||||||
|
|
||||||
archipelago_item_names = self.item_names["Archipelago"]
|
|
||||||
archipelago_location_names = self.location_names["Archipelago"]
|
|
||||||
for game in [game_name for game_name in self.reduced_games_package if game_name != "Archipelago"]:
|
|
||||||
# Add Archipelago items and locations to each data package.
|
|
||||||
self.item_names[game].update(archipelago_item_names)
|
|
||||||
self.location_names[game].update(archipelago_location_names)
|
|
||||||
|
|
||||||
# saving
|
# saving
|
||||||
|
|
||||||
def save(self, now=False) -> bool:
|
def save(self, now=False) -> bool:
|
||||||
@@ -941,10 +919,12 @@ async def server(websocket: "ServerConnection", path: str = "/", ctx: Context =
|
|||||||
|
|
||||||
|
|
||||||
async def on_client_connected(ctx: Context, client: Client):
|
async def on_client_connected(ctx: Context, client: Client):
|
||||||
|
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
||||||
|
games.add("Archipelago")
|
||||||
await ctx.send_msgs(client, [{
|
await ctx.send_msgs(client, [{
|
||||||
'cmd': 'RoomInfo',
|
'cmd': 'RoomInfo',
|
||||||
'password': bool(ctx.password),
|
'password': bool(ctx.password),
|
||||||
'games': sorted(ctx.played_games),
|
'games': games,
|
||||||
# tags are for additional features in the communication.
|
# tags are for additional features in the communication.
|
||||||
# Name them by feature or fork, as you feel is appropriate.
|
# Name them by feature or fork, as you feel is appropriate.
|
||||||
'tags': ctx.tags,
|
'tags': ctx.tags,
|
||||||
@@ -953,7 +933,8 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
'permissions': get_permissions(ctx),
|
'permissions': get_permissions(ctx),
|
||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
'location_check_points': ctx.location_check_points,
|
'location_check_points': ctx.location_check_points,
|
||||||
'datapackage_checksums': ctx.checksums,
|
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
|
||||||
|
in ctx.gamespackage.items() if game in games and "checksum" in game_data},
|
||||||
'seed_name': ctx.seed_name,
|
'seed_name': ctx.seed_name,
|
||||||
'time': time.time(),
|
'time': time.time(),
|
||||||
}])
|
}])
|
||||||
@@ -1959,11 +1940,25 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
await ctx.send_msgs(client, reply)
|
await ctx.send_msgs(client, reply)
|
||||||
|
|
||||||
elif cmd == "GetDataPackage":
|
elif cmd == "GetDataPackage":
|
||||||
games = {
|
exclusions = args.get("exclusions", [])
|
||||||
name: game_data for name, game_data in ctx.reduced_games_package.items()
|
if "games" in args:
|
||||||
if name in set(args.get("games", []))
|
games = {name: game_data for name, game_data in ctx.gamespackage.items()
|
||||||
}
|
if name in set(args.get("games", []))}
|
||||||
await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": {"games": games}}])
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
|
"data": {"games": games}}])
|
||||||
|
# TODO: remove exclusions behaviour around 0.5.0
|
||||||
|
elif exclusions:
|
||||||
|
exclusions = set(exclusions)
|
||||||
|
games = {name: game_data for name, game_data in ctx.gamespackage.items()
|
||||||
|
if name not in exclusions}
|
||||||
|
|
||||||
|
package = {"games": games}
|
||||||
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
|
"data": package}])
|
||||||
|
|
||||||
|
else:
|
||||||
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
|
"data": {"games": ctx.gamespackage}}])
|
||||||
|
|
||||||
elif client.auth:
|
elif client.auth:
|
||||||
if cmd == "ConnectUpdate":
|
if cmd == "ConnectUpdate":
|
||||||
|
|||||||
39
Options.py
39
Options.py
@@ -212,6 +212,13 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
|||||||
else:
|
else:
|
||||||
return cls.name_lookup[value]
|
return cls.name_lookup[value]
|
||||||
|
|
||||||
|
def __eq__(self, other: typing.Any) -> bool:
|
||||||
|
if isinstance(other, self.__class__):
|
||||||
|
return self.value == other.value
|
||||||
|
if isinstance(other, Option):
|
||||||
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||||
|
return self.value == other
|
||||||
|
|
||||||
def __int__(self) -> T:
|
def __int__(self) -> T:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
@@ -930,13 +937,34 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
|||||||
class OptionCounter(OptionDict):
|
class OptionCounter(OptionDict):
|
||||||
min: int | None = None
|
min: int | None = None
|
||||||
max: int | None = None
|
max: int | None = None
|
||||||
|
cull_zeroes: bool = False
|
||||||
|
|
||||||
def __init__(self, value: dict[str, int]) -> None:
|
def __init__(self, value: dict[str, int]) -> None:
|
||||||
super(OptionCounter, self).__init__(collections.Counter(value))
|
cleaned_dict = {}
|
||||||
|
|
||||||
|
invalid_value_errors = []
|
||||||
|
for key, value in value.items():
|
||||||
|
if not isinstance(value, (int, float)) or int(value) != value:
|
||||||
|
invalid_value_errors += [f"Invalid value {value} for key {key}, must be an integer."]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.cull_zeroes and value == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cleaned_dict[key] = int(value)
|
||||||
|
|
||||||
|
if invalid_value_errors:
|
||||||
|
type_errors = [f"For option {self.__class__.__name__}:"] + invalid_value_errors
|
||||||
|
raise TypeError("\n".join(invalid_value_errors))
|
||||||
|
|
||||||
|
super(OptionCounter, self).__init__(collections.Counter(cleaned_dict))
|
||||||
|
|
||||||
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||||
super(OptionCounter, self).verify(world, player_name, plando_options)
|
super(OptionCounter, self).verify(world, player_name, plando_options)
|
||||||
|
|
||||||
|
self.verify_values()
|
||||||
|
|
||||||
|
def verify_values(self):
|
||||||
range_errors = []
|
range_errors = []
|
||||||
|
|
||||||
if self.max is not None:
|
if self.max is not None:
|
||||||
@@ -959,13 +987,8 @@ class OptionCounter(OptionDict):
|
|||||||
class ItemDict(OptionCounter):
|
class ItemDict(OptionCounter):
|
||||||
verify_item_name = True
|
verify_item_name = True
|
||||||
|
|
||||||
min = 0
|
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
|
||||||
|
cull_zeroes = True
|
||||||
def __init__(self, value: dict[str, int]) -> None:
|
|
||||||
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
|
|
||||||
value = {item_name: amount for item_name, amount in value.items() if amount != 0}
|
|
||||||
|
|
||||||
super(ItemDict, self).__init__(value)
|
|
||||||
|
|
||||||
|
|
||||||
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||||
|
|||||||
@@ -384,10 +384,11 @@ class OptionsCreator(ThemedApp):
|
|||||||
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
|
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
|
||||||
text = VisualFreeText(option=option, name=name)
|
text = VisualFreeText(option=option, name=name)
|
||||||
|
|
||||||
def set_value(instance):
|
def set_value(instance, value):
|
||||||
self.options[name] = instance.text
|
self.options[name] = value
|
||||||
|
|
||||||
text.bind(on_text_validate=set_value)
|
text.bind(text=set_value)
|
||||||
|
self.options[name] = option.default
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def create_choice(self, option: typing.Type[Choice], name: str):
|
def create_choice(self, option: typing.Type[Choice], name: str):
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ Currently, the following games are supported:
|
|||||||
* The Witness
|
* The Witness
|
||||||
* Sonic Adventure 2: Battle
|
* Sonic Adventure 2: Battle
|
||||||
* Starcraft 2
|
* Starcraft 2
|
||||||
* Donkey Kong Country 3
|
|
||||||
* Dark Souls 3
|
* Dark Souls 3
|
||||||
* Super Mario World
|
* Super Mario World
|
||||||
* Pokémon Red and Blue
|
* Pokémon Red and Blue
|
||||||
|
|||||||
40
Utils.py
40
Utils.py
@@ -22,7 +22,7 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from settings import Settings, get_settings
|
from settings import Settings, get_settings
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
from typing import BinaryIO, Coroutine, Mapping, Optional, Set, Dict, Any, Union, TypeGuard
|
||||||
from yaml import load, load_all, dump
|
from yaml import load, load_all, dump
|
||||||
from pathspec import PathSpec, GitIgnoreSpec
|
from pathspec import PathSpec, GitIgnoreSpec
|
||||||
from typing_extensions import deprecated
|
from typing_extensions import deprecated
|
||||||
@@ -236,10 +236,7 @@ def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
|||||||
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
||||||
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
||||||
|
|
||||||
env = os.environ
|
env = env_cleared_lib_path()
|
||||||
if "LD_LIBRARY_PATH" in env:
|
|
||||||
env = env.copy()
|
|
||||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
|
||||||
subprocess.call([open_command, filename], env=env)
|
subprocess.call([open_command, filename], env=env)
|
||||||
|
|
||||||
|
|
||||||
@@ -345,6 +342,9 @@ def persistent_load() -> Dict[str, Dict[str, Any]]:
|
|||||||
try:
|
try:
|
||||||
with open(path, "r") as f:
|
with open(path, "r") as f:
|
||||||
storage = unsafe_parse_yaml(f.read())
|
storage = unsafe_parse_yaml(f.read())
|
||||||
|
if "datapackage" in storage:
|
||||||
|
del storage["datapackage"]
|
||||||
|
logging.debug("Removed old datapackage from persistent storage")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug(f"Could not read store: {e}")
|
logging.debug(f"Could not read store: {e}")
|
||||||
if storage is None:
|
if storage is None:
|
||||||
@@ -369,11 +369,6 @@ def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) ->
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug(f"Could not load data package: {e}")
|
logging.debug(f"Could not load data package: {e}")
|
||||||
|
|
||||||
# fall back to old cache
|
|
||||||
cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
|
|
||||||
if cache.get("checksum") == checksum:
|
|
||||||
return cache
|
|
||||||
|
|
||||||
# cache does not match
|
# cache does not match
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -455,13 +450,10 @@ safe_builtins = frozenset((
|
|||||||
|
|
||||||
|
|
||||||
class RestrictedUnpickler(pickle.Unpickler):
|
class RestrictedUnpickler(pickle.Unpickler):
|
||||||
generic_properties_module: Optional[object]
|
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||||
self.options_module = importlib.import_module("Options")
|
self.options_module = importlib.import_module("Options")
|
||||||
self.net_utils_module = importlib.import_module("NetUtils")
|
self.net_utils_module = importlib.import_module("NetUtils")
|
||||||
self.generic_properties_module = None
|
|
||||||
|
|
||||||
def find_class(self, module: str, name: str) -> type:
|
def find_class(self, module: str, name: str) -> type:
|
||||||
if module == "builtins" and name in safe_builtins:
|
if module == "builtins" and name in safe_builtins:
|
||||||
@@ -475,10 +467,6 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
"SlotType", "NetworkSlot", "HintStatus"}:
|
"SlotType", "NetworkSlot", "HintStatus"}:
|
||||||
return getattr(self.net_utils_module, name)
|
return getattr(self.net_utils_module, name)
|
||||||
# Options and Plando are unpickled by WebHost -> Generate
|
# Options and Plando are unpickled by WebHost -> Generate
|
||||||
if module == "worlds.generic" and name == "PlandoItem":
|
|
||||||
if not self.generic_properties_module:
|
|
||||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
|
||||||
return getattr(self.generic_properties_module, name)
|
|
||||||
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
||||||
if module.lower().endswith("options"):
|
if module.lower().endswith("options"):
|
||||||
if module == "Options":
|
if module == "Options":
|
||||||
@@ -758,6 +746,19 @@ def is_kivy_running() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def env_cleared_lib_path() -> Mapping[str, str]:
|
||||||
|
"""
|
||||||
|
Creates a copy of the current environment vars with the LD_LIBRARY_PATH removed if set, as this can interfere when
|
||||||
|
launching something in a subprocess.
|
||||||
|
"""
|
||||||
|
env = os.environ
|
||||||
|
if "LD_LIBRARY_PATH" in env:
|
||||||
|
env = env.copy()
|
||||||
|
del env["LD_LIBRARY_PATH"]
|
||||||
|
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
|
def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
|
||||||
if is_kivy_running():
|
if is_kivy_running():
|
||||||
raise RuntimeError("kivy should not be running in multiprocess")
|
raise RuntimeError("kivy should not be running in multiprocess")
|
||||||
@@ -770,10 +771,7 @@ def _mp_save_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args:
|
|||||||
res.put(save_filename(*args))
|
res.put(save_filename(*args))
|
||||||
|
|
||||||
def _run_for_stdout(*args: str):
|
def _run_for_stdout(*args: str):
|
||||||
env = os.environ
|
env = env_cleared_lib_path()
|
||||||
if "LD_LIBRARY_PATH" in env:
|
|
||||||
env = env.copy()
|
|
||||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
|
||||||
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
|
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,13 +110,14 @@ if __name__ == "__main__":
|
|||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
logging.warning("Could not update LttP sprites.")
|
logging.warning("Could not update LttP sprites.")
|
||||||
app = get_app()
|
app = get_app()
|
||||||
from worlds import AutoWorldRegister
|
from worlds import AutoWorldRegister, network_data_package
|
||||||
# Update to only valid WebHost worlds
|
# Update to only valid WebHost worlds
|
||||||
invalid_worlds = {name for name, world in AutoWorldRegister.world_types.items()
|
invalid_worlds = {name for name, world in AutoWorldRegister.world_types.items()
|
||||||
if not hasattr(world.web, "tutorials")}
|
if not hasattr(world.web, "tutorials")}
|
||||||
if invalid_worlds:
|
if invalid_worlds:
|
||||||
logging.error(f"Following worlds not loaded as they are invalid for WebHost: {invalid_worlds}")
|
logging.error(f"Following worlds not loaded as they are invalid for WebHost: {invalid_worlds}")
|
||||||
AutoWorldRegister.world_types = {k: v for k, v in AutoWorldRegister.world_types.items() if k not in invalid_worlds}
|
AutoWorldRegister.world_types = {k: v for k, v in AutoWorldRegister.world_types.items() if k not in invalid_worlds}
|
||||||
|
network_data_package["games"] = {k: v for k, v in network_data_package["games"].items() if k not in invalid_worlds}
|
||||||
create_options_files()
|
create_options_files()
|
||||||
copy_tutorials_files_to_static()
|
copy_tutorials_files_to_static()
|
||||||
if app.config["SELFLAUNCH"]:
|
if app.config["SELFLAUNCH"]:
|
||||||
|
|||||||
@@ -42,11 +42,12 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
|
|||||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||||
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
||||||
app.config["GAME_PORTS"] = ["49152-65535", 0]
|
|
||||||
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
||||||
app.config["JOB_THRESHOLD"] = 1
|
app.config["JOB_THRESHOLD"] = 1
|
||||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||||
app.config["JOB_TIME"] = 600
|
app.config["JOB_TIME"] = 600
|
||||||
|
# maximum time in seconds since last activity for a room to be hosted
|
||||||
|
app.config["MAX_ROOM_TIMEOUT"] = 259200
|
||||||
# memory limit for generator processes in bytes
|
# memory limit for generator processes in bytes
|
||||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||||
|
|
||||||
@@ -70,7 +71,9 @@ CLI(app)
|
|||||||
|
|
||||||
|
|
||||||
def to_python(value: str) -> uuid.UUID:
|
def to_python(value: str) -> uuid.UUID:
|
||||||
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
|
if "=" in value or any(c.isspace() for c in value):
|
||||||
|
raise ValueError("Invalid UUID format")
|
||||||
|
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=' * (-len(value) % 4)))
|
||||||
|
|
||||||
|
|
||||||
def to_url(value: uuid.UUID) -> str:
|
def to_url(value: uuid.UUID) -> str:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from threading import Event, Thread
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pony.orm import db_session, select, commit, PrimaryKey
|
from pony.orm import db_session, select, commit, PrimaryKey, desc
|
||||||
|
|
||||||
from Utils import restricted_loads, utcnow
|
from Utils import restricted_loads, utcnow
|
||||||
from .locker import Locker, AlreadyRunningException
|
from .locker import Locker, AlreadyRunningException
|
||||||
@@ -129,7 +129,8 @@ def autohost(config: dict):
|
|||||||
with db_session:
|
with db_session:
|
||||||
rooms = select(
|
rooms = select(
|
||||||
room for room in Room if
|
room for room in Room if
|
||||||
room.last_activity >= utcnow() - timedelta(days=3))
|
room.last_activity >= utcnow() - timedelta(
|
||||||
|
seconds=config["MAX_ROOM_TIMEOUT"])).order_by(desc(Room.last_port))
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
||||||
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
|
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
|
||||||
@@ -187,7 +188,6 @@ class MultiworldInstance():
|
|||||||
self.cert = config["SELFLAUNCHCERT"]
|
self.cert = config["SELFLAUNCHCERT"]
|
||||||
self.key = config["SELFLAUNCHKEY"]
|
self.key = config["SELFLAUNCHKEY"]
|
||||||
self.host = config["HOST_ADDRESS"]
|
self.host = config["HOST_ADDRESS"]
|
||||||
self.game_ports = config["GAME_PORTS"]
|
|
||||||
self.rooms_to_start = multiprocessing.Queue()
|
self.rooms_to_start = multiprocessing.Queue()
|
||||||
self.rooms_shutting_down = multiprocessing.Queue()
|
self.rooms_shutting_down = multiprocessing.Queue()
|
||||||
self.name = f"MultiHoster{id}"
|
self.name = f"MultiHoster{id}"
|
||||||
@@ -198,7 +198,7 @@ class MultiworldInstance():
|
|||||||
|
|
||||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||||
args=(self.name, self.ponyconfig, get_static_server_data(),
|
args=(self.name, self.ponyconfig, get_static_server_data(),
|
||||||
self.cert, self.key, self.host, self.game_ports,
|
self.cert, self.key, self.host,
|
||||||
self.rooms_to_start, self.rooms_shutting_down),
|
self.rooms_to_start, self.rooms_shutting_down),
|
||||||
name=self.name)
|
name=self.name)
|
||||||
process.start()
|
process.start()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import asyncio
|
|||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import pickle
|
import pickle
|
||||||
@@ -14,9 +13,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
import sys
|
import sys
|
||||||
from asyncio import AbstractEventLoop
|
|
||||||
|
|
||||||
import psutil
|
|
||||||
import websockets
|
import websockets
|
||||||
from pony.orm import commit, db_session, select
|
from pony.orm import commit, db_session, select
|
||||||
|
|
||||||
@@ -27,10 +24,8 @@ from MultiServer import (
|
|||||||
server_per_message_deflate_factory,
|
server_per_message_deflate_factory,
|
||||||
)
|
)
|
||||||
from Utils import restricted_loads, cache_argsless
|
from Utils import restricted_loads, cache_argsless
|
||||||
from NetUtils import GamesPackage
|
|
||||||
from apmw.webhost.customserver.gamespackagecache import DBGamesPackageCache
|
|
||||||
from .locker import Locker
|
from .locker import Locker
|
||||||
from .models import Command, Room, db
|
from .models import Command, GameDataPackage, Room, db
|
||||||
|
|
||||||
|
|
||||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||||
@@ -67,39 +62,18 @@ class DBCommandProcessor(ServerCommandProcessor):
|
|||||||
|
|
||||||
class WebHostContext(Context):
|
class WebHostContext(Context):
|
||||||
room_id: int
|
room_id: int
|
||||||
video: dict[tuple[int, int], tuple[str, str]]
|
|
||||||
main_loop: AbstractEventLoop
|
|
||||||
static_server_data: StaticServerData
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, static_server_data: dict, logger: logging.Logger):
|
||||||
self,
|
|
||||||
static_server_data: StaticServerData,
|
|
||||||
games_package_cache: DBGamesPackageCache,
|
|
||||||
logger: logging.Logger,
|
|
||||||
) -> None:
|
|
||||||
# static server data is used during _load_game_data to load required data,
|
# static server data is used during _load_game_data to load required data,
|
||||||
# without needing to import worlds system, which takes quite a bit of memory
|
# without needing to import worlds system, which takes quite a bit of memory
|
||||||
super(WebHostContext, self).__init__(
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
1,
|
|
||||||
40,
|
|
||||||
True,
|
|
||||||
"enabled",
|
|
||||||
"enabled",
|
|
||||||
"enabled",
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
games_package_cache=games_package_cache,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
self.tags = ["AP", "WebHost"]
|
|
||||||
self.video = {}
|
|
||||||
self.main_loop = asyncio.get_running_loop()
|
|
||||||
self.static_server_data = static_server_data
|
self.static_server_data = static_server_data
|
||||||
self.games_package_cache = games_package_cache
|
super(WebHostContext, self).__init__("", 0, "", "", 1,
|
||||||
|
40, True, "enabled", "enabled",
|
||||||
|
"enabled", 0, 2, logger=logger)
|
||||||
|
del self.static_server_data
|
||||||
|
self.main_loop = asyncio.get_running_loop()
|
||||||
|
self.video = {}
|
||||||
|
self.tags = ["AP", "WebHost"]
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
try:
|
try:
|
||||||
@@ -109,6 +83,12 @@ class WebHostContext(Context):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
self.logger.debug("Context destroyed")
|
self.logger.debug("Context destroyed")
|
||||||
|
|
||||||
|
def _load_game_data(self):
|
||||||
|
for key, value in self.static_server_data.items():
|
||||||
|
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
|
||||||
|
setattr(self, key, value)
|
||||||
|
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||||
|
|
||||||
async def listen_to_db_commands(self):
|
async def listen_to_db_commands(self):
|
||||||
cmdprocessor = DBCommandProcessor(self)
|
cmdprocessor = DBCommandProcessor(self)
|
||||||
|
|
||||||
@@ -135,17 +115,45 @@ class WebHostContext(Context):
|
|||||||
if room.last_port:
|
if room.last_port:
|
||||||
self.port = room.last_port
|
self.port = room.last_port
|
||||||
else:
|
else:
|
||||||
self.port = 0
|
self.port = get_random_port()
|
||||||
|
|
||||||
multidata = self.decompress(room.seed.multidata)
|
multidata = self.decompress(room.seed.multidata)
|
||||||
return self._load(multidata, True)
|
game_data_packages = {}
|
||||||
|
|
||||||
def _load_world_data(self):
|
static_gamespackage = self.gamespackage # this is shared across all rooms
|
||||||
# Use static_server_data, but skip static data package since that is in cache anyway.
|
static_item_name_groups = self.item_name_groups
|
||||||
# Also NOT importing worlds here!
|
static_location_name_groups = self.location_name_groups
|
||||||
# FIXME: does this copy the non_hintable_names (also for games not part of the room)?
|
self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load
|
||||||
self.non_hintable_names = collections.defaultdict(frozenset, self.static_server_data["non_hintable_names"])
|
self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})}
|
||||||
del self.static_server_data # Not used past this point. Free memory.
|
self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})}
|
||||||
|
missing_checksum = False
|
||||||
|
|
||||||
|
for game in list(multidata.get("datapackage", {})):
|
||||||
|
game_data = multidata["datapackage"][game]
|
||||||
|
if "checksum" in game_data:
|
||||||
|
if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||||
|
# non-custom. remove from multidata and use static data
|
||||||
|
# games package could be dropped from static data once all rooms embed data package
|
||||||
|
del multidata["datapackage"][game]
|
||||||
|
else:
|
||||||
|
row = GameDataPackage.get(checksum=game_data["checksum"])
|
||||||
|
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete
|
||||||
|
game_data_packages[game] = restricted_loads(row.data)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
|
||||||
|
else:
|
||||||
|
missing_checksum = True # Game rolled on old AP and will load data package from multidata
|
||||||
|
self.gamespackage[game] = static_gamespackage.get(game, {})
|
||||||
|
self.item_name_groups[game] = static_item_name_groups.get(game, {})
|
||||||
|
self.location_name_groups[game] = static_location_name_groups.get(game, {})
|
||||||
|
|
||||||
|
if not game_data_packages and not missing_checksum:
|
||||||
|
# all static -> use the static dicts directly
|
||||||
|
self.gamespackage = static_gamespackage
|
||||||
|
self.item_name_groups = static_item_name_groups
|
||||||
|
self.location_name_groups = static_location_name_groups
|
||||||
|
return self._load(multidata, game_data_packages, True)
|
||||||
|
|
||||||
def init_save(self, enabled: bool = True):
|
def init_save(self, enabled: bool = True):
|
||||||
self.saving = enabled
|
self.saving = enabled
|
||||||
@@ -173,117 +181,38 @@ class WebHostContext(Context):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class GameRangePorts(typing.NamedTuple):
|
def get_random_port():
|
||||||
parsed_ports: list[range]
|
return random.randint(49152, 65535)
|
||||||
weights: list[int]
|
|
||||||
ephemeral_allowed: bool
|
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
|
||||||
def parse_game_ports(game_ports: tuple[str | int, ...]) -> GameRangePorts:
|
|
||||||
parsed_ports: list[range] = []
|
|
||||||
weights: list[int] = []
|
|
||||||
ephemeral_allowed = False
|
|
||||||
total_length = 0
|
|
||||||
|
|
||||||
for item in game_ports:
|
|
||||||
if isinstance(item, str) and "-" in item:
|
|
||||||
start, end = map(int, item.split("-"))
|
|
||||||
x = range(start, end + 1)
|
|
||||||
total_length += len(x)
|
|
||||||
weights.append(total_length)
|
|
||||||
parsed_ports.append(x)
|
|
||||||
elif int(item) == 0:
|
|
||||||
ephemeral_allowed = True
|
|
||||||
else:
|
|
||||||
total_length += 1
|
|
||||||
weights.append(total_length)
|
|
||||||
num = int(item)
|
|
||||||
parsed_ports.append(range(num, num + 1))
|
|
||||||
|
|
||||||
return GameRangePorts(parsed_ports, weights, ephemeral_allowed)
|
|
||||||
|
|
||||||
|
|
||||||
def weighted_random(ranges: list[range], cum_weights: list[int]) -> int:
|
|
||||||
[picked] = random.choices(ranges, cum_weights=cum_weights)
|
|
||||||
return random.randrange(picked.start, picked.stop, picked.step)
|
|
||||||
|
|
||||||
|
|
||||||
def create_random_port_socket(game_ports: tuple[str | int, ...], host: str) -> socket.socket:
|
|
||||||
parsed_ports, weights, ephemeral_allowed = parse_game_ports(game_ports)
|
|
||||||
used_ports = get_used_ports()
|
|
||||||
i = 1024 if len(parsed_ports) > 0 else 0
|
|
||||||
while i > 0:
|
|
||||||
port_num = weighted_random(parsed_ports, weights)
|
|
||||||
if port_num in used_ports:
|
|
||||||
used_ports = get_used_ports()
|
|
||||||
continue
|
|
||||||
|
|
||||||
i -= 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
return socket.create_server((host, port_num))
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if ephemeral_allowed:
|
|
||||||
return socket.create_server((host, 0))
|
|
||||||
|
|
||||||
raise OSError(98, "No available ports")
|
|
||||||
|
|
||||||
|
|
||||||
def try_conns_per_process(p: psutil.Process) -> typing.Iterable[int]:
|
|
||||||
try:
|
|
||||||
return (c.laddr.port for c in p.net_connections("tcp4"))
|
|
||||||
except psutil.AccessDenied:
|
|
||||||
return ()
|
|
||||||
|
|
||||||
|
|
||||||
def get_active_net_connections() -> typing.Iterable[int]:
|
|
||||||
# Don't even try to check if system using AIX
|
|
||||||
if psutil.AIX:
|
|
||||||
return ()
|
|
||||||
|
|
||||||
try:
|
|
||||||
return (c.laddr.port for c in psutil.net_connections("tcp4"))
|
|
||||||
# raises AccessDenied when done on macOS
|
|
||||||
except psutil.AccessDenied:
|
|
||||||
# flatten the list of iterables
|
|
||||||
return itertools.chain.from_iterable(map(
|
|
||||||
# get the net connections of the process and then map its ports
|
|
||||||
try_conns_per_process,
|
|
||||||
# this method has caching handled by psutil
|
|
||||||
psutil.process_iter(["net_connections"])
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def get_used_ports():
|
|
||||||
last_used_ports: tuple[frozenset[int], float] | None = getattr(get_used_ports, "last", None)
|
|
||||||
t_hash = round(time.time() / 90) # cache for 90 seconds
|
|
||||||
if last_used_ports is None or last_used_ports[1] != t_hash:
|
|
||||||
last_used_ports = (frozenset(get_active_net_connections()), t_hash)
|
|
||||||
setattr(get_used_ports, "last", last_used_ports)
|
|
||||||
|
|
||||||
return last_used_ports[0]
|
|
||||||
|
|
||||||
|
|
||||||
class StaticServerData(typing.TypedDict, total=True):
|
|
||||||
non_hintable_names: dict[str, typing.AbstractSet[str]]
|
|
||||||
games_package: dict[str, GamesPackage]
|
|
||||||
|
|
||||||
|
|
||||||
@cache_argsless
|
@cache_argsless
|
||||||
def get_static_server_data() -> StaticServerData:
|
def get_static_server_data() -> dict:
|
||||||
import worlds
|
import worlds
|
||||||
|
data = {
|
||||||
return {
|
|
||||||
"non_hintable_names": {
|
"non_hintable_names": {
|
||||||
world_name: world.hint_blacklist
|
world_name: world.hint_blacklist
|
||||||
for world_name, world in worlds.AutoWorldRegister.world_types.items()
|
for world_name, world in worlds.AutoWorldRegister.world_types.items()
|
||||||
},
|
},
|
||||||
"games_package": worlds.network_data_package["games"]
|
"gamespackage": {
|
||||||
|
world_name: {
|
||||||
|
key: value
|
||||||
|
for key, value in game_package.items()
|
||||||
|
if key not in ("item_name_groups", "location_name_groups")
|
||||||
|
}
|
||||||
|
for world_name, game_package in worlds.network_data_package["games"].items()
|
||||||
|
},
|
||||||
|
"item_name_groups": {
|
||||||
|
world_name: world.item_name_groups
|
||||||
|
for world_name, world in worlds.AutoWorldRegister.world_types.items()
|
||||||
|
},
|
||||||
|
"location_name_groups": {
|
||||||
|
world_name: world.location_name_groups
|
||||||
|
for world_name, world in worlds.AutoWorldRegister.world_types.items()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def set_up_logging(room_id) -> logging.Logger:
|
def set_up_logging(room_id) -> logging.Logger:
|
||||||
import os
|
import os
|
||||||
@@ -316,19 +245,9 @@ def tear_down_logging(room_id):
|
|||||||
del logging.Logger.manager.loggerDict[logger_name]
|
del logging.Logger.manager.loggerDict[logger_name]
|
||||||
|
|
||||||
|
|
||||||
def run_server_process(
|
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||||
name: str,
|
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||||
ponyconfig: dict[str, typing.Any],
|
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||||
static_server_data: StaticServerData,
|
|
||||||
cert_file: typing.Optional[str],
|
|
||||||
cert_key_file: typing.Optional[str],
|
|
||||||
host: str,
|
|
||||||
game_ports: typing.Iterable[str | int],
|
|
||||||
rooms_to_run: multiprocessing.Queue,
|
|
||||||
rooms_shutting_down: multiprocessing.Queue,
|
|
||||||
) -> None:
|
|
||||||
import gc
|
|
||||||
|
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
setproctitle(name)
|
setproctitle(name)
|
||||||
@@ -344,11 +263,6 @@ def run_server_process(
|
|||||||
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
|
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
|
||||||
del resource, file_limit
|
del resource, file_limit
|
||||||
|
|
||||||
# prime the data package cache with static data
|
|
||||||
games_package_cache = DBGamesPackageCache(static_server_data["games_package"])
|
|
||||||
# convert to tuple because its hashable
|
|
||||||
game_ports = tuple(game_ports)
|
|
||||||
|
|
||||||
# establish DB connection for multidata and multisave
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
@@ -356,6 +270,8 @@ def run_server_process(
|
|||||||
if "worlds" in sys.modules:
|
if "worlds" in sys.modules:
|
||||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||||
|
|
||||||
|
import gc
|
||||||
|
|
||||||
if not cert_file:
|
if not cert_file:
|
||||||
def get_ssl_context():
|
def get_ssl_context():
|
||||||
return None
|
return None
|
||||||
@@ -380,30 +296,24 @@ def run_server_process(
|
|||||||
with Locker(f"RoomLocker {room_id}"):
|
with Locker(f"RoomLocker {room_id}"):
|
||||||
try:
|
try:
|
||||||
logger = set_up_logging(room_id)
|
logger = set_up_logging(room_id)
|
||||||
ctx = WebHostContext(static_server_data, games_package_cache, logger)
|
ctx = WebHostContext(static_server_data, logger)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
assert ctx.server is None
|
assert ctx.server is None
|
||||||
if ctx.port != 0:
|
try:
|
||||||
try:
|
|
||||||
ctx.server = websockets.serve(
|
|
||||||
functools.partial(server, ctx=ctx),
|
|
||||||
ctx.host,
|
|
||||||
ctx.port,
|
|
||||||
ssl=get_ssl_context(),
|
|
||||||
extensions=[server_per_message_deflate_factory],
|
|
||||||
)
|
|
||||||
await ctx.server
|
|
||||||
except OSError:
|
|
||||||
ctx.port = 0
|
|
||||||
if ctx.port == 0:
|
|
||||||
ctx.server = websockets.serve(
|
ctx.server = websockets.serve(
|
||||||
functools.partial(server, ctx=ctx),
|
functools.partial(server, ctx=ctx),
|
||||||
sock=create_random_port_socket(game_ports, ctx.host),
|
ctx.host,
|
||||||
|
ctx.port,
|
||||||
ssl=get_ssl_context(),
|
ssl=get_ssl_context(),
|
||||||
extensions=[server_per_message_deflate_factory],
|
extensions=[server_per_message_deflate_factory],
|
||||||
)
|
)
|
||||||
await ctx.server
|
await ctx.server
|
||||||
|
except OSError: # likely port in use
|
||||||
|
ctx.server = websockets.serve(
|
||||||
|
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
|
||||||
|
|
||||||
|
await ctx.server
|
||||||
port = 0
|
port = 0
|
||||||
for wssocket in ctx.server.ws_server.sockets:
|
for wssocket in ctx.server.ws_server.sockets:
|
||||||
socketname = wssocket.getsockname()
|
socketname = wssocket.getsockname()
|
||||||
@@ -478,7 +388,7 @@ def run_server_process(
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while 1:
|
while 1:
|
||||||
next_room = rooms_to_run.get(block=True, timeout=None)
|
next_room = rooms_to_run.get(block=True, timeout=None)
|
||||||
gc.collect()
|
gc.collect()
|
||||||
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
||||||
self._tasks.append(task)
|
self._tasks.append(task)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
flask>=3.1.1
|
flask==3.1.3
|
||||||
werkzeug>=3.1.3
|
werkzeug==3.1.6
|
||||||
pony>=0.7.19; python_version <= '3.12'
|
pony==0.7.19; python_version <= '3.12'
|
||||||
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
|
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
|
||||||
waitress>=3.0.2
|
waitress==3.0.2
|
||||||
Flask-Caching>=2.3.0
|
Flask-Caching==2.3.1
|
||||||
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
|
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
|
||||||
Flask-Limiter>=3.12
|
Flask-Limiter==4.1.1
|
||||||
Flask-Cors>=6.0.2
|
Flask-Cors==6.0.2
|
||||||
bokeh>=3.6.3
|
bokeh==3.8.2
|
||||||
markupsafe>=3.0.2
|
markupsafe==3.0.3
|
||||||
setproctitle>=1.3.5
|
setproctitle==1.3.7
|
||||||
mistune>=3.1.3
|
mistune==3.2.0
|
||||||
docutils>=0.22.2
|
docutils==0.22.4
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -33,17 +33,6 @@ html{
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-header h5 {
|
|
||||||
color: #ffffff;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 28px;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: -43px;
|
|
||||||
text-shadow: 1px 1px 7px #000000;
|
|
||||||
font-kerning: none;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
#landing-links{
|
#landing-links{
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% block footer %}
|
{% block footer %}
|
||||||
<footer id="island-footer">
|
<footer id="island-footer">
|
||||||
<div id="copyright-notice">Copyright 2025 Archipelago</div>
|
<div id="copyright-notice">Copyright 2026 Archipelago</div>
|
||||||
<div id="links">
|
<div id="links">
|
||||||
<a href="/sitemap">Site Map</a>
|
<a href="/sitemap">Site Map</a>
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div id="landing-wrapper">
|
<div id="landing-wrapper">
|
||||||
<div id="landing-header">
|
<div id="landing-header">
|
||||||
<img id="landing-logo" src="static/static/branding/landing-logo.png" alt="Archipelago Logo" />
|
<img id="landing-logo" src="static/static/branding/landing-logo.png" alt="Archipelago Logo" />
|
||||||
<h4>multiworld multi-game randomizer</h4><h5>beta</h5>
|
<h4>multiworld multi-game randomizer</h4>
|
||||||
</div>
|
</div>
|
||||||
<div id="landing-links">
|
<div id="landing-links">
|
||||||
<a href="/games" id="far-left-button">Supported<br />Games</a>
|
<a href="/games" id="far-left-button">Supported<br />Games</a>
|
||||||
@@ -35,8 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="landing" class="grass-island">
|
<div id="landing" class="grass-island">
|
||||||
<div id="landing-body">
|
<div id="landing-body">
|
||||||
<p id="first-line">Welcome to Archipelago Beta!</p>
|
<p id="first-line">Welcome to Archipelago!</p>
|
||||||
<p>For the stable version, visit <a href="//archipelago.gg">Archipelago.gg</a>!</p>
|
|
||||||
<p>
|
<p>
|
||||||
This is a cross-game modification system which randomizes different games, then uses the result to
|
This is a cross-game modification system which randomizes different games, then uses the result to
|
||||||
build a single unified multi-player game. Items from one game may be present in another, and
|
build a single unified multi-player game. Items from one game may be present in another, and
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="user-message">This is the beta site! For the stable version, visit <a href="https://archipelago.gg">Archipelago.gg</a>!</div>
|
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
<h1>Currently Supported Games</h1>
|
<h1>Currently Supported Games</h1>
|
||||||
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
|
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
|
||||||
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
|
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
|
||||||
custom worlds</a> section of the setup guide.</p>
|
custom worlds</a> section of the setup guide and the
|
||||||
|
<a href="{{ url_for("tutorial", game="Archipelago", file="other_en") }}">other games and tools guide</a>
|
||||||
|
to find more.</p>
|
||||||
<div class="js-only">
|
<div class="js-only">
|
||||||
<label for="game-search">Search for your game below!</label><br />
|
<label for="game-search">Search for your game below!</label><br />
|
||||||
<div class="page-controls">
|
<div class="page-controls">
|
||||||
|
|||||||
@@ -20,11 +20,7 @@
|
|||||||
{% for file_name, file_data in tutorial_data.files.items() %}
|
{% for file_name, file_data in tutorial_data.files.items() %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for("tutorial", game=world_name, file=file_name) }}">{{ file_data.language }}</a>
|
<a href="{{ url_for("tutorial", game=world_name, file=file_name) }}">{{ file_data.language }}</a>
|
||||||
by
|
by {{ file_data.authors | join(", ") }}
|
||||||
{% for author in file_data.authors %}
|
|
||||||
{{ author }}
|
|
||||||
{% if not loop.last %}, {% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import typing as t
|
|
||||||
from weakref import WeakValueDictionary
|
|
||||||
|
|
||||||
from NetUtils import GamesPackage
|
|
||||||
|
|
||||||
GameAndChecksum = tuple[str, str | None]
|
|
||||||
ItemNameGroups = dict[str, list[str]]
|
|
||||||
LocationNameGroups = dict[str, list[str]]
|
|
||||||
|
|
||||||
|
|
||||||
K = t.TypeVar("K")
|
|
||||||
V = t.TypeVar("V")
|
|
||||||
|
|
||||||
|
|
||||||
class DictLike(dict[K, V]):
|
|
||||||
__slots__ = ("__weakref__",)
|
|
||||||
|
|
||||||
|
|
||||||
class GamesPackageCache:
|
|
||||||
# NOTE: this uses 3 separate collections because unpacking the get() result would end the container lifetime
|
|
||||||
_reduced_games_packages: WeakValueDictionary[GameAndChecksum, GamesPackage]
|
|
||||||
"""Does not include item_name_groups nor location_name_groups"""
|
|
||||||
_item_name_groups: WeakValueDictionary[GameAndChecksum, dict[str, list[str]]]
|
|
||||||
_location_name_groups: WeakValueDictionary[GameAndChecksum, dict[str, list[str]]]
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._reduced_games_packages = WeakValueDictionary()
|
|
||||||
self._item_name_groups = WeakValueDictionary()
|
|
||||||
self._location_name_groups = WeakValueDictionary()
|
|
||||||
|
|
||||||
def _get(
|
|
||||||
self,
|
|
||||||
cache_key: GameAndChecksum,
|
|
||||||
) -> tuple[GamesPackage | None, ItemNameGroups | None, LocationNameGroups | None]:
|
|
||||||
if cache_key[1] is None:
|
|
||||||
return None, None, None
|
|
||||||
return (
|
|
||||||
self._reduced_games_packages.get(cache_key, None),
|
|
||||||
self._item_name_groups.get(cache_key, None),
|
|
||||||
self._location_name_groups.get(cache_key, None),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(
|
|
||||||
self,
|
|
||||||
game: str,
|
|
||||||
full_games_package: GamesPackage,
|
|
||||||
) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
|
|
||||||
"""Loads and caches embedded data package provided by multidata"""
|
|
||||||
cache_key = (game, full_games_package.get("checksum", None))
|
|
||||||
cached_reduced_games_package, cached_item_name_groups, cached_location_name_groups = self._get(cache_key)
|
|
||||||
|
|
||||||
if cached_reduced_games_package is None:
|
|
||||||
cached_reduced_games_package = t.cast(
|
|
||||||
t.Any,
|
|
||||||
DictLike(
|
|
||||||
{
|
|
||||||
"item_name_to_id": full_games_package["item_name_to_id"],
|
|
||||||
"location_name_to_id": full_games_package["location_name_to_id"],
|
|
||||||
"checksum": full_games_package.get("checksum", None),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if cache_key[1] is not None: # only cache if checksum is available
|
|
||||||
self._reduced_games_packages[cache_key] = cached_reduced_games_package
|
|
||||||
|
|
||||||
if cached_item_name_groups is None:
|
|
||||||
# optimize strings to be references instead of copies
|
|
||||||
item_names = {name: name for name in cached_reduced_games_package["item_name_to_id"].keys()}
|
|
||||||
cached_item_name_groups = DictLike(
|
|
||||||
{
|
|
||||||
group_name: [item_names.get(item_name, item_name) for item_name in group_items]
|
|
||||||
for group_name, group_items in full_games_package["item_name_groups"].items()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if cache_key[1] is not None: # only cache if checksum is available
|
|
||||||
self._item_name_groups[cache_key] = cached_item_name_groups
|
|
||||||
|
|
||||||
if cached_location_name_groups is None:
|
|
||||||
# optimize strings to be references instead of copies
|
|
||||||
location_names = {name: name for name in cached_reduced_games_package["location_name_to_id"].keys()}
|
|
||||||
cached_location_name_groups = DictLike(
|
|
||||||
{
|
|
||||||
group_name: [location_names.get(location_name, location_name) for location_name in group_locations]
|
|
||||||
for group_name, group_locations in full_games_package.get("location_name_groups", {}).items()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if cache_key[1] is not None: # only cache if checksum is available
|
|
||||||
self._location_name_groups[cache_key] = cached_location_name_groups
|
|
||||||
|
|
||||||
return cached_reduced_games_package, cached_item_name_groups, cached_location_name_groups
|
|
||||||
|
|
||||||
def get_static(self, game: str) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
|
|
||||||
"""Loads legacy data package from installed worlds"""
|
|
||||||
import worlds
|
|
||||||
|
|
||||||
return self.get(game, worlds.network_data_package["games"][game])
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from typing_extensions import override
|
|
||||||
|
|
||||||
from NetUtils import GamesPackage
|
|
||||||
from Utils import restricted_loads
|
|
||||||
from apmw.multiserver.gamespackagecache import GamesPackageCache, ItemNameGroups, LocationNameGroups
|
|
||||||
|
|
||||||
|
|
||||||
class DBGamesPackageCache(GamesPackageCache):
|
|
||||||
_static: dict[str, tuple[GamesPackage, ItemNameGroups, LocationNameGroups]]
|
|
||||||
|
|
||||||
def __init__(self, static_games_package: dict[str, GamesPackage]) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self._static = {
|
|
||||||
game: GamesPackageCache.get(self, game, games_package)
|
|
||||||
for game, games_package in static_games_package.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
def get(
|
|
||||||
self,
|
|
||||||
game: str,
|
|
||||||
full_games_package: GamesPackage,
|
|
||||||
) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
|
|
||||||
# for games started on webhost, full_games_package is likely unpopulated and only has the checksum field
|
|
||||||
cache_key = (game, full_games_package.get("checksum", None))
|
|
||||||
cached = self._get(cache_key)
|
|
||||||
if any(value is None for value in cached):
|
|
||||||
if "checksum" not in full_games_package:
|
|
||||||
return super().get(game, full_games_package) # no checksum, assume fully populated
|
|
||||||
|
|
||||||
from WebHostLib.models import GameDataPackage
|
|
||||||
|
|
||||||
row: GameDataPackage | None = GameDataPackage.get(checksum=full_games_package["checksum"])
|
|
||||||
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8 ...
|
|
||||||
return super().get(game, restricted_loads(row.data))
|
|
||||||
return super().get(game, full_games_package) # ... in which case full_games_package should be populated
|
|
||||||
|
|
||||||
return cached # type: ignore # mypy doesn't understand any value is None
|
|
||||||
|
|
||||||
@override
|
|
||||||
def get_static(self, game: str) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
|
|
||||||
return self._static[game]
|
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
||||||
/worlds/apquest/ @NewSoupVi
|
/worlds/apquest/ @NewSoupVi
|
||||||
|
|
||||||
# Sudoku (APSudoku)
|
|
||||||
/worlds/apsudoku/ @EmilyV99
|
|
||||||
|
|
||||||
# Aquaria
|
# Aquaria
|
||||||
/worlds/aquaria/ @tioui
|
/worlds/aquaria/ @tioui
|
||||||
@@ -58,9 +56,6 @@
|
|||||||
# Dark Souls III
|
# Dark Souls III
|
||||||
/worlds/dark_souls_3/ @Marechal-L @nex3
|
/worlds/dark_souls_3/ @Marechal-L @nex3
|
||||||
|
|
||||||
# Donkey Kong Country 3
|
|
||||||
/worlds/dkc3/ @PoryGone
|
|
||||||
|
|
||||||
# DLCQuest
|
# DLCQuest
|
||||||
/worlds/dlcquest/ @axe-y @agilbert1412
|
/worlds/dlcquest/ @axe-y @agilbert1412
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,6 @@ flowchart LR
|
|||||||
end
|
end
|
||||||
SNI <-- Various, depending on SNES device --> SMZ
|
SNI <-- Various, depending on SNES device --> SMZ
|
||||||
|
|
||||||
%% Donkey Kong Country 3
|
|
||||||
subgraph Donkey Kong Country 3
|
|
||||||
DK3[SNES]
|
|
||||||
end
|
|
||||||
SNI <-- Various, depending on SNES device --> DK3
|
|
||||||
|
|
||||||
%% Super Mario World
|
%% Super Mario World
|
||||||
subgraph Super Mario World
|
subgraph Super Mario World
|
||||||
SMW[SNES]
|
SMW[SNES]
|
||||||
|
|||||||
@@ -129,6 +129,42 @@ common_rule_only_on_easy = common_rule & easy_filter
|
|||||||
common_rule_skipped_on_easy = common_rule | easy_filter
|
common_rule_skipped_on_easy = common_rule | easy_filter
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Field resolvers
|
||||||
|
|
||||||
|
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a `FieldResolver` to define how to populate that field when the rule is being resolved.
|
||||||
|
|
||||||
|
There are two build-in field resolvers:
|
||||||
|
|
||||||
|
- `FromOption`: Resolves to the value of the given option
|
||||||
|
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get a nested attribute or dict item
|
||||||
|
|
||||||
|
```python
|
||||||
|
world.options.mcguffin_count = 5
|
||||||
|
world.precalculated_value = 99
|
||||||
|
rule = (
|
||||||
|
Has("A", count=FromOption(McguffinCount))
|
||||||
|
| HasGroup("Important items", count=FromWorldAttr("precalculated_value"))
|
||||||
|
)
|
||||||
|
# Results in Has("A", count=5) | HasGroup("Important items", count=99)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and implements a `resolve` function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class FromCustomResolution(FieldResolver, game="MyGame"):
|
||||||
|
modifier: str
|
||||||
|
|
||||||
|
@override
|
||||||
|
def resolve(self, world: "World") -> Any:
|
||||||
|
return some_math_calculation(world, self.modifier)
|
||||||
|
|
||||||
|
|
||||||
|
rule = Has("Combat Level", count=FromCustomResolution("combat"))
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to support rule serialization and your resolver contains non-serializable properties you may need to override `to_dict` or `from_dict`.
|
||||||
|
|
||||||
## Enabling caching
|
## Enabling caching
|
||||||
|
|
||||||
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
|
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ Unless you configured PyCharm to use pytest as a test runner, you may get import
|
|||||||
edit the run configuration, and set the working directory to the Archipelago directory which contains all the project files.
|
edit the run configuration, and set the working directory to the Archipelago directory which contains all the project files.
|
||||||
|
|
||||||
If you only want to run your world's defined tests, repeat the steps for the test directory within your world.
|
If you only want to run your world's defined tests, repeat the steps for the test directory within your world.
|
||||||
Your working directory should be the directory of your world in the worlds directory and the script should be the
|
Your working directory should be the root Archipelago directory and the script should be the
|
||||||
tests folder within your world.
|
tests folder within your world.
|
||||||
|
|
||||||
You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window
|
You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ Example:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
...
|
...
|
||||||
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
|
|
||||||
"Factorio":"a699194a9589db3ebc0d821915864b422c782f44",
|
"Factorio":"a699194a9589db3ebc0d821915864b422c782f44",
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,6 @@
|
|||||||
# Web hosting port
|
# Web hosting port
|
||||||
#PORT: 80
|
#PORT: 80
|
||||||
|
|
||||||
# Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: [49152-65535, 0]
|
|
||||||
# Zero means it will use a random free port if there is no port in the next 1024 randomly chosen ports from the range
|
|
||||||
# Examples of valid values: [40000-41000, 49152-65535]
|
|
||||||
# If ports within the range(s) are already in use, the WebHost will fallback to the default [49152-65535, 0] range.
|
|
||||||
#GAME_PORTS: [49152-65535, 0]
|
|
||||||
|
|
||||||
# Place where uploads go.
|
# Place where uploads go.
|
||||||
#UPLOAD_FOLDER: uploads
|
#UPLOAD_FOLDER: uploads
|
||||||
|
|
||||||
|
|||||||
@@ -98,11 +98,6 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Arc
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
|
||||||
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".apsmw"; ValueData: "{#MyAppName}smwpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}smwpatch"; ValueData: "Archipelago Super Mario World Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}smwpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
colorama>=0.4.6
|
colorama==0.4.6
|
||||||
websockets>=13.0.1,<14
|
websockets==13.1 # ,<14
|
||||||
PyYAML>=6.0.3
|
PyYAML==6.0.3
|
||||||
jellyfish>=1.2.1
|
jellyfish==1.2.1
|
||||||
jinja2>=3.1.6
|
jinja2==3.1.6
|
||||||
schema>=0.7.8
|
schema==0.7.8
|
||||||
kivy>=2.3.1
|
kivy==2.3.1
|
||||||
bsdiff4>=1.2.6
|
bsdiff4==1.2.6
|
||||||
platformdirs>=4.5.0
|
platformdirs==4.9.4
|
||||||
certifi>=2025.11.12
|
certifi==2026.2.25
|
||||||
cython>=3.2.1
|
cython==3.2.4
|
||||||
cymem>=2.0.13
|
cymem==2.0.13
|
||||||
orjson>=3.11.4
|
orjson==3.11.7
|
||||||
typing_extensions>=4.15.0
|
typing_extensions==4.15.0
|
||||||
pyshortcuts>=1.9.6
|
pyshortcuts==1.9.7
|
||||||
pathspec>=0.12.1
|
pathspec==1.0.4
|
||||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||||
kivymd>=2.0.1.dev0
|
kivymd>=2.0.1.dev0
|
||||||
|
|
||||||
# Legacy world dependencies that custom worlds rely on
|
# Legacy world dependencies that custom worlds rely on
|
||||||
Pymem>=1.13.0
|
Pymem==1.14.0
|
||||||
|
|||||||
162
rule_builder/field_resolvers.py
Normal file
162
rule_builder/field_resolvers.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import dataclasses
|
||||||
|
import importlib
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import TYPE_CHECKING, Any, ClassVar, Self, TypeVar, cast, overload
|
||||||
|
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from Options import Option
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
|
||||||
|
|
||||||
|
class FieldResolverRegister:
|
||||||
|
"""A container class to contain world custom resolvers"""
|
||||||
|
|
||||||
|
custom_resolvers: ClassVar[dict[str, dict[str, type["FieldResolver"]]]] = {}
|
||||||
|
"""
|
||||||
|
A mapping of game name to mapping of resolver name to resolver class
|
||||||
|
to hold custom resolvers implemented by worlds
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_resolver_cls(cls, game_name: str, resolver_name: str) -> type["FieldResolver"]:
|
||||||
|
"""Returns the world-registered or default resolver with the given name"""
|
||||||
|
custom_resolver_classes = cls.custom_resolvers.get(game_name, {})
|
||||||
|
if resolver_name not in DEFAULT_RESOLVERS and resolver_name not in custom_resolver_classes:
|
||||||
|
raise ValueError(f"Resolver '{resolver_name}' for game '{game_name}' not found")
|
||||||
|
return custom_resolver_classes.get(resolver_name) or DEFAULT_RESOLVERS[resolver_name]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class FieldResolver(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def resolve(self, world: "World") -> Any: ...
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
"""Returns a JSON compatible dict representation of this resolver"""
|
||||||
|
fields = {field.name: getattr(self, field.name, None) for field in dataclasses.fields(self)}
|
||||||
|
return {
|
||||||
|
"resolver": self.__class__.__name__,
|
||||||
|
**fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||||
|
"""Returns a new instance of this resolver from a serialized dict representation"""
|
||||||
|
assert data.get("resolver", None) == cls.__name__
|
||||||
|
return cls(**{k: v for k, v in data.items() if k != "resolver"})
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(cls, /, game: str) -> None:
|
||||||
|
if game != "Archipelago":
|
||||||
|
custom_resolvers = FieldResolverRegister.custom_resolvers.setdefault(game, {})
|
||||||
|
if cls.__qualname__ in custom_resolvers:
|
||||||
|
raise TypeError(f"Resolver {cls.__qualname__} has already been registered for game {game}")
|
||||||
|
custom_resolvers[cls.__qualname__] = cls
|
||||||
|
elif cls.__module__ != "rule_builder.field_resolvers":
|
||||||
|
raise TypeError("You cannot define custom resolvers for the base Archipelago world")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class FromOption(FieldResolver, game="Archipelago"):
|
||||||
|
option: type[Option[Any]]
|
||||||
|
field: str = "value"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def resolve(self, world: "World") -> Any:
|
||||||
|
option_name = next(
|
||||||
|
(name for name, cls in world.options.__class__.type_hints.items() if cls is self.option),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if option_name is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot find option {self.option.__name__} in options class {world.options.__class__.__name__}"
|
||||||
|
)
|
||||||
|
opt = cast(Option[Any] | None, getattr(world.options, option_name, None))
|
||||||
|
if opt is None:
|
||||||
|
raise ValueError(f"Invalid option: {option_name}")
|
||||||
|
return getattr(opt, self.field)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"resolver": "FromOption",
|
||||||
|
"option": f"{self.option.__module__}.{self.option.__name__}",
|
||||||
|
"field": self.field,
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||||
|
if "option" not in data:
|
||||||
|
raise ValueError("Missing required option")
|
||||||
|
|
||||||
|
option_path = data["option"]
|
||||||
|
try:
|
||||||
|
option_mod_name, option_cls_name = option_path.rsplit(".", 1)
|
||||||
|
option_module = importlib.import_module(option_mod_name)
|
||||||
|
option = getattr(option_module, option_cls_name, None)
|
||||||
|
except (ValueError, ImportError) as e:
|
||||||
|
raise ValueError(f"Cannot parse option '{option_path}'") from e
|
||||||
|
if option is None or not issubclass(option, Option):
|
||||||
|
raise ValueError(f"Invalid option '{option_path}' returns type '{option}' instead of Option subclass")
|
||||||
|
|
||||||
|
return cls(cast(type[Option[Any]], option), data.get("field", "value"))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
field = f".{self.field}" if self.field != "value" else ""
|
||||||
|
return f"FromOption({self.option.__name__}{field})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class FromWorldAttr(FieldResolver, game="Archipelago"):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
@override
|
||||||
|
def resolve(self, world: "World") -> Any:
|
||||||
|
obj: Any = world
|
||||||
|
for field in self.name.split("."):
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
if isinstance(obj, Mapping):
|
||||||
|
obj = obj.get(field, None) # pyright: ignore[reportUnknownMemberType]
|
||||||
|
else:
|
||||||
|
obj = getattr(obj, field, None)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"FromWorldAttr({self.name})"
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def resolve_field(field: Any, world: "World", expected_type: type[T]) -> T: ...
|
||||||
|
@overload
|
||||||
|
def resolve_field(field: Any, world: "World", expected_type: None = None) -> Any: ...
|
||||||
|
def resolve_field(field: Any, world: "World", expected_type: type[T] | None = None) -> T | Any:
|
||||||
|
if isinstance(field, FieldResolver):
|
||||||
|
field = field.resolve(world)
|
||||||
|
if expected_type:
|
||||||
|
assert isinstance(field, expected_type), f"Expected type {expected_type} but got {type(field)}"
|
||||||
|
return field
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_RESOLVERS = {
|
||||||
|
resolver_name: resolver_class
|
||||||
|
for resolver_name, resolver_class in locals().items()
|
||||||
|
if isinstance(resolver_class, type)
|
||||||
|
and issubclass(resolver_class, FieldResolver)
|
||||||
|
and resolver_class is not FieldResolver
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ from typing_extensions import TypeVar, dataclass_transform, override
|
|||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
from NetUtils import JSONMessagePart
|
from NetUtils import JSONMessagePart
|
||||||
|
|
||||||
|
from .field_resolvers import FieldResolver, FieldResolverRegister, resolve_field
|
||||||
from .options import OptionFilter
|
from .options import OptionFilter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -108,11 +109,14 @@ class Rule(Generic[TWorld]):
|
|||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
"""Returns a JSON compatible dict representation of this rule"""
|
"""Returns a JSON compatible dict representation of this rule"""
|
||||||
args = {
|
args = {}
|
||||||
field.name: getattr(self, field.name, None)
|
for field in dataclasses.fields(self):
|
||||||
for field in dataclasses.fields(self)
|
if field.name in ("options", "filtered_resolution"):
|
||||||
if field.name not in ("options", "filtered_resolution")
|
continue
|
||||||
}
|
value = getattr(self, field.name, None)
|
||||||
|
if isinstance(value, FieldResolver):
|
||||||
|
value = value.to_dict()
|
||||||
|
args[field.name] = value
|
||||||
return {
|
return {
|
||||||
"rule": self.__class__.__qualname__,
|
"rule": self.__class__.__qualname__,
|
||||||
"options": [o.to_dict() for o in self.options],
|
"options": [o.to_dict() for o in self.options],
|
||||||
@@ -124,7 +128,19 @@ class Rule(Generic[TWorld]):
|
|||||||
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
||||||
"""Returns a new instance of this rule from a serialized dict representation"""
|
"""Returns a new instance of this rule from a serialized dict representation"""
|
||||||
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||||
return cls(**data.get("args", {}), options=options, filtered_resolution=data.get("filtered_resolution", False))
|
args = cls._parse_field_resolvers(data.get("args", {}), world_cls.game)
|
||||||
|
return cls(**args, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_field_resolvers(cls, data: Mapping[str, Any], game_name: str) -> dict[str, Any]:
|
||||||
|
result: dict[str, Any] = {}
|
||||||
|
for name, value in data.items():
|
||||||
|
if isinstance(value, dict) and "resolver" in value:
|
||||||
|
resolver_cls = FieldResolverRegister.get_resolver_cls(game_name, value["resolver"]) # pyright: ignore[reportUnknownArgumentType]
|
||||||
|
result[name] = resolver_cls.from_dict(value) # pyright: ignore[reportUnknownArgumentType]
|
||||||
|
else:
|
||||||
|
result[name] = value
|
||||||
|
return result
|
||||||
|
|
||||||
def __and__(self, other: "Rule[Any] | Iterable[OptionFilter] | OptionFilter") -> "Rule[TWorld]":
|
def __and__(self, other: "Rule[Any] | Iterable[OptionFilter] | OptionFilter") -> "Rule[TWorld]":
|
||||||
"""Combines two rules or a rule and an option filter into an And rule"""
|
"""Combines two rules or a rule and an option filter into an And rule"""
|
||||||
@@ -527,7 +543,7 @@ class Or(NestedRule[TWorld], game="Archipelago"):
|
|||||||
items[item] = 1
|
items[item] = 1
|
||||||
elif isinstance(child, HasAnyCount.Resolved):
|
elif isinstance(child, HasAnyCount.Resolved):
|
||||||
for item, count in child.item_counts:
|
for item, count in child.item_counts:
|
||||||
if item not in items or items[item] < count:
|
if item not in items or count < items[item]:
|
||||||
items[item] = count
|
items[item] = count
|
||||||
else:
|
else:
|
||||||
clauses.append(child)
|
clauses.append(child)
|
||||||
@@ -688,24 +704,24 @@ class Filtered(WrapperRule[TWorld], game="Archipelago"):
|
|||||||
class Has(Rule[TWorld], game="Archipelago"):
|
class Has(Rule[TWorld], game="Archipelago"):
|
||||||
"""A rule that checks if the player has at least `count` of a given item"""
|
"""A rule that checks if the player has at least `count` of a given item"""
|
||||||
|
|
||||||
item_name: str
|
item_name: str | FieldResolver
|
||||||
"""The item to check for"""
|
"""The item to check for"""
|
||||||
|
|
||||||
count: int = 1
|
count: int | FieldResolver = 1
|
||||||
"""The count the player is required to have"""
|
"""The count the player is required to have"""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||||
return self.Resolved(
|
return self.Resolved(
|
||||||
self.item_name,
|
resolve_field(self.item_name, world, str),
|
||||||
self.count,
|
count=resolve_field(self.count, world, int),
|
||||||
player=world.player,
|
player=world.player,
|
||||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
count = f", count={self.count}" if self.count > 1 else ""
|
count = f", count={self.count}" if isinstance(self.count, FieldResolver) or self.count > 1 else ""
|
||||||
options = f", options={self.options}" if self.options else ""
|
options = f", options={self.options}" if self.options else ""
|
||||||
return f"{self.__class__.__name__}({self.item_name}{count}{options})"
|
return f"{self.__class__.__name__}({self.item_name}{count}{options})"
|
||||||
|
|
||||||
@@ -991,7 +1007,7 @@ class HasAny(Rule[TWorld], game="Archipelago"):
|
|||||||
class HasAllCounts(Rule[TWorld], game="Archipelago"):
|
class HasAllCounts(Rule[TWorld], game="Archipelago"):
|
||||||
"""A rule that checks if the player has all of the specified counts of the given items"""
|
"""A rule that checks if the player has all of the specified counts of the given items"""
|
||||||
|
|
||||||
item_counts: dict[str, int]
|
item_counts: Mapping[str, int | FieldResolver]
|
||||||
"""A mapping of item name to count to check for"""
|
"""A mapping of item name to count to check for"""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1002,12 +1018,30 @@ class HasAllCounts(Rule[TWorld], game="Archipelago"):
|
|||||||
if len(self.item_counts) == 1:
|
if len(self.item_counts) == 1:
|
||||||
item = next(iter(self.item_counts))
|
item = next(iter(self.item_counts))
|
||||||
return Has(item, self.item_counts[item]).resolve(world)
|
return Has(item, self.item_counts[item]).resolve(world)
|
||||||
|
item_counts = tuple((name, resolve_field(count, world, int)) for name, count in self.item_counts.items())
|
||||||
return self.Resolved(
|
return self.Resolved(
|
||||||
tuple(self.item_counts.items()),
|
item_counts,
|
||||||
player=world.player,
|
player=world.player,
|
||||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
output = super().to_dict()
|
||||||
|
output["args"]["item_counts"] = {
|
||||||
|
key: value.to_dict() if isinstance(value, FieldResolver) else value
|
||||||
|
for key, value in output["args"]["item_counts"].items()
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
||||||
|
args = data.get("args", {})
|
||||||
|
item_counts = cls._parse_field_resolvers(args.get("item_counts", {}), world_cls.game)
|
||||||
|
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||||
|
return cls(item_counts, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
|
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
|
||||||
@@ -1096,7 +1130,7 @@ class HasAllCounts(Rule[TWorld], game="Archipelago"):
|
|||||||
class HasAnyCount(Rule[TWorld], game="Archipelago"):
|
class HasAnyCount(Rule[TWorld], game="Archipelago"):
|
||||||
"""A rule that checks if the player has any of the specified counts of the given items"""
|
"""A rule that checks if the player has any of the specified counts of the given items"""
|
||||||
|
|
||||||
item_counts: dict[str, int]
|
item_counts: Mapping[str, int | FieldResolver]
|
||||||
"""A mapping of item name to count to check for"""
|
"""A mapping of item name to count to check for"""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1107,12 +1141,30 @@ class HasAnyCount(Rule[TWorld], game="Archipelago"):
|
|||||||
if len(self.item_counts) == 1:
|
if len(self.item_counts) == 1:
|
||||||
item = next(iter(self.item_counts))
|
item = next(iter(self.item_counts))
|
||||||
return Has(item, self.item_counts[item]).resolve(world)
|
return Has(item, self.item_counts[item]).resolve(world)
|
||||||
|
item_counts = tuple((name, resolve_field(count, world, int)) for name, count in self.item_counts.items())
|
||||||
return self.Resolved(
|
return self.Resolved(
|
||||||
tuple(self.item_counts.items()),
|
item_counts,
|
||||||
player=world.player,
|
player=world.player,
|
||||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
output = super().to_dict()
|
||||||
|
output["args"]["item_counts"] = {
|
||||||
|
key: value.to_dict() if isinstance(value, FieldResolver) else value
|
||||||
|
for key, value in output["args"]["item_counts"].items()
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
||||||
|
args = data.get("args", {})
|
||||||
|
item_counts = cls._parse_field_resolvers(args.get("item_counts", {}), world_cls.game)
|
||||||
|
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||||
|
return cls(item_counts, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
|
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
|
||||||
@@ -1204,13 +1256,13 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
|
|||||||
item_names: tuple[str, ...]
|
item_names: tuple[str, ...]
|
||||||
"""A tuple of item names to check for"""
|
"""A tuple of item names to check for"""
|
||||||
|
|
||||||
count: int = 1
|
count: int | FieldResolver = 1
|
||||||
"""The number of items the player needs to have"""
|
"""The number of items the player needs to have"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*item_names: str,
|
*item_names: str,
|
||||||
count: int = 1,
|
count: int | FieldResolver = 1,
|
||||||
options: Iterable[OptionFilter] = (),
|
options: Iterable[OptionFilter] = (),
|
||||||
filtered_resolution: bool = False,
|
filtered_resolution: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1227,7 +1279,7 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
|
|||||||
return Has(self.item_names[0], self.count).resolve(world)
|
return Has(self.item_names[0], self.count).resolve(world)
|
||||||
return self.Resolved(
|
return self.Resolved(
|
||||||
self.item_names,
|
self.item_names,
|
||||||
self.count,
|
count=resolve_field(self.count, world, int),
|
||||||
player=world.player,
|
player=world.player,
|
||||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||||
)
|
)
|
||||||
@@ -1235,7 +1287,7 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
|
|||||||
@override
|
@override
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
||||||
args = {**data.get("args", {})}
|
args = cls._parse_field_resolvers(data.get("args", {}), world_cls.game)
|
||||||
item_names = args.pop("item_names", ())
|
item_names = args.pop("item_names", ())
|
||||||
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||||
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
||||||
@@ -1338,13 +1390,13 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
|
|||||||
item_names: tuple[str, ...]
|
item_names: tuple[str, ...]
|
||||||
"""A tuple of item names to check for"""
|
"""A tuple of item names to check for"""
|
||||||
|
|
||||||
count: int = 1
|
count: int | FieldResolver = 1
|
||||||
"""The number of items the player needs to have"""
|
"""The number of items the player needs to have"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*item_names: str,
|
*item_names: str,
|
||||||
count: int = 1,
|
count: int | FieldResolver = 1,
|
||||||
options: Iterable[OptionFilter] = (),
|
options: Iterable[OptionFilter] = (),
|
||||||
filtered_resolution: bool = False,
|
filtered_resolution: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1354,14 +1406,15 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||||
if len(self.item_names) == 0 or len(self.item_names) < self.count:
|
count = resolve_field(self.count, world, int)
|
||||||
|
if len(self.item_names) == 0 or len(self.item_names) < count:
|
||||||
# match state.has_from_list_unique
|
# match state.has_from_list_unique
|
||||||
return False_().resolve(world)
|
return False_().resolve(world)
|
||||||
if len(self.item_names) == 1:
|
if len(self.item_names) == 1:
|
||||||
return Has(self.item_names[0]).resolve(world)
|
return Has(self.item_names[0]).resolve(world)
|
||||||
return self.Resolved(
|
return self.Resolved(
|
||||||
self.item_names,
|
self.item_names,
|
||||||
self.count,
|
count,
|
||||||
player=world.player,
|
player=world.player,
|
||||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||||
)
|
)
|
||||||
@@ -1369,7 +1422,7 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
|
|||||||
@override
|
@override
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
||||||
args = {**data.get("args", {})}
|
args = cls._parse_field_resolvers(data.get("args", {}), world_cls.game)
|
||||||
item_names = args.pop("item_names", ())
|
item_names = args.pop("item_names", ())
|
||||||
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||||
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
||||||
@@ -1468,7 +1521,7 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
|
|||||||
item_name_group: str
|
item_name_group: str
|
||||||
"""The name of the item group containing the items"""
|
"""The name of the item group containing the items"""
|
||||||
|
|
||||||
count: int = 1
|
count: int | FieldResolver = 1
|
||||||
"""The number of items the player needs to have"""
|
"""The number of items the player needs to have"""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1477,14 +1530,14 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
|
|||||||
return self.Resolved(
|
return self.Resolved(
|
||||||
self.item_name_group,
|
self.item_name_group,
|
||||||
item_names,
|
item_names,
|
||||||
self.count,
|
count=resolve_field(self.count, world, int),
|
||||||
player=world.player,
|
player=world.player,
|
||||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
count = f", count={self.count}" if self.count > 1 else ""
|
count = f", count={self.count}" if isinstance(self.count, FieldResolver) or self.count > 1 else ""
|
||||||
options = f", options={self.options}" if self.options else ""
|
options = f", options={self.options}" if self.options else ""
|
||||||
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
|
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
|
||||||
|
|
||||||
@@ -1542,7 +1595,7 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
|
|||||||
item_name_group: str
|
item_name_group: str
|
||||||
"""The name of the item group containing the items"""
|
"""The name of the item group containing the items"""
|
||||||
|
|
||||||
count: int = 1
|
count: int | FieldResolver = 1
|
||||||
"""The number of items the player needs to have"""
|
"""The number of items the player needs to have"""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1551,14 +1604,14 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
|
|||||||
return self.Resolved(
|
return self.Resolved(
|
||||||
self.item_name_group,
|
self.item_name_group,
|
||||||
item_names,
|
item_names,
|
||||||
self.count,
|
count=resolve_field(self.count, world, int),
|
||||||
player=world.player,
|
player=world.player,
|
||||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
count = f", count={self.count}" if self.count > 1 else ""
|
count = f", count={self.count}" if isinstance(self.count, FieldResolver) or self.count > 1 else ""
|
||||||
options = f", options={self.options}" if self.options else ""
|
options = f", options={self.options}" if self.options else ""
|
||||||
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
|
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
|
||||||
|
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -71,7 +71,6 @@ non_apworlds: set[str] = {
|
|||||||
"Ocarina of Time",
|
"Ocarina of Time",
|
||||||
"Overcooked! 2",
|
"Overcooked! 2",
|
||||||
"Raft",
|
"Raft",
|
||||||
"Sudoku",
|
|
||||||
"Super Mario 64",
|
"Super Mario 64",
|
||||||
"VVVVVV",
|
"VVVVVV",
|
||||||
"Wargroove",
|
"Wargroove",
|
||||||
@@ -658,7 +657,7 @@ cx_Freeze.setup(
|
|||||||
options={
|
options={
|
||||||
"build_exe": {
|
"build_exe": {
|
||||||
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||||
"includes": [],
|
"includes": ["rule_builder.cached_world"],
|
||||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas"],
|
"pandas"],
|
||||||
"zip_includes": [],
|
"zip_includes": [],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class TestImplemented(unittest.TestCase):
|
|||||||
def test_completion_condition(self):
|
def test_completion_condition(self):
|
||||||
"""Ensure a completion condition is set that has requirements."""
|
"""Ensure a completion condition is set that has requirements."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden and game_name not in {"Sudoku"}:
|
if not world_type.hidden:
|
||||||
with self.subTest(game_name):
|
with self.subTest(game_name):
|
||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
||||||
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
||||||
@@ -59,7 +59,7 @@ class TestImplemented(unittest.TestCase):
|
|||||||
def test_prefill_items(self):
|
def test_prefill_items(self):
|
||||||
"""Test that every world can reach every location from allstate before pre_fill."""
|
"""Test that every world can reach every location from allstate before pre_fill."""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"):
|
if gamename not in ("Archipelago", "Final Fantasy", "Test Game"):
|
||||||
with self.subTest(gamename):
|
with self.subTest(gamename):
|
||||||
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
||||||
"set_rules", "connect_entrances", "generate_basic"))
|
"set_rules", "connect_entrances", "generate_basic"))
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class TestOptions(unittest.TestCase):
|
|||||||
def test_option_set_keys_random(self):
|
def test_option_set_keys_random(self):
|
||||||
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
|
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if game_name not in ("Archipelago", "Sudoku", "Super Metroid"):
|
if game_name not in ("Archipelago", "Super Metroid"):
|
||||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||||
if issubclass(option, OptionSet):
|
if issubclass(option, OptionSet):
|
||||||
with self.subTest(game=game_name, option=option_key):
|
with self.subTest(game=game_name, option=option_key):
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ from typing_extensions import override
|
|||||||
|
|
||||||
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
|
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
|
||||||
from NetUtils import JSONMessagePart
|
from NetUtils import JSONMessagePart
|
||||||
from Options import Choice, FreeText, Option, OptionSet, PerGameCommonOptions, Toggle
|
from Options import Choice, FreeText, Option, OptionSet, PerGameCommonOptions, Range, Toggle
|
||||||
from rule_builder.cached_world import CachedRuleBuilderWorld
|
from rule_builder.cached_world import CachedRuleBuilderWorld
|
||||||
|
from rule_builder.field_resolvers import FieldResolver, FromOption, FromWorldAttr, resolve_field
|
||||||
from rule_builder.options import Operator, OptionFilter
|
from rule_builder.options import Operator, OptionFilter
|
||||||
from rule_builder.rules import (
|
from rule_builder.rules import (
|
||||||
And,
|
And,
|
||||||
@@ -59,12 +60,20 @@ class SetOption(OptionSet):
|
|||||||
valid_keys: ClassVar[set[str]] = {"one", "two", "three"} # pyright: ignore[reportIncompatibleVariableOverride]
|
valid_keys: ClassVar[set[str]] = {"one", "two", "three"} # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
||||||
|
class RangeOption(Range):
|
||||||
|
auto_display_name = True
|
||||||
|
range_start = 1
|
||||||
|
range_end = 10
|
||||||
|
default = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RuleBuilderOptions(PerGameCommonOptions):
|
class RuleBuilderOptions(PerGameCommonOptions):
|
||||||
toggle_option: ToggleOption
|
toggle_option: ToggleOption
|
||||||
choice_option: ChoiceOption
|
choice_option: ChoiceOption
|
||||||
text_option: FreeTextOption
|
text_option: FreeTextOption
|
||||||
set_option: SetOption
|
set_option: SetOption
|
||||||
|
range_option: RangeOption
|
||||||
|
|
||||||
|
|
||||||
GAME_NAME = "Rule Builder Test Game"
|
GAME_NAME = "Rule Builder Test Game"
|
||||||
@@ -233,6 +242,14 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
|
|||||||
Or(Has("A"), HasAny("B", "C"), HasAnyCount({"D": 1, "E": 1})),
|
Or(Has("A"), HasAny("B", "C"), HasAnyCount({"D": 1, "E": 1})),
|
||||||
HasAny.Resolved(("A", "B", "C", "D", "E"), player=1),
|
HasAny.Resolved(("A", "B", "C", "D", "E"), player=1),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
And(HasAllCounts({"A": 1, "B": 2}), HasAllCounts({"A": 2, "B": 2})),
|
||||||
|
HasAllCounts.Resolved((("A", 2), ("B", 2)), player=1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})),
|
||||||
|
HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
class TestSimplify(RuleBuilderTestCase):
|
class TestSimplify(RuleBuilderTestCase):
|
||||||
@@ -651,14 +668,15 @@ class TestRules(RuleBuilderTestCase):
|
|||||||
self.assertFalse(resolved_rule(self.state))
|
self.assertFalse(resolved_rule(self.state))
|
||||||
|
|
||||||
def test_has_any_count(self) -> None:
|
def test_has_any_count(self) -> None:
|
||||||
item_counts = {"Item 1": 1, "Item 2": 2}
|
item_counts: dict[str, int | FieldResolver] = {"Item 1": 1, "Item 2": 2}
|
||||||
rule = HasAnyCount(item_counts)
|
rule = HasAnyCount(item_counts)
|
||||||
resolved_rule = rule.resolve(self.world)
|
resolved_rule = rule.resolve(self.world)
|
||||||
self.world.register_rule_dependencies(resolved_rule)
|
self.world.register_rule_dependencies(resolved_rule)
|
||||||
|
|
||||||
for item_name, count in item_counts.items():
|
for item_name, count in item_counts.items():
|
||||||
item = self.world.create_item(item_name)
|
item = self.world.create_item(item_name)
|
||||||
for _ in range(count):
|
num_items = resolve_field(count, self.world, int)
|
||||||
|
for _ in range(num_items):
|
||||||
self.assertFalse(resolved_rule(self.state))
|
self.assertFalse(resolved_rule(self.state))
|
||||||
self.state.collect(item)
|
self.state.collect(item)
|
||||||
self.assertTrue(resolved_rule(self.state))
|
self.assertTrue(resolved_rule(self.state))
|
||||||
@@ -755,7 +773,7 @@ class TestSerialization(RuleBuilderTestCase):
|
|||||||
|
|
||||||
rule: ClassVar[Rule[Any]] = And(
|
rule: ClassVar[Rule[Any]] = And(
|
||||||
Or(
|
Or(
|
||||||
Has("i1", count=4),
|
Has("i1", count=FromOption(RangeOption)),
|
||||||
HasFromList("i2", "i3", "i4", count=2),
|
HasFromList("i2", "i3", "i4", count=2),
|
||||||
HasAnyCount({"i5": 2, "i6": 3}),
|
HasAnyCount({"i5": 2, "i6": 3}),
|
||||||
options=[OptionFilter(ToggleOption, 0)],
|
options=[OptionFilter(ToggleOption, 0)],
|
||||||
@@ -763,7 +781,7 @@ class TestSerialization(RuleBuilderTestCase):
|
|||||||
Or(
|
Or(
|
||||||
HasAll("i7", "i8"),
|
HasAll("i7", "i8"),
|
||||||
HasAllCounts(
|
HasAllCounts(
|
||||||
{"i9": 1, "i10": 5},
|
{"i9": 1, "i10": FromWorldAttr("instance_data.i10_count")},
|
||||||
options=[OptionFilter(ToggleOption, 1, operator="ne")],
|
options=[OptionFilter(ToggleOption, 1, operator="ne")],
|
||||||
filtered_resolution=True,
|
filtered_resolution=True,
|
||||||
),
|
),
|
||||||
@@ -803,7 +821,14 @@ class TestSerialization(RuleBuilderTestCase):
|
|||||||
"rule": "Has",
|
"rule": "Has",
|
||||||
"options": [],
|
"options": [],
|
||||||
"filtered_resolution": False,
|
"filtered_resolution": False,
|
||||||
"args": {"item_name": "i1", "count": 4},
|
"args": {
|
||||||
|
"item_name": "i1",
|
||||||
|
"count": {
|
||||||
|
"resolver": "FromOption",
|
||||||
|
"option": "test.general.test_rule_builder.RangeOption",
|
||||||
|
"field": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "HasFromList",
|
"rule": "HasFromList",
|
||||||
@@ -840,7 +865,12 @@ class TestSerialization(RuleBuilderTestCase):
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"filtered_resolution": True,
|
"filtered_resolution": True,
|
||||||
"args": {"item_counts": {"i9": 1, "i10": 5}},
|
"args": {
|
||||||
|
"item_counts": {
|
||||||
|
"i9": 1,
|
||||||
|
"i10": {"resolver": "FromWorldAttr", "name": "instance_data.i10_count"},
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "CanReachRegion",
|
"rule": "CanReachRegion",
|
||||||
@@ -915,7 +945,7 @@ class TestSerialization(RuleBuilderTestCase):
|
|||||||
multiworld = setup_solo_multiworld(self.world_cls, steps=(), seed=0)
|
multiworld = setup_solo_multiworld(self.world_cls, steps=(), seed=0)
|
||||||
world = multiworld.worlds[1]
|
world = multiworld.worlds[1]
|
||||||
deserialized_rule = world.rule_from_dict(self.rule_dict)
|
deserialized_rule = world.rule_from_dict(self.rule_dict)
|
||||||
self.assertEqual(deserialized_rule, self.rule, str(deserialized_rule))
|
self.assertEqual(deserialized_rule, self.rule, f"\n{deserialized_rule}\n{self.rule}")
|
||||||
|
|
||||||
|
|
||||||
class TestExplain(RuleBuilderTestCase):
|
class TestExplain(RuleBuilderTestCase):
|
||||||
@@ -1334,3 +1364,32 @@ class TestExplain(RuleBuilderTestCase):
|
|||||||
"& False)",
|
"& False)",
|
||||||
)
|
)
|
||||||
assert str(self.resolved_rule) == " ".join(expected)
|
assert str(self.resolved_rule) == " ".join(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@classvar_matrix(
|
||||||
|
rules=(
|
||||||
|
(
|
||||||
|
Has("A", FromOption(RangeOption)),
|
||||||
|
Has.Resolved("A", count=5, player=1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Has("B", FromWorldAttr("pre_calculated")),
|
||||||
|
Has.Resolved("B", count=3, player=1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Has("C", FromWorldAttr("instance_data.key")),
|
||||||
|
Has.Resolved("C", count=7, player=1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class TestFieldResolvers(RuleBuilderTestCase):
|
||||||
|
rules: ClassVar[tuple[Rule[Any], Rule.Resolved]]
|
||||||
|
|
||||||
|
def test_simplify(self) -> None:
|
||||||
|
multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
|
||||||
|
world = multiworld.worlds[1]
|
||||||
|
world.pre_calculated = 3 # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
world.instance_data = {"key": 7} # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
rule, expected = self.rules
|
||||||
|
resolved_rule = rule.resolve(world)
|
||||||
|
self.assertEqual(resolved_rule, expected, f"\n{resolved_rule}\n{expected}")
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import typing as t
|
|
||||||
from copy import deepcopy
|
|
||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from typing_extensions import override
|
|
||||||
|
|
||||||
import NetUtils
|
|
||||||
from NetUtils import GamesPackage
|
|
||||||
from apmw.multiserver.gamespackagecache import GamesPackageCache
|
|
||||||
|
|
||||||
|
|
||||||
class GamesPackageCacheTest(TestCase):
|
|
||||||
cache: GamesPackageCache
|
|
||||||
any_game: t.ClassVar[str] = "APQuest"
|
|
||||||
example_games_package: GamesPackage = {
|
|
||||||
"item_name_to_id": {"Item 1": 1},
|
|
||||||
"item_name_groups": {"Everything": ["Item 1"]},
|
|
||||||
"location_name_to_id": {"Location 1": 1},
|
|
||||||
"location_name_groups": {"Everywhere": ["Location 1"]},
|
|
||||||
"checksum": "1234",
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.cache = GamesPackageCache()
|
|
||||||
|
|
||||||
def test_get_static_is_same(self) -> None:
|
|
||||||
"""Tests that get_static returns the same objects twice"""
|
|
||||||
reduced_games_package1, item_name_groups1, location_name_groups1 = self.cache.get_static(self.any_game)
|
|
||||||
reduced_games_package2, item_name_groups2, location_name_groups2 = self.cache.get_static(self.any_game)
|
|
||||||
self.assertIs(reduced_games_package1, reduced_games_package2)
|
|
||||||
self.assertIs(item_name_groups1, item_name_groups2)
|
|
||||||
self.assertIs(location_name_groups1, location_name_groups2)
|
|
||||||
|
|
||||||
def test_get_static_data_format(self) -> None:
|
|
||||||
"""Tests that get_static returns data in the correct format"""
|
|
||||||
reduced_games_package, item_name_groups, location_name_groups = self.cache.get_static(self.any_game)
|
|
||||||
self.assertTrue(reduced_games_package["checksum"])
|
|
||||||
self.assertTrue(reduced_games_package["item_name_to_id"])
|
|
||||||
self.assertTrue(reduced_games_package["location_name_to_id"])
|
|
||||||
self.assertNotIn("item_name_groups", reduced_games_package)
|
|
||||||
self.assertNotIn("location_name_groups", reduced_games_package)
|
|
||||||
self.assertTrue(item_name_groups["Everything"])
|
|
||||||
self.assertTrue(location_name_groups["Everywhere"])
|
|
||||||
|
|
||||||
def test_get_static_is_serializable(self) -> None:
|
|
||||||
"""Tests that get_static returns data that can be serialized"""
|
|
||||||
NetUtils.encode(self.cache.get_static(self.any_game))
|
|
||||||
|
|
||||||
def test_get_static_missing_raises(self) -> None:
|
|
||||||
"""Tests that get_static raises KeyError if the world is missing"""
|
|
||||||
with self.assertRaises(KeyError):
|
|
||||||
_ = self.cache.get_static("Does not exist")
|
|
||||||
|
|
||||||
def test_eviction(self) -> None:
|
|
||||||
"""Tests that unused items get evicted from cache"""
|
|
||||||
game_name = "Test"
|
|
||||||
before_add = len(self.cache._reduced_games_packages)
|
|
||||||
data = self.cache.get(game_name, self.example_games_package)
|
|
||||||
self.assertTrue(data)
|
|
||||||
self.assertEqual(before_add + 1, len(self.cache._reduced_games_packages))
|
|
||||||
|
|
||||||
del data
|
|
||||||
if len(self.cache._reduced_games_packages) != before_add: # gc.collect() may not even be required
|
|
||||||
import gc
|
|
||||||
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
self.assertEqual(before_add, len(self.cache._reduced_games_packages))
|
|
||||||
|
|
||||||
def test_get_required_field(self) -> None:
|
|
||||||
"""Tests that missing required field raises a KeyError"""
|
|
||||||
for field in ("item_name_to_id", "location_name_to_id", "item_name_groups"):
|
|
||||||
with self.subTest(field=field):
|
|
||||||
games_package = deepcopy(self.example_games_package)
|
|
||||||
del games_package[field] # type: ignore
|
|
||||||
with self.assertRaises(KeyError):
|
|
||||||
_ = self.cache.get(self.any_game, games_package)
|
|
||||||
|
|
||||||
def test_get_optional_properties(self) -> None:
|
|
||||||
"""Tests that missing optional field works"""
|
|
||||||
for field in ("checksum", "location_name_groups"):
|
|
||||||
with self.subTest(field=field):
|
|
||||||
games_package = deepcopy(self.example_games_package)
|
|
||||||
del games_package[field] # type: ignore
|
|
||||||
_, item_name_groups, location_name_groups = self.cache.get(self.any_game, games_package)
|
|
||||||
self.assertTrue(item_name_groups)
|
|
||||||
self.assertEqual(field != "location_name_groups", bool(location_name_groups))
|
|
||||||
|
|
||||||
def test_item_name_deduplication(self) -> None:
|
|
||||||
n = 1
|
|
||||||
s1 = f"Item {n}"
|
|
||||||
s2 = f"Item {n}"
|
|
||||||
# check if the deduplication is actually gonna do anything
|
|
||||||
self.assertIsNot(s1, s2)
|
|
||||||
self.assertEqual(s1, s2)
|
|
||||||
# do the thing
|
|
||||||
game_name = "Test"
|
|
||||||
games_package: GamesPackage = {
|
|
||||||
"item_name_to_id": {s1: n},
|
|
||||||
"item_name_groups": {"Everything": [s2]},
|
|
||||||
"location_name_to_id": {},
|
|
||||||
"location_name_groups": {},
|
|
||||||
"checksum": "1234",
|
|
||||||
}
|
|
||||||
reduced_games_package, item_name_groups, location_name_groups = self.cache.get(game_name, games_package)
|
|
||||||
self.assertIs(
|
|
||||||
next(iter(reduced_games_package["item_name_to_id"].keys())),
|
|
||||||
item_name_groups["Everything"][0],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_location_name_deduplication(self) -> None:
|
|
||||||
n = 1
|
|
||||||
s1 = f"Location {n}"
|
|
||||||
s2 = f"Location {n}"
|
|
||||||
# check if the deduplication is actually gonna do anything
|
|
||||||
self.assertIsNot(s1, s2)
|
|
||||||
self.assertEqual(s1, s2)
|
|
||||||
# do the thing
|
|
||||||
game_name = "Test"
|
|
||||||
games_package: GamesPackage = {
|
|
||||||
"item_name_to_id": {},
|
|
||||||
"item_name_groups": {},
|
|
||||||
"location_name_to_id": {s1: n},
|
|
||||||
"location_name_groups": {"Everywhere": [s2]},
|
|
||||||
"checksum": "1234",
|
|
||||||
}
|
|
||||||
reduced_games_package, item_name_groups, location_name_groups = self.cache.get(game_name, games_package)
|
|
||||||
self.assertIs(
|
|
||||||
next(iter(reduced_games_package["location_name_to_id"].keys())),
|
|
||||||
location_name_groups["Everywhere"][0],
|
|
||||||
)
|
|
||||||
37
test/netutils/test_enum.py
Normal file
37
test/netutils/test_enum.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""Verify that NetUtils' enums work correctly with all supported Python versions."""
|
||||||
|
|
||||||
|
import pickle
|
||||||
|
import unittest
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from NetUtils import ClientStatus, HintStatus, SlotType
|
||||||
|
from Utils import restricted_loads
|
||||||
|
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
class DataEnumTest(unittest.TestCase):
|
||||||
|
type: Type[Enum]
|
||||||
|
value: Enum
|
||||||
|
|
||||||
|
def test_unpickle(self) -> None:
|
||||||
|
"""Tests that enums used in multidata or multisave can be pickled and unpickled."""
|
||||||
|
pickled = pickle.dumps(self.value)
|
||||||
|
unpickled = restricted_loads(pickled)
|
||||||
|
self.assertEqual(unpickled, self.value)
|
||||||
|
self.assertIsInstance(unpickled, self.type)
|
||||||
|
|
||||||
|
|
||||||
|
class HintStatusTest(Base.DataEnumTest):
|
||||||
|
type = HintStatus
|
||||||
|
value = HintStatus.HINT_AVOID
|
||||||
|
|
||||||
|
|
||||||
|
class ClientStatusTest(Base.DataEnumTest):
|
||||||
|
type = ClientStatus
|
||||||
|
value = ClientStatus.CLIENT_GOAL
|
||||||
|
|
||||||
|
|
||||||
|
class SlotTypeTest(Base.DataEnumTest):
|
||||||
|
type = SlotType
|
||||||
|
value = SlotType.player
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from Options import Choice, DefaultOnToggle, Toggle
|
from collections import Counter
|
||||||
|
|
||||||
|
from Options import Choice, DefaultOnToggle, Toggle, OptionDict, OptionError, OptionSet, OptionList, OptionCounter
|
||||||
|
|
||||||
|
|
||||||
class TestNumericOptions(unittest.TestCase):
|
class TestNumericOptions(unittest.TestCase):
|
||||||
@@ -74,3 +76,97 @@ class TestNumericOptions(unittest.TestCase):
|
|||||||
self.assertTrue(toggle_string)
|
self.assertTrue(toggle_string)
|
||||||
self.assertTrue(toggle_int)
|
self.assertTrue(toggle_int)
|
||||||
self.assertTrue(toggle_alias)
|
self.assertTrue(toggle_alias)
|
||||||
|
|
||||||
|
|
||||||
|
class TestContainerOptions(unittest.TestCase):
|
||||||
|
def test_option_dict(self):
|
||||||
|
class TestOptionDict(OptionDict):
|
||||||
|
valid_keys = frozenset({"A", "B", "C"})
|
||||||
|
|
||||||
|
unknown_key_init_dict = {"D": "Foo"}
|
||||||
|
test_option_dict = TestOptionDict(unknown_key_init_dict)
|
||||||
|
self.assertRaises(OptionError, test_option_dict.verify_keys)
|
||||||
|
|
||||||
|
init_dict = {"A": "foo", "B": "bar"}
|
||||||
|
test_option_dict = TestOptionDict(init_dict)
|
||||||
|
|
||||||
|
self.assertEqual(test_option_dict, init_dict) # Implicit value comparison
|
||||||
|
self.assertEqual(test_option_dict["A"], "foo")
|
||||||
|
self.assertIn("B", test_option_dict)
|
||||||
|
self.assertNotIn("C", test_option_dict)
|
||||||
|
self.assertRaises(KeyError, lambda: test_option_dict["C"])
|
||||||
|
|
||||||
|
def test_option_set(self):
|
||||||
|
class TestOptionSet(OptionSet):
|
||||||
|
valid_keys = frozenset({"A", "B", "C"})
|
||||||
|
|
||||||
|
unknown_key_init_set = {"D"}
|
||||||
|
test_option_set = TestOptionSet(unknown_key_init_set)
|
||||||
|
self.assertRaises(OptionError, test_option_set.verify_keys)
|
||||||
|
|
||||||
|
init_set = {"A", "B"}
|
||||||
|
test_option_set = TestOptionSet(init_set)
|
||||||
|
|
||||||
|
self.assertEqual(test_option_set, init_set) # Implicit value comparison
|
||||||
|
self.assertIn("B", test_option_set)
|
||||||
|
self.assertNotIn("C", test_option_set)
|
||||||
|
|
||||||
|
def test_option_list(self):
|
||||||
|
class TestOptionList(OptionList):
|
||||||
|
valid_keys = frozenset({"A", "B", "C"})
|
||||||
|
|
||||||
|
unknown_key_init_list = ["D"]
|
||||||
|
test_option_list = TestOptionList(unknown_key_init_list)
|
||||||
|
self.assertRaises(OptionError, test_option_list.verify_keys)
|
||||||
|
|
||||||
|
init_list = ["A", "B"]
|
||||||
|
test_option_list = TestOptionList(init_list)
|
||||||
|
|
||||||
|
self.assertEqual(test_option_list, init_list)
|
||||||
|
self.assertIn("B", test_option_list)
|
||||||
|
self.assertNotIn("C", test_option_list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_option_counter(self):
|
||||||
|
class TestOptionCounter(OptionCounter):
|
||||||
|
valid_keys = frozenset({"A", "B", "C"})
|
||||||
|
|
||||||
|
max = 10
|
||||||
|
min = 0
|
||||||
|
|
||||||
|
unknown_key_init_dict = {"D": 5}
|
||||||
|
test_option_counter = TestOptionCounter(unknown_key_init_dict)
|
||||||
|
self.assertRaises(OptionError, test_option_counter.verify_keys)
|
||||||
|
|
||||||
|
wrong_value_type_init_dict = {"A": "B"}
|
||||||
|
self.assertRaises(TypeError, TestOptionCounter, wrong_value_type_init_dict)
|
||||||
|
|
||||||
|
violates_max_init_dict = {"A": 5, "B": 11}
|
||||||
|
test_option_counter = TestOptionCounter(violates_max_init_dict)
|
||||||
|
self.assertRaises(OptionError, test_option_counter.verify_values)
|
||||||
|
|
||||||
|
violates_min_init_dict = {"A": -1, "B": 5}
|
||||||
|
test_option_counter = TestOptionCounter(violates_min_init_dict)
|
||||||
|
self.assertRaises(OptionError, test_option_counter.verify_values)
|
||||||
|
|
||||||
|
init_dict = {"A": 0, "B": 10}
|
||||||
|
test_option_counter = TestOptionCounter(init_dict)
|
||||||
|
self.assertEqual(test_option_counter, Counter(init_dict))
|
||||||
|
self.assertIn("A", test_option_counter)
|
||||||
|
self.assertNotIn("C", test_option_counter)
|
||||||
|
self.assertEqual(test_option_counter["A"], 0)
|
||||||
|
self.assertEqual(test_option_counter["B"], 10)
|
||||||
|
self.assertEqual(test_option_counter["C"], 0)
|
||||||
|
|
||||||
|
def test_culling_option_counter(self):
|
||||||
|
class TestCullingCounter(OptionCounter):
|
||||||
|
valid_keys = frozenset({"A", "B", "C"})
|
||||||
|
cull_zeroes = True
|
||||||
|
|
||||||
|
init_dict = {"A": 0, "B": 10}
|
||||||
|
test_option_counter = TestCullingCounter(init_dict)
|
||||||
|
self.assertNotIn("A", test_option_counter)
|
||||||
|
self.assertIn("B", test_option_counter)
|
||||||
|
self.assertNotIn("C", test_option_counter)
|
||||||
|
self.assertEqual(test_option_counter["A"], 0) # It's still a Counter! cull_zeroes is about "in" checks.
|
||||||
|
self.assertEqual(test_option_counter, Counter({"B": 10}))
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import os
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from Utils import is_macos
|
|
||||||
from WebHostLib.customserver import parse_game_ports, create_random_port_socket, get_used_ports
|
|
||||||
|
|
||||||
ci = bool(os.environ.get("CI"))
|
|
||||||
|
|
||||||
|
|
||||||
class TestPortAllocating(unittest.TestCase):
|
|
||||||
def test_parse_game_ports(self) -> None:
|
|
||||||
"""Ensure that game ports with ranges are parsed correctly"""
|
|
||||||
val = parse_game_ports(("1000-2000", "2000-5000", "1000-2000", 20, 40, "20", "0"))
|
|
||||||
|
|
||||||
self.assertListEqual(val.parsed_ports,
|
|
||||||
[range(1000, 2001), range(2000, 5001), range(1000, 2001), range(20, 21), range(40, 41),
|
|
||||||
range(20, 21)], "The parsed game ports are not the expected values")
|
|
||||||
self.assertTrue(val.ephemeral_allowed, "The ephemeral allowed flag is not set even though it was passed")
|
|
||||||
self.assertListEqual(val.weights, [1001, 4002, 5003, 5004, 5005, 5006],
|
|
||||||
"Cumulative weights are not the expected value")
|
|
||||||
|
|
||||||
val = parse_game_ports(())
|
|
||||||
self.assertListEqual(val.parsed_ports, [], "Empty list of game port returned something")
|
|
||||||
self.assertFalse(val.ephemeral_allowed, "Empty list returned that ephemeral is allowed")
|
|
||||||
|
|
||||||
val = parse_game_ports((0,))
|
|
||||||
self.assertListEqual(val.parsed_ports, [], "Empty list of ranges returned something")
|
|
||||||
self.assertTrue(val.ephemeral_allowed, "List with just 0 is not allowing ephemeral ports")
|
|
||||||
|
|
||||||
val = parse_game_ports((1,))
|
|
||||||
self.assertEqual(val.parsed_ports, [range(1, 2)], "Parsed ports doesn't contain the expected values")
|
|
||||||
self.assertFalse(val.ephemeral_allowed, "List with just single port returned that ephemeral is allowed")
|
|
||||||
|
|
||||||
def test_parse_game_port_errors(self) -> None:
|
|
||||||
"""Ensure that game ports with incorrect values raise the expected error"""
|
|
||||||
with self.assertRaises(ValueError, msg="Negative numbers didn't get interpreted as an invalid range"):
|
|
||||||
parse_game_ports(tuple("-50215"))
|
|
||||||
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number"):
|
|
||||||
parse_game_ports(tuple("dwafawg"))
|
|
||||||
with self.assertRaises(
|
|
||||||
ValueError,
|
|
||||||
msg="A range with an extra dash at the end didn't get interpreted as an invalid number because of it's end dash"
|
|
||||||
):
|
|
||||||
parse_game_ports(tuple("20-21215-"))
|
|
||||||
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number for the start of a range"):
|
|
||||||
parse_game_ports(tuple("f-21215"))
|
|
||||||
|
|
||||||
def test_random_port_socket_edge_cases(self) -> None:
|
|
||||||
"""Verify if edge cases on creation of random port socket is working fine"""
|
|
||||||
# Try giving an empty tuple and fail over it
|
|
||||||
with self.assertRaises(OSError) as err:
|
|
||||||
create_random_port_socket(tuple(), "127.0.0.1")
|
|
||||||
self.assertEqual(err.exception.errno, 98, "Raised an unexpected error code")
|
|
||||||
self.assertEqual(err.exception.strerror, "No available ports", "Raised an unexpected error string")
|
|
||||||
|
|
||||||
# Try only having ephemeral ports enabled
|
|
||||||
try:
|
|
||||||
create_random_port_socket(("0",), "127.0.0.1").close()
|
|
||||||
except OSError as err:
|
|
||||||
self.assertEqual(err.errno, 98, "Raised an unexpected error code")
|
|
||||||
# If it returns our error string that means something is wrong with our code
|
|
||||||
self.assertNotEqual(err.strerror, "No available ports",
|
|
||||||
"Raised an unexpected error string")
|
|
||||||
|
|
||||||
@unittest.skipUnless(ci, "can't guarantee free ports outside of CI")
|
|
||||||
def test_random_port_socket(self) -> None:
|
|
||||||
"""Verify if returned sockets use the correct port ranges"""
|
|
||||||
sockets = []
|
|
||||||
for _ in range(6):
|
|
||||||
socket = create_random_port_socket(("8080-8085",), "127.0.0.1")
|
|
||||||
sockets.append(socket)
|
|
||||||
_, port = socket.getsockname()
|
|
||||||
self.assertIn(port, range(8080, 8086), "Port of socket was not inside the expected range")
|
|
||||||
for s in sockets:
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
sockets.clear()
|
|
||||||
length = 5_000 if is_macos else (30_000 - len(get_used_ports()))
|
|
||||||
for _ in range(length):
|
|
||||||
socket = create_random_port_socket(("30000-65535",), "127.0.0.1")
|
|
||||||
sockets.append(socket)
|
|
||||||
_, port = socket.getsockname()
|
|
||||||
self.assertIn(port, range(30_000, 65536), "Port of socket was not inside the expected range")
|
|
||||||
|
|
||||||
for s in sockets:
|
|
||||||
s.close()
|
|
||||||
76
test/webhost/test_suuid.py
Normal file
76
test/webhost/test_suuid.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import math
|
||||||
|
from typing import Any, Callable
|
||||||
|
from typing_extensions import override
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
|
from . import TestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestSUUID(TestBase):
|
||||||
|
converter: BaseConverter
|
||||||
|
filter: Callable[[Any], str]
|
||||||
|
|
||||||
|
@override
|
||||||
|
def setUp(self) -> None:
|
||||||
|
from werkzeug.routing import Map
|
||||||
|
|
||||||
|
super().setUp()
|
||||||
|
self.converter = self.app.url_map.converters["suuid"](Map())
|
||||||
|
self.filter = self.app.jinja_env.filters["suuid"] # type: ignore # defines how we use it, not what it can be
|
||||||
|
|
||||||
|
def test_is_reversible(self) -> None:
|
||||||
|
u = uuid4()
|
||||||
|
self.assertEqual(u, self.converter.to_python(self.converter.to_url(u)))
|
||||||
|
s = "A" * 22 # uuid with all zeros
|
||||||
|
self.assertEqual(s, self.converter.to_url(self.converter.to_python(s)))
|
||||||
|
|
||||||
|
def test_uuid_length(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.converter.to_python("AAAA")
|
||||||
|
|
||||||
|
def test_padding(self) -> None:
|
||||||
|
self.converter.to_python("A" * 22) # check that the correct value works
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.converter.to_python("A" * 22 + "==") # converter should not allow padding
|
||||||
|
|
||||||
|
def test_empty(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.converter.to_python("")
|
||||||
|
|
||||||
|
def test_stray_equal_signs(self) -> None:
|
||||||
|
self.converter.to_python("A" * 22) # check that the correct value works
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.converter.to_python("A" * 22 + "==" + "AA") # the "==AA" should not be ignored, but error out
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.converter.to_python("A" * 20 + "==" + "AA") # the final "A"s should not be appended to the first "A"s
|
||||||
|
|
||||||
|
def test_stray_whitespace(self) -> None:
|
||||||
|
s = "A" * 22
|
||||||
|
self.converter.to_python(s) # check that the correct value works
|
||||||
|
for char in " \t\r\n\v":
|
||||||
|
for pos in (0, 11, 22):
|
||||||
|
with self.subTest(char=char, pos=pos):
|
||||||
|
s_with_whitespace = s[0:pos] + char * 4 + s[pos:] # insert 4 to make padding correct
|
||||||
|
# check that the constructed s_with_whitespace is correct
|
||||||
|
self.assertEqual(len(s_with_whitespace), len(s) + 4)
|
||||||
|
self.assertEqual(s_with_whitespace[pos], char)
|
||||||
|
# s_with_whitespace should be invalid as SUUID
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.converter.to_python(s_with_whitespace)
|
||||||
|
|
||||||
|
def test_filter_returns_valid_string(self) -> None:
|
||||||
|
u = uuid4()
|
||||||
|
s = self.filter(u)
|
||||||
|
self.assertIsInstance(s, str)
|
||||||
|
self.assertNotIn("=", s)
|
||||||
|
self.assertEqual(len(s), math.ceil(len(u.bytes) * 4 / 3))
|
||||||
|
|
||||||
|
def test_filter_is_same_as_converter(self) -> None:
|
||||||
|
u = uuid4()
|
||||||
|
self.assertEqual(self.filter(u), self.converter.to_url(u))
|
||||||
|
|
||||||
|
def test_filter_bad_type(self) -> None:
|
||||||
|
with self.assertRaises(Exception): # currently the type is not checked directly, so any exception is valid
|
||||||
|
self.filter(None)
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import typing as t
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from typing_extensions import override
|
|
||||||
|
|
||||||
from test.multiserver.test_gamespackage_cache import GamesPackageCacheTest
|
|
||||||
|
|
||||||
import Utils
|
|
||||||
from NetUtils import GamesPackage
|
|
||||||
from apmw.webhost.customserver.gamespackagecache import DBGamesPackageCache
|
|
||||||
|
|
||||||
|
|
||||||
class FakeGameDataPackage:
|
|
||||||
_rows: "t.ClassVar[dict[str, FakeGameDataPackage]]" = {}
|
|
||||||
data: bytes
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, checksum: str) -> "FakeGameDataPackage | None":
|
|
||||||
return cls._rows.get(checksum, None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add(cls, checksum: str, full_games_package: GamesPackage) -> None:
|
|
||||||
row = FakeGameDataPackage()
|
|
||||||
row.data = Utils.restricted_dumps(full_games_package)
|
|
||||||
cls._rows[checksum] = row
|
|
||||||
|
|
||||||
|
|
||||||
class DBGamesPackageCacheTest(GamesPackageCacheTest):
|
|
||||||
cache: DBGamesPackageCache
|
|
||||||
any_game: t.ClassVar[str] = "My Game"
|
|
||||||
static_data: t.ClassVar[dict[str, GamesPackage]] = { # noqa: pycharm doesn't understand this
|
|
||||||
"My Game": {
|
|
||||||
"item_name_to_id": {"Item 1": 1},
|
|
||||||
"location_name_to_id": {"Location 1": 1},
|
|
||||||
"item_name_groups": {"Everything": ["Item 1"]},
|
|
||||||
"location_name_groups": {"Everywhere": ["Location 1"]},
|
|
||||||
"checksum": "2345",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
orig_db_type: t.ClassVar[type]
|
|
||||||
|
|
||||||
@override
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls) -> None:
|
|
||||||
import WebHostLib.models
|
|
||||||
|
|
||||||
cls.orig_db_type = WebHostLib.models.GameDataPackage
|
|
||||||
WebHostLib.models.GameDataPackage = FakeGameDataPackage # type: ignore
|
|
||||||
|
|
||||||
@override
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.cache = DBGamesPackageCache(self.static_data)
|
|
||||||
|
|
||||||
@override
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls) -> None:
|
|
||||||
import WebHostLib.models
|
|
||||||
|
|
||||||
WebHostLib.models.GameDataPackage = cls.orig_db_type # type: ignore
|
|
||||||
|
|
||||||
def assert_conversion(
|
|
||||||
self,
|
|
||||||
full_games_package: GamesPackage,
|
|
||||||
reduced_games_package: dict[str, t.Any],
|
|
||||||
item_name_groups: dict[str, t.Any],
|
|
||||||
location_name_groups: dict[str, t.Any],
|
|
||||||
) -> None:
|
|
||||||
for key in ("item_name_to_id", "location_name_to_id", "checksum"):
|
|
||||||
if key in full_games_package:
|
|
||||||
self.assertEqual(reduced_games_package[key], full_games_package[key]) # noqa: pycharm
|
|
||||||
self.assertEqual(item_name_groups, full_games_package["item_name_groups"])
|
|
||||||
self.assertEqual(location_name_groups, full_games_package["location_name_groups"])
|
|
||||||
|
|
||||||
def assert_static_conversion(
|
|
||||||
self,
|
|
||||||
full_games_package: GamesPackage,
|
|
||||||
reduced_games_package: dict[str, t.Any],
|
|
||||||
item_name_groups: dict[str, t.Any],
|
|
||||||
location_name_groups: dict[str, t.Any],
|
|
||||||
) -> None:
|
|
||||||
self.assert_conversion(full_games_package, reduced_games_package, item_name_groups, location_name_groups)
|
|
||||||
for key in ("item_name_to_id", "location_name_to_id", "checksum"):
|
|
||||||
self.assertIs(reduced_games_package[key], full_games_package[key]) # noqa: pycharm
|
|
||||||
|
|
||||||
def test_get_static_contents(self) -> None:
|
|
||||||
"""Tests that get_static returns the correct data"""
|
|
||||||
reduced_games_package, item_name_groups, location_name_groups = self.cache.get_static(self.any_game)
|
|
||||||
for key in ("item_name_to_id", "location_name_to_id", "checksum"):
|
|
||||||
self.assertIs(reduced_games_package[key], self.static_data[self.any_game][key]) # noqa: pycharm
|
|
||||||
self.assertEqual(item_name_groups, self.static_data[self.any_game]["item_name_groups"])
|
|
||||||
self.assertEqual(location_name_groups, self.static_data[self.any_game]["location_name_groups"])
|
|
||||||
|
|
||||||
def test_static_not_evicted(self) -> None:
|
|
||||||
"""Tests that static data is not evicted from cache during gc"""
|
|
||||||
import gc
|
|
||||||
|
|
||||||
game_name = next(iter(self.static_data.keys()))
|
|
||||||
ids = [id(o) for o in self.cache.get_static(game_name)]
|
|
||||||
gc.collect()
|
|
||||||
self.assertEqual(ids, [id(o) for o in self.cache.get_static(game_name)])
|
|
||||||
|
|
||||||
def test_get_is_static(self) -> None:
|
|
||||||
"""Tests that a get with correct checksum return the static items"""
|
|
||||||
# NOTE: this is only true for the DB cache, not the "regular" one, since we want to avoid loading worlds there
|
|
||||||
cks: GamesPackage = {"checksum": self.static_data[self.any_game]["checksum"]} # noqa: pycharm doesn't like this
|
|
||||||
reduced_games_package1, item_name_groups1, location_name_groups1 = self.cache.get(self.any_game, cks)
|
|
||||||
reduced_games_package2, item_name_groups2, location_name_groups2 = self.cache.get_static(self.any_game)
|
|
||||||
self.assertIs(reduced_games_package1, reduced_games_package2)
|
|
||||||
self.assertEqual(location_name_groups1, location_name_groups2)
|
|
||||||
self.assertEqual(item_name_groups1, item_name_groups2)
|
|
||||||
|
|
||||||
def test_get_from_db(self) -> None:
|
|
||||||
"""Tests that a get with only checksum will load the full data from db and is cached"""
|
|
||||||
game_name = "Another Game"
|
|
||||||
full_games_package = deepcopy(self.static_data[self.any_game])
|
|
||||||
full_games_package["checksum"] = "3456"
|
|
||||||
cks: GamesPackage = {"checksum": full_games_package["checksum"]} # noqa: pycharm doesn't like this
|
|
||||||
FakeGameDataPackage.add(full_games_package["checksum"], full_games_package)
|
|
||||||
before_add = len(self.cache._reduced_games_packages)
|
|
||||||
data = self.cache.get(game_name, cks)
|
|
||||||
self.assert_conversion(full_games_package, *data) # type: ignore
|
|
||||||
self.assertEqual(before_add + 1, len(self.cache._reduced_games_packages))
|
|
||||||
|
|
||||||
def test_get_missing_from_db_uses_full_games_package(self) -> None:
|
|
||||||
"""Tests that a get with full data (missing from db) will use the full data and is cached"""
|
|
||||||
game_name = "Yet Another Game"
|
|
||||||
full_games_package = deepcopy(self.static_data[self.any_game])
|
|
||||||
full_games_package["checksum"] = "4567"
|
|
||||||
before_add = len(self.cache._reduced_games_packages)
|
|
||||||
data = self.cache.get(game_name, full_games_package)
|
|
||||||
self.assert_conversion(full_games_package, *data) # type: ignore
|
|
||||||
self.assertEqual(before_add + 1, len(self.cache._reduced_games_packages))
|
|
||||||
|
|
||||||
def test_get_without_checksum_uses_full_games_package(self) -> None:
|
|
||||||
"""Tests that a get with full data and no checksum will use the full data and is not cached"""
|
|
||||||
game_name = "Yet Another Game"
|
|
||||||
full_games_package = deepcopy(self.static_data[self.any_game])
|
|
||||||
del full_games_package["checksum"]
|
|
||||||
before_add = len(self.cache._reduced_games_packages)
|
|
||||||
data = self.cache.get(game_name, full_games_package)
|
|
||||||
self.assert_conversion(full_games_package, *data) # type: ignore
|
|
||||||
self.assertEqual(before_add, len(self.cache._reduced_games_packages))
|
|
||||||
|
|
||||||
def test_get_missing_from_db_raises(self) -> None:
|
|
||||||
"""Tests that a get that requires a row to exist raise an exception if it doesn't"""
|
|
||||||
with self.assertRaises(Exception):
|
|
||||||
_ = self.cache.get("Does not exist", {"checksum": "0000"})
|
|
||||||
@@ -269,8 +269,9 @@ if not is_frozen():
|
|||||||
from Launcher import open_folder
|
from Launcher import open_folder
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
parser = argparse.ArgumentParser("Build script for APWorlds")
|
parser = argparse.ArgumentParser(prog="Build APWorlds", description="Build script for APWorlds")
|
||||||
parser.add_argument("worlds", type=str, default=(), nargs="*", help="Names of APWorlds to build.")
|
parser.add_argument("worlds", type=str, default=(), nargs="*", help="names of APWorlds to build")
|
||||||
|
parser.add_argument("--skip_open_folder", action="store_true", help="don't open the output build folder")
|
||||||
args = parser.parse_args(launch_args)
|
args = parser.parse_args(launch_args)
|
||||||
|
|
||||||
if args.worlds:
|
if args.worlds:
|
||||||
@@ -320,7 +321,9 @@ if not is_frozen():
|
|||||||
zf.write(pathlib.Path(world_directory, file), pathlib.Path(file_name, file))
|
zf.write(pathlib.Path(world_directory, file), pathlib.Path(file_name, file))
|
||||||
|
|
||||||
zf.writestr(apworld.manifest_path, json.dumps(manifest))
|
zf.writestr(apworld.manifest_path, json.dumps(manifest))
|
||||||
open_folder(apworlds_folder)
|
|
||||||
|
if not args.skip_open_folder:
|
||||||
|
open_folder(apworlds_folder)
|
||||||
|
|
||||||
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
||||||
description="Build APWorlds from loose-file world folders."))
|
description="Build APWorlds from loose-file world folders."))
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ from Options import PerGameCommonOptions
|
|||||||
from .Locations import location_table, AdventureLocation, dragon_room_to_region
|
from .Locations import location_table, AdventureLocation, dragon_room_to_region
|
||||||
|
|
||||||
|
|
||||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
def connect(multiworld: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
||||||
one_way=False, name=None):
|
one_way=False, name=None):
|
||||||
source_region = world.get_region(source, player)
|
source_region = multiworld.get_region(source, player)
|
||||||
target_region = world.get_region(target, player)
|
target_region = multiworld.get_region(target, player)
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
name = source + " to " + target
|
name = source + " to " + target
|
||||||
@@ -22,7 +22,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
|
|||||||
source_region.exits.append(connection)
|
source_region.exits.append(connection)
|
||||||
connection.connect(target_region)
|
connection.connect(target_region)
|
||||||
if not one_way:
|
if not one_way:
|
||||||
connect(world, player, target, source, rule, True)
|
connect(multiworld, player, target, source, rule, True)
|
||||||
|
|
||||||
|
|
||||||
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||||
|
|||||||
@@ -3,47 +3,47 @@ from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
|||||||
|
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
world = self.multiworld
|
multiworld = self.multiworld
|
||||||
use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic
|
use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic
|
||||||
|
|
||||||
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
set_rule(multiworld.get_entrance("YellowCastlePort", self.player),
|
||||||
lambda state: state.has("Yellow Key", self.player))
|
lambda state: state.has("Yellow Key", self.player))
|
||||||
set_rule(world.get_entrance("BlackCastlePort", self.player),
|
set_rule(multiworld.get_entrance("BlackCastlePort", self.player),
|
||||||
lambda state: state.has("Black Key", self.player))
|
lambda state: state.has("Black Key", self.player))
|
||||||
set_rule(world.get_entrance("WhiteCastlePort", self.player),
|
set_rule(multiworld.get_entrance("WhiteCastlePort", self.player),
|
||||||
lambda state: state.has("White Key", self.player))
|
lambda state: state.has("White Key", self.player))
|
||||||
|
|
||||||
# a future thing would be to make the bat an actual item, or at least allow it to
|
# a future thing would be to make the bat an actual item, or at least allow it to
|
||||||
# be placed in a castle, which would require some additions to the rules when
|
# be placed in a castle, which would require some additions to the rules when
|
||||||
# use_bat_logic is true
|
# use_bat_logic is true
|
||||||
if not use_bat_logic:
|
if not use_bat_logic:
|
||||||
set_rule(world.get_entrance("WhiteCastleSecretPassage", self.player),
|
set_rule(multiworld.get_entrance("WhiteCastleSecretPassage", self.player),
|
||||||
lambda state: state.has("Bridge", self.player))
|
lambda state: state.has("Bridge", self.player))
|
||||||
set_rule(world.get_entrance("WhiteCastlePeekPassage", self.player),
|
set_rule(multiworld.get_entrance("WhiteCastlePeekPassage", self.player),
|
||||||
lambda state: state.has("Bridge", self.player) or
|
lambda state: state.has("Bridge", self.player) or
|
||||||
state.has("Magnet", self.player))
|
state.has("Magnet", self.player))
|
||||||
set_rule(world.get_entrance("BlackCastleVaultEntrance", self.player),
|
set_rule(multiworld.get_entrance("BlackCastleVaultEntrance", self.player),
|
||||||
lambda state: state.has("Bridge", self.player) or
|
lambda state: state.has("Bridge", self.player) or
|
||||||
state.has("Magnet", self.player))
|
state.has("Magnet", self.player))
|
||||||
|
|
||||||
dragon_slay_check = self.options.dragon_slay_check.value
|
dragon_slay_check = self.options.dragon_slay_check.value
|
||||||
if dragon_slay_check:
|
if dragon_slay_check:
|
||||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
set_rule(multiworld.get_location("Slay Yorgle", self.player),
|
||||||
lambda state: state.has("Sword", self.player) and
|
lambda state: state.has("Sword", self.player) and
|
||||||
state.has("Right Difficulty Switch", self.player))
|
state.has("Right Difficulty Switch", self.player))
|
||||||
set_rule(world.get_location("Slay Grundle", self.player),
|
set_rule(multiworld.get_location("Slay Grundle", self.player),
|
||||||
lambda state: state.has("Sword", self.player) and
|
lambda state: state.has("Sword", self.player) and
|
||||||
state.has("Right Difficulty Switch", self.player))
|
state.has("Right Difficulty Switch", self.player))
|
||||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
set_rule(multiworld.get_location("Slay Rhindle", self.player),
|
||||||
lambda state: state.has("Sword", self.player) and
|
lambda state: state.has("Sword", self.player) and
|
||||||
state.has("Right Difficulty Switch", self.player))
|
state.has("Right Difficulty Switch", self.player))
|
||||||
else:
|
else:
|
||||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
set_rule(multiworld.get_location("Slay Yorgle", self.player),
|
||||||
lambda state: state.has("Sword", self.player))
|
lambda state: state.has("Sword", self.player))
|
||||||
set_rule(world.get_location("Slay Grundle", self.player),
|
set_rule(multiworld.get_location("Slay Grundle", self.player),
|
||||||
lambda state: state.has("Sword", self.player))
|
lambda state: state.has("Sword", self.player))
|
||||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
set_rule(multiworld.get_location("Slay Rhindle", self.player),
|
||||||
lambda state: state.has("Sword", self.player))
|
lambda state: state.has("Sword", self.player))
|
||||||
|
|
||||||
# really this requires getting the dot item, and having another item or enemy
|
# really this requires getting the dot item, and having another item or enemy
|
||||||
@@ -51,37 +51,37 @@ def set_rules(self) -> None:
|
|||||||
# to actually make randomized, since it is invisible. May add some options
|
# to actually make randomized, since it is invisible. May add some options
|
||||||
# for how that works in the distant future, but for now, just say you need
|
# for how that works in the distant future, but for now, just say you need
|
||||||
# the bridge and black key to get to it, as that simplifies things a lot
|
# the bridge and black key to get to it, as that simplifies things a lot
|
||||||
set_rule(world.get_entrance("CreditsWall", self.player),
|
set_rule(multiworld.get_entrance("CreditsWall", self.player),
|
||||||
lambda state: state.has("Bridge", self.player) and
|
lambda state: state.has("Bridge", self.player) and
|
||||||
state.has("Black Key", self.player))
|
state.has("Black Key", self.player))
|
||||||
|
|
||||||
if not use_bat_logic:
|
if not use_bat_logic:
|
||||||
set_rule(world.get_entrance("CreditsToFarSide", self.player),
|
set_rule(multiworld.get_entrance("CreditsToFarSide", self.player),
|
||||||
lambda state: state.has("Magnet", self.player))
|
lambda state: state.has("Magnet", self.player))
|
||||||
|
|
||||||
# bridge literally does not fit in this space, I think. I'll just exclude it
|
# bridge literally does not fit in this space, I think. I'll just exclude it
|
||||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Bridge", self.player)
|
forbid_item(multiworld.get_location("Dungeon Vault", self.player), "Bridge", self.player)
|
||||||
# don't put magnet in locations that can pull in-logic items out of reach unless the bat is in play
|
# don't put magnet in locations that can pull in-logic items out of reach unless the bat is in play
|
||||||
if not use_bat_logic:
|
if not use_bat_logic:
|
||||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Magnet", self.player)
|
forbid_item(multiworld.get_location("Dungeon Vault", self.player), "Magnet", self.player)
|
||||||
forbid_item(world.get_location("Red Maze Vault Entrance", self.player), "Magnet", self.player)
|
forbid_item(multiworld.get_location("Red Maze Vault Entrance", self.player), "Magnet", self.player)
|
||||||
forbid_item(world.get_location("Credits Right Side", self.player), "Magnet", self.player)
|
forbid_item(multiworld.get_location("Credits Right Side", self.player), "Magnet", self.player)
|
||||||
|
|
||||||
# and obviously we don't want to start with the game already won
|
# and obviously we don't want to start with the game already won
|
||||||
forbid_item(world.get_location("Inside Yellow Castle", self.player), "Chalice", self.player)
|
forbid_item(multiworld.get_location("Inside Yellow Castle", self.player), "Chalice", self.player)
|
||||||
overworld = world.get_region("Overworld", self.player)
|
overworld = multiworld.get_region("Overworld", self.player)
|
||||||
|
|
||||||
for loc in overworld.locations:
|
for loc in overworld.locations:
|
||||||
forbid_item(loc, "Chalice", self.player)
|
forbid_item(loc, "Chalice", self.player)
|
||||||
|
|
||||||
add_rule(world.get_location("Chalice Home", self.player),
|
add_rule(multiworld.get_location("Chalice Home", self.player),
|
||||||
lambda state: state.has("Chalice", self.player) and state.has("Yellow Key", self.player))
|
lambda state: state.has("Chalice", self.player) and state.has("Yellow Key", self.player))
|
||||||
|
|
||||||
# world.random.choice(overworld.locations).progress_type = LocationProgressType.PRIORITY
|
# multiworld.random.choice(overworld.locations).progress_type = LocationProgressType.PRIORITY
|
||||||
|
|
||||||
# all_locations = world.get_locations(self.player).copy()
|
# all_locations = multiworld.get_locations(self.player).copy()
|
||||||
# while priority_count < get_num_items():
|
# while priority_count < get_num_items():
|
||||||
# loc = world.random.choice(all_locations)
|
# loc = multiworld.random.choice(all_locations)
|
||||||
# if loc.progress_type == LocationProgressType.DEFAULT:
|
# if loc.progress_type == LocationProgressType.DEFAULT:
|
||||||
# loc.progress_type = LocationProgressType.PRIORITY
|
# loc.progress_type = LocationProgressType.PRIORITY
|
||||||
# priority_count += 1
|
# priority_count += 1
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ class AdventureWorld(World):
|
|||||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||||
required_client_version: Tuple[int, int, int] = (0, 3, 9)
|
required_client_version: Tuple[int, int, int] = (0, 3, 9)
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, multiworld: MultiWorld, player: int):
|
||||||
super().__init__(world, player)
|
super().__init__(multiworld, player)
|
||||||
self.rom_name: Optional[bytearray] = bytearray("", "utf8" )
|
self.rom_name: Optional[bytearray] = bytearray("", "utf8" )
|
||||||
self.dragon_rooms: [int] = [0x14, 0x19, 0x4]
|
self.dragon_rooms: [int] = [0x14, 0x19, 0x4]
|
||||||
self.dragon_slay_check: Optional[int] = 0
|
self.dragon_slay_check: Optional[int] = 0
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
maseya-z3pr>=1.0.0rc1
|
maseya-z3pr==1.0.0rc1
|
||||||
xxtea>=3.0.0
|
xxtea==3.7.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"game": "APQuest",
|
"game": "APQuest",
|
||||||
"minimum_ap_version": "0.6.4",
|
"minimum_ap_version": "0.6.7",
|
||||||
"world_version": "1.0.1",
|
"world_version": "2.0.0",
|
||||||
"authors": ["NewSoupVi"]
|
"authors": ["NewSoupVi"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,10 @@
|
|||||||
C to fire available Confetti Cannons
|
C to fire available Confetti Cannons
|
||||||
Number Keys + Backspace for Math Trap\n
|
Number Keys + Backspace for Math Trap\n
|
||||||
|
|
||||||
Rebinding controls might be added in the future :)"""
|
[b]Click to move also works![/b]
|
||||||
|
|
||||||
|
Click/tap Confetti Cannon to fire it
|
||||||
|
Submit Math Trap solution in the command line at the bottom"""
|
||||||
|
|
||||||
<VolumeSliderView>:
|
<VolumeSliderView>:
|
||||||
orientation: "horizontal"
|
orientation: "horizontal"
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ from argparse import Namespace
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from CommonClient import CommonContext, gui_enabled, logger, server_loop
|
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop
|
||||||
from NetUtils import ClientStatus
|
from NetUtils import ClientStatus
|
||||||
|
from Utils import gui_enabled
|
||||||
|
|
||||||
from ..game.events import ConfettiFired, LocationClearedEvent, MathProblemSolved, MathProblemStarted, VictoryEvent
|
from ..game.events import ConfettiFired, LocationClearedEvent, MathProblemSolved, MathProblemStarted, VictoryEvent
|
||||||
from ..game.game import Game
|
from ..game.game import Game
|
||||||
@@ -41,6 +42,16 @@ class ConnectionStatus(Enum):
|
|||||||
GAME_RUNNING = 3
|
GAME_RUNNING = 3
|
||||||
|
|
||||||
|
|
||||||
|
class APQuestClientCommandProcessor(ClientCommandProcessor):
|
||||||
|
ctx: "APQuestContext"
|
||||||
|
|
||||||
|
def default(self, raw: str) -> None:
|
||||||
|
if self.ctx.external_math_trap_input(raw):
|
||||||
|
return
|
||||||
|
|
||||||
|
super().default(raw)
|
||||||
|
|
||||||
|
|
||||||
class APQuestContext(CommonContext):
|
class APQuestContext(CommonContext):
|
||||||
game = "APQuest"
|
game = "APQuest"
|
||||||
items_handling = 0b111 # full remote
|
items_handling = 0b111 # full remote
|
||||||
@@ -65,6 +76,7 @@ class APQuestContext(CommonContext):
|
|||||||
delay_intro_song: bool
|
delay_intro_song: bool
|
||||||
|
|
||||||
ui: APQuestManager
|
ui: APQuestManager
|
||||||
|
command_processor = APQuestClientCommandProcessor
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, server_address: str | None = None, password: str | None = None, delay_intro_song: bool = False
|
self, server_address: str | None = None, password: str | None = None, delay_intro_song: bool = False
|
||||||
@@ -172,7 +184,6 @@ class APQuestContext(CommonContext):
|
|||||||
assert self.ap_quest_game is not None
|
assert self.ap_quest_game is not None
|
||||||
self.ap_quest_game.gameboard.fill_remote_location_content(remote_item_graphic_overrides)
|
self.ap_quest_game.gameboard.fill_remote_location_content(remote_item_graphic_overrides)
|
||||||
self.render()
|
self.render()
|
||||||
self.ui.game_view.bind_keyboard()
|
|
||||||
|
|
||||||
self.connection_status = ConnectionStatus.GAME_RUNNING
|
self.connection_status = ConnectionStatus.GAME_RUNNING
|
||||||
self.ui.game_started()
|
self.ui.game_started()
|
||||||
@@ -187,7 +198,7 @@ class APQuestContext(CommonContext):
|
|||||||
if self.ap_quest_game is None:
|
if self.ap_quest_game is None:
|
||||||
raise RuntimeError("Tried to render before self.ap_quest_game was initialized.")
|
raise RuntimeError("Tried to render before self.ap_quest_game was initialized.")
|
||||||
|
|
||||||
self.ui.render(self.ap_quest_game, self.player_sprite)
|
self.ui.render(self.ap_quest_game, self.player_sprite, self.hard_mode)
|
||||||
self.handle_game_events()
|
self.handle_game_events()
|
||||||
|
|
||||||
def location_checked_side_effects(self, location: int) -> None:
|
def location_checked_side_effects(self, location: int) -> None:
|
||||||
@@ -244,6 +255,59 @@ class APQuestContext(CommonContext):
|
|||||||
self.ap_quest_game.input(input_key)
|
self.ap_quest_game.input(input_key)
|
||||||
self.render()
|
self.render()
|
||||||
|
|
||||||
|
def queue_auto_move(self, target_x: int, target_y: int) -> None:
|
||||||
|
if self.ap_quest_game is None:
|
||||||
|
return
|
||||||
|
if not self.ap_quest_game.gameboard.ready:
|
||||||
|
return
|
||||||
|
if not self.ui.game_view.focused > 1: # Must already be in focus
|
||||||
|
return
|
||||||
|
self.ap_quest_game.queue_auto_move(target_x, target_y)
|
||||||
|
self.ui.start_auto_move()
|
||||||
|
|
||||||
|
def do_auto_move_and_rerender(self) -> None:
|
||||||
|
if self.ap_quest_game is None:
|
||||||
|
return
|
||||||
|
if not self.ap_quest_game.gameboard.ready:
|
||||||
|
return
|
||||||
|
changed = self.ap_quest_game.do_auto_move()
|
||||||
|
if changed:
|
||||||
|
self.render()
|
||||||
|
|
||||||
|
def confetti_and_rerender(self) -> None:
|
||||||
|
# Used by tap mode
|
||||||
|
if self.ap_quest_game is None:
|
||||||
|
return
|
||||||
|
if not self.ap_quest_game.gameboard.ready:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.ap_quest_game.attempt_fire_confetti_cannon():
|
||||||
|
self.render()
|
||||||
|
|
||||||
|
def external_math_trap_input(self, raw: str) -> bool:
|
||||||
|
if self.ap_quest_game is None:
|
||||||
|
return False
|
||||||
|
if not self.ap_quest_game.gameboard.ready:
|
||||||
|
return False
|
||||||
|
if not self.ap_quest_game.active_math_problem:
|
||||||
|
return False
|
||||||
|
|
||||||
|
raw = raw.strip()
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
return False
|
||||||
|
if not raw.isnumeric():
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.ap_quest_game.math_problem_replace([int(digit) for digit in raw])
|
||||||
|
|
||||||
|
if not self.ap_quest_game.active_math_problem:
|
||||||
|
self.ui.game_view.force_focus()
|
||||||
|
|
||||||
|
self.render()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def make_gui(self) -> "type[kvui.GameManager]":
|
def make_gui(self) -> "type[kvui.GameManager]":
|
||||||
self.load_kv()
|
self.load_kv()
|
||||||
return APQuestManager
|
return APQuestManager
|
||||||
|
|||||||
@@ -4,29 +4,26 @@ from math import sqrt
|
|||||||
from random import choice, random
|
from random import choice, random
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from kivy.core.window import Keyboard, Window
|
from kivy.core.window import Window
|
||||||
from kivy.graphics import Color, Triangle
|
from kivy.graphics import Color, Triangle
|
||||||
from kivy.graphics.instructions import Canvas
|
from kivy.graphics.instructions import Canvas
|
||||||
from kivy.input import MotionEvent
|
from kivy.uix.behaviors import ButtonBehavior
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
|
from kivy.uix.image import Image
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
from kivymd.uix.recycleview import MDRecycleView
|
from kivymd.uix.recycleview import MDRecycleView
|
||||||
|
|
||||||
from CommonClient import logger
|
from CommonClient import logger
|
||||||
|
|
||||||
from ..game.inputs import Input
|
from ..game.inputs import Input
|
||||||
|
|
||||||
|
INPUT_MAP_STR = {
|
||||||
INPUT_MAP = {
|
|
||||||
"up": Input.UP,
|
|
||||||
"w": Input.UP,
|
"w": Input.UP,
|
||||||
"down": Input.DOWN,
|
|
||||||
"s": Input.DOWN,
|
"s": Input.DOWN,
|
||||||
"right": Input.RIGHT,
|
|
||||||
"d": Input.RIGHT,
|
"d": Input.RIGHT,
|
||||||
"left": Input.LEFT,
|
|
||||||
"a": Input.LEFT,
|
"a": Input.LEFT,
|
||||||
"spacebar": Input.ACTION,
|
" ": Input.ACTION,
|
||||||
"c": Input.CONFETTI,
|
"c": Input.CONFETTI,
|
||||||
"0": Input.ZERO,
|
"0": Input.ZERO,
|
||||||
"1": Input.ONE,
|
"1": Input.ONE,
|
||||||
@@ -38,38 +35,52 @@ INPUT_MAP = {
|
|||||||
"7": Input.SEVEN,
|
"7": Input.SEVEN,
|
||||||
"8": Input.EIGHT,
|
"8": Input.EIGHT,
|
||||||
"9": Input.NINE,
|
"9": Input.NINE,
|
||||||
"backspace": Input.BACKSPACE,
|
}
|
||||||
|
|
||||||
|
INPUT_MAP_SPECIAL_INT = {
|
||||||
|
# Arrow Keys and Backspace
|
||||||
|
273: Input.UP,
|
||||||
|
274: Input.DOWN,
|
||||||
|
275: Input.RIGHT,
|
||||||
|
276: Input.LEFT,
|
||||||
|
8: Input.BACKSPACE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class APQuestGameView(MDRecycleView):
|
class APQuestGameView(MDRecycleView):
|
||||||
_keyboard: Keyboard | None = None
|
focused: int = 1
|
||||||
input_function: Callable[[Input], None]
|
input_function: Callable[[Input], None]
|
||||||
|
|
||||||
def __init__(self, input_function: Callable[[Input], None], **kwargs: Any) -> None:
|
def __init__(self, input_function: Callable[[Input], None], **kwargs: Any) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.input_function = input_function
|
self.input_function = input_function
|
||||||
self.bind_keyboard()
|
Window.bind(on_key_down=self._on_keyboard_down)
|
||||||
|
Window.bind(on_touch_down=self.check_focus)
|
||||||
|
self.opacity = 0.5
|
||||||
|
|
||||||
def on_touch_down(self, touch: MotionEvent) -> None:
|
def check_focus(self, _, touch, *args, **kwargs) -> None:
|
||||||
self.bind_keyboard()
|
if self.parent.collide_point(*touch.pos):
|
||||||
|
self.focused += 1
|
||||||
def bind_keyboard(self) -> None:
|
self.opacity = 1
|
||||||
if self._keyboard is not None:
|
|
||||||
return
|
return
|
||||||
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
|
|
||||||
self._keyboard.bind(on_key_down=self._on_keyboard_down)
|
|
||||||
|
|
||||||
def _keyboard_closed(self) -> None:
|
self.focused = 0
|
||||||
if self._keyboard is None:
|
self.opacity = 0.5
|
||||||
return
|
|
||||||
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
|
|
||||||
self._keyboard = None
|
|
||||||
|
|
||||||
def _on_keyboard_down(self, _: Any, keycode: tuple[int, str], _1: Any, _2: Any) -> bool:
|
def force_focus(self) -> None:
|
||||||
if keycode[1] in INPUT_MAP:
|
Window.release_keyboard()
|
||||||
self.input_function(INPUT_MAP[keycode[1]])
|
self.focused = 1
|
||||||
return True
|
self.opacity = 1
|
||||||
|
|
||||||
|
def _on_keyboard_down(self, _: Any, keycode_int: int, _2: Any, keycode: str, _4: Any) -> bool:
|
||||||
|
if not self.focused:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if keycode in INPUT_MAP_STR:
|
||||||
|
self.input_function(INPUT_MAP_STR[keycode])
|
||||||
|
elif keycode_int in INPUT_MAP_SPECIAL_INT:
|
||||||
|
self.input_function(INPUT_MAP_SPECIAL_INT[keycode_int])
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class APQuestGrid(GridLayout):
|
class APQuestGrid(GridLayout):
|
||||||
@@ -77,7 +88,7 @@ class APQuestGrid(GridLayout):
|
|||||||
parent_width, parent_height = self.parent.size
|
parent_width, parent_height = self.parent.size
|
||||||
|
|
||||||
self_width_according_to_parent_height = parent_height * 12 / 11
|
self_width_according_to_parent_height = parent_height * 12 / 11
|
||||||
self_height_according_to_parent_width = parent_height * 11 / 12
|
self_height_according_to_parent_width = parent_width * 11 / 12
|
||||||
|
|
||||||
if self_width_according_to_parent_height > parent_width:
|
if self_width_according_to_parent_height > parent_width:
|
||||||
self.size = parent_width, self_height_according_to_parent_width
|
self.size = parent_width, self_height_according_to_parent_width
|
||||||
@@ -203,13 +214,23 @@ class Confetti:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ConfettiView(MDRecycleView):
|
class ConfettiView(Widget):
|
||||||
confetti: list[Confetti]
|
confetti: list[Confetti]
|
||||||
|
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.confetti = []
|
self.confetti = []
|
||||||
|
|
||||||
|
# Don't eat tap events for the game grid under the confetti view
|
||||||
|
def on_touch_down(self, touch) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_touch_move(self, touch) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_touch_up(self, touch) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
def check_resize(self, _: int, _1: int) -> None:
|
def check_resize(self, _: int, _1: int) -> None:
|
||||||
parent_width, parent_height = self.parent.size
|
parent_width, parent_height = self.parent.size
|
||||||
|
|
||||||
@@ -254,3 +275,32 @@ class VolumeSliderView(BoxLayout):
|
|||||||
|
|
||||||
class APQuestControlsView(BoxLayout):
|
class APQuestControlsView(BoxLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TapImage(ButtonBehavior, Image):
|
||||||
|
callback: Callable[[], None]
|
||||||
|
|
||||||
|
def __init__(self, callback: Callable[[], None], **kwargs) -> None:
|
||||||
|
self.callback = callback
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def on_release(self) -> bool:
|
||||||
|
self.callback()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class TapIfConfettiCannonImage(ButtonBehavior, Image):
|
||||||
|
callback: Callable[[], None]
|
||||||
|
|
||||||
|
is_confetti_cannon: bool = False
|
||||||
|
|
||||||
|
def __init__(self, callback: Callable[[], None], **kwargs: dict[str, Any]) -> None:
|
||||||
|
self.callback = callback
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def on_release(self) -> bool:
|
||||||
|
if self.is_confetti_cannon:
|
||||||
|
self.callback()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from kvui import GameManager, MDNavigationItemBase
|
|||||||
# isort: on
|
# isort: on
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from kivy._clock import ClockEvent
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
from kivy.uix.image import Image
|
from kivy.uix.image import Image
|
||||||
@@ -13,7 +14,16 @@ from kivy.uix.layout import Layout
|
|||||||
from kivymd.uix.recycleview import MDRecycleView
|
from kivymd.uix.recycleview import MDRecycleView
|
||||||
|
|
||||||
from ..game.game import Game
|
from ..game.game import Game
|
||||||
from .custom_views import APQuestControlsView, APQuestGameView, APQuestGrid, ConfettiView, VolumeSliderView
|
from ..game.graphics import Graphic
|
||||||
|
from .custom_views import (
|
||||||
|
APQuestControlsView,
|
||||||
|
APQuestGameView,
|
||||||
|
APQuestGrid,
|
||||||
|
ConfettiView,
|
||||||
|
TapIfConfettiCannonImage,
|
||||||
|
TapImage,
|
||||||
|
VolumeSliderView,
|
||||||
|
)
|
||||||
from .graphics import PlayerSprite, get_texture
|
from .graphics import PlayerSprite, get_texture
|
||||||
from .sounds import SoundManager
|
from .sounds import SoundManager
|
||||||
|
|
||||||
@@ -28,15 +38,17 @@ class APQuestManager(GameManager):
|
|||||||
lower_game_grid: GridLayout
|
lower_game_grid: GridLayout
|
||||||
upper_game_grid: GridLayout
|
upper_game_grid: GridLayout
|
||||||
|
|
||||||
game_view: MDRecycleView
|
game_view: MDRecycleView | None = None
|
||||||
game_view_tab: MDNavigationItemBase
|
game_view_tab: MDNavigationItemBase
|
||||||
|
|
||||||
sound_manager: SoundManager
|
sound_manager: SoundManager
|
||||||
|
|
||||||
bottom_image_grid: list[list[Image]]
|
bottom_image_grid: list[list[Image]]
|
||||||
top_image_grid: list[list[Image]]
|
top_image_grid: list[list[TapImage]]
|
||||||
confetti_view: ConfettiView
|
confetti_view: ConfettiView
|
||||||
|
|
||||||
|
move_event: ClockEvent | None
|
||||||
|
|
||||||
bottom_grid_is_grass: bool
|
bottom_grid_is_grass: bool
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
@@ -45,6 +57,7 @@ class APQuestManager(GameManager):
|
|||||||
self.sound_manager.allow_intro_to_play = not self.ctx.delay_intro_song
|
self.sound_manager.allow_intro_to_play = not self.ctx.delay_intro_song
|
||||||
self.top_image_grid = []
|
self.top_image_grid = []
|
||||||
self.bottom_image_grid = []
|
self.bottom_image_grid = []
|
||||||
|
self.move_event = None
|
||||||
self.bottom_grid_is_grass = False
|
self.bottom_grid_is_grass = False
|
||||||
|
|
||||||
def allow_intro_song(self) -> None:
|
def allow_intro_song(self) -> None:
|
||||||
@@ -71,25 +84,27 @@ class APQuestManager(GameManager):
|
|||||||
|
|
||||||
def game_started(self) -> None:
|
def game_started(self) -> None:
|
||||||
self.switch_to_game_tab()
|
self.switch_to_game_tab()
|
||||||
|
if self.game_view is not None:
|
||||||
|
self.game_view.force_focus()
|
||||||
self.sound_manager.game_started = True
|
self.sound_manager.game_started = True
|
||||||
|
|
||||||
def render(self, game: Game, player_sprite: PlayerSprite) -> None:
|
def render(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
|
||||||
self.setup_game_grid_if_not_setup(game.gameboard.size)
|
self.setup_game_grid_if_not_setup(game)
|
||||||
|
|
||||||
# This calls game.render(), which needs to happen to update the state of math traps
|
# This calls game.render(), which needs to happen to update the state of math traps
|
||||||
self.render_gameboard(game, player_sprite)
|
self.render_gameboard(game, player_sprite, hard_mode)
|
||||||
# Only now can we check whether a math problem is active
|
# Only now can we check whether a math problem is active
|
||||||
self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None)
|
self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None)
|
||||||
self.sound_manager.math_trap_active = game.active_math_problem is not None
|
self.sound_manager.math_trap_active = game.active_math_problem is not None
|
||||||
|
|
||||||
self.render_item_column(game)
|
self.render_item_column(game)
|
||||||
|
|
||||||
def render_gameboard(self, game: Game, player_sprite: PlayerSprite) -> None:
|
def render_gameboard(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
|
||||||
rendered_gameboard = game.render()
|
rendered_gameboard = game.render()
|
||||||
|
|
||||||
for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False):
|
for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False):
|
||||||
for graphic, image in zip(gameboard_row, image_row[:11], strict=False):
|
for graphic, image in zip(gameboard_row, image_row[:11], strict=False):
|
||||||
texture = get_texture(graphic, player_sprite)
|
texture = get_texture(graphic, player_sprite, hard_mode)
|
||||||
|
|
||||||
if texture is None:
|
if texture is None:
|
||||||
image.opacity = 0
|
image.opacity = 0
|
||||||
@@ -104,6 +119,8 @@ class APQuestManager(GameManager):
|
|||||||
for item_graphic, image_row in zip(rendered_item_column, self.top_image_grid, strict=False):
|
for item_graphic, image_row in zip(rendered_item_column, self.top_image_grid, strict=False):
|
||||||
image = image_row[-1]
|
image = image_row[-1]
|
||||||
|
|
||||||
|
image.is_confetti_cannon = item_graphic == Graphic.CONFETTI_CANNON
|
||||||
|
|
||||||
texture = get_texture(item_graphic)
|
texture = get_texture(item_graphic)
|
||||||
if texture is None:
|
if texture is None:
|
||||||
image.opacity = 0
|
image.opacity = 0
|
||||||
@@ -136,23 +153,25 @@ class APQuestManager(GameManager):
|
|||||||
|
|
||||||
self.bottom_grid_is_grass = grass
|
self.bottom_grid_is_grass = grass
|
||||||
|
|
||||||
def setup_game_grid_if_not_setup(self, size: tuple[int, int]) -> None:
|
def setup_game_grid_if_not_setup(self, game: Game) -> None:
|
||||||
if self.upper_game_grid.children:
|
if self.upper_game_grid.children:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.top_image_grid = []
|
self.top_image_grid = []
|
||||||
self.bottom_image_grid = []
|
self.bottom_image_grid = []
|
||||||
|
|
||||||
for _row in range(size[1]):
|
size = game.gameboard.size
|
||||||
|
|
||||||
|
for row in range(size[1]):
|
||||||
self.top_image_grid.append([])
|
self.top_image_grid.append([])
|
||||||
self.bottom_image_grid.append([])
|
self.bottom_image_grid.append([])
|
||||||
|
|
||||||
for _column in range(size[0]):
|
for column in range(size[0]):
|
||||||
bottom_image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
|
bottom_image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
|
||||||
self.lower_game_grid.add_widget(bottom_image)
|
self.lower_game_grid.add_widget(bottom_image)
|
||||||
self.bottom_image_grid[-1].append(bottom_image)
|
self.bottom_image_grid[-1].append(bottom_image)
|
||||||
|
|
||||||
top_image = Image(fit_mode="fill")
|
top_image = TapImage(lambda y=row, x=column: self.ctx.queue_auto_move(x, y), fit_mode="fill")
|
||||||
self.upper_game_grid.add_widget(top_image)
|
self.upper_game_grid.add_widget(top_image)
|
||||||
self.top_image_grid[-1].append(top_image)
|
self.top_image_grid[-1].append(top_image)
|
||||||
|
|
||||||
@@ -160,11 +179,19 @@ class APQuestManager(GameManager):
|
|||||||
image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
|
image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
|
||||||
self.lower_game_grid.add_widget(image)
|
self.lower_game_grid.add_widget(image)
|
||||||
|
|
||||||
image2 = Image(fit_mode="fill", opacity=0)
|
image2 = TapIfConfettiCannonImage(lambda: self.ctx.confetti_and_rerender(), fit_mode="fill", opacity=0)
|
||||||
self.upper_game_grid.add_widget(image2)
|
self.upper_game_grid.add_widget(image2)
|
||||||
|
|
||||||
self.top_image_grid[-1].append(image2)
|
self.top_image_grid[-1].append(image2)
|
||||||
|
|
||||||
|
def start_auto_move(self) -> None:
|
||||||
|
if self.move_event is not None:
|
||||||
|
self.move_event.cancel()
|
||||||
|
|
||||||
|
self.ctx.do_auto_move_and_rerender()
|
||||||
|
|
||||||
|
self.move_event = Clock.schedule_interval(lambda _: self.ctx.do_auto_move_and_rerender(), 0.10)
|
||||||
|
|
||||||
def build(self) -> Layout:
|
def build(self) -> Layout:
|
||||||
container = super().build()
|
container = super().build()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import pkgutil
|
import pkgutil
|
||||||
from collections.abc import Buffer
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Literal, NamedTuple, Protocol, cast
|
from typing import Literal, NamedTuple, Protocol, cast
|
||||||
|
|
||||||
from kivy.uix.image import CoreImage
|
from kivy.uix.image import CoreImage
|
||||||
|
from typing_extensions import Buffer
|
||||||
|
|
||||||
from CommonClient import logger
|
from CommonClient import logger
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ class RelatedTexture(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
||||||
|
# Inanimates
|
||||||
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
|
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
|
||||||
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16),
|
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16),
|
||||||
Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16),
|
Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16),
|
||||||
@@ -37,29 +38,25 @@ IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
|||||||
Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16),
|
Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16),
|
||||||
Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16),
|
Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16),
|
||||||
Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16),
|
Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16),
|
||||||
|
# Enemies
|
||||||
Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16),
|
Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16),
|
||||||
Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16),
|
Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16),
|
||||||
|
|
||||||
Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16),
|
Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16),
|
||||||
Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16),
|
Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16),
|
||||||
Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16),
|
Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16),
|
||||||
Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16),
|
Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16),
|
||||||
Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16),
|
Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16),
|
||||||
|
# Items
|
||||||
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
|
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
|
||||||
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
|
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
|
||||||
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
|
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
|
||||||
|
|
||||||
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
|
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
|
||||||
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
|
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
|
||||||
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
|
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
|
||||||
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
|
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
|
||||||
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
|
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
|
||||||
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
|
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
|
||||||
|
# Numbers
|
||||||
Graphic.ITEMS_TEXT: "items_text.png",
|
|
||||||
|
|
||||||
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
|
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
|
||||||
Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16),
|
Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16),
|
||||||
Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16),
|
Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16),
|
||||||
@@ -70,26 +67,29 @@ IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
|||||||
Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16),
|
Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16),
|
||||||
Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16),
|
Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16),
|
||||||
Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16),
|
Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16),
|
||||||
|
# Letters
|
||||||
Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16),
|
Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16),
|
||||||
Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16),
|
Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16),
|
||||||
Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16),
|
Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16),
|
||||||
Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16),
|
Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16),
|
||||||
Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16),
|
Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16),
|
||||||
Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16),
|
Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16),
|
||||||
|
# Mathematical symbols
|
||||||
Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16),
|
Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16),
|
||||||
Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
|
Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
|
||||||
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
|
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
|
||||||
Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16),
|
Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16),
|
||||||
Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16),
|
Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16),
|
||||||
|
# Other visual-only elements
|
||||||
|
Graphic.ITEMS_TEXT: "items_text.png",
|
||||||
Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16),
|
Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16),
|
||||||
|
|
||||||
Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
|
Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
|
||||||
}
|
}
|
||||||
|
|
||||||
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
|
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
|
||||||
|
|
||||||
|
EASY_MODE_BOSS_2_HEALTH = RelatedTexture("boss.png", 16, 0, 16, 16)
|
||||||
|
|
||||||
|
|
||||||
class PlayerSprite(Enum):
|
class PlayerSprite(Enum):
|
||||||
HUMAN = 0
|
HUMAN = 0
|
||||||
@@ -160,13 +160,18 @@ def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Textu
|
|||||||
return sub_texture
|
return sub_texture
|
||||||
|
|
||||||
|
|
||||||
def get_texture(graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None) -> Texture | None:
|
def get_texture(
|
||||||
|
graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None, hard_mode: bool = False
|
||||||
|
) -> Texture | None:
|
||||||
if graphic == Graphic.EMPTY:
|
if graphic == Graphic.EMPTY:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if graphic == "Grass":
|
if graphic == "Grass":
|
||||||
return get_texture_by_identifier(BACKGROUND_TILE)
|
return get_texture_by_identifier(BACKGROUND_TILE)
|
||||||
|
|
||||||
|
if graphic == Graphic.BOSS_2_HEALTH and not hard_mode:
|
||||||
|
return get_texture_by_identifier(EASY_MODE_BOSS_2_HEALTH)
|
||||||
|
|
||||||
if graphic in IMAGE_GRAPHICS:
|
if graphic in IMAGE_GRAPHICS:
|
||||||
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
|
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from asyncio import Task
|
from asyncio import Task
|
||||||
from collections.abc import Buffer
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from kivy import Config
|
from kivy import Config
|
||||||
from kivy.core.audio import Sound, SoundLoader
|
from kivy.core.audio import Sound, SoundLoader
|
||||||
|
from typing_extensions import Buffer
|
||||||
|
|
||||||
from CommonClient import logger
|
from CommonClient import logger
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class SoundManager:
|
|||||||
|
|
||||||
def ensure_config(self) -> None:
|
def ensure_config(self) -> None:
|
||||||
Config.adddefaultsection("APQuest")
|
Config.adddefaultsection("APQuest")
|
||||||
Config.setdefault("APQuest", "volume", 50)
|
Config.setdefault("APQuest", "volume", 30)
|
||||||
self.set_volume_percentage(Config.getint("APQuest", "volume"))
|
self.set_volume_percentage(Config.getint("APQuest", "volume"))
|
||||||
|
|
||||||
async def sound_manager_loop(self) -> None:
|
async def sound_manager_loop(self) -> None:
|
||||||
@@ -149,6 +149,7 @@ class SoundManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if sound_name == audio_filename:
|
if sound_name == audio_filename:
|
||||||
|
sound.volume = self.volume_percentage / 100
|
||||||
sound.play()
|
sound.play()
|
||||||
self.update_background_music()
|
self.update_background_music()
|
||||||
higher_priority_sound_is_playing = True
|
higher_priority_sound_is_playing = True
|
||||||
@@ -213,6 +214,7 @@ class SoundManager:
|
|||||||
# It ends up feeling better if this just always continues playing quietly after being started.
|
# It ends up feeling better if this just always continues playing quietly after being started.
|
||||||
# Even "fading in at a random spot" is better than restarting the song after a jingle / math trap.
|
# Even "fading in at a random spot" is better than restarting the song after a jingle / math trap.
|
||||||
if self.game_started and song.state == "stop":
|
if self.game_started and song.state == "stop":
|
||||||
|
song.volume = self.current_background_music_volume * self.volume_percentage / 100
|
||||||
song.play()
|
song.play()
|
||||||
song.seek(0)
|
song.seek(0)
|
||||||
continue
|
continue
|
||||||
@@ -228,6 +230,7 @@ class SoundManager:
|
|||||||
|
|
||||||
if self.current_background_music_volume != 0:
|
if self.current_background_music_volume != 0:
|
||||||
if song.state == "stop":
|
if song.state == "stop":
|
||||||
|
song.volume = self.current_background_music_volume * self.volume_percentage / 100
|
||||||
song.play()
|
song.play()
|
||||||
song.seek(0)
|
song.seek(0)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||||
falls diese nicht mit deiner Version von Archipelago gebündelt ist.
|
falls diese nicht mit deiner Version von Archipelago gebündelt ist.
|
||||||
|
|
||||||
|
## Optionale Software
|
||||||
|
|
||||||
|
- [APQuest AP Tracker](https://github.com/palex00/ap-quest-tracker/releases/latest), zur Verwendung mit
|
||||||
|
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||||
|
|
||||||
## Wie man spielt
|
## Wie man spielt
|
||||||
|
|
||||||
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
|
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
|
||||||
@@ -41,3 +46,15 @@ Du solltest jetzt verbunden sein und kannst APQuest spielen.
|
|||||||
Der APQuest Client kann zwischen verschiedenen Slots wechseln, ohne neugestartet werden zu müssen,
|
Der APQuest Client kann zwischen verschiedenen Slots wechseln, ohne neugestartet werden zu müssen,
|
||||||
|
|
||||||
Klicke einfach den "Disconnect"-Knopf. Dann verbinde dich mit dem anderen Raum / Slot.
|
Klicke einfach den "Disconnect"-Knopf. Dann verbinde dich mit dem anderen Raum / Slot.
|
||||||
|
|
||||||
|
|
||||||
|
## Automatisches Tracken
|
||||||
|
|
||||||
|
AP Quest verfügt über einen voll funktionsfähigen, automatischen Tracker mit Karten der Spielwelt.
|
||||||
|
|
||||||
|
1. Lade [APQuest AP Tracker](https://github.com/palex00/ap-quest-tracker/releases/latest) und
|
||||||
|
[PopTracker](https://github.com/black-sliver/PopTracker/releases) herunter.
|
||||||
|
2. Lege das Tracker-Pack im Ordner „packs/“ deiner PopTracker-Installation ab.
|
||||||
|
3. Öffne PopTracker und lade das APQuest-Pack.
|
||||||
|
4. Für das automatische Tracking klick oben auf das „AP“-Symbol.
|
||||||
|
5. Gib die Serveradresse von Archipelago (die, mit der du deinen Client verbunden hast), den Slot-Namen und das Passwort ein.
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||||
if not bundled with your version of Archipelago
|
if not bundled with your version of Archipelago
|
||||||
|
|
||||||
|
## Optional Software
|
||||||
|
|
||||||
|
- [APQuest AP Tracker](https://github.com/palex00/ap-quest-tracker/releases/latest), for use with
|
||||||
|
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
|
||||||
|
|
||||||
## How to play
|
## How to play
|
||||||
|
|
||||||
First, you need a room to connect to. For this, you or someone you know has to generate a game.
|
First, you need a room to connect to. For this, you or someone you know has to generate a game.
|
||||||
@@ -40,3 +45,14 @@ You should now be connected and able to play APQuest.
|
|||||||
The APQuest Client can seamlessly switch rooms without restarting.
|
The APQuest Client can seamlessly switch rooms without restarting.
|
||||||
|
|
||||||
Simply click the "Disconnect" button, then connect to a different slot/room.
|
Simply click the "Disconnect" button, then connect to a different slot/room.
|
||||||
|
|
||||||
|
## Auto-Tracking
|
||||||
|
|
||||||
|
AP Quest has a fully functional map tracker that supports auto-tracking.
|
||||||
|
|
||||||
|
1. Download [APQuest AP Tracker](https://github.com/palex00/ap-quest-tracker/releases/latest) and
|
||||||
|
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
|
||||||
|
2. Put the tracker pack into packs/ in your PopTracker install.
|
||||||
|
3. Open PopTracker, and load the APQuest pack.
|
||||||
|
4. For autotracking, click on the "AP" symbol at the top.
|
||||||
|
5. Enter the Archipelago server address (the one you connected your client to), slot name, and password.
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ class Entity:
|
|||||||
|
|
||||||
|
|
||||||
class InteractableMixin:
|
class InteractableMixin:
|
||||||
|
auto_move_attempt_passing_through = False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -89,15 +91,16 @@ class Chest(Entity, InteractableMixin, LocationMixin):
|
|||||||
self.is_open = True
|
self.is_open = True
|
||||||
self.update_solidity()
|
self.update_solidity()
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
if self.has_given_content:
|
if self.has_given_content:
|
||||||
return
|
return False
|
||||||
|
|
||||||
if self.is_open:
|
if self.is_open:
|
||||||
self.give_content(player)
|
self.give_content(player)
|
||||||
return
|
return True
|
||||||
|
|
||||||
self.open()
|
self.open()
|
||||||
|
return True
|
||||||
|
|
||||||
def content_success(self) -> None:
|
def content_success(self) -> None:
|
||||||
self.update_solidity()
|
self.update_solidity()
|
||||||
@@ -135,47 +138,59 @@ class Door(Entity):
|
|||||||
|
|
||||||
|
|
||||||
class KeyDoor(Door, InteractableMixin):
|
class KeyDoor(Door, InteractableMixin):
|
||||||
|
auto_move_attempt_passing_through = True
|
||||||
|
|
||||||
closed_graphic = Graphic.KEY_DOOR
|
closed_graphic = Graphic.KEY_DOOR
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
if self.is_open:
|
if self.is_open:
|
||||||
return
|
return False
|
||||||
|
|
||||||
if not player.has_item(Item.KEY):
|
if not player.has_item(Item.KEY):
|
||||||
return
|
return False
|
||||||
|
|
||||||
player.remove_item(Item.KEY)
|
player.remove_item(Item.KEY)
|
||||||
|
|
||||||
self.open()
|
self.open()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class BreakableBlock(Door, InteractableMixin):
|
class BreakableBlock(Door, InteractableMixin):
|
||||||
|
auto_move_attempt_passing_through = True
|
||||||
|
|
||||||
closed_graphic = Graphic.BREAKABLE_BLOCK
|
closed_graphic = Graphic.BREAKABLE_BLOCK
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
if self.is_open:
|
if self.is_open:
|
||||||
return
|
return False
|
||||||
|
|
||||||
if not player.has_item(Item.HAMMER):
|
if not player.has_item(Item.HAMMER):
|
||||||
return
|
return False
|
||||||
|
|
||||||
player.remove_item(Item.HAMMER)
|
player.remove_item(Item.HAMMER)
|
||||||
|
|
||||||
self.open()
|
self.open()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Bush(Door, InteractableMixin):
|
class Bush(Door, InteractableMixin):
|
||||||
|
auto_move_attempt_passing_through = True
|
||||||
|
|
||||||
closed_graphic = Graphic.BUSH
|
closed_graphic = Graphic.BUSH
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
if self.is_open:
|
if self.is_open:
|
||||||
return
|
return False
|
||||||
|
|
||||||
if not player.has_item(Item.SWORD):
|
if not player.has_item(Item.SWORD):
|
||||||
return
|
return False
|
||||||
|
|
||||||
self.open()
|
self.open()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Button(Entity, InteractableMixin):
|
class Button(Entity, InteractableMixin):
|
||||||
solid = True
|
solid = True
|
||||||
@@ -186,12 +201,13 @@ class Button(Entity, InteractableMixin):
|
|||||||
def __init__(self, activates: ActivatableMixin) -> None:
|
def __init__(self, activates: ActivatableMixin) -> None:
|
||||||
self.activates = activates
|
self.activates = activates
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
if self.activated:
|
if self.activated:
|
||||||
return
|
return False
|
||||||
|
|
||||||
self.activated = True
|
self.activated = True
|
||||||
self.activates.activate(player)
|
self.activates.activate(player)
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def graphic(self) -> Graphic:
|
def graphic(self) -> Graphic:
|
||||||
@@ -240,9 +256,9 @@ class Enemy(Entity, InteractableMixin):
|
|||||||
return
|
return
|
||||||
self.current_health = self.max_health
|
self.current_health = self.max_health
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
if self.dead:
|
if self.dead:
|
||||||
return
|
return False
|
||||||
|
|
||||||
if player.has_item(Item.SWORD):
|
if player.has_item(Item.SWORD):
|
||||||
self.current_health = max(0, self.current_health - 1)
|
self.current_health = max(0, self.current_health - 1)
|
||||||
@@ -250,9 +266,10 @@ class Enemy(Entity, InteractableMixin):
|
|||||||
if self.current_health == 0:
|
if self.current_health == 0:
|
||||||
if not self.dead:
|
if not self.dead:
|
||||||
self.die()
|
self.die()
|
||||||
return
|
return True
|
||||||
|
|
||||||
player.damage(2)
|
player.damage(2)
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def graphic(self) -> Graphic:
|
def graphic(self) -> Graphic:
|
||||||
@@ -270,13 +287,15 @@ class EnemyWithLoot(Enemy, LocationMixin):
|
|||||||
self.dead = True
|
self.dead = True
|
||||||
self.solid = not self.has_given_content
|
self.solid = not self.has_given_content
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
if self.dead:
|
if self.dead:
|
||||||
if not self.has_given_content:
|
if not self.has_given_content:
|
||||||
self.give_content(player)
|
self.give_content(player)
|
||||||
return
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
super().interact(player)
|
super().interact(player)
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def graphic(self) -> Graphic:
|
def graphic(self) -> Graphic:
|
||||||
@@ -303,10 +322,12 @@ class FinalBoss(Enemy):
|
|||||||
}
|
}
|
||||||
enemy_default_graphic = Graphic.BOSS_1_HEALTH
|
enemy_default_graphic = Graphic.BOSS_1_HEALTH
|
||||||
|
|
||||||
def interact(self, player: Player) -> None:
|
def interact(self, player: Player) -> bool:
|
||||||
dead_before = self.dead
|
dead_before = self.dead
|
||||||
|
|
||||||
super().interact(player)
|
changed = super().interact(player)
|
||||||
|
|
||||||
if not dead_before and self.dead:
|
if not dead_before and self.dead:
|
||||||
player.victory()
|
player.victory()
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class Game:
|
|||||||
active_math_problem: MathProblem | None
|
active_math_problem: MathProblem | None
|
||||||
active_math_problem_input: list[int] | None
|
active_math_problem_input: list[int] | None
|
||||||
|
|
||||||
|
auto_target_path: list[tuple[int, int]] = []
|
||||||
|
|
||||||
remotely_received_items: set[tuple[int, int, int]]
|
remotely_received_items: set[tuple[int, int, int]]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -32,6 +34,7 @@ class Game:
|
|||||||
self.gameboard = create_gameboard(hard_mode, hammer_exists, extra_chest)
|
self.gameboard = create_gameboard(hard_mode, hammer_exists, extra_chest)
|
||||||
self.player = Player(self.gameboard, self.queued_events.append)
|
self.player = Player(self.gameboard, self.queued_events.append)
|
||||||
self.active_math_problem = None
|
self.active_math_problem = None
|
||||||
|
self.active_math_problem_input = None
|
||||||
self.remotely_received_items = set()
|
self.remotely_received_items = set()
|
||||||
|
|
||||||
if random_object is None:
|
if random_object is None:
|
||||||
@@ -94,29 +97,40 @@ class Game:
|
|||||||
|
|
||||||
return tuple(graphics_array)
|
return tuple(graphics_array)
|
||||||
|
|
||||||
def attempt_player_movement(self, direction: Direction) -> None:
|
def attempt_player_movement(self, direction: Direction, cancel_auto_move: bool = True) -> bool:
|
||||||
|
if cancel_auto_move:
|
||||||
|
self.cancel_auto_move()
|
||||||
|
|
||||||
self.player.facing = direction
|
self.player.facing = direction
|
||||||
|
|
||||||
delta_x, delta_y = direction.value
|
delta_x, delta_y = direction.value
|
||||||
new_x, new_y = self.player.current_x + delta_x, self.player.current_y + delta_y
|
new_x, new_y = self.player.current_x + delta_x, self.player.current_y + delta_y
|
||||||
|
|
||||||
if not self.gameboard.get_entity_at(new_x, new_y).solid:
|
if self.gameboard.get_entity_at(new_x, new_y).solid:
|
||||||
self.player.current_x = new_x
|
return False
|
||||||
self.player.current_y = new_y
|
|
||||||
|
|
||||||
def attempt_interact(self) -> None:
|
self.player.current_x = new_x
|
||||||
|
self.player.current_y = new_y
|
||||||
|
return True
|
||||||
|
|
||||||
|
def attempt_interact(self) -> bool:
|
||||||
delta_x, delta_y = self.player.facing.value
|
delta_x, delta_y = self.player.facing.value
|
||||||
entity_x, entity_y = self.player.current_x + delta_x, self.player.current_y + delta_y
|
entity_x, entity_y = self.player.current_x + delta_x, self.player.current_y + delta_y
|
||||||
|
|
||||||
entity = self.gameboard.get_entity_at(entity_x, entity_y)
|
entity = self.gameboard.get_entity_at(entity_x, entity_y)
|
||||||
|
|
||||||
if isinstance(entity, InteractableMixin):
|
if isinstance(entity, InteractableMixin):
|
||||||
entity.interact(self.player)
|
return entity.interact(self.player)
|
||||||
|
|
||||||
def attempt_fire_confetti_cannon(self) -> None:
|
return False
|
||||||
if self.player.has_item(Item.CONFETTI_CANNON):
|
|
||||||
self.player.remove_item(Item.CONFETTI_CANNON)
|
def attempt_fire_confetti_cannon(self) -> bool:
|
||||||
self.queued_events.append(ConfettiFired(self.player.current_x, self.player.current_y))
|
if not self.player.has_item(Item.CONFETTI_CANNON):
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.player.remove_item(Item.CONFETTI_CANNON)
|
||||||
|
self.queued_events.append(ConfettiFired(self.player.current_x, self.player.current_y))
|
||||||
|
return True
|
||||||
|
|
||||||
def math_problem_success(self) -> None:
|
def math_problem_success(self) -> None:
|
||||||
self.active_math_problem = None
|
self.active_math_problem = None
|
||||||
@@ -154,6 +168,12 @@ class Game:
|
|||||||
self.active_math_problem_input.pop()
|
self.active_math_problem_input.pop()
|
||||||
self.check_math_problem_result()
|
self.check_math_problem_result()
|
||||||
|
|
||||||
|
def math_problem_replace(self, input: list[int]) -> None:
|
||||||
|
if self.active_math_problem_input is None:
|
||||||
|
return
|
||||||
|
self.active_math_problem_input = input[:2]
|
||||||
|
self.check_math_problem_result()
|
||||||
|
|
||||||
def input(self, input_key: Input) -> None:
|
def input(self, input_key: Input) -> None:
|
||||||
if not self.gameboard.ready:
|
if not self.gameboard.ready:
|
||||||
return
|
return
|
||||||
@@ -201,3 +221,47 @@ class Game:
|
|||||||
def force_clear_location(self, location_id: int) -> None:
|
def force_clear_location(self, location_id: int) -> None:
|
||||||
location = Location(location_id)
|
location = Location(location_id)
|
||||||
self.gameboard.force_clear_location(location)
|
self.gameboard.force_clear_location(location)
|
||||||
|
|
||||||
|
def cancel_auto_move(self) -> None:
|
||||||
|
self.auto_target_path = []
|
||||||
|
|
||||||
|
def queue_auto_move(self, target_x: int, target_y: int) -> None:
|
||||||
|
self.cancel_auto_move()
|
||||||
|
path = self.gameboard.calculate_shortest_path(self.player.current_x, self.player.current_y, target_x, target_y)
|
||||||
|
self.auto_target_path = path
|
||||||
|
|
||||||
|
def do_auto_move(self) -> bool:
|
||||||
|
if not self.auto_target_path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
target_x, target_y = self.auto_target_path.pop(0)
|
||||||
|
movement = target_x - self.player.current_x, target_y - self.player.current_y
|
||||||
|
direction = Direction(movement)
|
||||||
|
moved = self.attempt_player_movement(direction, cancel_auto_move=False)
|
||||||
|
|
||||||
|
if moved:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# We are attempting to interact with something on the path.
|
||||||
|
# First, make the player face it.
|
||||||
|
if self.player.facing != direction:
|
||||||
|
self.player.facing = direction
|
||||||
|
self.auto_target_path.insert(0, (target_x, target_y))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If we are facing it, attempt to interact with it.
|
||||||
|
changed = self.attempt_interact()
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
self.cancel_auto_move()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If the interaction was successful, and this was the end of the path, stop
|
||||||
|
# (i.e. don't try to attack the attacked enemy over and over until it's dead)
|
||||||
|
if not self.auto_target_path:
|
||||||
|
self.cancel_auto_move()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If there is more to go, keep going along the path
|
||||||
|
self.auto_target_path.insert(0, (target_x, target_y))
|
||||||
|
return True
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .entities import (
|
|||||||
EnemyWithLoot,
|
EnemyWithLoot,
|
||||||
Entity,
|
Entity,
|
||||||
FinalBoss,
|
FinalBoss,
|
||||||
|
InteractableMixin,
|
||||||
KeyDoor,
|
KeyDoor,
|
||||||
LocationMixin,
|
LocationMixin,
|
||||||
Wall,
|
Wall,
|
||||||
@@ -23,6 +24,7 @@ from .generate_math_problem import MathProblem
|
|||||||
from .graphics import DIGIT_TO_GRAPHIC, DIGIT_TO_GRAPHIC_ZERO_EMPTY, MATH_PROBLEM_TYPE_TO_GRAPHIC, Graphic
|
from .graphics import DIGIT_TO_GRAPHIC, DIGIT_TO_GRAPHIC_ZERO_EMPTY, MATH_PROBLEM_TYPE_TO_GRAPHIC, Graphic
|
||||||
from .items import Item
|
from .items import Item
|
||||||
from .locations import DEFAULT_CONTENT, Location
|
from .locations import DEFAULT_CONTENT, Location
|
||||||
|
from .path_finding import find_path_or_closest
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .player import Player
|
from .player import Player
|
||||||
@@ -107,6 +109,21 @@ class Gameboard:
|
|||||||
|
|
||||||
return tuple(graphics)
|
return tuple(graphics)
|
||||||
|
|
||||||
|
def as_traversability_bools(self) -> tuple[tuple[bool, ...], ...]:
|
||||||
|
traversability = []
|
||||||
|
|
||||||
|
for y, row in enumerate(self.gameboard):
|
||||||
|
traversable_row = []
|
||||||
|
for x, entity in enumerate(row):
|
||||||
|
traversable_row.append(
|
||||||
|
not entity.solid
|
||||||
|
or (isinstance(entity, InteractableMixin) and entity.auto_move_attempt_passing_through)
|
||||||
|
)
|
||||||
|
|
||||||
|
traversability.append(tuple(traversable_row))
|
||||||
|
|
||||||
|
return tuple(traversability)
|
||||||
|
|
||||||
def render_math_problem(
|
def render_math_problem(
|
||||||
self, problem: MathProblem, current_input_digits: list[int], current_input_int: int | None
|
self, problem: MathProblem, current_input_digits: list[int], current_input_int: int | None
|
||||||
) -> tuple[tuple[Graphic, ...], ...]:
|
) -> tuple[tuple[Graphic, ...], ...]:
|
||||||
@@ -186,6 +203,23 @@ class Gameboard:
|
|||||||
entity = self.remote_entity_by_location_id[location]
|
entity = self.remote_entity_by_location_id[location]
|
||||||
entity.force_clear()
|
entity.force_clear()
|
||||||
|
|
||||||
|
def calculate_shortest_path(
|
||||||
|
self, source_x: int, source_y: int, target_x: int, target_y: int
|
||||||
|
) -> list[tuple[int, int]]:
|
||||||
|
gameboard_traversability = self.as_traversability_bools()
|
||||||
|
|
||||||
|
path = find_path_or_closest(gameboard_traversability, source_x, source_y, target_x, target_y)
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
return path
|
||||||
|
|
||||||
|
# If the path stops just short of target, attempt interacting with it at the end
|
||||||
|
if abs(path[-1][0] - target_x) + abs(path[-1][1] - target_y) == 1:
|
||||||
|
if isinstance(self.gameboard[target_y][target_x], InteractableMixin):
|
||||||
|
path.append((target_x, target_y))
|
||||||
|
|
||||||
|
return path[1:] # Cut off starting tile
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
return self.content_filled
|
return self.content_filled
|
||||||
@@ -212,7 +246,7 @@ def create_gameboard(hard_mode: bool, hammer_exists: bool, extra_chest: bool) ->
|
|||||||
breakable_block = BreakableBlock() if hammer_exists else Empty()
|
breakable_block = BreakableBlock() if hammer_exists else Empty()
|
||||||
|
|
||||||
normal_enemy = EnemyWithLoot(2 if hard_mode else 1, Location.ENEMY_DROP)
|
normal_enemy = EnemyWithLoot(2 if hard_mode else 1, Location.ENEMY_DROP)
|
||||||
boss = FinalBoss(5 if hard_mode else 3)
|
boss = FinalBoss(5 if hard_mode else 2)
|
||||||
|
|
||||||
gameboard = (
|
gameboard = (
|
||||||
(Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty()),
|
(Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty()),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import NamedTuple
|
|||||||
|
|
||||||
_random = random.Random()
|
_random = random.Random()
|
||||||
|
|
||||||
|
|
||||||
class NumberChoiceConstraints(NamedTuple):
|
class NumberChoiceConstraints(NamedTuple):
|
||||||
num_1_min: int
|
num_1_min: int
|
||||||
num_1_max: int
|
num_1_max: int
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 754 B |
84
worlds/apquest/game/path_finding.py
Normal file
84
worlds/apquest/game/path_finding.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import heapq
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
Point = tuple[int, int]
|
||||||
|
|
||||||
|
|
||||||
|
def heuristic(a: Point, b: Point) -> int:
|
||||||
|
# Manhattan distance (good for 4-directional grids)
|
||||||
|
return abs(a[0] - b[0]) + abs(a[1] - b[1])
|
||||||
|
|
||||||
|
|
||||||
|
def reconstruct_path(came_from: dict[Point, Point], current: Point) -> list[Point]:
|
||||||
|
path = [current]
|
||||||
|
while current in came_from:
|
||||||
|
current = came_from[current]
|
||||||
|
path.append(current)
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def find_path_or_closest(
|
||||||
|
grid: tuple[tuple[bool, ...], ...], source_x: int, source_y: int, target_x: int, target_y: int
|
||||||
|
) -> list[Point]:
|
||||||
|
start = source_x, source_y
|
||||||
|
goal = target_x, target_y
|
||||||
|
|
||||||
|
rows, cols = len(grid), len(grid[0])
|
||||||
|
|
||||||
|
def in_bounds(p: Point) -> bool:
|
||||||
|
return 0 <= p[0] < rows and 0 <= p[1] < cols
|
||||||
|
|
||||||
|
def passable(p: Point) -> bool:
|
||||||
|
return grid[p[1]][p[0]]
|
||||||
|
|
||||||
|
def neighbors(p: Point) -> Generator[Point, None, None]:
|
||||||
|
x, y = p
|
||||||
|
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||||
|
np = (x + dx, y + dy)
|
||||||
|
if in_bounds(np) and passable(np):
|
||||||
|
yield np
|
||||||
|
|
||||||
|
open_heap: list[tuple[int, tuple[int, int]]] = []
|
||||||
|
heapq.heappush(open_heap, (0, start))
|
||||||
|
|
||||||
|
came_from: dict[Point, Point] = {}
|
||||||
|
g_score = {start: 0}
|
||||||
|
|
||||||
|
# Track best fallback node
|
||||||
|
best_node = start
|
||||||
|
best_dist = heuristic(start, goal)
|
||||||
|
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
while open_heap:
|
||||||
|
_, current = heapq.heappop(open_heap)
|
||||||
|
|
||||||
|
if current in visited:
|
||||||
|
continue
|
||||||
|
visited.add(current)
|
||||||
|
|
||||||
|
# Check if we reached the goal
|
||||||
|
if current == goal:
|
||||||
|
return reconstruct_path(came_from, current)
|
||||||
|
|
||||||
|
# Update "closest node" fallback
|
||||||
|
dist = heuristic(current, goal)
|
||||||
|
if dist < best_dist or (dist == best_dist and g_score[current] < g_score.get(best_node, float("inf"))):
|
||||||
|
best_node = current
|
||||||
|
best_dist = dist
|
||||||
|
|
||||||
|
for neighbor in neighbors(current):
|
||||||
|
tentative_g = g_score[current] + 1 # cost is 1 per move
|
||||||
|
|
||||||
|
if tentative_g < g_score.get(neighbor, float("inf")):
|
||||||
|
came_from[neighbor] = current
|
||||||
|
g_score[neighbor] = tentative_g
|
||||||
|
f_score = tentative_g + heuristic(neighbor, goal)
|
||||||
|
heapq.heappush(open_heap, (f_score, neighbor))
|
||||||
|
|
||||||
|
# Goal not reachable → return path to closest node
|
||||||
|
if best_node is not None:
|
||||||
|
return reconstruct_path(came_from, best_node)
|
||||||
|
|
||||||
|
return []
|
||||||
@@ -2,12 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from BaseClasses import CollectionState
|
from rule_builder.options import OptionFilter
|
||||||
from worlds.generic.Rules import add_rule, set_rule
|
from rule_builder.rules import Has, HasAll, Rule
|
||||||
|
|
||||||
|
from .options import HardMode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .world import APQuestWorld
|
from .world import APQuestWorld
|
||||||
|
|
||||||
|
HAS_KEY = Has("Key") # Hmm, what could this be? A little foreshadowing perhaps? :) You'll find out if you keep reading!
|
||||||
|
|
||||||
|
|
||||||
def set_all_rules(world: APQuestWorld) -> None:
|
def set_all_rules(world: APQuestWorld) -> None:
|
||||||
# In order for AP to generate an item layout that is actually possible for the player to complete,
|
# In order for AP to generate an item layout that is actually possible for the player to complete,
|
||||||
@@ -26,36 +30,46 @@ def set_all_entrance_rules(world: APQuestWorld) -> None:
|
|||||||
overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
|
overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
|
||||||
right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
|
right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
|
||||||
|
|
||||||
# An access rule is a function. We can define this function like any other function.
|
# Now, let's make some rules!
|
||||||
# This function must accept exactly one parameter: A "CollectionState".
|
# First, let's handle the transition from the overworld to the bottom right room,
|
||||||
# A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have,
|
# which requires slashing a bush with the Sword.
|
||||||
# which regions they've reached, etc.
|
# For this, we need a rule that says "player has a Sword".
|
||||||
# In an access rule, we can ask whether the player has a collected a certain item.
|
# We can use a "Has"-type rule from the rule_builder module for this.
|
||||||
# We can do this via the state.has(...) function.
|
can_destroy_bush = Has("Sword")
|
||||||
# This function takes an item name, a player number, and an optional count parameter (more on that below)
|
|
||||||
# Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call,
|
|
||||||
# our function needs to be locally defined so that it has access to the player number from the outer scope.
|
|
||||||
# In our case, we are inside a function that has access to the "world" parameter, so we can use world.player.
|
|
||||||
def can_destroy_bush(state: CollectionState) -> bool:
|
|
||||||
return state.has("Sword", world.player)
|
|
||||||
|
|
||||||
# Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path.
|
# Now we can set our "can_destroy_bush" rule to the entrance which requires slashing a bush to clear the path.
|
||||||
# One way to set rules is via the set_rule() function, which works on both Entrances and Locations.
|
# The easiest way to do this is by calling world.set_rule, which works for both Locations and Entrances.
|
||||||
set_rule(overworld_to_bottom_right_room, can_destroy_bush)
|
world.set_rule(overworld_to_bottom_right_room, can_destroy_bush)
|
||||||
|
|
||||||
# Because the function has to be defined locally, most worlds prefer the lambda syntax.
|
# Conditions can also depend on event items.
|
||||||
set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
|
button_pressed = Has("Top Left Room Button Pressed")
|
||||||
|
world.set_rule(right_room_to_final_boss_room, button_pressed)
|
||||||
# Conditions can depend on event items.
|
|
||||||
set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player))
|
|
||||||
|
|
||||||
# Some entrance rules may only apply if the player enabled certain options.
|
# Some entrance rules may only apply if the player enabled certain options.
|
||||||
# In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
|
# In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
|
||||||
# Overworld to the Top Middle Room.
|
# Overworld to the Top Middle Room.
|
||||||
if world.options.hammer:
|
if world.options.hammer:
|
||||||
overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
|
overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
|
||||||
set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player))
|
can_smash_brick = Has("Hammer")
|
||||||
|
world.set_rule(overworld_to_top_middle_room, can_smash_brick)
|
||||||
|
|
||||||
|
# So far, we've been using "Has" from the Rule Builder to make our rules.
|
||||||
|
# There is another way to make rules that you will see in a lot of older worlds.
|
||||||
|
# A rule can just be a function that takes a "state" argument and returns a bool.
|
||||||
|
# As a demonstration of what that looks like, let's do it with our final Entrance rule:
|
||||||
|
world.set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
|
||||||
|
# This style is not really recommended anymore, though.
|
||||||
|
# Notice how you have to explicitly capture world.player here so that the rule applies to the correct player?
|
||||||
|
# Well, Rule Builder does this part for you, inside of world.set_rule.
|
||||||
|
# This doesn't just result in shorter code, it also means you can define rules statically (at the module level).
|
||||||
|
# APQuest opts to create its Rule objects locally, but just to show what this would look like,
|
||||||
|
# we'll re-set the "Overworld to Top Left Room" rule to a constant defined at the top of this file:
|
||||||
|
world.set_rule(overworld_to_top_left_room, HAS_KEY)
|
||||||
|
|
||||||
|
# Beyond these structural advantages,
|
||||||
|
# Rule Builder also allows the core AP code to do a lot of under-the-hood optimizations.
|
||||||
|
# Rule Builder is quite comprehensive, and even if you have really esoteric rules,
|
||||||
|
# you can make custom rules by subclassing CustomRule.
|
||||||
|
|
||||||
def set_all_location_rules(world: APQuestWorld) -> None:
|
def set_all_location_rules(world: APQuestWorld) -> None:
|
||||||
# Location rules work no differently from Entrance rules.
|
# Location rules work no differently from Entrance rules.
|
||||||
@@ -67,65 +81,72 @@ def set_all_location_rules(world: APQuestWorld) -> None:
|
|||||||
# So, we need to set requirements on the Locations themselves.
|
# So, we need to set requirements on the Locations themselves.
|
||||||
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
|
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
|
||||||
|
|
||||||
# Sometimes, you may want to have different rules depending on the player's chosen options.
|
# In "set_all_entrance_rules", we had a rule for a location that doesn't always exist.
|
||||||
# There is a wrong way to do this, and a right way to do this. Let's do the wrong way first.
|
# In this case, we had to check for its existence (by checking the player's chosen options) before setting the rule.
|
||||||
|
# Other times, you may have a situation where a location can have two different rules depending on the options.
|
||||||
|
# In our case, the enemy in the right room has more health if hard mode is selected,
|
||||||
|
# so ontop of the Sword, the player will either need one more health or a Shield in hard mode.
|
||||||
|
# First, let's make our sword condition.
|
||||||
|
can_defeat_basic_enemy: Rule = Has("Sword")
|
||||||
|
|
||||||
|
# Next, we'll check whether hard mode has been chosen in the player options.
|
||||||
|
if world.options.hard_mode:
|
||||||
|
# We'll make the condition for "Has a Shield or a Health Upgrade".
|
||||||
|
# We can chain two "Has" conditions together with the | operator to make "Has Shield or has Health Upgrade".
|
||||||
|
can_withstand_a_hit = Has("Shield") | Has("Health Upgrade")
|
||||||
|
|
||||||
|
# Now, we chain this rule to our Sword rule.
|
||||||
|
# Since we want both conditions to be true, in this case, we have to chain them in an "and" way.
|
||||||
|
# For this, we can use the & operator.
|
||||||
|
can_defeat_basic_enemy = can_defeat_basic_enemy & can_withstand_a_hit
|
||||||
|
|
||||||
|
# Finally, we set our rule onto the Right Room Eney Drop location.
|
||||||
right_room_enemy = world.get_location("Right Room Enemy Drop")
|
right_room_enemy = world.get_location("Right Room Enemy Drop")
|
||||||
|
world.set_rule(right_room_enemy, can_defeat_basic_enemy)
|
||||||
|
|
||||||
# DON'T DO THIS!!!!
|
# For the final boss, we also need to chain multiple conditions.
|
||||||
set_rule(
|
# First of all, you always need a Sword and a Shield.
|
||||||
right_room_enemy,
|
# So far, we used the | and & operators to chain "Has" rules.
|
||||||
lambda state: (
|
# Instead, we can also use HasAny for an or-chain of items, or HasAll for an and-chain of items.
|
||||||
state.has("Sword", world.player)
|
has_sword_and_shield: Rule = HasAll("Sword", "Shield")
|
||||||
and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player))
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# DON'T DO THIS!!!!
|
|
||||||
|
|
||||||
# Now, what's actually wrong with this? It works perfectly fine, right?
|
# In hard mode, the player also needs both Health Upgrades to survive long enough to defeat the boss.
|
||||||
# If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade.
|
# For this, we can use the optional "count" parameter for "Has".
|
||||||
# The access rule we just wrote does this correctly, so what's the problem?
|
has_both_health_upgrades = Has("Health Upgrade", count=2)
|
||||||
# The problem is performance.
|
|
||||||
# Most of your world code doesn't need to be perfectly performant, since it just runs once per slot.
|
|
||||||
# However, access rules in particular are by far the hottest code path in Archipelago.
|
|
||||||
# An access rule will potentially be called thousands or even millions of times over the course of one generation.
|
|
||||||
# As a result, access rules are the one place where it's really worth putting in some effort to optimize.
|
|
||||||
# What's the performance problem here?
|
|
||||||
# Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False.
|
|
||||||
# Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with?
|
|
||||||
# Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well?
|
|
||||||
# Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call,
|
|
||||||
# and instead having two *different* set_rule calls depending on which case we're in.
|
|
||||||
|
|
||||||
if world.options.hard_mode:
|
# Previously, we used an "if world.options.hard_mode" condition to check if we should apply the extra requirement.
|
||||||
# If you have multiple conditions, you can obviously chain them via "or" or "and".
|
# However, if you're comfortable with boolean logic, there is another way.
|
||||||
# However, there are also the nice helper functions "state.has_any" and "state.has_all".
|
# OptionFilter is a rule component which isn't a "Rule" on its own, but when used in a boolean expression with
|
||||||
set_rule(
|
# rules, it acts like True if the option has the specified value, and acts like False otherwise.
|
||||||
right_room_enemy,
|
hard_mode_is_off = OptionFilter(HardMode, False)
|
||||||
lambda state: (
|
|
||||||
state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
set_rule(right_room_enemy, lambda state: state.has("Sword", world.player))
|
|
||||||
|
|
||||||
# Another way to chain multiple conditions is via the add_rule function.
|
# So with this option-checking rule component in hand, we can write our boss condition like this:
|
||||||
# This makes the access rules a bit slower though, so it should only be used if your structure justifies it.
|
can_defeat_final_boss = has_sword_and_shield & (hard_mode_is_off | has_both_health_upgrades)
|
||||||
# In our case, it's pretty useful because hard mode and easy mode have different requirements.
|
# If you're not as comfortable with boolean logic, it might be somewhat confusing why this is correct.
|
||||||
|
# There is nothing wrong with using "if" conditions to check for options, if you find that easier to understand.
|
||||||
|
|
||||||
|
# Finally, we apply the rule to our "Final Boss Defeated" event location.
|
||||||
final_boss = world.get_location("Final Boss Defeated")
|
final_boss = world.get_location("Final Boss Defeated")
|
||||||
|
world.set_rule(final_boss, can_defeat_final_boss)
|
||||||
# For the "known" requirements, it's still better to chain them using a normal "and" condition.
|
|
||||||
add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player))
|
|
||||||
|
|
||||||
if world.options.hard_mode:
|
|
||||||
# You can check for multiple copies of an item by using the optional count parameter of state.has().
|
|
||||||
add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2))
|
|
||||||
|
|
||||||
|
|
||||||
def set_completion_condition(world: APQuestWorld) -> None:
|
def set_completion_condition(world: APQuestWorld) -> None:
|
||||||
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
|
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
|
||||||
|
# For this, we can use world.set_completion_rule.
|
||||||
# You can just set a completion condition directly like any other condition, referencing items the player receives:
|
# You can just set a completion condition directly like any other condition, referencing items the player receives:
|
||||||
world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Sword", "Shield"), world.player)
|
world.set_completion_rule(HasAll("Sword", "Shield"))
|
||||||
|
|
||||||
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
|
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
|
||||||
# So lets undo what we just did, and instead set the completion condition to:
|
# So lets undo what we just did, and instead set the completion condition to:
|
||||||
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
|
world.set_completion_rule(Has("Victory"))
|
||||||
|
|
||||||
|
|
||||||
|
# One final comment about rules:
|
||||||
|
# If your world exclusively uses Rule Builder rules (like APQuest), it's worth trying CachedRuleBuilderWorld.
|
||||||
|
# CachedRuleBuilderWorld is a subclass of World that has a bunch of caching magic to make rules faster.
|
||||||
|
# Just have your world class subclass CachedRuleBuilderWorld instead of World:
|
||||||
|
# class APQuestWorld(CachedRuleBuilderWorld): ...
|
||||||
|
# This may speed up your world, or it may make it slower.
|
||||||
|
# The exact factors are complex and not well understood, but there is no harm in trying it.
|
||||||
|
# Generate a few seeds and see if there is a noticeable difference!
|
||||||
|
# If you're wondering, author has checked: APQuest is too simple to see any benefits, so we'll stick with "World".
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
from typing import Dict
|
|
||||||
|
|
||||||
from BaseClasses import Tutorial
|
|
||||||
from ..AutoWorld import WebWorld, World
|
|
||||||
|
|
||||||
class AP_SudokuWebWorld(WebWorld):
|
|
||||||
options_page = False
|
|
||||||
theme = 'partyTime'
|
|
||||||
|
|
||||||
setup_en = Tutorial(
|
|
||||||
tutorial_name='Setup Guide',
|
|
||||||
description='A guide to playing APSudoku',
|
|
||||||
language='English',
|
|
||||||
file_name='setup_en.md',
|
|
||||||
link='setup/en',
|
|
||||||
authors=['EmilyV']
|
|
||||||
)
|
|
||||||
|
|
||||||
tutorials = [setup_en]
|
|
||||||
|
|
||||||
class AP_SudokuWorld(World):
|
|
||||||
"""
|
|
||||||
Play a little Sudoku while you're in BK mode to maybe get some useful hints
|
|
||||||
"""
|
|
||||||
game = "Sudoku"
|
|
||||||
web = AP_SudokuWebWorld()
|
|
||||||
|
|
||||||
item_name_to_id: Dict[str, int] = {}
|
|
||||||
location_name_to_id: Dict[str, int] = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def stage_assert_generate(cls, multiworld):
|
|
||||||
raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world")
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# APSudoku
|
|
||||||
|
|
||||||
## Hint Games
|
|
||||||
|
|
||||||
HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot.
|
|
||||||
|
|
||||||
## What is this game?
|
|
||||||
|
|
||||||
Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random.
|
|
||||||
|
|
||||||
## Where is the options page?
|
|
||||||
|
|
||||||
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
|
||||||
|
|
||||||
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. This allows disabling hints entirely, as well as altering the hint odds for each difficulty.
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# APSudoku Setup Guide
|
|
||||||
|
|
||||||
## Required Software
|
|
||||||
- [APSudoku](https://github.com/APSudoku/APSudoku)
|
|
||||||
|
|
||||||
## General Concept
|
|
||||||
|
|
||||||
This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations.
|
|
||||||
|
|
||||||
Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files.
|
|
||||||
|
|
||||||
## Installation Procedures
|
|
||||||
|
|
||||||
### Windows / Linux
|
|
||||||
Go to the latest release from the [github APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
|
||||||
|
|
||||||
### Web
|
|
||||||
Go to the [github pages](apsudoku.github.io) or [itch.io](https://emilyv99.itch.io/apsudoku) site, and play in the browser.
|
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
|
||||||
|
|
||||||
1. Run the APSudoku executable.
|
|
||||||
2. Under `Settings` → `Connection` at the top-right:
|
|
||||||
- Enter the server address and port number
|
|
||||||
- Enter the name of the slot you wish to connect to
|
|
||||||
- Enter the room password (optional)
|
|
||||||
- Select DeathLink related settings (optional)
|
|
||||||
- Press `Connect`
|
|
||||||
4. Under the `Sudoku` tab
|
|
||||||
- Choose puzzle difficulty
|
|
||||||
- Click `Start` to generate a puzzle
|
|
||||||
5. Try to solve the Sudoku. Click `Check` when done
|
|
||||||
- A correct solution rewards you with 1 hint for a location in the world you are connected to
|
|
||||||
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
|
|
||||||
|
|
||||||
Info:
|
|
||||||
- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
|
|
||||||
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
|
|
||||||
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
|
||||||
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
|
||||||
- Click the various `?` buttons for information on controls/how to play
|
|
||||||
|
|
||||||
## Admin Settings
|
|
||||||
|
|
||||||
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room.
|
|
||||||
|
|
||||||
- You can disable APSudoku for the entire room, preventing any hints from being granted.
|
|
||||||
- You can customize the reward weights for each difficulty, making progression hints more or less likely, and/or adding a chance to get "no hint" after a solve.
|
|
||||||
|
|
||||||
## DeathLink Support
|
|
||||||
|
|
||||||
If `DeathLink` is enabled when you click `Connect`:
|
|
||||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
|
|
||||||
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
|
||||||
- On receiving a DeathLink from another player, your puzzle resets.
|
|
||||||
@@ -271,7 +271,7 @@ item_table = {
|
|||||||
ItemNames.TRIDENT: ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
|
ItemNames.TRIDENT: ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
|
||||||
ItemNames.TURTLE_EGG: ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
|
ItemNames.TURTLE_EGG: ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
|
||||||
ItemNames.JELLY_EGG: ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
|
ItemNames.JELLY_EGG: ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
|
||||||
ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.PROGRESSION, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
||||||
ItemNames.BABY_WALKER: ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
|
ItemNames.BABY_WALKER: ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
|
||||||
ItemNames.VEDHA_S_CURE_ALL: ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
|
ItemNames.VEDHA_S_CURE_ALL: ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
|
||||||
ItemNames.ZUUNA_S_PEROGI: ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
|
ItemNames.ZUUNA_S_PEROGI: ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
|
||||||
@@ -384,8 +384,8 @@ four_gods_excludes = [ItemNames.ANEMONE, ItemNames.ARNASSI_STATUE, ItemNames.BIG
|
|||||||
ItemNames.MITHALAS_BANNER, ItemNames.MITHALAS_POT, ItemNames.MUTANT_COSTUME, ItemNames.SEED_BAG,
|
ItemNames.MITHALAS_BANNER, ItemNames.MITHALAS_POT, ItemNames.MUTANT_COSTUME, ItemNames.SEED_BAG,
|
||||||
ItemNames.KING_S_SKULL, ItemNames.SONG_PLANT_SPORE, ItemNames.STONE_HEAD, ItemNames.SUN_KEY,
|
ItemNames.KING_S_SKULL, ItemNames.SONG_PLANT_SPORE, ItemNames.STONE_HEAD, ItemNames.SUN_KEY,
|
||||||
ItemNames.GIRL_COSTUME, ItemNames.ODD_CONTAINER, ItemNames.TRIDENT, ItemNames.TURTLE_EGG,
|
ItemNames.GIRL_COSTUME, ItemNames.ODD_CONTAINER, ItemNames.TRIDENT, ItemNames.TURTLE_EGG,
|
||||||
ItemNames.JELLY_EGG, ItemNames.URCHIN_COSTUME, ItemNames.BABY_WALKER,
|
ItemNames.JELLY_EGG, ItemNames.BABY_WALKER, ItemNames.RAINBOW_MUSHROOM,
|
||||||
ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM,
|
ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM, ItemNames.FISH_OIL,
|
||||||
ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE,
|
ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE,
|
||||||
ItemNames.LEECHING_POULTICE, ItemNames.LEECHING_POULTICE, ItemNames.ARCANE_POULTICE,
|
ItemNames.LEECHING_POULTICE, ItemNames.LEECHING_POULTICE, ItemNames.ARCANE_POULTICE,
|
||||||
ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT,
|
ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def _has_li(state: CollectionState, player: int) -> bool:
|
|||||||
DAMAGING_ITEMS:Iterable[str] = [
|
DAMAGING_ITEMS:Iterable[str] = [
|
||||||
ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
||||||
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
||||||
ItemNames.BABY_BLASTER
|
ItemNames.BABY_BLASTER, ItemNames.URCHIN_COSTUME
|
||||||
]
|
]
|
||||||
|
|
||||||
def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool:
|
def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool:
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class AquariaWorld(World):
|
|||||||
item_name_groups = {
|
item_name_groups = {
|
||||||
"Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
"Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
||||||
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
||||||
ItemNames.BABY_BLASTER},
|
ItemNames.BABY_BLASTER, ItemNames.URCHIN_COSTUME},
|
||||||
"Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO}
|
"Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO}
|
||||||
}
|
}
|
||||||
"""Grouping item make it easier to find them"""
|
"""Grouping item make it easier to find them"""
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ def versum_hill_rave(state: CollectionState, player: int, limit: bool, glitched:
|
|||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
graffitiL(state, player, limit, 85)
|
graffitiL(state, player, limit, 85)
|
||||||
and graffitiXL(state, player, limit, 48)
|
and graffitiXL(state, player, limit, 49)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
|
|||||||
5
worlds/bomb_rush_cyberfunk/archipelago.json
Normal file
5
worlds/bomb_rush_cyberfunk/archipelago.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"game": "Bomb Rush Cyberfunk",
|
||||||
|
"world_version": "1.0.6",
|
||||||
|
"authors": ["TRPG"]
|
||||||
|
}
|
||||||
@@ -16,213 +16,213 @@ from .Locations import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_regions(world: MultiWorld, options: CCCharlesOptions, player: int) -> None:
|
def create_regions(multiworld: MultiWorld, options: CCCharlesOptions, player: int) -> None:
|
||||||
menu_region = Region("Menu", player, world, "Aranearum")
|
menu_region = Region("Menu", player, multiworld, "Aranearum")
|
||||||
world.regions.append(menu_region)
|
multiworld.regions.append(menu_region)
|
||||||
|
|
||||||
start_camp_region = Region("Start Camp", player, world)
|
start_camp_region = Region("Start Camp", player, multiworld)
|
||||||
start_camp_region.add_locations(loc_start_camp, CCCharlesLocation)
|
start_camp_region.add_locations(loc_start_camp, CCCharlesLocation)
|
||||||
world.regions.append(start_camp_region)
|
multiworld.regions.append(start_camp_region)
|
||||||
|
|
||||||
tony_tiddle_mission_region = Region("Tony Tiddle Mission", player, world)
|
tony_tiddle_mission_region = Region("Tony Tiddle Mission", player, multiworld)
|
||||||
tony_tiddle_mission_region.add_locations(loc_tony_tiddle_mission, CCCharlesLocation)
|
tony_tiddle_mission_region.add_locations(loc_tony_tiddle_mission, CCCharlesLocation)
|
||||||
world.regions.append(tony_tiddle_mission_region)
|
multiworld.regions.append(tony_tiddle_mission_region)
|
||||||
|
|
||||||
barn_region = Region("Barn", player, world)
|
barn_region = Region("Barn", player, multiworld)
|
||||||
barn_region.add_locations(loc_barn, CCCharlesLocation)
|
barn_region.add_locations(loc_barn, CCCharlesLocation)
|
||||||
world.regions.append(barn_region)
|
multiworld.regions.append(barn_region)
|
||||||
|
|
||||||
candice_mission_region = Region("Candice Mission", player, world)
|
candice_mission_region = Region("Candice Mission", player, multiworld)
|
||||||
candice_mission_region.add_locations(loc_candice_mission, CCCharlesLocation)
|
candice_mission_region.add_locations(loc_candice_mission, CCCharlesLocation)
|
||||||
world.regions.append(candice_mission_region)
|
multiworld.regions.append(candice_mission_region)
|
||||||
|
|
||||||
tutorial_house_region = Region("Tutorial House", player, world)
|
tutorial_house_region = Region("Tutorial House", player, multiworld)
|
||||||
tutorial_house_region.add_locations(loc_tutorial_house, CCCharlesLocation)
|
tutorial_house_region.add_locations(loc_tutorial_house, CCCharlesLocation)
|
||||||
world.regions.append(tutorial_house_region)
|
multiworld.regions.append(tutorial_house_region)
|
||||||
|
|
||||||
swamp_edges_region = Region("Swamp Edges", player, world)
|
swamp_edges_region = Region("Swamp Edges", player, multiworld)
|
||||||
swamp_edges_region.add_locations(loc_swamp_edges, CCCharlesLocation)
|
swamp_edges_region.add_locations(loc_swamp_edges, CCCharlesLocation)
|
||||||
world.regions.append(swamp_edges_region)
|
multiworld.regions.append(swamp_edges_region)
|
||||||
|
|
||||||
swamp_mission_region = Region("Swamp Mission", player, world)
|
swamp_mission_region = Region("Swamp Mission", player, multiworld)
|
||||||
swamp_mission_region.add_locations(loc_swamp_mission, CCCharlesLocation)
|
swamp_mission_region.add_locations(loc_swamp_mission, CCCharlesLocation)
|
||||||
world.regions.append(swamp_mission_region)
|
multiworld.regions.append(swamp_mission_region)
|
||||||
|
|
||||||
junkyard_area_region = Region("Junkyard Area", player, world)
|
junkyard_area_region = Region("Junkyard Area", player, multiworld)
|
||||||
junkyard_area_region.add_locations(loc_junkyard_area, CCCharlesLocation)
|
junkyard_area_region.add_locations(loc_junkyard_area, CCCharlesLocation)
|
||||||
world.regions.append(junkyard_area_region)
|
multiworld.regions.append(junkyard_area_region)
|
||||||
|
|
||||||
south_house_region = Region("South House", player, world)
|
south_house_region = Region("South House", player, multiworld)
|
||||||
south_house_region.add_locations(loc_south_house, CCCharlesLocation)
|
south_house_region.add_locations(loc_south_house, CCCharlesLocation)
|
||||||
world.regions.append(south_house_region)
|
multiworld.regions.append(south_house_region)
|
||||||
|
|
||||||
junkyard_shed_region = Region("Junkyard Shed", player, world)
|
junkyard_shed_region = Region("Junkyard Shed", player, multiworld)
|
||||||
junkyard_shed_region.add_locations(loc_junkyard_shed, CCCharlesLocation)
|
junkyard_shed_region.add_locations(loc_junkyard_shed, CCCharlesLocation)
|
||||||
world.regions.append(junkyard_shed_region)
|
multiworld.regions.append(junkyard_shed_region)
|
||||||
|
|
||||||
military_base_region = Region("Military Base", player, world)
|
military_base_region = Region("Military Base", player, multiworld)
|
||||||
military_base_region.add_locations(loc_military_base, CCCharlesLocation)
|
military_base_region.add_locations(loc_military_base, CCCharlesLocation)
|
||||||
world.regions.append(military_base_region)
|
multiworld.regions.append(military_base_region)
|
||||||
|
|
||||||
south_mine_outside_region = Region("South Mine Outside", player, world)
|
south_mine_outside_region = Region("South Mine Outside", player, multiworld)
|
||||||
south_mine_outside_region.add_locations(loc_south_mine_outside, CCCharlesLocation)
|
south_mine_outside_region.add_locations(loc_south_mine_outside, CCCharlesLocation)
|
||||||
world.regions.append(south_mine_outside_region)
|
multiworld.regions.append(south_mine_outside_region)
|
||||||
|
|
||||||
south_mine_inside_region = Region("South Mine Inside", player, world)
|
south_mine_inside_region = Region("South Mine Inside", player, multiworld)
|
||||||
south_mine_inside_region.add_locations(loc_south_mine_inside, CCCharlesLocation)
|
south_mine_inside_region.add_locations(loc_south_mine_inside, CCCharlesLocation)
|
||||||
world.regions.append(south_mine_inside_region)
|
multiworld.regions.append(south_mine_inside_region)
|
||||||
|
|
||||||
middle_station_region = Region("Middle Station", player, world)
|
middle_station_region = Region("Middle Station", player, multiworld)
|
||||||
middle_station_region.add_locations(loc_middle_station, CCCharlesLocation)
|
middle_station_region.add_locations(loc_middle_station, CCCharlesLocation)
|
||||||
world.regions.append(middle_station_region)
|
multiworld.regions.append(middle_station_region)
|
||||||
|
|
||||||
canyon_region = Region("Canyon", player, world)
|
canyon_region = Region("Canyon", player, multiworld)
|
||||||
canyon_region.add_locations(loc_canyon, CCCharlesLocation)
|
canyon_region.add_locations(loc_canyon, CCCharlesLocation)
|
||||||
world.regions.append(canyon_region)
|
multiworld.regions.append(canyon_region)
|
||||||
|
|
||||||
watchtower_region = Region("Watchtower", player, world)
|
watchtower_region = Region("Watchtower", player, multiworld)
|
||||||
watchtower_region.add_locations(loc_watchtower, CCCharlesLocation)
|
watchtower_region.add_locations(loc_watchtower, CCCharlesLocation)
|
||||||
world.regions.append(watchtower_region)
|
multiworld.regions.append(watchtower_region)
|
||||||
|
|
||||||
boulder_field_region = Region("Boulder Field", player, world)
|
boulder_field_region = Region("Boulder Field", player, multiworld)
|
||||||
boulder_field_region.add_locations(loc_boulder_field, CCCharlesLocation)
|
boulder_field_region.add_locations(loc_boulder_field, CCCharlesLocation)
|
||||||
world.regions.append(boulder_field_region)
|
multiworld.regions.append(boulder_field_region)
|
||||||
|
|
||||||
haunted_house_region = Region("Haunted House", player, world)
|
haunted_house_region = Region("Haunted House", player, multiworld)
|
||||||
haunted_house_region.add_locations(loc_haunted_house, CCCharlesLocation)
|
haunted_house_region.add_locations(loc_haunted_house, CCCharlesLocation)
|
||||||
world.regions.append(haunted_house_region)
|
multiworld.regions.append(haunted_house_region)
|
||||||
|
|
||||||
santiago_house_region = Region("Santiago House", player, world)
|
santiago_house_region = Region("Santiago House", player, multiworld)
|
||||||
santiago_house_region.add_locations(loc_santiago_house, CCCharlesLocation)
|
santiago_house_region.add_locations(loc_santiago_house, CCCharlesLocation)
|
||||||
world.regions.append(santiago_house_region)
|
multiworld.regions.append(santiago_house_region)
|
||||||
|
|
||||||
port_region = Region("Port", player, world)
|
port_region = Region("Port", player, multiworld)
|
||||||
port_region.add_locations(loc_port, CCCharlesLocation)
|
port_region.add_locations(loc_port, CCCharlesLocation)
|
||||||
world.regions.append(port_region)
|
multiworld.regions.append(port_region)
|
||||||
|
|
||||||
trench_house_region = Region("Trench House", player, world)
|
trench_house_region = Region("Trench House", player, multiworld)
|
||||||
trench_house_region.add_locations(loc_trench_house, CCCharlesLocation)
|
trench_house_region.add_locations(loc_trench_house, CCCharlesLocation)
|
||||||
world.regions.append(trench_house_region)
|
multiworld.regions.append(trench_house_region)
|
||||||
|
|
||||||
doll_woods_region = Region("Doll Woods", player, world)
|
doll_woods_region = Region("Doll Woods", player, multiworld)
|
||||||
doll_woods_region.add_locations(loc_doll_woods, CCCharlesLocation)
|
doll_woods_region.add_locations(loc_doll_woods, CCCharlesLocation)
|
||||||
world.regions.append(doll_woods_region)
|
multiworld.regions.append(doll_woods_region)
|
||||||
|
|
||||||
lost_stairs_region = Region("Lost Stairs", player, world)
|
lost_stairs_region = Region("Lost Stairs", player, multiworld)
|
||||||
lost_stairs_region.add_locations(loc_lost_stairs, CCCharlesLocation)
|
lost_stairs_region.add_locations(loc_lost_stairs, CCCharlesLocation)
|
||||||
world.regions.append(lost_stairs_region)
|
multiworld.regions.append(lost_stairs_region)
|
||||||
|
|
||||||
east_house_region = Region("East House", player, world)
|
east_house_region = Region("East House", player, multiworld)
|
||||||
east_house_region.add_locations(loc_east_house, CCCharlesLocation)
|
east_house_region.add_locations(loc_east_house, CCCharlesLocation)
|
||||||
world.regions.append(east_house_region)
|
multiworld.regions.append(east_house_region)
|
||||||
|
|
||||||
rockets_testing_ground_region = Region("Rockets Testing Ground", player, world)
|
rockets_testing_ground_region = Region("Rockets Testing Ground", player, multiworld)
|
||||||
rockets_testing_ground_region.add_locations(loc_rockets_testing_ground, CCCharlesLocation)
|
rockets_testing_ground_region.add_locations(loc_rockets_testing_ground, CCCharlesLocation)
|
||||||
world.regions.append(rockets_testing_ground_region)
|
multiworld.regions.append(rockets_testing_ground_region)
|
||||||
|
|
||||||
rockets_testing_bunker_region = Region("Rockets Testing Bunker", player, world)
|
rockets_testing_bunker_region = Region("Rockets Testing Bunker", player, multiworld)
|
||||||
rockets_testing_bunker_region.add_locations(loc_rockets_testing_bunker, CCCharlesLocation)
|
rockets_testing_bunker_region.add_locations(loc_rockets_testing_bunker, CCCharlesLocation)
|
||||||
world.regions.append(rockets_testing_bunker_region)
|
multiworld.regions.append(rockets_testing_bunker_region)
|
||||||
|
|
||||||
workshop_region = Region("Workshop", player, world)
|
workshop_region = Region("Workshop", player, multiworld)
|
||||||
workshop_region.add_locations(loc_workshop, CCCharlesLocation)
|
workshop_region.add_locations(loc_workshop, CCCharlesLocation)
|
||||||
world.regions.append(workshop_region)
|
multiworld.regions.append(workshop_region)
|
||||||
|
|
||||||
east_tower_region = Region("East Tower", player, world)
|
east_tower_region = Region("East Tower", player, multiworld)
|
||||||
east_tower_region.add_locations(loc_east_tower, CCCharlesLocation)
|
east_tower_region.add_locations(loc_east_tower, CCCharlesLocation)
|
||||||
world.regions.append(east_tower_region)
|
multiworld.regions.append(east_tower_region)
|
||||||
|
|
||||||
lighthouse_region = Region("Lighthouse", player, world)
|
lighthouse_region = Region("Lighthouse", player, multiworld)
|
||||||
lighthouse_region.add_locations(loc_lighthouse, CCCharlesLocation)
|
lighthouse_region.add_locations(loc_lighthouse, CCCharlesLocation)
|
||||||
world.regions.append(lighthouse_region)
|
multiworld.regions.append(lighthouse_region)
|
||||||
|
|
||||||
north_mine_outside_region = Region("North Mine Outside", player, world)
|
north_mine_outside_region = Region("North Mine Outside", player, multiworld)
|
||||||
north_mine_outside_region.add_locations(loc_north_mine_outside, CCCharlesLocation)
|
north_mine_outside_region.add_locations(loc_north_mine_outside, CCCharlesLocation)
|
||||||
world.regions.append(north_mine_outside_region)
|
multiworld.regions.append(north_mine_outside_region)
|
||||||
|
|
||||||
north_mine_inside_region = Region("North Mine Inside", player, world)
|
north_mine_inside_region = Region("North Mine Inside", player, multiworld)
|
||||||
north_mine_inside_region.add_locations(loc_north_mine_inside, CCCharlesLocation)
|
north_mine_inside_region.add_locations(loc_north_mine_inside, CCCharlesLocation)
|
||||||
world.regions.append(north_mine_inside_region)
|
multiworld.regions.append(north_mine_inside_region)
|
||||||
|
|
||||||
wood_bridge_region = Region("Wood Bridge", player, world)
|
wood_bridge_region = Region("Wood Bridge", player, multiworld)
|
||||||
wood_bridge_region.add_locations(loc_wood_bridge, CCCharlesLocation)
|
wood_bridge_region.add_locations(loc_wood_bridge, CCCharlesLocation)
|
||||||
world.regions.append(wood_bridge_region)
|
multiworld.regions.append(wood_bridge_region)
|
||||||
|
|
||||||
museum_region = Region("Museum", player, world)
|
museum_region = Region("Museum", player, multiworld)
|
||||||
museum_region.add_locations(loc_museum, CCCharlesLocation)
|
museum_region.add_locations(loc_museum, CCCharlesLocation)
|
||||||
world.regions.append(museum_region)
|
multiworld.regions.append(museum_region)
|
||||||
|
|
||||||
barbed_shelter_region = Region("Barbed Shelter", player, world)
|
barbed_shelter_region = Region("Barbed Shelter", player, multiworld)
|
||||||
barbed_shelter_region.add_locations(loc_barbed_shelter, CCCharlesLocation)
|
barbed_shelter_region.add_locations(loc_barbed_shelter, CCCharlesLocation)
|
||||||
world.regions.append(barbed_shelter_region)
|
multiworld.regions.append(barbed_shelter_region)
|
||||||
|
|
||||||
west_beach_region = Region("West Beach", player, world)
|
west_beach_region = Region("West Beach", player, multiworld)
|
||||||
west_beach_region.add_locations(loc_west_beach, CCCharlesLocation)
|
west_beach_region.add_locations(loc_west_beach, CCCharlesLocation)
|
||||||
world.regions.append(west_beach_region)
|
multiworld.regions.append(west_beach_region)
|
||||||
|
|
||||||
church_region = Region("Church", player, world)
|
church_region = Region("Church", player, multiworld)
|
||||||
church_region.add_locations(loc_church, CCCharlesLocation)
|
church_region.add_locations(loc_church, CCCharlesLocation)
|
||||||
world.regions.append(church_region)
|
multiworld.regions.append(church_region)
|
||||||
|
|
||||||
west_cottage_region = Region("West Cottage", player, world)
|
west_cottage_region = Region("West Cottage", player, multiworld)
|
||||||
west_cottage_region.add_locations(loc_west_cottage, CCCharlesLocation)
|
west_cottage_region.add_locations(loc_west_cottage, CCCharlesLocation)
|
||||||
world.regions.append(west_cottage_region)
|
multiworld.regions.append(west_cottage_region)
|
||||||
|
|
||||||
caravan_region = Region("Caravan", player, world)
|
caravan_region = Region("Caravan", player, multiworld)
|
||||||
caravan_region.add_locations(loc_caravan, CCCharlesLocation)
|
caravan_region.add_locations(loc_caravan, CCCharlesLocation)
|
||||||
world.regions.append(caravan_region)
|
multiworld.regions.append(caravan_region)
|
||||||
|
|
||||||
trailer_cabin_region = Region("Trailer Cabin", player, world)
|
trailer_cabin_region = Region("Trailer Cabin", player, multiworld)
|
||||||
trailer_cabin_region.add_locations(loc_trailer_cabin, CCCharlesLocation)
|
trailer_cabin_region.add_locations(loc_trailer_cabin, CCCharlesLocation)
|
||||||
world.regions.append(trailer_cabin_region)
|
multiworld.regions.append(trailer_cabin_region)
|
||||||
|
|
||||||
towers_region = Region("Towers", player, world)
|
towers_region = Region("Towers", player, multiworld)
|
||||||
towers_region.add_locations(loc_towers, CCCharlesLocation)
|
towers_region.add_locations(loc_towers, CCCharlesLocation)
|
||||||
world.regions.append(towers_region)
|
multiworld.regions.append(towers_region)
|
||||||
|
|
||||||
north_beach_region = Region("North beach", player, world)
|
north_beach_region = Region("North beach", player, multiworld)
|
||||||
north_beach_region.add_locations(loc_north_beach, CCCharlesLocation)
|
north_beach_region.add_locations(loc_north_beach, CCCharlesLocation)
|
||||||
world.regions.append(north_beach_region)
|
multiworld.regions.append(north_beach_region)
|
||||||
|
|
||||||
mine_shaft_region = Region("Mine Shaft", player, world)
|
mine_shaft_region = Region("Mine Shaft", player, multiworld)
|
||||||
mine_shaft_region.add_locations(loc_mine_shaft, CCCharlesLocation)
|
mine_shaft_region.add_locations(loc_mine_shaft, CCCharlesLocation)
|
||||||
world.regions.append(mine_shaft_region)
|
multiworld.regions.append(mine_shaft_region)
|
||||||
|
|
||||||
mob_camp_region = Region("Mob Camp", player, world)
|
mob_camp_region = Region("Mob Camp", player, multiworld)
|
||||||
mob_camp_region.add_locations(loc_mob_camp, CCCharlesLocation)
|
mob_camp_region.add_locations(loc_mob_camp, CCCharlesLocation)
|
||||||
world.regions.append(mob_camp_region)
|
multiworld.regions.append(mob_camp_region)
|
||||||
|
|
||||||
mob_camp_locked_room_region = Region("Mob Camp Locked Room", player, world)
|
mob_camp_locked_room_region = Region("Mob Camp Locked Room", player, multiworld)
|
||||||
mob_camp_locked_room_region.add_locations(loc_mob_camp_locked_room, CCCharlesLocation)
|
mob_camp_locked_room_region.add_locations(loc_mob_camp_locked_room, CCCharlesLocation)
|
||||||
world.regions.append(mob_camp_locked_room_region)
|
multiworld.regions.append(mob_camp_locked_room_region)
|
||||||
|
|
||||||
mine_elevator_exit_region = Region("Mine Elevator Exit", player, world)
|
mine_elevator_exit_region = Region("Mine Elevator Exit", player, multiworld)
|
||||||
mine_elevator_exit_region.add_locations(loc_mine_elevator_exit, CCCharlesLocation)
|
mine_elevator_exit_region.add_locations(loc_mine_elevator_exit, CCCharlesLocation)
|
||||||
world.regions.append(mine_elevator_exit_region)
|
multiworld.regions.append(mine_elevator_exit_region)
|
||||||
|
|
||||||
mountain_ruin_outside_region = Region("Mountain Ruin Outside", player, world)
|
mountain_ruin_outside_region = Region("Mountain Ruin Outside", player, multiworld)
|
||||||
mountain_ruin_outside_region.add_locations(loc_mountain_ruin_outside, CCCharlesLocation)
|
mountain_ruin_outside_region.add_locations(loc_mountain_ruin_outside, CCCharlesLocation)
|
||||||
world.regions.append(mountain_ruin_outside_region)
|
multiworld.regions.append(mountain_ruin_outside_region)
|
||||||
|
|
||||||
mountain_ruin_inside_region = Region("Mountain Ruin Inside", player, world)
|
mountain_ruin_inside_region = Region("Mountain Ruin Inside", player, multiworld)
|
||||||
mountain_ruin_inside_region.add_locations(loc_mountain_ruin_inside, CCCharlesLocation)
|
mountain_ruin_inside_region.add_locations(loc_mountain_ruin_inside, CCCharlesLocation)
|
||||||
world.regions.append(mountain_ruin_inside_region)
|
multiworld.regions.append(mountain_ruin_inside_region)
|
||||||
|
|
||||||
prism_temple_region = Region("Prism Temple", player, world)
|
prism_temple_region = Region("Prism Temple", player, multiworld)
|
||||||
prism_temple_region.add_locations(loc_prism_temple, CCCharlesLocation)
|
prism_temple_region.add_locations(loc_prism_temple, CCCharlesLocation)
|
||||||
world.regions.append(prism_temple_region)
|
multiworld.regions.append(prism_temple_region)
|
||||||
|
|
||||||
pickle_val_region = Region("Pickle Val", player, world)
|
pickle_val_region = Region("Pickle Val", player, multiworld)
|
||||||
pickle_val_region.add_locations(loc_pickle_val, CCCharlesLocation)
|
pickle_val_region.add_locations(loc_pickle_val, CCCharlesLocation)
|
||||||
world.regions.append(pickle_val_region)
|
multiworld.regions.append(pickle_val_region)
|
||||||
|
|
||||||
shrine_near_temple_region = Region("Shrine Near Temple", player, world)
|
shrine_near_temple_region = Region("Shrine Near Temple", player, multiworld)
|
||||||
shrine_near_temple_region.add_locations(loc_shrine_near_temple, CCCharlesLocation)
|
shrine_near_temple_region.add_locations(loc_shrine_near_temple, CCCharlesLocation)
|
||||||
world.regions.append(shrine_near_temple_region)
|
multiworld.regions.append(shrine_near_temple_region)
|
||||||
|
|
||||||
morse_bunker_region = Region("Morse Bunker", player, world)
|
morse_bunker_region = Region("Morse Bunker", player, multiworld)
|
||||||
morse_bunker_region.add_locations(loc_morse_bunker, CCCharlesLocation)
|
morse_bunker_region.add_locations(loc_morse_bunker, CCCharlesLocation)
|
||||||
world.regions.append(morse_bunker_region)
|
multiworld.regions.append(morse_bunker_region)
|
||||||
|
|
||||||
# Place "Victory" event at "Final Boss" location
|
# Place "Victory" event at "Final Boss" location
|
||||||
loc_final_boss = CCCharlesLocation(player, "Final Boss", None, prism_temple_region)
|
loc_final_boss = CCCharlesLocation(player, "Final Boss", None, prism_temple_region)
|
||||||
|
|||||||
@@ -4,212 +4,212 @@ from .Options import CCCharlesOptions
|
|||||||
|
|
||||||
# Go mode: Green Egg + Blue Egg + Red Egg + Temple Key + Bug Spray (+ Remote Explosive x8 but the base game ignores it)
|
# Go mode: Green Egg + Blue Egg + Red Egg + Temple Key + Bug Spray (+ Remote Explosive x8 but the base game ignores it)
|
||||||
|
|
||||||
def set_rules(world: MultiWorld, options: CCCharlesOptions, player: int) -> None:
|
def set_rules(multiworld: MultiWorld, options: CCCharlesOptions, player: int) -> None:
|
||||||
# Tony Tiddle
|
# Tony Tiddle
|
||||||
set_rule(world.get_entrance("Barn Door", player),
|
set_rule(multiworld.get_entrance("Barn Door", player),
|
||||||
lambda state: state.has("Barn Key", player))
|
lambda state: state.has("Barn Key", player))
|
||||||
|
|
||||||
# Candice
|
# Candice
|
||||||
set_rule(world.get_entrance("Tutorial House Door", player),
|
set_rule(multiworld.get_entrance("Tutorial House Door", player),
|
||||||
lambda state: state.has("Candice's Key", player))
|
lambda state: state.has("Candice's Key", player))
|
||||||
|
|
||||||
# Lizbeth Murkwater
|
# Lizbeth Murkwater
|
||||||
set_rule(world.get_location("Swamp Lizbeth Murkwater Mission End", player),
|
set_rule(multiworld.get_location("Swamp Lizbeth Murkwater Mission End", player),
|
||||||
lambda state: state.has("Dead Fish", player))
|
lambda state: state.has("Dead Fish", player))
|
||||||
|
|
||||||
# Daryl
|
# Daryl
|
||||||
set_rule(world.get_location("Junkyard Area Chest Ancient Tablet", player),
|
set_rule(multiworld.get_location("Junkyard Area Chest Ancient Tablet", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Junkyard Area Daryl Mission End", player),
|
set_rule(multiworld.get_location("Junkyard Area Daryl Mission End", player),
|
||||||
lambda state: state.has("Ancient Tablet", player))
|
lambda state: state.has("Ancient Tablet", player))
|
||||||
|
|
||||||
# South House
|
# South House
|
||||||
set_rule(world.get_location("South House Chest Scraps 1", player),
|
set_rule(multiworld.get_location("South House Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("South House Chest Scraps 2", player),
|
set_rule(multiworld.get_location("South House Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("South House Chest Scraps 3", player),
|
set_rule(multiworld.get_location("South House Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("South House Chest Scraps 4", player),
|
set_rule(multiworld.get_location("South House Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("South House Chest Scraps 5", player),
|
set_rule(multiworld.get_location("South House Chest Scraps 5", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("South House Chest Scraps 6", player),
|
set_rule(multiworld.get_location("South House Chest Scraps 6", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# South Mine
|
# South Mine
|
||||||
set_rule(world.get_entrance("South Mine Gate", player),
|
set_rule(multiworld.get_entrance("South Mine Gate", player),
|
||||||
lambda state: state.has("South Mine Key", player))
|
lambda state: state.has("South Mine Key", player))
|
||||||
|
|
||||||
set_rule(world.get_location("South Mine Inside Green Paint Can", player),
|
set_rule(multiworld.get_location("South Mine Inside Green Paint Can", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Theodore
|
# Theodore
|
||||||
set_rule(world.get_location("Middle Station Theodore Mission End", player),
|
set_rule(multiworld.get_location("Middle Station Theodore Mission End", player),
|
||||||
lambda state: state.has("Blue Box", player))
|
lambda state: state.has("Blue Box", player))
|
||||||
|
|
||||||
# Watchtower
|
# Watchtower
|
||||||
set_rule(world.get_location("Watchtower Pink Paint Can", player),
|
set_rule(multiworld.get_location("Watchtower Pink Paint Can", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Sasha
|
# Sasha
|
||||||
set_rule(world.get_location("Haunted House Sasha Mission End", player),
|
set_rule(multiworld.get_location("Haunted House Sasha Mission End", player),
|
||||||
lambda state: state.has("Page Drawing", player, 8))
|
lambda state: state.has("Page Drawing", player, 8))
|
||||||
|
|
||||||
# Santiago
|
# Santiago
|
||||||
set_rule(world.get_location("Port Santiago Mission End", player),
|
set_rule(multiworld.get_location("Port Santiago Mission End", player),
|
||||||
lambda state: state.has("Journal", player))
|
lambda state: state.has("Journal", player))
|
||||||
|
|
||||||
# Trench House
|
# Trench House
|
||||||
set_rule(world.get_location("Trench House Chest Scraps 1", player),
|
set_rule(multiworld.get_location("Trench House Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Trench House Chest Scraps 2", player),
|
set_rule(multiworld.get_location("Trench House Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Trench House Chest Scraps 3", player),
|
set_rule(multiworld.get_location("Trench House Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Trench House Chest Scraps 4", player),
|
set_rule(multiworld.get_location("Trench House Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Trench House Chest Scraps 5", player),
|
set_rule(multiworld.get_location("Trench House Chest Scraps 5", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Trench House Chest Scraps 6", player),
|
set_rule(multiworld.get_location("Trench House Chest Scraps 6", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# East House
|
# East House
|
||||||
set_rule(world.get_location("East House Chest Scraps 1", player),
|
set_rule(multiworld.get_location("East House Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("East House Chest Scraps 2", player),
|
set_rule(multiworld.get_location("East House Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("East House Chest Scraps 3", player),
|
set_rule(multiworld.get_location("East House Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("East House Chest Scraps 4", player),
|
set_rule(multiworld.get_location("East House Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("East House Chest Scraps 5", player),
|
set_rule(multiworld.get_location("East House Chest Scraps 5", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Rocket Testing Bunker
|
# Rocket Testing Bunker
|
||||||
set_rule(world.get_entrance("Stuck Bunker Door", player),
|
set_rule(multiworld.get_entrance("Stuck Bunker Door", player),
|
||||||
lambda state: state.has("Timed Dynamite", player))
|
lambda state: state.has("Timed Dynamite", player))
|
||||||
|
|
||||||
# John Smith
|
# John Smith
|
||||||
set_rule(world.get_location("Workshop John Smith Mission End", player),
|
set_rule(multiworld.get_location("Workshop John Smith Mission End", player),
|
||||||
lambda state: state.has("Box of Rockets", player))
|
lambda state: state.has("Box of Rockets", player))
|
||||||
|
|
||||||
# Claire
|
# Claire
|
||||||
set_rule(world.get_location("Lighthouse Claire Mission End", player),
|
set_rule(multiworld.get_location("Lighthouse Claire Mission End", player),
|
||||||
lambda state: state.has("Breaker", player, 4))
|
lambda state: state.has("Breaker", player, 4))
|
||||||
|
|
||||||
# North Mine
|
# North Mine
|
||||||
set_rule(world.get_entrance("North Mine Gate", player),
|
set_rule(multiworld.get_entrance("North Mine Gate", player),
|
||||||
lambda state: state.has("North Mine Key", player))
|
lambda state: state.has("North Mine Key", player))
|
||||||
|
|
||||||
set_rule(world.get_location("North Mine Inside Blue Paint Can", player),
|
set_rule(multiworld.get_location("North Mine Inside Blue Paint Can", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Paul
|
# Paul
|
||||||
set_rule(world.get_location("Museum Paul Mission End", player),
|
set_rule(multiworld.get_location("Museum Paul Mission End", player),
|
||||||
lambda state: state.has("Remote Explosive x8", player))
|
lambda state: state.has("Remote Explosive x8", player))
|
||||||
# lambda state: state.has("Remote Explosive", player, 8)) # TODO: Add an option to split remote explosives
|
# lambda state: state.has("Remote Explosive", player, 8)) # TODO: Add an option to split remote explosives
|
||||||
|
|
||||||
# West Beach
|
# West Beach
|
||||||
set_rule(world.get_location("West Beach Chest Scraps 1", player),
|
set_rule(multiworld.get_location("West Beach Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("West Beach Chest Scraps 2", player),
|
set_rule(multiworld.get_location("West Beach Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("West Beach Chest Scraps 3", player),
|
set_rule(multiworld.get_location("West Beach Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("West Beach Chest Scraps 4", player),
|
set_rule(multiworld.get_location("West Beach Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("West Beach Chest Scraps 5", player),
|
set_rule(multiworld.get_location("West Beach Chest Scraps 5", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("West Beach Chest Scraps 6", player),
|
set_rule(multiworld.get_location("West Beach Chest Scraps 6", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Caravan
|
# Caravan
|
||||||
set_rule(world.get_location("Caravan Chest Scraps 1", player),
|
set_rule(multiworld.get_location("Caravan Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Caravan Chest Scraps 2", player),
|
set_rule(multiworld.get_location("Caravan Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Caravan Chest Scraps 3", player),
|
set_rule(multiworld.get_location("Caravan Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Caravan Chest Scraps 4", player),
|
set_rule(multiworld.get_location("Caravan Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Caravan Chest Scraps 5", player),
|
set_rule(multiworld.get_location("Caravan Chest Scraps 5", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Ronny
|
# Ronny
|
||||||
set_rule(world.get_location("Towers Ronny Mission End", player),
|
set_rule(multiworld.get_location("Towers Ronny Mission End", player),
|
||||||
lambda state: state.has("Employment Contracts", player))
|
lambda state: state.has("Employment Contracts", player))
|
||||||
|
|
||||||
# North Beach
|
# North Beach
|
||||||
set_rule(world.get_location("North Beach Chest Scraps 1", player),
|
set_rule(multiworld.get_location("North Beach Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("North Beach Chest Scraps 2", player),
|
set_rule(multiworld.get_location("North Beach Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("North Beach Chest Scraps 3", player),
|
set_rule(multiworld.get_location("North Beach Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("North Beach Chest Scraps 4", player),
|
set_rule(multiworld.get_location("North Beach Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Mine Shaft
|
# Mine Shaft
|
||||||
set_rule(world.get_location("Mine Shaft Chest Scraps 1", player),
|
set_rule(multiworld.get_location("Mine Shaft Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Mine Shaft Chest Scraps 2", player),
|
set_rule(multiworld.get_location("Mine Shaft Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Mine Shaft Chest Scraps 3", player),
|
set_rule(multiworld.get_location("Mine Shaft Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Mine Shaft Chest Scraps 4", player),
|
set_rule(multiworld.get_location("Mine Shaft Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Mine Shaft Chest Scraps 5", player),
|
set_rule(multiworld.get_location("Mine Shaft Chest Scraps 5", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Mine Shaft Chest Scraps 6", player),
|
set_rule(multiworld.get_location("Mine Shaft Chest Scraps 6", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Mine Shaft Chest Scraps 7", player),
|
set_rule(multiworld.get_location("Mine Shaft Chest Scraps 7", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Mob Camp
|
# Mob Camp
|
||||||
set_rule(world.get_entrance("Mob Camp Locked Door", player),
|
set_rule(multiworld.get_entrance("Mob Camp Locked Door", player),
|
||||||
lambda state: state.has("Mob Camp Key", player))
|
lambda state: state.has("Mob Camp Key", player))
|
||||||
|
|
||||||
set_rule(world.get_location("Mob Camp Locked Room Stolen Bob", player),
|
set_rule(multiworld.get_location("Mob Camp Locked Room Stolen Bob", player),
|
||||||
lambda state: state.has("Broken Bob", player))
|
lambda state: state.has("Broken Bob", player))
|
||||||
|
|
||||||
# Mountain Ruin
|
# Mountain Ruin
|
||||||
set_rule(world.get_entrance("Mountain Ruin Gate", player),
|
set_rule(multiworld.get_entrance("Mountain Ruin Gate", player),
|
||||||
lambda state: state.has("Mountain Ruin Key", player))
|
lambda state: state.has("Mountain Ruin Key", player))
|
||||||
|
|
||||||
set_rule(world.get_location("Mountain Ruin Inside Red Paint Can", player),
|
set_rule(multiworld.get_location("Mountain Ruin Inside Red Paint Can", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Prism Temple
|
# Prism Temple
|
||||||
set_rule(world.get_location("Prism Temple Chest Scraps 1", player),
|
set_rule(multiworld.get_location("Prism Temple Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Prism Temple Chest Scraps 2", player),
|
set_rule(multiworld.get_location("Prism Temple Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Prism Temple Chest Scraps 3", player),
|
set_rule(multiworld.get_location("Prism Temple Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Pickle Lady
|
# Pickle Lady
|
||||||
set_rule(world.get_location("Pickle Val Jar of Pickles", player),
|
set_rule(multiworld.get_location("Pickle Val Jar of Pickles", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Pickle Val Pickle Lady Mission End", player),
|
set_rule(multiworld.get_location("Pickle Val Pickle Lady Mission End", player),
|
||||||
lambda state: state.has("Jar of Pickles", player))
|
lambda state: state.has("Jar of Pickles", player))
|
||||||
|
|
||||||
# Morse Bunker
|
# Morse Bunker
|
||||||
set_rule(world.get_location("Morse Bunker Chest Scraps 1", player),
|
set_rule(multiworld.get_location("Morse Bunker Chest Scraps 1", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Morse Bunker Chest Scraps 2", player),
|
set_rule(multiworld.get_location("Morse Bunker Chest Scraps 2", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Morse Bunker Chest Scraps 3", player),
|
set_rule(multiworld.get_location("Morse Bunker Chest Scraps 3", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Morse Bunker Chest Scraps 4", player),
|
set_rule(multiworld.get_location("Morse Bunker Chest Scraps 4", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
set_rule(world.get_location("Morse Bunker Chest Scraps 5", player),
|
set_rule(multiworld.get_location("Morse Bunker Chest Scraps 5", player),
|
||||||
lambda state: state.has("Lockpicks", player))
|
lambda state: state.has("Lockpicks", player))
|
||||||
|
|
||||||
# Add rules to reach the "Go mode"
|
# Add rules to reach the "Go mode"
|
||||||
set_rule(world.get_location("Final Boss", player),
|
set_rule(multiworld.get_location("Final Boss", player),
|
||||||
lambda state: state.has("Temple Key", player)
|
lambda state: state.has("Temple Key", player)
|
||||||
and state.has("Green Egg", player)
|
and state.has("Green Egg", player)
|
||||||
and state.has("Blue Egg", player)
|
and state.has("Blue Egg", player)
|
||||||
and state.has("Red Egg", player))
|
and state.has("Red Egg", player))
|
||||||
world.completion_condition[player] = lambda state: state.has("Victory", player)
|
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# Donkey Kong Country 3 - Changelog
|
|
||||||
|
|
||||||
|
|
||||||
## v1.1
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
|
|
||||||
- KONGsanity option (Collect all KONG letters in each level for a check)
|
|
||||||
- Autosave option
|
|
||||||
- Difficulty option
|
|
||||||
- MERRY option
|
|
||||||
- Handle collected/co-op locations
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
- Fixed Mekanos softlock
|
|
||||||
- Prevent Brothers Bear giving extra Banana Birds
|
|
||||||
- Fixed Banana Bird Mother check sending prematurely
|
|
||||||
- Fix Logic bug with Krematoa level costs
|
|
||||||
|
|
||||||
|
|
||||||
## v1.0
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
|
|
||||||
- Goal
|
|
||||||
- Knautilus
|
|
||||||
- Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein to win
|
|
||||||
- Banana Bird Hunt
|
|
||||||
- Find the Banana Birds and rescue their mother to win
|
|
||||||
- Locations included:
|
|
||||||
- Level Flags
|
|
||||||
- Bonuses
|
|
||||||
- DK Coins
|
|
||||||
- Banana Bird Caves
|
|
||||||
- Items included:
|
|
||||||
- Progressive Boat Upgrade
|
|
||||||
- Three are placed into the item pool (Patch -> First Ski -> Second Ski)
|
|
||||||
- Bonus Coins
|
|
||||||
- DK Coins
|
|
||||||
- Krematoa Cogs
|
|
||||||
- Bear Coins
|
|
||||||
- 1-Up Balloons
|
|
||||||
- Level Shuffle is supported
|
|
||||||
- Music Shuffle is supported
|
|
||||||
- Kong Palette options are supported
|
|
||||||
- Starting life count can be set
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from NetUtils import ClientStatus, color
|
|
||||||
from worlds.AutoSNIClient import SNIClient
|
|
||||||
|
|
||||||
snes_logger = logging.getLogger("SNES")
|
|
||||||
|
|
||||||
# FXPAK Pro protocol memory mapping used by SNI
|
|
||||||
ROM_START = 0x000000
|
|
||||||
WRAM_START = 0xF50000
|
|
||||||
WRAM_SIZE = 0x20000
|
|
||||||
SRAM_START = 0xE00000
|
|
||||||
|
|
||||||
DKC3_ROMNAME_START = 0x00FFC0
|
|
||||||
DKC3_ROMHASH_START = 0x7FC0
|
|
||||||
ROMNAME_SIZE = 0x15
|
|
||||||
ROMHASH_SIZE = 0x15
|
|
||||||
|
|
||||||
DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632
|
|
||||||
DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9
|
|
||||||
DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this
|
|
||||||
|
|
||||||
|
|
||||||
class DKC3SNIClient(SNIClient):
|
|
||||||
game = "Donkey Kong Country 3"
|
|
||||||
patch_suffix = ".apdkc3"
|
|
||||||
|
|
||||||
async def deathlink_kill_player(self, ctx):
|
|
||||||
pass
|
|
||||||
# DKC3_TODO: Handle Receiving Deathlink
|
|
||||||
|
|
||||||
|
|
||||||
async def validate_rom(self, ctx):
|
|
||||||
from SNIClient import snes_read
|
|
||||||
|
|
||||||
rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
|
||||||
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3":
|
|
||||||
return False
|
|
||||||
|
|
||||||
ctx.game = self.game
|
|
||||||
ctx.items_handling = 0b111 # remote items
|
|
||||||
|
|
||||||
ctx.rom = rom_name
|
|
||||||
|
|
||||||
#death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
|
|
||||||
## DKC3_TODO: Handle Deathlink
|
|
||||||
#if death_link:
|
|
||||||
# ctx.allow_collect = bool(death_link[0] & 0b100)
|
|
||||||
# await ctx.update_death_link(bool(death_link[0] & 0b1))
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def game_watcher(self, ctx):
|
|
||||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
|
||||||
# DKC3_TODO: Handle Deathlink
|
|
||||||
save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
|
|
||||||
if save_file_name is None or save_file_name[0] == 0x00 or save_file_name == bytes([0x55] * 0x05):
|
|
||||||
# We haven't loaded a save file
|
|
||||||
return
|
|
||||||
|
|
||||||
new_checks = []
|
|
||||||
from .Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
|
||||||
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
|
|
||||||
for loc_id, loc_data in location_rom_data.items():
|
|
||||||
if loc_id not in ctx.locations_checked:
|
|
||||||
data = location_ram_data[loc_data[0] - 0x5FE]
|
|
||||||
masked_data = data & (1 << loc_data[1])
|
|
||||||
bit_set = (masked_data != 0)
|
|
||||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
|
||||||
if bit_set != invert_bit:
|
|
||||||
# DKC3_TODO: Handle non-included checks
|
|
||||||
new_checks.append(loc_id)
|
|
||||||
|
|
||||||
verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
|
|
||||||
if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name == bytes([0x55] * 0x05) or verify_save_file_name != save_file_name:
|
|
||||||
# We have somehow exited the save file (or worse)
|
|
||||||
ctx.rom = None
|
|
||||||
return
|
|
||||||
|
|
||||||
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
|
||||||
if rom != ctx.rom:
|
|
||||||
ctx.rom = None
|
|
||||||
# We have somehow loaded a different ROM
|
|
||||||
return
|
|
||||||
|
|
||||||
for new_check_id in new_checks:
|
|
||||||
ctx.locations_checked.add(new_check_id)
|
|
||||||
location = ctx.location_names.lookup_in_game(new_check_id)
|
|
||||||
snes_logger.info(
|
|
||||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
|
||||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
|
||||||
|
|
||||||
# DKC3_TODO: Make this actually visually display new things received (ASM Hook required)
|
|
||||||
recv_count = await snes_read(ctx, DKC3_RECV_PROGRESS_ADDR, 1)
|
|
||||||
recv_index = recv_count[0]
|
|
||||||
|
|
||||||
if recv_index < len(ctx.items_received):
|
|
||||||
item = ctx.items_received[recv_index]
|
|
||||||
recv_index += 1
|
|
||||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
|
||||||
color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
|
|
||||||
color(ctx.player_names[item.player], 'yellow'),
|
|
||||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
|
||||||
|
|
||||||
snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index]))
|
|
||||||
if item.item in item_rom_data:
|
|
||||||
item_count = await snes_read(ctx, WRAM_START + item_rom_data[item.item][0], 0x1)
|
|
||||||
new_item_count = item_count[0] + 1
|
|
||||||
for address in item_rom_data[item.item]:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + address, bytes([new_item_count]))
|
|
||||||
|
|
||||||
# Handle Coin Displays
|
|
||||||
current_level = await snes_read(ctx, WRAM_START + 0x5E3, 0x5)
|
|
||||||
overworld_locked = ((await snes_read(ctx, WRAM_START + 0x5FC, 0x1))[0] == 0x01)
|
|
||||||
if item.item == 0xDC3002 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03):
|
|
||||||
# Bazaar and Barter
|
|
||||||
item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1)
|
|
||||||
new_item_count = item_count[0] + 1
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count]))
|
|
||||||
elif item.item == 0xDC3002 and not overworld_locked and current_level[0] == 0x04:
|
|
||||||
# Swanky
|
|
||||||
item_count = await snes_read(ctx, WRAM_START + 0xA26, 0x1)
|
|
||||||
new_item_count = item_count[0] + 1
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0xA26, bytes([new_item_count]))
|
|
||||||
elif item.item == 0xDC3003 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01):
|
|
||||||
# Boomer
|
|
||||||
item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1)
|
|
||||||
new_item_count = item_count[0] + 1
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count]))
|
|
||||||
else:
|
|
||||||
# Handle Patch and Skis
|
|
||||||
if item.item == 0xDC3007:
|
|
||||||
num_upgrades = 1
|
|
||||||
inventory = await snes_read(ctx, WRAM_START + 0x605, 0xF)
|
|
||||||
|
|
||||||
if (inventory[0] & 0x02):
|
|
||||||
num_upgrades = 3
|
|
||||||
elif (inventory[13] & 0x08) or (inventory[0] & 0x01):
|
|
||||||
num_upgrades = 2
|
|
||||||
|
|
||||||
if num_upgrades == 1:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x01]))
|
|
||||||
if inventory[4] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x01]))
|
|
||||||
elif inventory[6] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x01]))
|
|
||||||
elif inventory[8] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x01]))
|
|
||||||
elif inventory[10] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x01]))
|
|
||||||
|
|
||||||
cove_mekanos_progress = await snes_read(ctx, WRAM_START + 0x691, 0x2)
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x691, bytes([cove_mekanos_progress[0] | 0x01]))
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x692, bytes([cove_mekanos_progress[1] | 0x01]))
|
|
||||||
elif num_upgrades == 2:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x02]))
|
|
||||||
if inventory[4] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x02]))
|
|
||||||
elif inventory[6] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x02]))
|
|
||||||
elif inventory[8] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x02]))
|
|
||||||
elif inventory[10] == 0:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x02]))
|
|
||||||
elif num_upgrades == 3:
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x606, bytes([inventory[1] | 0x20]))
|
|
||||||
|
|
||||||
k3_ridge_progress = await snes_read(ctx, WRAM_START + 0x693, 0x2)
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x693, bytes([k3_ridge_progress[0] | 0x01]))
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0x694, bytes([k3_ridge_progress[1] | 0x01]))
|
|
||||||
elif item.item == 0xDC3000:
|
|
||||||
# Handle Victory
|
|
||||||
if not ctx.finished_game:
|
|
||||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
|
||||||
ctx.finished_game = True
|
|
||||||
else:
|
|
||||||
print("Item Not Recognized: ", item.item)
|
|
||||||
pass
|
|
||||||
|
|
||||||
await snes_flush_writes(ctx)
|
|
||||||
|
|
||||||
# Handle Collected Locations
|
|
||||||
levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60)
|
|
||||||
tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60)
|
|
||||||
for loc_id in ctx.checked_locations:
|
|
||||||
if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids:
|
|
||||||
loc_data = location_rom_data[loc_id]
|
|
||||||
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
|
|
||||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
|
||||||
if not invert_bit:
|
|
||||||
masked_data = data[0] | (1 << loc_data[1])
|
|
||||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
|
||||||
|
|
||||||
if (loc_data[1] == 1):
|
|
||||||
# Make the next levels accessible
|
|
||||||
level_id = loc_data[0] - 0x632
|
|
||||||
tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id
|
|
||||||
tile_id = tile_id + 0x632
|
|
||||||
if tile_id in level_unlock_map:
|
|
||||||
for next_level_address in level_unlock_map[tile_id]:
|
|
||||||
next_level_id = next_level_address - 0x632
|
|
||||||
next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id
|
|
||||||
next_tile_id = next_tile_id + 0x632
|
|
||||||
next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1)
|
|
||||||
snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01]))
|
|
||||||
|
|
||||||
await snes_flush_writes(ctx)
|
|
||||||
else:
|
|
||||||
masked_data = data[0] & ~(1 << loc_data[1])
|
|
||||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
|
||||||
await snes_flush_writes(ctx)
|
|
||||||
ctx.locations_checked.add(loc_id)
|
|
||||||
|
|
||||||
# Calculate Boomer Cost Text
|
|
||||||
boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2)
|
|
||||||
if boomer_cost_text[0] == 0x31 and boomer_cost_text[1] == 0x35:
|
|
||||||
boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1)
|
|
||||||
boomer_cost_tens = int(boomer_cost[0]) // 10
|
|
||||||
boomer_cost_ones = int(boomer_cost[0]) % 10
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0xAAFD, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones]))
|
|
||||||
await snes_flush_writes(ctx)
|
|
||||||
|
|
||||||
boomer_final_cost_text = await snes_read(ctx, WRAM_START + 0xAB9B, 2)
|
|
||||||
if boomer_final_cost_text[0] == 0x32 and boomer_final_cost_text[1] == 0x35:
|
|
||||||
boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1)
|
|
||||||
boomer_cost_tens = boomer_cost[0] // 10
|
|
||||||
boomer_cost_ones = boomer_cost[0] % 10
|
|
||||||
snes_buffered_write(ctx, WRAM_START + 0xAB9B, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones]))
|
|
||||||
await snes_flush_writes(ctx)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import typing
|
|
||||||
|
|
||||||
from BaseClasses import Item
|
|
||||||
from .Names import ItemName
|
|
||||||
|
|
||||||
|
|
||||||
class ItemData(typing.NamedTuple):
|
|
||||||
code: typing.Optional[int]
|
|
||||||
progression: bool
|
|
||||||
quantity: int = 1
|
|
||||||
event: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class DKC3Item(Item):
|
|
||||||
game: str = "Donkey Kong Country 3"
|
|
||||||
|
|
||||||
|
|
||||||
# Separate tables for each type of item.
|
|
||||||
junk_table = {
|
|
||||||
ItemName.one_up_balloon: ItemData(0xDC3001, False),
|
|
||||||
ItemName.bear_coin: ItemData(0xDC3002, False),
|
|
||||||
}
|
|
||||||
|
|
||||||
collectable_table = {
|
|
||||||
ItemName.bonus_coin: ItemData(0xDC3003, True),
|
|
||||||
ItemName.dk_coin: ItemData(0xDC3004, True),
|
|
||||||
ItemName.banana_bird: ItemData(0xDC3005, True),
|
|
||||||
ItemName.krematoa_cog: ItemData(0xDC3006, True),
|
|
||||||
ItemName.progressive_boat: ItemData(0xDC3007, True),
|
|
||||||
}
|
|
||||||
|
|
||||||
inventory_table = {
|
|
||||||
ItemName.present: ItemData(0xDC3008, True),
|
|
||||||
ItemName.bowling_ball: ItemData(0xDC3009, True),
|
|
||||||
ItemName.shell: ItemData(0xDC300A, True),
|
|
||||||
ItemName.mirror: ItemData(0xDC300B, True),
|
|
||||||
ItemName.flower: ItemData(0xDC300C, True),
|
|
||||||
ItemName.wrench: ItemData(0xDC300D, True),
|
|
||||||
}
|
|
||||||
|
|
||||||
event_table = {
|
|
||||||
ItemName.victory: ItemData(0xDC3000, True),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Complete item table.
|
|
||||||
item_table = {
|
|
||||||
**junk_table,
|
|
||||||
**collectable_table,
|
|
||||||
**event_table,
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
Modified MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 PoryGone
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
|
|
||||||
and to permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
No copy or substantial portion of the Software shall be sublicensed or relicensed
|
|
||||||
without the express written permission of the copyright holder(s)
|
|
||||||
|
|
||||||
No copy or substantial portion of the Software shall be sold without the express
|
|
||||||
written permission of the copyright holder(s)
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
|
|
||||||
from .Names import LocationName
|
|
||||||
|
|
||||||
class DKC3Level():
|
|
||||||
nameIDAddress: int
|
|
||||||
levelIDAddress: int
|
|
||||||
nameID: int
|
|
||||||
levelID: int
|
|
||||||
|
|
||||||
def __init__(self, nameIDAddress: int, levelIDAddress: int, nameID: int, levelID: int):
|
|
||||||
self.nameIDAddress = nameIDAddress
|
|
||||||
self.levelIDAddress = levelIDAddress
|
|
||||||
self.nameID = nameID
|
|
||||||
self.levelID = levelID
|
|
||||||
|
|
||||||
|
|
||||||
level_dict = {
|
|
||||||
LocationName.lakeside_limbo_region: DKC3Level(0x34D19C, 0x34D19D, 0x01, 0x25),
|
|
||||||
LocationName.doorstop_dash_region: DKC3Level(0x34D1A7, 0x34D1A8, 0x02, 0x28),
|
|
||||||
LocationName.tidal_trouble_region: DKC3Level(0x34D1BD, 0x34D1BE, 0x04, 0x27),
|
|
||||||
LocationName.skiddas_row_region: DKC3Level(0x34D1C8, 0x34D1C9, 0x05, 0x2B),
|
|
||||||
LocationName.murky_mill_region: DKC3Level(0x34D1D3, 0x34D1D4, 0x0D, 0x2A),
|
|
||||||
|
|
||||||
LocationName.barrel_shield_bust_up_region: DKC3Level(0x34D217, 0x34D218, 0x0B, 0x30),
|
|
||||||
LocationName.riverside_race_region: DKC3Level(0x34D22D, 0x34D22E, 0x0C, 0x32),
|
|
||||||
LocationName.squeals_on_wheels_region: DKC3Level(0x34D238, 0x34D239, 0x06, 0x29),
|
|
||||||
LocationName.springin_spiders_region: DKC3Level(0x34D24E, 0x34D24F, 0x0E, 0x2F),
|
|
||||||
LocationName.bobbing_barrel_brawl_region: DKC3Level(0x34D264, 0x34D265, 0x37, 0x34),
|
|
||||||
|
|
||||||
LocationName.bazzas_blockade_region: DKC3Level(0x34D29D, 0x34D29E, 0x14, 0x35),
|
|
||||||
LocationName.rocket_barrel_ride_region: DKC3Level(0x34D2A8, 0x34D2A9, 0x15, 0x38),
|
|
||||||
LocationName.kreeping_klasps_region: DKC3Level(0x34D2BE, 0x34D2BF, 0x16, 0x26),
|
|
||||||
LocationName.tracker_barrel_trek_region: DKC3Level(0x34D2D4, 0x34D2D5, 0x17, 0x39),
|
|
||||||
LocationName.fish_food_frenzy_region: DKC3Level(0x34D2DF, 0x34D2E0, 0x18, 0x36),
|
|
||||||
|
|
||||||
LocationName.fire_ball_frenzy_region: DKC3Level(0x34D30D, 0x34D30E, 0x1B, 0x3B),
|
|
||||||
LocationName.demolition_drain_pipe_region: DKC3Level(0x34D323, 0x34D324, 0x1D, 0x40),
|
|
||||||
LocationName.ripsaw_rage_region: DKC3Level(0x34D339, 0x34D33A, 0x1E, 0x2E),
|
|
||||||
LocationName.blazing_bazookas_region: DKC3Level(0x34D34F, 0x34D350, 0x1F, 0x3C),
|
|
||||||
LocationName.low_g_labyrinth_region: DKC3Level(0x34D35A, 0x34D35B, 0x20, 0x3E),
|
|
||||||
|
|
||||||
LocationName.krevice_kreepers_region: DKC3Level(0x34D388, 0x34D389, 0x23, 0x41),
|
|
||||||
LocationName.tearaway_toboggan_region: DKC3Level(0x34D393, 0x34D394, 0x24, 0x2D),
|
|
||||||
LocationName.barrel_drop_bounce_region: DKC3Level(0x34D39E, 0x34D39F, 0x25, 0x3A),
|
|
||||||
LocationName.krack_shot_kroc_region: DKC3Level(0x34D3A9, 0x34D3AA, 0x26, 0x3D),
|
|
||||||
LocationName.lemguin_lunge_region: DKC3Level(0x34D3B4, 0x34D3B5, 0x27, 0x2C),
|
|
||||||
|
|
||||||
LocationName.buzzer_barrage_region: DKC3Level(0x34D40E, 0x34D40F, 0x2B, 0x44),
|
|
||||||
LocationName.kong_fused_cliffs_region: DKC3Level(0x34D424, 0x34D425, 0x2D, 0x42),
|
|
||||||
LocationName.floodlit_fish_region: DKC3Level(0x34D42F, 0x34D430, 0x2E, 0x37),
|
|
||||||
LocationName.pothole_panic_region: DKC3Level(0x34D43A, 0x34D43B, 0x2F, 0x45),
|
|
||||||
LocationName.ropey_rumpus_region: DKC3Level(0x34D450, 0x34D451, 0x30, 0x43),
|
|
||||||
|
|
||||||
LocationName.konveyor_rope_clash_region: DKC3Level(0x34D489, 0x34D48A, 0x38, 0x48),
|
|
||||||
LocationName.creepy_caverns_region: DKC3Level(0x34D49F, 0x34D4A0, 0x36, 0x46),
|
|
||||||
LocationName.lightning_lookout_region: DKC3Level(0x34D4AA, 0x34D4AB, 0x10, 0x33),
|
|
||||||
LocationName.koindozer_klamber_region: DKC3Level(0x34D4C0, 0x34D4C1, 0x34, 0x47),
|
|
||||||
LocationName.poisonous_pipeline_region: DKC3Level(0x34D4D6, 0x34D4D7, 0x39, 0x3F),
|
|
||||||
|
|
||||||
LocationName.stampede_sprint_region: DKC3Level(0x34D51A, 0x34D51B, 0x3D, 0x49),
|
|
||||||
LocationName.criss_cross_cliffs_region: DKC3Level(0x34D525, 0x34D526, 0x3E, 0x4A),
|
|
||||||
LocationName.tyrant_twin_tussle_region: DKC3Level(0x34D530, 0x34D531, 0x3F, 0x4B),
|
|
||||||
LocationName.swoopy_salvo_region: DKC3Level(0x34D53B, 0x34D53C, 0x40, 0x31),
|
|
||||||
#LocationName.rocket_rush_region: DKC3Level(0x34D546, 0x34D547, 0x05, 0x4C), # Rocket Rush is not getting shuffled
|
|
||||||
}
|
|
||||||
|
|
||||||
level_list = [
|
|
||||||
LocationName.lakeside_limbo_region,
|
|
||||||
LocationName.doorstop_dash_region,
|
|
||||||
LocationName.tidal_trouble_region,
|
|
||||||
LocationName.skiddas_row_region,
|
|
||||||
LocationName.murky_mill_region,
|
|
||||||
|
|
||||||
LocationName.barrel_shield_bust_up_region,
|
|
||||||
LocationName.riverside_race_region,
|
|
||||||
LocationName.squeals_on_wheels_region,
|
|
||||||
LocationName.springin_spiders_region,
|
|
||||||
LocationName.bobbing_barrel_brawl_region,
|
|
||||||
|
|
||||||
LocationName.bazzas_blockade_region,
|
|
||||||
LocationName.rocket_barrel_ride_region,
|
|
||||||
LocationName.kreeping_klasps_region,
|
|
||||||
LocationName.tracker_barrel_trek_region,
|
|
||||||
LocationName.fish_food_frenzy_region,
|
|
||||||
|
|
||||||
LocationName.fire_ball_frenzy_region,
|
|
||||||
LocationName.demolition_drain_pipe_region,
|
|
||||||
LocationName.ripsaw_rage_region,
|
|
||||||
LocationName.blazing_bazookas_region,
|
|
||||||
LocationName.low_g_labyrinth_region,
|
|
||||||
|
|
||||||
LocationName.krevice_kreepers_region,
|
|
||||||
LocationName.tearaway_toboggan_region,
|
|
||||||
LocationName.barrel_drop_bounce_region,
|
|
||||||
LocationName.krack_shot_kroc_region,
|
|
||||||
LocationName.lemguin_lunge_region,
|
|
||||||
|
|
||||||
LocationName.buzzer_barrage_region,
|
|
||||||
LocationName.kong_fused_cliffs_region,
|
|
||||||
LocationName.floodlit_fish_region,
|
|
||||||
LocationName.pothole_panic_region,
|
|
||||||
LocationName.ropey_rumpus_region,
|
|
||||||
|
|
||||||
LocationName.konveyor_rope_clash_region,
|
|
||||||
LocationName.creepy_caverns_region,
|
|
||||||
LocationName.lightning_lookout_region,
|
|
||||||
LocationName.koindozer_klamber_region,
|
|
||||||
LocationName.poisonous_pipeline_region,
|
|
||||||
|
|
||||||
LocationName.stampede_sprint_region,
|
|
||||||
LocationName.criss_cross_cliffs_region,
|
|
||||||
LocationName.tyrant_twin_tussle_region,
|
|
||||||
LocationName.swoopy_salvo_region,
|
|
||||||
#LocationName.rocket_rush_region,
|
|
||||||
]
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
import typing
|
|
||||||
|
|
||||||
from BaseClasses import Location
|
|
||||||
from .Names import LocationName
|
|
||||||
from worlds.AutoWorld import World
|
|
||||||
|
|
||||||
|
|
||||||
class DKC3Location(Location):
|
|
||||||
game: str = "Donkey Kong Country 3"
|
|
||||||
|
|
||||||
progress_byte: int = 0x000000
|
|
||||||
progress_bit: int = 0
|
|
||||||
inverted_bit: bool = False
|
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = '', address: int = None, parent=None, prog_byte: int = None, prog_bit: int = None, invert: bool = False):
|
|
||||||
super().__init__(player, name, address, parent)
|
|
||||||
self.progress_byte = prog_byte
|
|
||||||
self.progress_bit = prog_bit
|
|
||||||
self.inverted_bit = invert
|
|
||||||
|
|
||||||
|
|
||||||
level_location_table = {
|
|
||||||
LocationName.lakeside_limbo_flag: 0xDC3000,
|
|
||||||
LocationName.lakeside_limbo_bonus_1: 0xDC3001,
|
|
||||||
LocationName.lakeside_limbo_bonus_2: 0xDC3002,
|
|
||||||
LocationName.lakeside_limbo_dk: 0xDC3003,
|
|
||||||
|
|
||||||
LocationName.doorstop_dash_flag: 0xDC3004,
|
|
||||||
LocationName.doorstop_dash_bonus_1: 0xDC3005,
|
|
||||||
LocationName.doorstop_dash_bonus_2: 0xDC3006,
|
|
||||||
LocationName.doorstop_dash_dk: 0xDC3007,
|
|
||||||
|
|
||||||
LocationName.tidal_trouble_flag: 0xDC3008,
|
|
||||||
LocationName.tidal_trouble_bonus_1: 0xDC3009,
|
|
||||||
LocationName.tidal_trouble_bonus_2: 0xDC300A,
|
|
||||||
LocationName.tidal_trouble_dk: 0xDC300B,
|
|
||||||
|
|
||||||
LocationName.skiddas_row_flag: 0xDC300C,
|
|
||||||
LocationName.skiddas_row_bonus_1: 0xDC300D,
|
|
||||||
LocationName.skiddas_row_bonus_2: 0xDC300E,
|
|
||||||
LocationName.skiddas_row_dk: 0xDC300F,
|
|
||||||
|
|
||||||
LocationName.murky_mill_flag: 0xDC3010,
|
|
||||||
LocationName.murky_mill_bonus_1: 0xDC3011,
|
|
||||||
LocationName.murky_mill_bonus_2: 0xDC3012,
|
|
||||||
LocationName.murky_mill_dk: 0xDC3013,
|
|
||||||
|
|
||||||
LocationName.barrel_shield_bust_up_flag: 0xDC3014,
|
|
||||||
LocationName.barrel_shield_bust_up_bonus_1: 0xDC3015,
|
|
||||||
LocationName.barrel_shield_bust_up_bonus_2: 0xDC3016,
|
|
||||||
LocationName.barrel_shield_bust_up_dk: 0xDC3017,
|
|
||||||
|
|
||||||
LocationName.riverside_race_flag: 0xDC3018,
|
|
||||||
LocationName.riverside_race_bonus_1: 0xDC3019,
|
|
||||||
LocationName.riverside_race_bonus_2: 0xDC301A,
|
|
||||||
LocationName.riverside_race_dk: 0xDC301B,
|
|
||||||
|
|
||||||
LocationName.squeals_on_wheels_flag: 0xDC301C,
|
|
||||||
LocationName.squeals_on_wheels_bonus_1: 0xDC301D,
|
|
||||||
LocationName.squeals_on_wheels_bonus_2: 0xDC301E,
|
|
||||||
LocationName.squeals_on_wheels_dk: 0xDC301F,
|
|
||||||
|
|
||||||
LocationName.springin_spiders_flag: 0xDC3020,
|
|
||||||
LocationName.springin_spiders_bonus_1: 0xDC3021,
|
|
||||||
LocationName.springin_spiders_bonus_2: 0xDC3022,
|
|
||||||
LocationName.springin_spiders_dk: 0xDC3023,
|
|
||||||
|
|
||||||
LocationName.bobbing_barrel_brawl_flag: 0xDC3024,
|
|
||||||
LocationName.bobbing_barrel_brawl_bonus_1: 0xDC3025,
|
|
||||||
LocationName.bobbing_barrel_brawl_bonus_2: 0xDC3026,
|
|
||||||
LocationName.bobbing_barrel_brawl_dk: 0xDC3027,
|
|
||||||
|
|
||||||
LocationName.bazzas_blockade_flag: 0xDC3028,
|
|
||||||
LocationName.bazzas_blockade_bonus_1: 0xDC3029,
|
|
||||||
LocationName.bazzas_blockade_bonus_2: 0xDC302A,
|
|
||||||
LocationName.bazzas_blockade_dk: 0xDC302B,
|
|
||||||
|
|
||||||
LocationName.rocket_barrel_ride_flag: 0xDC302C,
|
|
||||||
LocationName.rocket_barrel_ride_bonus_1: 0xDC302D,
|
|
||||||
LocationName.rocket_barrel_ride_bonus_2: 0xDC302E,
|
|
||||||
LocationName.rocket_barrel_ride_dk: 0xDC302F,
|
|
||||||
|
|
||||||
LocationName.kreeping_klasps_flag: 0xDC3030,
|
|
||||||
LocationName.kreeping_klasps_bonus_1: 0xDC3031,
|
|
||||||
LocationName.kreeping_klasps_bonus_2: 0xDC3032,
|
|
||||||
LocationName.kreeping_klasps_dk: 0xDC3033,
|
|
||||||
|
|
||||||
LocationName.tracker_barrel_trek_flag: 0xDC3034,
|
|
||||||
LocationName.tracker_barrel_trek_bonus_1: 0xDC3035,
|
|
||||||
LocationName.tracker_barrel_trek_bonus_2: 0xDC3036,
|
|
||||||
LocationName.tracker_barrel_trek_dk: 0xDC3037,
|
|
||||||
|
|
||||||
LocationName.fish_food_frenzy_flag: 0xDC3038,
|
|
||||||
LocationName.fish_food_frenzy_bonus_1: 0xDC3039,
|
|
||||||
LocationName.fish_food_frenzy_bonus_2: 0xDC303A,
|
|
||||||
LocationName.fish_food_frenzy_dk: 0xDC303B,
|
|
||||||
|
|
||||||
LocationName.fire_ball_frenzy_flag: 0xDC303C,
|
|
||||||
LocationName.fire_ball_frenzy_bonus_1: 0xDC303D,
|
|
||||||
LocationName.fire_ball_frenzy_bonus_2: 0xDC303E,
|
|
||||||
LocationName.fire_ball_frenzy_dk: 0xDC303F,
|
|
||||||
|
|
||||||
LocationName.demolition_drain_pipe_flag: 0xDC3040,
|
|
||||||
LocationName.demolition_drain_pipe_bonus_1: 0xDC3041,
|
|
||||||
LocationName.demolition_drain_pipe_bonus_2: 0xDC3042,
|
|
||||||
LocationName.demolition_drain_pipe_dk: 0xDC3043,
|
|
||||||
|
|
||||||
LocationName.ripsaw_rage_flag: 0xDC3044,
|
|
||||||
LocationName.ripsaw_rage_bonus_1: 0xDC3045,
|
|
||||||
LocationName.ripsaw_rage_bonus_2: 0xDC3046,
|
|
||||||
LocationName.ripsaw_rage_dk: 0xDC3047,
|
|
||||||
|
|
||||||
LocationName.blazing_bazookas_flag: 0xDC3048,
|
|
||||||
LocationName.blazing_bazookas_bonus_1: 0xDC3049,
|
|
||||||
LocationName.blazing_bazookas_bonus_2: 0xDC304A,
|
|
||||||
LocationName.blazing_bazookas_dk: 0xDC304B,
|
|
||||||
|
|
||||||
LocationName.low_g_labyrinth_flag: 0xDC304C,
|
|
||||||
LocationName.low_g_labyrinth_bonus_1: 0xDC304D,
|
|
||||||
LocationName.low_g_labyrinth_bonus_2: 0xDC304E,
|
|
||||||
LocationName.low_g_labyrinth_dk: 0xDC304F,
|
|
||||||
|
|
||||||
LocationName.krevice_kreepers_flag: 0xDC3050,
|
|
||||||
LocationName.krevice_kreepers_bonus_1: 0xDC3051,
|
|
||||||
LocationName.krevice_kreepers_bonus_2: 0xDC3052,
|
|
||||||
LocationName.krevice_kreepers_dk: 0xDC3053,
|
|
||||||
|
|
||||||
LocationName.tearaway_toboggan_flag: 0xDC3054,
|
|
||||||
LocationName.tearaway_toboggan_bonus_1: 0xDC3055,
|
|
||||||
LocationName.tearaway_toboggan_bonus_2: 0xDC3056,
|
|
||||||
LocationName.tearaway_toboggan_dk: 0xDC3057,
|
|
||||||
|
|
||||||
LocationName.barrel_drop_bounce_flag: 0xDC3058,
|
|
||||||
LocationName.barrel_drop_bounce_bonus_1: 0xDC3059,
|
|
||||||
LocationName.barrel_drop_bounce_bonus_2: 0xDC305A,
|
|
||||||
LocationName.barrel_drop_bounce_dk: 0xDC305B,
|
|
||||||
|
|
||||||
LocationName.krack_shot_kroc_flag: 0xDC305C,
|
|
||||||
LocationName.krack_shot_kroc_bonus_1: 0xDC305D,
|
|
||||||
LocationName.krack_shot_kroc_bonus_2: 0xDC305E,
|
|
||||||
LocationName.krack_shot_kroc_dk: 0xDC305F,
|
|
||||||
|
|
||||||
LocationName.lemguin_lunge_flag: 0xDC3060,
|
|
||||||
LocationName.lemguin_lunge_bonus_1: 0xDC3061,
|
|
||||||
LocationName.lemguin_lunge_bonus_2: 0xDC3062,
|
|
||||||
LocationName.lemguin_lunge_dk: 0xDC3063,
|
|
||||||
|
|
||||||
LocationName.buzzer_barrage_flag: 0xDC3064,
|
|
||||||
LocationName.buzzer_barrage_bonus_1: 0xDC3065,
|
|
||||||
LocationName.buzzer_barrage_bonus_2: 0xDC3066,
|
|
||||||
LocationName.buzzer_barrage_dk: 0xDC3067,
|
|
||||||
|
|
||||||
LocationName.kong_fused_cliffs_flag: 0xDC3068,
|
|
||||||
LocationName.kong_fused_cliffs_bonus_1: 0xDC3069,
|
|
||||||
LocationName.kong_fused_cliffs_bonus_2: 0xDC306A,
|
|
||||||
LocationName.kong_fused_cliffs_dk: 0xDC306B,
|
|
||||||
|
|
||||||
LocationName.floodlit_fish_flag: 0xDC306C,
|
|
||||||
LocationName.floodlit_fish_bonus_1: 0xDC306D,
|
|
||||||
LocationName.floodlit_fish_bonus_2: 0xDC306E,
|
|
||||||
LocationName.floodlit_fish_dk: 0xDC306F,
|
|
||||||
|
|
||||||
LocationName.pothole_panic_flag: 0xDC3070,
|
|
||||||
LocationName.pothole_panic_bonus_1: 0xDC3071,
|
|
||||||
LocationName.pothole_panic_bonus_2: 0xDC3072,
|
|
||||||
LocationName.pothole_panic_dk: 0xDC3073,
|
|
||||||
|
|
||||||
LocationName.ropey_rumpus_flag: 0xDC3074,
|
|
||||||
LocationName.ropey_rumpus_bonus_1: 0xDC3075,
|
|
||||||
LocationName.ropey_rumpus_bonus_2: 0xDC3076,
|
|
||||||
LocationName.ropey_rumpus_dk: 0xDC3077,
|
|
||||||
|
|
||||||
LocationName.konveyor_rope_clash_flag: 0xDC3078,
|
|
||||||
LocationName.konveyor_rope_clash_bonus_1: 0xDC3079,
|
|
||||||
LocationName.konveyor_rope_clash_bonus_2: 0xDC307A,
|
|
||||||
LocationName.konveyor_rope_clash_dk: 0xDC307B,
|
|
||||||
|
|
||||||
LocationName.creepy_caverns_flag: 0xDC307C,
|
|
||||||
LocationName.creepy_caverns_bonus_1: 0xDC307D,
|
|
||||||
LocationName.creepy_caverns_bonus_2: 0xDC307E,
|
|
||||||
LocationName.creepy_caverns_dk: 0xDC307F,
|
|
||||||
|
|
||||||
LocationName.lightning_lookout_flag: 0xDC3080,
|
|
||||||
LocationName.lightning_lookout_bonus_1: 0xDC3081,
|
|
||||||
LocationName.lightning_lookout_bonus_2: 0xDC3082,
|
|
||||||
LocationName.lightning_lookout_dk: 0xDC3083,
|
|
||||||
|
|
||||||
LocationName.koindozer_klamber_flag: 0xDC3084,
|
|
||||||
LocationName.koindozer_klamber_bonus_1: 0xDC3085,
|
|
||||||
LocationName.koindozer_klamber_bonus_2: 0xDC3086,
|
|
||||||
LocationName.koindozer_klamber_dk: 0xDC3087,
|
|
||||||
|
|
||||||
LocationName.poisonous_pipeline_flag: 0xDC3088,
|
|
||||||
LocationName.poisonous_pipeline_bonus_1: 0xDC3089,
|
|
||||||
LocationName.poisonous_pipeline_bonus_2: 0xDC308A,
|
|
||||||
LocationName.poisonous_pipeline_dk: 0xDC308B,
|
|
||||||
|
|
||||||
LocationName.stampede_sprint_flag: 0xDC308C,
|
|
||||||
LocationName.stampede_sprint_bonus_1: 0xDC308D,
|
|
||||||
LocationName.stampede_sprint_bonus_2: 0xDC308E,
|
|
||||||
LocationName.stampede_sprint_bonus_3: 0xDC308F,
|
|
||||||
LocationName.stampede_sprint_dk: 0xDC3090,
|
|
||||||
|
|
||||||
LocationName.criss_cross_cliffs_flag: 0xDC3091,
|
|
||||||
LocationName.criss_cross_cliffs_bonus_1: 0xDC3092,
|
|
||||||
LocationName.criss_cross_cliffs_bonus_2: 0xDC3093,
|
|
||||||
LocationName.criss_cross_cliffs_dk: 0xDC3094,
|
|
||||||
|
|
||||||
LocationName.tyrant_twin_tussle_flag: 0xDC3095,
|
|
||||||
LocationName.tyrant_twin_tussle_bonus_1: 0xDC3096,
|
|
||||||
LocationName.tyrant_twin_tussle_bonus_2: 0xDC3097,
|
|
||||||
LocationName.tyrant_twin_tussle_bonus_3: 0xDC3098,
|
|
||||||
LocationName.tyrant_twin_tussle_dk: 0xDC3099,
|
|
||||||
|
|
||||||
LocationName.swoopy_salvo_flag: 0xDC309A,
|
|
||||||
LocationName.swoopy_salvo_bonus_1: 0xDC309B,
|
|
||||||
LocationName.swoopy_salvo_bonus_2: 0xDC309C,
|
|
||||||
LocationName.swoopy_salvo_bonus_3: 0xDC309D,
|
|
||||||
LocationName.swoopy_salvo_dk: 0xDC309E,
|
|
||||||
|
|
||||||
LocationName.rocket_rush_flag: 0xDC309F,
|
|
||||||
LocationName.rocket_rush_dk: 0xDC30A0,
|
|
||||||
}
|
|
||||||
|
|
||||||
kong_location_table = {
|
|
||||||
LocationName.lakeside_limbo_kong: 0xDC3100,
|
|
||||||
LocationName.doorstop_dash_kong: 0xDC3104,
|
|
||||||
LocationName.tidal_trouble_kong: 0xDC3108,
|
|
||||||
LocationName.skiddas_row_kong: 0xDC310C,
|
|
||||||
LocationName.murky_mill_kong: 0xDC3110,
|
|
||||||
|
|
||||||
LocationName.barrel_shield_bust_up_kong: 0xDC3114,
|
|
||||||
LocationName.riverside_race_kong: 0xDC3118,
|
|
||||||
LocationName.squeals_on_wheels_kong: 0xDC311C,
|
|
||||||
LocationName.springin_spiders_kong: 0xDC3120,
|
|
||||||
LocationName.bobbing_barrel_brawl_kong: 0xDC3124,
|
|
||||||
|
|
||||||
LocationName.bazzas_blockade_kong: 0xDC3128,
|
|
||||||
LocationName.rocket_barrel_ride_kong: 0xDC312C,
|
|
||||||
LocationName.kreeping_klasps_kong: 0xDC3130,
|
|
||||||
LocationName.tracker_barrel_trek_kong: 0xDC3134,
|
|
||||||
LocationName.fish_food_frenzy_kong: 0xDC3138,
|
|
||||||
|
|
||||||
LocationName.fire_ball_frenzy_kong: 0xDC313C,
|
|
||||||
LocationName.demolition_drain_pipe_kong: 0xDC3140,
|
|
||||||
LocationName.ripsaw_rage_kong: 0xDC3144,
|
|
||||||
LocationName.blazing_bazookas_kong: 0xDC3148,
|
|
||||||
LocationName.low_g_labyrinth_kong: 0xDC314C,
|
|
||||||
|
|
||||||
LocationName.krevice_kreepers_kong: 0xDC3150,
|
|
||||||
LocationName.tearaway_toboggan_kong: 0xDC3154,
|
|
||||||
LocationName.barrel_drop_bounce_kong: 0xDC3158,
|
|
||||||
LocationName.krack_shot_kroc_kong: 0xDC315C,
|
|
||||||
LocationName.lemguin_lunge_kong: 0xDC3160,
|
|
||||||
|
|
||||||
LocationName.buzzer_barrage_kong: 0xDC3164,
|
|
||||||
LocationName.kong_fused_cliffs_kong: 0xDC3168,
|
|
||||||
LocationName.floodlit_fish_kong: 0xDC316C,
|
|
||||||
LocationName.pothole_panic_kong: 0xDC3170,
|
|
||||||
LocationName.ropey_rumpus_kong: 0xDC3174,
|
|
||||||
|
|
||||||
LocationName.konveyor_rope_clash_kong: 0xDC3178,
|
|
||||||
LocationName.creepy_caverns_kong: 0xDC317C,
|
|
||||||
LocationName.lightning_lookout_kong: 0xDC3180,
|
|
||||||
LocationName.koindozer_klamber_kong: 0xDC3184,
|
|
||||||
LocationName.poisonous_pipeline_kong: 0xDC3188,
|
|
||||||
|
|
||||||
LocationName.stampede_sprint_kong: 0xDC318C,
|
|
||||||
LocationName.criss_cross_cliffs_kong: 0xDC3191,
|
|
||||||
LocationName.tyrant_twin_tussle_kong: 0xDC3195,
|
|
||||||
LocationName.swoopy_salvo_kong: 0xDC319A,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
boss_location_table = {
|
|
||||||
LocationName.belchas_barn: 0xDC30A1,
|
|
||||||
LocationName.arichs_ambush: 0xDC30A2,
|
|
||||||
LocationName.squirts_showdown: 0xDC30A3,
|
|
||||||
LocationName.kaos_karnage: 0xDC30A4,
|
|
||||||
LocationName.bleaks_house: 0xDC30A5,
|
|
||||||
LocationName.barboss_barrier: 0xDC30A6,
|
|
||||||
LocationName.kastle_kaos: 0xDC30A7,
|
|
||||||
LocationName.knautilus: 0xDC30A8,
|
|
||||||
}
|
|
||||||
|
|
||||||
secret_cave_location_table = {
|
|
||||||
LocationName.belchas_burrow: 0xDC30A9,
|
|
||||||
LocationName.kong_cave: 0xDC30AA,
|
|
||||||
LocationName.undercover_cove: 0xDC30AB,
|
|
||||||
LocationName.ks_cache: 0xDC30AC,
|
|
||||||
LocationName.hill_top_hoard: 0xDC30AD,
|
|
||||||
LocationName.bounty_beach: 0xDC30AE,
|
|
||||||
LocationName.smugglers_cove: 0xDC30AF,
|
|
||||||
LocationName.arichs_hoard: 0xDC30B0,
|
|
||||||
LocationName.bounty_bay: 0xDC30B1,
|
|
||||||
LocationName.sky_high_secret: 0xDC30B2,
|
|
||||||
LocationName.glacial_grotto: 0xDC30B3,
|
|
||||||
LocationName.cifftop_cache: 0xDC30B4,
|
|
||||||
LocationName.sewer_stockpile: 0xDC30B5,
|
|
||||||
LocationName.banana_bird_mother: 0xDC30B6,
|
|
||||||
}
|
|
||||||
|
|
||||||
brothers_bear_location_table = {
|
|
||||||
LocationName.bazaars_general_store_1: 0xDC30B7,
|
|
||||||
LocationName.bazaars_general_store_2: 0xDC30B8,
|
|
||||||
LocationName.brambles_bungalow: 0xDC30B9,
|
|
||||||
LocationName.flower_spot: 0xDC30BA,
|
|
||||||
LocationName.barters_swap_shop: 0xDC30BB,
|
|
||||||
LocationName.barnacles_island: 0xDC30BC,
|
|
||||||
LocationName.blues_beach_hut: 0xDC30BD,
|
|
||||||
LocationName.blizzards_basecamp: 0xDC30BE,
|
|
||||||
}
|
|
||||||
|
|
||||||
all_locations = {
|
|
||||||
**level_location_table,
|
|
||||||
**boss_location_table,
|
|
||||||
**secret_cave_location_table,
|
|
||||||
**brothers_bear_location_table,
|
|
||||||
**kong_location_table,
|
|
||||||
}
|
|
||||||
|
|
||||||
location_table = {}
|
|
||||||
|
|
||||||
|
|
||||||
def setup_locations(world: World):
|
|
||||||
location_table = {**level_location_table, **boss_location_table, **secret_cave_location_table}
|
|
||||||
|
|
||||||
if False:#world.options.include_trade_sequence:
|
|
||||||
location_table.update({**brothers_bear_location_table})
|
|
||||||
|
|
||||||
if world.options.kongsanity:
|
|
||||||
location_table.update({**kong_location_table})
|
|
||||||
|
|
||||||
return location_table
|
|
||||||
|
|
||||||
|
|
||||||
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Junk Definitions
|
|
||||||
one_up_balloon = "1-Up Balloon"
|
|
||||||
bear_coin = "Bear Coin"
|
|
||||||
|
|
||||||
# Collectable Definitions
|
|
||||||
bonus_coin = "Bonus Coin"
|
|
||||||
dk_coin = "DK Coin"
|
|
||||||
banana_bird = "Banana Bird"
|
|
||||||
krematoa_cog = "Krematoa Cog"
|
|
||||||
|
|
||||||
# Inventory Definitions
|
|
||||||
progressive_boat = "Progressive Boat Upgrade"
|
|
||||||
present = "Present"
|
|
||||||
bowling_ball = "Bowling Ball"
|
|
||||||
shell = "Shell"
|
|
||||||
mirror = "Mirror"
|
|
||||||
flower = "Flupperius Petallus Pongus"
|
|
||||||
wrench = "No. 6 Wrench"
|
|
||||||
|
|
||||||
# Other Definitions
|
|
||||||
victory = "Donkey Kong"
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
# Level Definitions
|
|
||||||
lakeside_limbo_flag = "Lakeside Limbo - Flag"
|
|
||||||
lakeside_limbo_kong = "Lakeside Limbo - KONG"
|
|
||||||
lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1"
|
|
||||||
lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2"
|
|
||||||
lakeside_limbo_dk = "Lakeside Limbo - DK Coin"
|
|
||||||
|
|
||||||
doorstop_dash_flag = "Doorstop Dash - Flag"
|
|
||||||
doorstop_dash_kong = "Doorstop Dash - KONG"
|
|
||||||
doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1"
|
|
||||||
doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2"
|
|
||||||
doorstop_dash_dk = "Doorstop Dash - DK Coin"
|
|
||||||
|
|
||||||
tidal_trouble_flag = "Tidal Trouble - Flag"
|
|
||||||
tidal_trouble_kong = "Tidal Trouble - KONG"
|
|
||||||
tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1"
|
|
||||||
tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2"
|
|
||||||
tidal_trouble_dk = "Tidal Trouble - DK Coin"
|
|
||||||
|
|
||||||
skiddas_row_flag = "Skidda's Row - Flag"
|
|
||||||
skiddas_row_kong = "Skidda's Row - KONG"
|
|
||||||
skiddas_row_bonus_1 = "Skidda's Row - Bonus 1"
|
|
||||||
skiddas_row_bonus_2 = "Skidda's Row - Bonus 2"
|
|
||||||
skiddas_row_dk = "Skidda's Row - DK Coin"
|
|
||||||
|
|
||||||
murky_mill_flag = "Murky Mill - Flag"
|
|
||||||
murky_mill_kong = "Murky Mill - KONG"
|
|
||||||
murky_mill_bonus_1 = "Murky Mill - Bonus 1"
|
|
||||||
murky_mill_bonus_2 = "Murky Mill - Bonus 2"
|
|
||||||
murky_mill_dk = "Murky Mill - DK Coin"
|
|
||||||
|
|
||||||
barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag"
|
|
||||||
barrel_shield_bust_up_kong = "Barrel Shield Bust-Up - KONG"
|
|
||||||
barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1"
|
|
||||||
barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2"
|
|
||||||
barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin"
|
|
||||||
|
|
||||||
riverside_race_flag = "Riverside Race - Flag"
|
|
||||||
riverside_race_kong = "Riverside Race - KONG"
|
|
||||||
riverside_race_bonus_1 = "Riverside Race - Bonus 1"
|
|
||||||
riverside_race_bonus_2 = "Riverside Race - Bonus 2"
|
|
||||||
riverside_race_dk = "Riverside Race - DK Coin"
|
|
||||||
|
|
||||||
squeals_on_wheels_flag = "Squeals On Wheels - Flag"
|
|
||||||
squeals_on_wheels_kong = "Squeals On Wheels - KONG"
|
|
||||||
squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1"
|
|
||||||
squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2"
|
|
||||||
squeals_on_wheels_dk = "Squeals On Wheels - DK Coin"
|
|
||||||
|
|
||||||
springin_spiders_flag = "Springin' Spiders - Flag"
|
|
||||||
springin_spiders_kong = "Springin' Spiders - KONG"
|
|
||||||
springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1"
|
|
||||||
springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2"
|
|
||||||
springin_spiders_dk = "Springin' Spiders - DK Coin"
|
|
||||||
|
|
||||||
bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag"
|
|
||||||
bobbing_barrel_brawl_kong = "Bobbing Barrel Brawl - KONG"
|
|
||||||
bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1"
|
|
||||||
bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2"
|
|
||||||
bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin"
|
|
||||||
|
|
||||||
bazzas_blockade_flag = "Bazza's Blockade - Flag"
|
|
||||||
bazzas_blockade_kong = "Bazza's Blockade - KONG"
|
|
||||||
bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1"
|
|
||||||
bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2"
|
|
||||||
bazzas_blockade_dk = "Bazza's Blockade - DK Coin"
|
|
||||||
|
|
||||||
rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag"
|
|
||||||
rocket_barrel_ride_kong = "Rocket Barrel Ride - KONG"
|
|
||||||
rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1"
|
|
||||||
rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2"
|
|
||||||
rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin"
|
|
||||||
|
|
||||||
kreeping_klasps_flag = "Kreeping Klasps - Flag"
|
|
||||||
kreeping_klasps_kong = "Kreeping Klasps - KONG"
|
|
||||||
kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1"
|
|
||||||
kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2"
|
|
||||||
kreeping_klasps_dk = "Kreeping Klasps - DK Coin"
|
|
||||||
|
|
||||||
tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag"
|
|
||||||
tracker_barrel_trek_kong = "Tracker Barrel Trek - KONG"
|
|
||||||
tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1"
|
|
||||||
tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2"
|
|
||||||
tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin"
|
|
||||||
|
|
||||||
fish_food_frenzy_flag = "Fish Food Frenzy - Flag"
|
|
||||||
fish_food_frenzy_kong = "Fish Food Frenzy - KONG"
|
|
||||||
fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1"
|
|
||||||
fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2"
|
|
||||||
fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin"
|
|
||||||
|
|
||||||
fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag"
|
|
||||||
fire_ball_frenzy_kong = "Fire-Ball Frenzy - KONG"
|
|
||||||
fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1"
|
|
||||||
fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2"
|
|
||||||
fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin"
|
|
||||||
|
|
||||||
demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag"
|
|
||||||
demolition_drain_pipe_kong = "Demolition Drain-Pipe - KONG"
|
|
||||||
demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1"
|
|
||||||
demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2"
|
|
||||||
demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin"
|
|
||||||
|
|
||||||
ripsaw_rage_flag = "Ripsaw Rage - Flag"
|
|
||||||
ripsaw_rage_kong = "Ripsaw Rage - KONG"
|
|
||||||
ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1"
|
|
||||||
ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2"
|
|
||||||
ripsaw_rage_dk = "Ripsaw Rage - DK Coin"
|
|
||||||
|
|
||||||
blazing_bazookas_flag = "Blazing Bazukas - Flag"
|
|
||||||
blazing_bazookas_kong = "Blazing Bazukas - KONG"
|
|
||||||
blazing_bazookas_bonus_1 = "Blazing Bazukas - Bonus 1"
|
|
||||||
blazing_bazookas_bonus_2 = "Blazing Bazukas - Bonus 2"
|
|
||||||
blazing_bazookas_dk = "Blazing Bazukas - DK Coin"
|
|
||||||
|
|
||||||
low_g_labyrinth_flag = "Low-G Labyrinth - Flag"
|
|
||||||
low_g_labyrinth_kong = "Low-G Labyrinth - KONG"
|
|
||||||
low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1"
|
|
||||||
low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2"
|
|
||||||
low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin"
|
|
||||||
|
|
||||||
krevice_kreepers_flag = "Krevice Kreepers - Flag"
|
|
||||||
krevice_kreepers_kong = "Krevice Kreepers - KONG"
|
|
||||||
krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1"
|
|
||||||
krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2"
|
|
||||||
krevice_kreepers_dk = "Krevice Kreepers - DK Coin"
|
|
||||||
|
|
||||||
tearaway_toboggan_flag = "Tearaway Toboggan - Flag"
|
|
||||||
tearaway_toboggan_kong = "Tearaway Toboggan - KONG"
|
|
||||||
tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1"
|
|
||||||
tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2"
|
|
||||||
tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin"
|
|
||||||
|
|
||||||
barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag"
|
|
||||||
barrel_drop_bounce_kong = "Barrel Drop Bounce - KONG"
|
|
||||||
barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1"
|
|
||||||
barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2"
|
|
||||||
barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin"
|
|
||||||
|
|
||||||
krack_shot_kroc_flag = "Krack-Shot Kroc - Flag"
|
|
||||||
krack_shot_kroc_kong = "Krack-Shot Kroc - KONG"
|
|
||||||
krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1"
|
|
||||||
krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2"
|
|
||||||
krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin"
|
|
||||||
|
|
||||||
lemguin_lunge_flag = "Lemguin Lunge - Flag"
|
|
||||||
lemguin_lunge_kong = "Lemguin Lunge - KONG"
|
|
||||||
lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1"
|
|
||||||
lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2"
|
|
||||||
lemguin_lunge_dk = "Lemguin Lunge - DK Coin"
|
|
||||||
|
|
||||||
buzzer_barrage_flag = "Buzzer Barrage - Flag"
|
|
||||||
buzzer_barrage_kong = "Buzzer Barrage - KONG"
|
|
||||||
buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1"
|
|
||||||
buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2"
|
|
||||||
buzzer_barrage_dk = "Buzzer Barrage - DK Coin"
|
|
||||||
|
|
||||||
kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag"
|
|
||||||
kong_fused_cliffs_kong = "Kong-Fused Cliffs - KONG"
|
|
||||||
kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1"
|
|
||||||
kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2"
|
|
||||||
kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin"
|
|
||||||
|
|
||||||
floodlit_fish_flag = "Floodlit Fish - Flag"
|
|
||||||
floodlit_fish_kong = "Floodlit Fish - KONG"
|
|
||||||
floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1"
|
|
||||||
floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2"
|
|
||||||
floodlit_fish_dk = "Floodlit Fish - DK Coin"
|
|
||||||
|
|
||||||
pothole_panic_flag = "Pothole Panic - Flag"
|
|
||||||
pothole_panic_kong = "Pothole Panic - KONG"
|
|
||||||
pothole_panic_bonus_1 = "Pothole Panic - Bonus 1"
|
|
||||||
pothole_panic_bonus_2 = "Pothole Panic - Bonus 2"
|
|
||||||
pothole_panic_dk = "Pothole Panic - DK Coin"
|
|
||||||
|
|
||||||
ropey_rumpus_flag = "Ropey Rumpus - Flag"
|
|
||||||
ropey_rumpus_kong = "Ropey Rumpus - KONG"
|
|
||||||
ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1"
|
|
||||||
ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2"
|
|
||||||
ropey_rumpus_dk = "Ropey Rumpus - DK Coin"
|
|
||||||
|
|
||||||
konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag"
|
|
||||||
konveyor_rope_clash_kong = "Konveyor Rope Klash - KONG"
|
|
||||||
konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1"
|
|
||||||
konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2"
|
|
||||||
konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin"
|
|
||||||
|
|
||||||
creepy_caverns_flag = "Creepy Caverns - Flag"
|
|
||||||
creepy_caverns_kong = "Creepy Caverns - KONG"
|
|
||||||
creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1"
|
|
||||||
creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2"
|
|
||||||
creepy_caverns_dk = "Creepy Caverns - DK Coin"
|
|
||||||
|
|
||||||
lightning_lookout_flag = "Lightning Lookout - Flag"
|
|
||||||
lightning_lookout_kong = "Lightning Lookout - KONG"
|
|
||||||
lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1"
|
|
||||||
lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2"
|
|
||||||
lightning_lookout_dk = "Lightning Lookout - DK Coin"
|
|
||||||
|
|
||||||
koindozer_klamber_flag = "Koindozer Klamber - Flag"
|
|
||||||
koindozer_klamber_kong = "Koindozer Klamber - KONG"
|
|
||||||
koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1"
|
|
||||||
koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2"
|
|
||||||
koindozer_klamber_dk = "Koindozer Klamber - DK Coin"
|
|
||||||
|
|
||||||
poisonous_pipeline_flag = "Poisonous Pipeline - Flag"
|
|
||||||
poisonous_pipeline_kong = "Poisonous Pipeline - KONG"
|
|
||||||
poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1"
|
|
||||||
poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2"
|
|
||||||
poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin"
|
|
||||||
|
|
||||||
stampede_sprint_flag = "Stampede Sprint - Flag"
|
|
||||||
stampede_sprint_kong = "Stampede Sprint - KONG"
|
|
||||||
stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1"
|
|
||||||
stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2"
|
|
||||||
stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3"
|
|
||||||
stampede_sprint_dk = "Stampede Sprint - DK Coin"
|
|
||||||
|
|
||||||
criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag"
|
|
||||||
criss_cross_cliffs_kong = "Criss Kross Cliffs - KONG"
|
|
||||||
criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1"
|
|
||||||
criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2"
|
|
||||||
criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin"
|
|
||||||
|
|
||||||
tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag"
|
|
||||||
tyrant_twin_tussle_kong = "Tyrant Twin Tussle - KONG"
|
|
||||||
tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1"
|
|
||||||
tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2"
|
|
||||||
tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3"
|
|
||||||
tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin"
|
|
||||||
|
|
||||||
swoopy_salvo_flag = "Swoopy Salvo - Flag"
|
|
||||||
swoopy_salvo_kong = "Swoopy Salvo - KONG"
|
|
||||||
swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1"
|
|
||||||
swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2"
|
|
||||||
swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3"
|
|
||||||
swoopy_salvo_dk = "Swoopy Salvo - DK Coin"
|
|
||||||
|
|
||||||
rocket_rush_flag = "Rocket Rush - Flag"
|
|
||||||
rocket_rush_dk = "Rocket Rush - DK Coin"
|
|
||||||
|
|
||||||
# Boss Definitions
|
|
||||||
belchas_barn = "Belcha's Barn"
|
|
||||||
arichs_ambush = "Arich's Ambush"
|
|
||||||
squirts_showdown = "Squirt's Showdown"
|
|
||||||
kaos_karnage = "KAOS Karnage"
|
|
||||||
bleaks_house = "Bleak's House"
|
|
||||||
barboss_barrier = "Barbos's Barrier"
|
|
||||||
kastle_kaos = "Kastle KAOS"
|
|
||||||
knautilus = "Knautilus"
|
|
||||||
|
|
||||||
|
|
||||||
# Banana Bird Cave Definitions
|
|
||||||
belchas_burrow = "Belcha's Burrow"
|
|
||||||
kong_cave = "Kong Cave"
|
|
||||||
undercover_cove = "Undercover Cove"
|
|
||||||
ks_cache = "K's Cache"
|
|
||||||
hill_top_hoard = "Hill-Top Hoard"
|
|
||||||
bounty_beach = "Bounty Beach"
|
|
||||||
smugglers_cove = "Smuggler's Cove"
|
|
||||||
arichs_hoard = "Arich's Hoard"
|
|
||||||
bounty_bay = "Bounty Bay"
|
|
||||||
sky_high_secret = "Sky-High Secret"
|
|
||||||
glacial_grotto = "Glacial Grotto"
|
|
||||||
cifftop_cache = "Clifftop Cache"
|
|
||||||
sewer_stockpile = "Sewer Stockpile"
|
|
||||||
|
|
||||||
banana_bird_mother = "Banana Bird Mother"
|
|
||||||
|
|
||||||
|
|
||||||
# Brothers Bear Definitions
|
|
||||||
bazaars_general_store_1 = "Bazaar's General Store - 1"
|
|
||||||
bazaars_general_store_2 = "Bazaar's General Store - 2"
|
|
||||||
brambles_bungalow = "Bramble's Bungalow"
|
|
||||||
flower_spot = "Flower Spot"
|
|
||||||
barters_swap_shop = "Barter's Swap Shop"
|
|
||||||
barnacles_island = "Barnacle's Island"
|
|
||||||
blues_beach_hut = "Blue's Beach Hut"
|
|
||||||
blizzards_basecamp = "Bizzard's Basecamp"
|
|
||||||
|
|
||||||
|
|
||||||
# Region Definitions
|
|
||||||
menu_region = "Menu"
|
|
||||||
overworld_1_region = "Overworld 1"
|
|
||||||
overworld_2_region = "Overworld 2"
|
|
||||||
overworld_3_region = "Overworld 3"
|
|
||||||
overworld_4_region = "Overworld 4"
|
|
||||||
|
|
||||||
bazaar_region = "Bazaar's General Store Region"
|
|
||||||
bramble_region = "Bramble's Bungalow Region"
|
|
||||||
flower_spot_region = "Flower Spot Region"
|
|
||||||
barter_region = "Barter's Swap Shop Region"
|
|
||||||
barnacle_region = "Barnacle's Island Region"
|
|
||||||
blue_region = "Blue's Beach Hut Region"
|
|
||||||
blizzard_region = "Bizzard's Basecamp Region"
|
|
||||||
|
|
||||||
lake_orangatanga_region = "Lake Orangatanga"
|
|
||||||
kremwood_forest_region = "Kremwood Forest"
|
|
||||||
cotton_top_cove_region = "Cotton-Top Cove"
|
|
||||||
mekanos_region = "Mekanos"
|
|
||||||
k3_region = "K3"
|
|
||||||
razor_ridge_region = "Razor Ridge"
|
|
||||||
kaos_kore_region = "KAOS Kore"
|
|
||||||
krematoa_region = "Krematoa"
|
|
||||||
|
|
||||||
belchas_barn_region = "Belcha's Barn Region"
|
|
||||||
arichs_ambush_region = "Arich's Ambush Region"
|
|
||||||
squirts_showdown_region = "Squirt's Showdown Region"
|
|
||||||
kaos_karnage_region = "KAOS Karnage Region"
|
|
||||||
bleaks_house_region = "Bleak's House Region"
|
|
||||||
barboss_barrier_region = "Barbos's Barrier Region"
|
|
||||||
kastle_kaos_region = "Kastle KAOS Region"
|
|
||||||
knautilus_region = "Knautilus Region"
|
|
||||||
|
|
||||||
belchas_burrow_region = "Belcha's Burrow Region"
|
|
||||||
kong_cave_region = "Kong Cave Region"
|
|
||||||
undercover_cove_region = "Undercover Cove Region"
|
|
||||||
ks_cache_region = "K's Cache Region"
|
|
||||||
hill_top_hoard_region = "Hill-Top Hoard Region"
|
|
||||||
bounty_beach_region = "Bounty Beach Region"
|
|
||||||
smugglers_cove_region = "Smuggler's Cove Region"
|
|
||||||
arichs_hoard_region = "Arich's Hoard Region"
|
|
||||||
bounty_bay_region = "Bounty Bay Region"
|
|
||||||
sky_high_secret_region = "Sky-High Secret Region"
|
|
||||||
glacial_grotto_region = "Glacial Grotto Region"
|
|
||||||
cifftop_cache_region = "Clifftop Cache Region"
|
|
||||||
sewer_stockpile_region = "Sewer Stockpile Region"
|
|
||||||
|
|
||||||
lakeside_limbo_region = "Lakeside Limbo"
|
|
||||||
doorstop_dash_region = "Doorstop Dash"
|
|
||||||
tidal_trouble_region = "Tidal Trouble"
|
|
||||||
skiddas_row_region = "Skidda's Row"
|
|
||||||
murky_mill_region = "Murky Mill"
|
|
||||||
|
|
||||||
barrel_shield_bust_up_region = "Barrel Shield Bust-Up"
|
|
||||||
riverside_race_region = "Riverside Race"
|
|
||||||
squeals_on_wheels_region = "Squeals On Wheels"
|
|
||||||
springin_spiders_region = "Springin' Spiders"
|
|
||||||
bobbing_barrel_brawl_region = "Bobbing Barrel Brawl"
|
|
||||||
|
|
||||||
bazzas_blockade_region = "Bazza's Blockade"
|
|
||||||
rocket_barrel_ride_region = "Rocket Barrel Ride"
|
|
||||||
kreeping_klasps_region = "Kreeping Klasps"
|
|
||||||
tracker_barrel_trek_region = "Tracker Barrel Trek"
|
|
||||||
fish_food_frenzy_region = "Fish Food Frenzy"
|
|
||||||
|
|
||||||
fire_ball_frenzy_region = "Fire-Ball Frenzy"
|
|
||||||
demolition_drain_pipe_region = "Demolition Drain-Pipe"
|
|
||||||
ripsaw_rage_region = "Ripsaw Rage"
|
|
||||||
blazing_bazookas_region = "Blazing Bazukas"
|
|
||||||
low_g_labyrinth_region = "Low-G Labyrinth"
|
|
||||||
|
|
||||||
krevice_kreepers_region = "Krevice Kreepers"
|
|
||||||
tearaway_toboggan_region = "Tearaway Toboggan"
|
|
||||||
barrel_drop_bounce_region = "Barrel Drop Bounce"
|
|
||||||
krack_shot_kroc_region = "Krack-Shot Kroc"
|
|
||||||
lemguin_lunge_region = "Lemguin Lunge"
|
|
||||||
|
|
||||||
buzzer_barrage_region = "Buzzer Barrage"
|
|
||||||
kong_fused_cliffs_region = "Kong-Fused Cliffs"
|
|
||||||
floodlit_fish_region = "Floodlit Fish"
|
|
||||||
pothole_panic_region = "Pothole Panic"
|
|
||||||
ropey_rumpus_region = "Ropey Rumpus"
|
|
||||||
|
|
||||||
konveyor_rope_clash_region = "Konveyor Rope Klash"
|
|
||||||
creepy_caverns_region = "Creepy Caverns"
|
|
||||||
lightning_lookout_region = "Lightning Lookout"
|
|
||||||
koindozer_klamber_region = "Koindozer Klamber"
|
|
||||||
poisonous_pipeline_region = "Poisonous Pipeline"
|
|
||||||
|
|
||||||
stampede_sprint_region = "Stampede Sprint"
|
|
||||||
criss_cross_cliffs_region = "Criss Kross Cliffs"
|
|
||||||
tyrant_twin_tussle_region = "Tyrant Twin Tussle"
|
|
||||||
swoopy_salvo_region = "Swoopy Salvo"
|
|
||||||
rocket_rush_region = "Rocket Rush"
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user