mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-06-01 06:30:00 -07:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d241829585 | |||
| b24b3d10b6 | |||
| 787ddc400c | |||
| 545171f3f4 | |||
| d0abfeb88b | |||
| b575983599 | |||
| d8d148ac13 | |||
| 24f75ba072 | |||
| 38e77e1b46 | |||
| 799e0b7b0f | |||
| 4ef1fb7630 | |||
| 0cd81ff500 | |||
| 55a1b12cb7 | |||
| 3fcd337f65 | |||
| 7a5acfeceb | |||
| 06dc1b897c | |||
| 4a28888a66 | |||
| d0407a74d6 | |||
| e0810022f8 | |||
| 29a6f40c2b | |||
| 2bd572c23d | |||
| b59e52a103 | |||
| 6d9d340c71 | |||
| 4486510fbc | |||
| a68109f5a7 | |||
| 234b0581c9 | |||
| 798eeaad91 | |||
| 1875062099 | |||
| 9da3c29990 | |||
| 90b72c0fa5 | |||
| fd8f3fcd8b | |||
| d36ba62243 | |||
| 8ac335f92d | |||
| 74a0cd6022 | |||
| 31f5a6c4ea | |||
| be9c7b1728 | |||
| a0236b2d74 | |||
| af39d7926a | |||
| a48bf0195f | |||
| 73856f63c8 | |||
| d5cc201138 | |||
| 4346191c55 | |||
| 0601494e39 | |||
| 78937054de | |||
| 59674926ed | |||
| b716096a4b | |||
| c851eff521 | |||
| 01033940d6 | |||
| 15561e1e2d | |||
| 9c78edc764 | |||
| d2395de9fa | |||
| d41cec6494 | |||
| 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 |
@@ -46,7 +46,6 @@ dist
|
||||
/prof/
|
||||
README.html
|
||||
.vs/
|
||||
EnemizerCLI/
|
||||
/Players/
|
||||
/SNI/
|
||||
/sni-*/
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"../BizHawkClient.py",
|
||||
"../Patch.py",
|
||||
"../rule_builder/cached_world.py",
|
||||
"../rule_builder/field_resolvers.py",
|
||||
"../rule_builder/options.py",
|
||||
"../rule_builder/rules.py",
|
||||
"../test/param.py",
|
||||
@@ -18,6 +19,7 @@
|
||||
"../test/programs/test_multi_server.py",
|
||||
"../test/utils/__init__.py",
|
||||
"../test/webhost/test_descriptions.py",
|
||||
"../test/webhost/test_suuid.py",
|
||||
"../worlds/AutoSNIClient.py",
|
||||
"type_check.py"
|
||||
],
|
||||
|
||||
@@ -14,6 +14,8 @@ env:
|
||||
BEFORE: ${{ github.event.before }}
|
||||
AFTER: ${{ github.event.after }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
flake8-or-mypy:
|
||||
strategy:
|
||||
@@ -25,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: "Determine modified files (pull_request)"
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -50,7 +52,7 @@ jobs:
|
||||
run: |
|
||||
echo "diff=." >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6.2.0
|
||||
if: env.diff != ''
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
+15
-16
@@ -41,9 +41,9 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
# - copy code above to release.yml -
|
||||
- name: Attest Build
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: actions/attest-build-provenance@v2
|
||||
uses: actions/attest@v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher.exe
|
||||
@@ -110,18 +110,17 @@ jobs:
|
||||
cp Players/Templates/VVVVVV.yaml Players/
|
||||
timeout 30 ./ArchipelagoGenerate
|
||||
- name: Store 7z
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7.0.0
|
||||
with:
|
||||
name: ${{ env.ZIP_NAME }}
|
||||
path: dist/${{ env.ZIP_NAME }}
|
||||
compression-level: 0 # .7z is incompressible by zip
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7 # keep for 7 days, should be enough
|
||||
- name: Store Setup
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7.0.0
|
||||
with:
|
||||
name: ${{ env.SETUP_NAME }}
|
||||
path: setups/${{ env.SETUP_NAME }}
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7 # keep for 7 days, should be enough
|
||||
|
||||
@@ -129,14 +128,14 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
@@ -173,7 +172,7 @@ jobs:
|
||||
# - copy code above to release.yml -
|
||||
- name: Attest Build
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: actions/attest-build-provenance@v2
|
||||
uses: actions/attest@v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher
|
||||
@@ -204,17 +203,17 @@ jobs:
|
||||
cp Players/Templates/VVVVVV.yaml Players/
|
||||
timeout 30 ./ArchipelagoGenerate
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7.0.0
|
||||
with:
|
||||
name: ${{ 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
|
||||
retention-days: 7
|
||||
- name: Store .tar.gz
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7.0.0
|
||||
with:
|
||||
name: ${{ env.TAR_NAME }}
|
||||
path: dist/${{ env.TAR_NAME }}
|
||||
compression-level: 0 # .gz is incompressible by zip
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
@@ -17,17 +17,26 @@ on:
|
||||
paths:
|
||||
- '**.py'
|
||||
- '**.js'
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
- '.github/workflows/*.yml'
|
||||
- '.github/workflows/*.yaml'
|
||||
- '**/action.yml'
|
||||
- '**/action.yaml'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '**.js'
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
- '.github/workflows/*.yml'
|
||||
- '.github/workflows/*.yaml'
|
||||
- '**/action.yml'
|
||||
- '**/action.yaml'
|
||||
schedule:
|
||||
- cron: '44 8 * * 1'
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -36,18 +45,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
language: [ 'javascript', 'python', 'actions' ]
|
||||
# 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
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4.35.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- 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.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -72,4 +80,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4.35.1
|
||||
|
||||
@@ -24,6 +24,8 @@ on:
|
||||
- '**/CMakeLists.txt'
|
||||
- '.github/workflows/ctest.yml'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
ctest:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -35,7 +37,7 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
if: startsWith(matrix.os,'windows')
|
||||
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
|
||||
|
||||
@@ -19,6 +19,8 @@ on:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -29,7 +31,7 @@ jobs:
|
||||
package-name: ${{ steps.package.outputs.name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Set lowercase image name
|
||||
id: image
|
||||
@@ -43,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
|
||||
tags: |
|
||||
@@ -92,13 +94,13 @@ jobs:
|
||||
cache-scope: arm64
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- 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
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -115,7 +117,7 @@ jobs:
|
||||
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v7.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -135,7 +137,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: 'Apply content-based labels'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@v6.0.1
|
||||
with:
|
||||
sync-labels: false
|
||||
peer_review:
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
draft: true # don't publish right away, especially since windows build is added by hand
|
||||
prerelease: false
|
||||
@@ -48,9 +48,9 @@ jobs:
|
||||
shell: bash
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
# - code below copied from build.yml -
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
||||
# - code above copied from build.yml -
|
||||
- name: Attest Build
|
||||
uses: actions/attest-build-provenance@v2
|
||||
uses: actions/attest@v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher.exe
|
||||
@@ -97,13 +97,15 @@ jobs:
|
||||
build/exe.*/ArchipelagoServer.exe
|
||||
setups/*
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
name: Archipelago ${{ env.RELEASE_VERSION }}
|
||||
files: |
|
||||
setups/*
|
||||
fail_on_unmatched_files: true
|
||||
overwrite_files: false # Windows release is usually built by hand
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -114,14 +116,14 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
# - code below copied from build.yml -
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
@@ -157,7 +159,7 @@ jobs:
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - code above copied from build.yml -
|
||||
- name: Attest Build
|
||||
uses: actions/attest-build-provenance@v2
|
||||
uses: actions/attest@v4.1.0
|
||||
with:
|
||||
subject-path: |
|
||||
build/exe.*/ArchipelagoLauncher
|
||||
@@ -165,12 +167,14 @@ jobs:
|
||||
build/exe.*/ArchipelagoServer
|
||||
dist/*
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
draft: true # see above
|
||||
prerelease: false
|
||||
name: Archipelago ${{ env.RELEASE_VERSION }}
|
||||
files: |
|
||||
dist/*
|
||||
fail_on_unmatched_files: true
|
||||
overwrite_files: false # should never happen; avoids accidentally changing a release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -28,12 +28,14 @@ on:
|
||||
- 'requirements.txt'
|
||||
- '.github/workflows/scan-build.yml'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
scan-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install newer Clang
|
||||
@@ -45,7 +47,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt install clang-tools-19
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- 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
|
||||
- name: Store report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7.0.0
|
||||
with:
|
||||
name: scan-build-reports
|
||||
path: scan-build-reports
|
||||
compression-level: 9 # highly compressible
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -14,13 +14,15 @@ on:
|
||||
- ".github/workflows/strict-type-check.yml"
|
||||
- "**.pyi"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ on:
|
||||
- '!.github/workflows/**'
|
||||
- '.github/workflows/unittests.yml'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -51,9 +53,9 @@ jobs:
|
||||
os: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- name: Set up Python ${{ matrix.python.version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
@@ -78,9 +80,9 @@ jobs:
|
||||
- {version: '3.13'} # current
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- name: Set up Python ${{ matrix.python.version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
|
||||
+5
-1
@@ -1069,7 +1069,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
if "players" in args:
|
||||
ctx.consume_players_package(args["players"])
|
||||
if "hint_points" in args:
|
||||
ctx.hint_points = args['hint_points']
|
||||
ctx.hint_points = args["hint_points"]
|
||||
if "checked_locations" in args:
|
||||
checked = set(args["checked_locations"])
|
||||
ctx.checked_locations |= checked
|
||||
@@ -1077,6 +1077,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
if "permissions" in args:
|
||||
ctx.update_permissions(args["permissions"])
|
||||
|
||||
# Update hint info for local display
|
||||
if "hint_cost" in args:
|
||||
ctx.hint_cost = int(args["hint_cost"])
|
||||
|
||||
elif cmd == 'Print':
|
||||
ctx.on_print(args)
|
||||
|
||||
|
||||
-27
@@ -1,23 +1,5 @@
|
||||
# hadolint global ignore=SC1090,SC1091
|
||||
|
||||
# Source
|
||||
FROM scratch AS release
|
||||
WORKDIR /release
|
||||
ADD https://github.com/Ijwu/Enemizer/releases/latest/download/ubuntu.16.04-x64.zip Enemizer.zip
|
||||
|
||||
# Enemizer
|
||||
FROM alpine:3.21 AS enemizer
|
||||
ARG TARGETARCH
|
||||
WORKDIR /release
|
||||
COPY --from=release /release/Enemizer.zip .
|
||||
|
||||
# No release for arm architecture. Skip.
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
apk add unzip=6.0-r15 --no-cache && \
|
||||
unzip -u Enemizer.zip -d EnemizerCLI && \
|
||||
chmod -R 777 EnemizerCLI; \
|
||||
else touch EnemizerCLI; fi
|
||||
|
||||
# Cython builder stage
|
||||
FROM python:3.12 AS cython-builder
|
||||
|
||||
@@ -81,15 +63,6 @@ RUN apt-get purge -y \
|
||||
g++ && \
|
||||
apt-get autoremove -y
|
||||
|
||||
# Copy necessary components
|
||||
COPY --from=enemizer /release/EnemizerCLI /tmp/EnemizerCLI
|
||||
|
||||
# No release for arm architecture. Skip.
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
cp -r /tmp/EnemizerCLI EnemizerCLI; \
|
||||
fi; \
|
||||
rm -rf /tmp/EnemizerCLI
|
||||
|
||||
# Define health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:${PORT:-80} || exit 1
|
||||
|
||||
+15
-3
@@ -40,6 +40,8 @@ def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
|
||||
parser.add_argument('--outputpath', default=settings.general_options.output_path,
|
||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
||||
parser.add_argument('--allow_quantity', action="store_true", default=defaults.allow_quantity,
|
||||
help='Allows the use of the quantity option in yamls. Default is the set value in the host.yaml.')
|
||||
parser.add_argument('--race', action='store_true', default=defaults.race)
|
||||
parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
|
||||
parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level')
|
||||
@@ -87,7 +89,8 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
|
||||
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)
|
||||
seed_name = get_seed_name(random)
|
||||
|
||||
@@ -122,6 +125,7 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
player_id: int = 1
|
||||
player_files: dict[int, str] = {}
|
||||
player_errors: list[str] = []
|
||||
allow_quantity = args.allow_quantity
|
||||
for file in os.scandir(args.player_files_path):
|
||||
fname = file.name
|
||||
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
|
||||
@@ -133,7 +137,14 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
if yaml is None:
|
||||
logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}")
|
||||
else:
|
||||
weights_for_file.append(yaml)
|
||||
quantity = yaml.get("quantity", 1)
|
||||
if quantity <= 0:
|
||||
raise ValueError("A quantity of 0 or less is invalid. Please change it to at least 1.")
|
||||
if not allow_quantity and quantity > 1:
|
||||
raise ValueError("Quantity greater than 1 is deactivated by host settings.")
|
||||
|
||||
for _ in range(quantity):
|
||||
weights_for_file.append(yaml)
|
||||
weights_cache[fname] = tuple(weights_for_file)
|
||||
|
||||
except Exception as e:
|
||||
@@ -574,7 +585,8 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
raise Exception(f"Invalid game: {ret.game}")
|
||||
if ret.game not in AutoWorldRegister.world_types:
|
||||
from worlds import failed_world_loads
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + list(failed_world_loads.keys()),
|
||||
limit=1)[0]
|
||||
if picks[0] in failed_world_loads:
|
||||
raise Exception(f"No functional world found to handle game {ret.game}. "
|
||||
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||
|
||||
+64
-20
@@ -29,13 +29,14 @@ if __name__ == "__main__":
|
||||
|
||||
import settings
|
||||
import Utils
|
||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
||||
user_path)
|
||||
from Utils import (env_cleared_lib_path, init_logging, is_frozen, is_linux, is_macos, is_windows, local_path,
|
||||
messagebox, open_filename, user_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging('Launcher')
|
||||
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||
from worlds import failed_world_loads
|
||||
|
||||
|
||||
def open_host_yaml():
|
||||
@@ -52,10 +53,7 @@ def open_host_yaml():
|
||||
webbrowser.open(file)
|
||||
return
|
||||
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
subprocess.Popen([exe, file], env=env)
|
||||
|
||||
def open_patch():
|
||||
@@ -106,10 +104,7 @@ def open_folder(folder_path):
|
||||
return
|
||||
|
||||
if exe:
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
subprocess.Popen([exe, folder_path], env=env)
|
||||
else:
|
||||
logging.warning(f"No file browser available to open {folder_path}")
|
||||
@@ -202,22 +197,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
|
||||
|
||||
|
||||
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 is_windows:
|
||||
# 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)
|
||||
return
|
||||
return True
|
||||
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:
|
||||
subprocess.Popen([terminal, '-e', shlex.join(exe)])
|
||||
return
|
||||
# Clear LD_LIB_PATH during terminal startup, but set it again when running command in case it's needed
|
||||
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:
|
||||
terminal = [which('open'), '-W', '-a', 'Terminal.app']
|
||||
terminal = [which("open"), "-W", "-a", "Terminal.app"]
|
||||
subprocess.Popen([*terminal, *exe])
|
||||
return
|
||||
return True
|
||||
subprocess.Popen(exe)
|
||||
return False
|
||||
|
||||
|
||||
def create_shortcut(button: Any, component: Component) -> None:
|
||||
@@ -271,6 +276,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
search_box: MDTextField = ObjectProperty(None)
|
||||
cards: list[LauncherCard]
|
||||
current_filter: Sequence[str | Type] | None
|
||||
failed_worlds: bool = bool(failed_world_loads)
|
||||
|
||||
def __init__(self, ctx=None, components=None, args=None):
|
||||
self.title = self.base_title + " " + Utils.__version__
|
||||
@@ -406,12 +412,50 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
|
||||
@staticmethod
|
||||
def component_action(button):
|
||||
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
open_text = "Opening in a new window..."
|
||||
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()
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
def copy_to_clipboard(text):
|
||||
from kivy.core.clipboard import Clipboard
|
||||
Clipboard.copy(text)
|
||||
MDSnackbar(MDSnackbarText(text="Copied to clipboard."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
|
||||
def display_failed(self):
|
||||
"""Display a dialog showing the exceptions produced by any world that failed to load during
|
||||
initialization."""
|
||||
if not self.failed_worlds:
|
||||
return
|
||||
from kivymd.uix.dialog import MDDialog, MDDialogIcon, MDDialogHeadlineText, MDDialogContentContainer
|
||||
from kivymd.uix.divider import MDDivider
|
||||
from kivymd.uix.list import MDListItem, MDListItemHeadlineText, MDListItemSupportingText
|
||||
entries = []
|
||||
for world, reason in failed_world_loads.items():
|
||||
entries.append(MDListItem(
|
||||
MDListItemHeadlineText(text=world),
|
||||
MDListItemSupportingText(text=reason),
|
||||
on_release=lambda x, r=reason: self.copy_to_clipboard(r)
|
||||
))
|
||||
dialog = MDDialog(
|
||||
MDDialogIcon(icon="alert"),
|
||||
MDDialogHeadlineText(text="Failed World Loads"),
|
||||
MDDialogContentContainer(
|
||||
MDDivider(),
|
||||
*entries,
|
||||
orientation="vertical",
|
||||
)
|
||||
)
|
||||
dialog.open()
|
||||
|
||||
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. """
|
||||
|
||||
+2
-2
@@ -241,8 +241,8 @@ async def gba_sync_task(ctx: MMBN3Context):
|
||||
await ctx.server_auth(False)
|
||||
else:
|
||||
if not ctx.version_warning:
|
||||
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}."
|
||||
"Please update to the latest version."
|
||||
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. "
|
||||
"Please update to the latest version. "
|
||||
"Your connection to the Archipelago server will not be accepted.")
|
||||
ctx.version_warning = True
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
+2
-2
@@ -2633,8 +2633,8 @@ def parse_args() -> argparse.Namespace:
|
||||
goal: !remaining can be used after goal completion
|
||||
''')
|
||||
parser.add_argument('--auto_shutdown', default=defaults["auto_shutdown"], type=int,
|
||||
help="automatically shut down the server after this many minutes without new location checks. "
|
||||
"0 to keep running. Not yet implemented.")
|
||||
help="automatically shut down the server after this many seconds without new location checks. "
|
||||
"0 to keep running.")
|
||||
parser.add_argument('--use_embedded_options', action="store_true",
|
||||
help='retrieve release, remaining and hint options from the multidata file,'
|
||||
' instead of host.yaml')
|
||||
|
||||
@@ -527,7 +527,11 @@ else:
|
||||
except ImportError:
|
||||
pyximport = None
|
||||
try:
|
||||
import logging
|
||||
logger = logging.getLogger()
|
||||
old_level = logger.level
|
||||
from _speedups import LocationStore
|
||||
logger.setLevel(old_level)
|
||||
except ImportError:
|
||||
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
||||
"Install a matching C++ compiler for your platform to compile _speedups.")
|
||||
|
||||
+56
-30
@@ -212,6 +212,13 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
else:
|
||||
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:
|
||||
return self.value
|
||||
|
||||
@@ -930,13 +937,34 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
||||
class OptionCounter(OptionDict):
|
||||
min: int | None = None
|
||||
max: int | None = None
|
||||
cull_zeroes: bool = False
|
||||
|
||||
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:
|
||||
super(OptionCounter, self).verify(world, player_name, plando_options)
|
||||
|
||||
self.verify_values()
|
||||
|
||||
def verify_values(self):
|
||||
range_errors = []
|
||||
|
||||
if self.max is not None:
|
||||
@@ -959,13 +987,8 @@ class OptionCounter(OptionDict):
|
||||
class ItemDict(OptionCounter):
|
||||
verify_item_name = True
|
||||
|
||||
min = 0
|
||||
|
||||
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)
|
||||
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
|
||||
cull_zeroes = True
|
||||
|
||||
|
||||
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
@@ -1446,7 +1469,7 @@ class NonLocalItems(ItemSet):
|
||||
|
||||
|
||||
class StartInventory(ItemDict):
|
||||
"""Start with the specified amount of these items. Example: "Bomb: 1" """
|
||||
"""Start with the specified amount of these items. Example: {Bomb: 1, Arrow: 3} """
|
||||
verify_item_name = True
|
||||
display_name = "Start Inventory"
|
||||
rich_text_doc = True
|
||||
@@ -1454,7 +1477,7 @@ class StartInventory(ItemDict):
|
||||
|
||||
|
||||
class StartInventoryPool(StartInventory):
|
||||
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1"
|
||||
"""Start with the specified amount of these items and don't place them in the world. Example: {Bomb: 1, Arrow: 3}
|
||||
|
||||
The game decides what the replacement items will be.
|
||||
"""
|
||||
@@ -1833,27 +1856,30 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
presets = world.web.options_presets.copy()
|
||||
presets.update({"": {}})
|
||||
try:
|
||||
presets = world.web.options_presets.copy()
|
||||
presets.update({"": {}})
|
||||
|
||||
option_groups = get_option_groups(world)
|
||||
for name, preset in presets.items():
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
preset_name=name,
|
||||
preset=preset,
|
||||
)
|
||||
preset_name = f" - {name}" if name else ""
|
||||
with open(os.path.join(preset_folder if name else target_folder,
|
||||
get_file_safe_name(game_name + preset_name) + ".yaml"),
|
||||
"w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
option_groups = get_option_groups(world)
|
||||
for name, preset in presets.items():
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
preset_name=name,
|
||||
preset=preset,
|
||||
)
|
||||
preset_name = f" - {name}" if name else ""
|
||||
with open(os.path.join(preset_folder if name else target_folder,
|
||||
get_file_safe_name(game_name + preset_name) + ".yaml"),
|
||||
"w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
except Exception as ex:
|
||||
raise Exception(f"Template generation failed for world {game_name}") from ex
|
||||
|
||||
|
||||
def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
|
||||
+4
-3
@@ -384,10 +384,11 @@ class OptionsCreator(ThemedApp):
|
||||
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
|
||||
text = VisualFreeText(option=option, name=name)
|
||||
|
||||
def set_value(instance):
|
||||
self.options[name] = instance.text
|
||||
def set_value(instance, value):
|
||||
self.options[name] = value
|
||||
|
||||
text.bind(on_text_validate=set_value)
|
||||
text.bind(text=set_value)
|
||||
self.options[name] = option.default
|
||||
return text
|
||||
|
||||
def create_choice(self, option: typing.Type[Choice], name: str):
|
||||
|
||||
@@ -24,7 +24,6 @@ Currently, the following games are supported:
|
||||
* The Witness
|
||||
* Sonic Adventure 2: Battle
|
||||
* Starcraft 2
|
||||
* Donkey Kong Country 3
|
||||
* Dark Souls 3
|
||||
* Super Mario World
|
||||
* Pokémon Red and Blue
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True)
|
||||
tempInstall = steaminstall
|
||||
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||
if tempInstall and not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||
tempInstall = None
|
||||
if tempInstall is None:
|
||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||
|
||||
@@ -22,7 +22,7 @@ from datetime import datetime, timezone
|
||||
|
||||
from settings import Settings, get_settings
|
||||
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 pathspec import PathSpec, GitIgnoreSpec
|
||||
from typing_extensions import deprecated
|
||||
@@ -52,7 +52,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.7"
|
||||
__version__ = "0.6.8"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -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"))
|
||||
assert open_command, "Didn't find program for open_file! Please report this together with system details."
|
||||
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
subprocess.call([open_command, filename], env=env)
|
||||
|
||||
|
||||
@@ -345,6 +342,9 @@ def persistent_load() -> Dict[str, Dict[str, Any]]:
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
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:
|
||||
logging.debug(f"Could not read store: {e}")
|
||||
if storage is None:
|
||||
@@ -369,11 +369,6 @@ def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) ->
|
||||
except Exception as 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
|
||||
return {}
|
||||
|
||||
@@ -455,13 +450,10 @@ safe_builtins = frozenset((
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
generic_properties_module: Optional[object]
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||
self.options_module = importlib.import_module("Options")
|
||||
self.net_utils_module = importlib.import_module("NetUtils")
|
||||
self.generic_properties_module = None
|
||||
|
||||
def find_class(self, module: str, name: str) -> type:
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
@@ -475,10 +467,6 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
"SlotType", "NetworkSlot", "HintStatus"}:
|
||||
return getattr(self.net_utils_module, name)
|
||||
# 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)
|
||||
if module.lower().endswith("options"):
|
||||
if module == "Options":
|
||||
@@ -758,6 +746,19 @@ def is_kivy_running() -> bool:
|
||||
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:
|
||||
if is_kivy_running():
|
||||
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))
|
||||
|
||||
def _run_for_stdout(*args: str):
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
env = env.copy()
|
||||
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
|
||||
env = env_cleared_lib_path()
|
||||
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -110,13 +110,14 @@ if __name__ == "__main__":
|
||||
logging.exception(e)
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
app = get_app()
|
||||
from worlds import AutoWorldRegister
|
||||
from worlds import AutoWorldRegister, network_data_package
|
||||
# Update to only valid WebHost worlds
|
||||
invalid_worlds = {name for name, world in AutoWorldRegister.world_types.items()
|
||||
if not hasattr(world.web, "tutorials")}
|
||||
if 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}
|
||||
network_data_package["games"] = {k: v for k, v in network_data_package["games"].items() if k not in invalid_worlds}
|
||||
create_options_files()
|
||||
copy_tutorials_files_to_static()
|
||||
if app.config["SELFLAUNCH"]:
|
||||
|
||||
@@ -48,6 +48,8 @@ app.config["JOB_THRESHOLD"] = 1
|
||||
app.config["JOB_TIME"] = 600
|
||||
# maximum time in seconds since last activity for a room to be hosted
|
||||
app.config["MAX_ROOM_TIMEOUT"] = 259200
|
||||
# minimum time in days since last activity for a room to be deleted. 0 to disable.
|
||||
app.config["ROOM_AUTO_DELETE"] = 0
|
||||
# memory limit for generator processes in bytes
|
||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||
|
||||
@@ -71,7 +73,9 @@ CLI(app)
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -100,13 +100,18 @@ def init_generator(config: dict[str, Any]) -> None:
|
||||
db.generate_mapping()
|
||||
|
||||
|
||||
def cleanup():
|
||||
"""delete unowned user-content"""
|
||||
def cleanup(config: dict[str, Any]):
|
||||
"""delete unowned or old user-content"""
|
||||
auto_delete: int = config.get("ROOM_AUTO_DELETE", 0)
|
||||
with db_session:
|
||||
# >>> bool(uuid.UUID(int=0))
|
||||
# True
|
||||
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
||||
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
||||
if auto_delete > 0:
|
||||
cutoff = utcnow() - timedelta(days=auto_delete)
|
||||
rooms += Room.select(lambda room: room.last_activity < cutoff).delete(bulk=True)
|
||||
seeds += Seed.select(lambda seed: not seed.rooms and seed.creation_time < cutoff).delete(bulk=True)
|
||||
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
||||
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
||||
if rooms or seeds or slots:
|
||||
@@ -118,7 +123,7 @@ def autohost(config: dict):
|
||||
stop_event = _stop_event
|
||||
try:
|
||||
with Locker("autohost"):
|
||||
cleanup()
|
||||
cleanup(config)
|
||||
hosters = []
|
||||
for x in range(config["HOSTERS"]):
|
||||
hoster = MultiworldInstance(config, x)
|
||||
|
||||
+12
-12
@@ -1,14 +1,14 @@
|
||||
flask>=3.1.1
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19; python_version <= '3.12'
|
||||
flask==3.1.3
|
||||
werkzeug==3.1.6
|
||||
pony==0.7.19; python_version <= '3.12'
|
||||
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
|
||||
waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
waitress==3.0.2
|
||||
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-Limiter>=3.12
|
||||
Flask-Cors>=6.0.2
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
setproctitle>=1.3.5
|
||||
mistune>=3.1.3
|
||||
docutils>=0.22.2
|
||||
Flask-Limiter==4.1.1
|
||||
Flask-Cors==6.0.2
|
||||
bokeh==3.8.2
|
||||
markupsafe==3.0.3
|
||||
setproctitle==1.3.7
|
||||
mistune==3.2.1
|
||||
docutils==0.22.4
|
||||
|
||||
@@ -123,12 +123,26 @@ window.addEventListener('load', () => {
|
||||
});
|
||||
|
||||
const addRangeRow = (optionName) => {
|
||||
const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
|
||||
const inputQuery = `input[data-option="${optionName}"]`;
|
||||
const inputTarget = document.querySelector(inputQuery);
|
||||
const newValue = inputTarget.value;
|
||||
if (!/^-?\d+$/.test(newValue)) {
|
||||
alert('Range values must be a positive or negative integer!');
|
||||
return;
|
||||
switch (inputTarget.type) {
|
||||
case 'number':
|
||||
if (!/^-?\d+$/.test(newValue)) {
|
||||
alert('Range values must be a positive or negative integer!');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'text':
|
||||
if (newValue === "") {
|
||||
alert('Range values for text must be a non-empty string!');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error(`Found unsupported input type: ${inputTarget.type}`);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
inputTarget.value = '';
|
||||
const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% block footer %}
|
||||
<footer id="island-footer">
|
||||
<div id="copyright-notice">Copyright 2025 Archipelago</div>
|
||||
<div id="copyright-notice">Copyright 2026 Archipelago</div>
|
||||
<div id="links">
|
||||
<a href="/sitemap">Site Map</a>
|
||||
-
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
<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
|
||||
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">
|
||||
<label for="game-search">Search for your game below!</label><br />
|
||||
<div class="page-controls">
|
||||
@@ -66,6 +68,9 @@
|
||||
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
|
||||
{% endif %}
|
||||
</details>
|
||||
{% if "authors" in world.manifest %}
|
||||
<p>Authors: {{ world.manifest["authors"] | join(", ") }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -20,11 +20,7 @@
|
||||
{% for file_name, file_data in tutorial_data.files.items() %}
|
||||
<li>
|
||||
<a href="{{ url_for("tutorial", game=world_name, file=file_name) }}">{{ file_data.language }}</a>
|
||||
by
|
||||
{% for author in file_data.authors %}
|
||||
{{ author }}
|
||||
{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
by {{ file_data.authors | join(", ") }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -71,10 +71,10 @@
|
||||
<div class="hint-text">
|
||||
This option allows custom values only. Please enter your desired values below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" data-option="{{ option_name }}">Add</button>
|
||||
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
<table>
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<tbody>
|
||||
{% if option.default %}
|
||||
{{ RangeRow(option_name, option, option.default, option.default) }}
|
||||
@@ -88,11 +88,11 @@
|
||||
<div class="hint-text">
|
||||
Custom values are also allowed for this option. To create one, enter it into the input box below.
|
||||
<div class="custom-value-wrapper">
|
||||
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" data-option="{{ option_name }}">Add</button>
|
||||
<input type="text" class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
|
||||
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<tbody>
|
||||
{% for id, name in option.name_lookup.items() %}
|
||||
{% if name != 'random' %}
|
||||
|
||||
@@ -140,6 +140,15 @@ MDFloatLayout:
|
||||
|
||||
MDNavigationDrawerDivider:
|
||||
|
||||
MDBoxLayout:
|
||||
orientation: "horizontal"
|
||||
MDIconButton:
|
||||
icon: "alert" if app.failed_worlds else ""
|
||||
theme_text_color: "Custom"
|
||||
text_color: "D23C42"
|
||||
disabled: not app.failed_worlds
|
||||
on_release: app.display_failed()
|
||||
|
||||
|
||||
MDGridLayout:
|
||||
id: main_layout
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
||||
/worlds/apquest/ @NewSoupVi
|
||||
|
||||
# Sudoku (APSudoku)
|
||||
/worlds/apsudoku/ @EmilyV99
|
||||
|
||||
# Aquaria
|
||||
/worlds/aquaria/ @tioui
|
||||
@@ -58,9 +56,6 @@
|
||||
# Dark Souls III
|
||||
/worlds/dark_souls_3/ @Marechal-L @nex3
|
||||
|
||||
# Donkey Kong Country 3
|
||||
/worlds/dkc3/ @PoryGone
|
||||
|
||||
# DLCQuest
|
||||
/worlds/dlcquest/ @axe-y @agilbert1412
|
||||
|
||||
|
||||
@@ -92,8 +92,9 @@ for setup).
|
||||
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
|
||||
check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md), and the [APQuest](/worlds/apquest/) world
|
||||
is a complete world implementation that functions as an introduction to world development. Before publishing, make sure
|
||||
to also check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ There are also the following optional fields:
|
||||
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
|
||||
An APWorld without a world_version is always treated as older than one with a version
|
||||
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
|
||||
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
|
||||
package managers. Should always be a list of strings.
|
||||
* `authors` - a list of authors of the world. Displayed in user-facing places like the Supported Games page
|
||||
on WebHost. Should always be a list of strings.
|
||||
|
||||
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
|
||||
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
|
||||
|
||||
@@ -77,15 +77,6 @@ Changes made to `docker-compose.yaml` can be applied by running `docker compose
|
||||
It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
error if it is required.
|
||||
Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory:
|
||||
`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`.
|
||||
Enemizer is not currently available for `aarch64`.
|
||||
|
||||
|
||||
## Optional: Git
|
||||
|
||||
Building the image requires a local copy of the ArchipelagoMW source code.
|
||||
|
||||
@@ -69,12 +69,6 @@ flowchart LR
|
||||
end
|
||||
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
|
||||
subgraph Super Mario World
|
||||
SMW[SNES]
|
||||
|
||||
+194
-57
@@ -1,6 +1,7 @@
|
||||
# Rule Builder
|
||||
|
||||
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
|
||||
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface
|
||||
to define rules and the following advantages:
|
||||
|
||||
- Rule classes that avoid all the common pitfalls
|
||||
- Logic optimization
|
||||
@@ -12,13 +13,21 @@ This document describes the API provided for the rule builder. Using this API pr
|
||||
|
||||
The rule builder consists of 3 main parts:
|
||||
|
||||
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
|
||||
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
|
||||
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic.
|
||||
They can be combined and take into account your world's options. There are a number of default rules listed below,
|
||||
and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance
|
||||
they must be resolved.
|
||||
2. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules
|
||||
specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly
|
||||
creating these but they'll be created when assigning rules to locations or entrances. These are what power the
|
||||
human-readable logic explanations.
|
||||
3. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from
|
||||
instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||
|
||||
## Usage
|
||||
|
||||
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule
|
||||
objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||
|
||||
```python
|
||||
# In your world's create_regions method
|
||||
@@ -32,6 +41,7 @@ The rule builder comes with a number of rules by default:
|
||||
- `False_`: Always returns false
|
||||
- `And`: Checks that all child rules are true (also provided by `&` operator)
|
||||
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
|
||||
- `AtLeast`: Checks that at least some count of rules is true
|
||||
- `Has`: Checks that the player has the given item with the given count (default 1)
|
||||
- `HasAll`: Checks that the player has all given items
|
||||
- `HasAny`: Checks that the player has at least one of the given items
|
||||
@@ -40,18 +50,22 @@ The rule builder comes with a number of rules by default:
|
||||
- `HasFromList`: Checks that the player has some number of given items
|
||||
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
|
||||
- `HasGroup`: Checks that the player has some number of items from a given item group
|
||||
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
|
||||
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the
|
||||
same item
|
||||
- `CanReachLocation`: Checks that the player can logically reach the given location
|
||||
- `CanReachRegion`: Checks that the player can logically reach the given region
|
||||
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
|
||||
|
||||
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||
You can combine these rules together to describe the logic required for something. For example, to check if a player
|
||||
either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||
|
||||
```python
|
||||
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
|
||||
```
|
||||
|
||||
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
|
||||
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In
|
||||
> order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check
|
||||
> if a rule is defined you must use `if rule is not None`.
|
||||
|
||||
### Assigning rules
|
||||
|
||||
@@ -61,13 +75,16 @@ When assigning the rule you must use the `set_rule` helper to correctly resolve
|
||||
self.set_rule(location_or_entrance, rule)
|
||||
```
|
||||
|
||||
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the
|
||||
entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify
|
||||
`force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||
|
||||
```python
|
||||
self.create_entrance(from_region, to_region, rule)
|
||||
```
|
||||
|
||||
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify
|
||||
> the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||
|
||||
You can also set a rule for your world's completion condition:
|
||||
|
||||
@@ -77,21 +94,42 @@ self.set_completion_rule(rule)
|
||||
|
||||
### Restricting options
|
||||
|
||||
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
|
||||
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an
|
||||
iterable of `OptionFilter` instances. When resolved, if no filters are provided or all of them pass then the rule will
|
||||
resolve as normal. Otherwise, the rule will be replaced with `True` or `False` depending on what `filtered_resolution`
|
||||
is set to, which defaults to `False`.
|
||||
|
||||
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
|
||||
```python
|
||||
rule1 = Has(
|
||||
"Fast Travel Spell",
|
||||
options=[OptionFilter(RandoFastTravel, RandoFastTravel.option_true)],
|
||||
)
|
||||
rule2 = Has(
|
||||
"Starting Party Member",
|
||||
options=[OptionFilter(RandoParty, 1)], # option attributes are suggested but any value works
|
||||
filtered_resolution=True,
|
||||
)
|
||||
```
|
||||
|
||||
- `eq`: `==`
|
||||
- `ne`: `!=`
|
||||
- `gt`: `>`
|
||||
- `lt`: `<`
|
||||
- `ge`: `>=`
|
||||
- `le`: `<=`
|
||||
- `contains`: `in`
|
||||
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are
|
||||
allowed:
|
||||
|
||||
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
|
||||
- `eq`: `option_value == filter_value`
|
||||
- `ne`: `option_value != filter_value`
|
||||
- `gt`: `option_value > filter_value`
|
||||
- `lt`: `option_value < filter_value`
|
||||
- `ge`: `option_value >= filter_value`
|
||||
- `le`: `option_value <= filter_value`
|
||||
- `in`: `option_value in filter_value`
|
||||
- `contains`: `filter_value in option_value` (note reversed operands)
|
||||
|
||||
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
|
||||
```python
|
||||
rule1 = Has("Movement Ability", options=[OptionFilter(SkipsLevel, SkipsLevel.option_hard, operator="lt")])
|
||||
rule2 = Has("Item", options=[OptionFilter(ChoiceOption, [1, 5], operator="in")])
|
||||
```
|
||||
|
||||
To check if the player has received the switch item if switches are randomized, or if they can reach the switch when not
|
||||
randomized:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
@@ -115,12 +153,12 @@ If you would like to provide option filters when reusing or composing rules, you
|
||||
common_rule = Has("A") | HasAny("B", "C")
|
||||
...
|
||||
rule = (
|
||||
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
|
||||
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
|
||||
Filtered(common_rule, options=[OptionFilter(Opt, 0)])
|
||||
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)])
|
||||
)
|
||||
```
|
||||
|
||||
You can also use the & and | operators to apply options to rules:
|
||||
For convenience, you can also use the `&` and `|` operators to apply options to rules:
|
||||
|
||||
```python
|
||||
common_rule = Has("A")
|
||||
@@ -129,22 +167,73 @@ common_rule_only_on_easy = common_rule & easy_filter
|
||||
common_rule_skipped_on_easy = common_rule | easy_filter
|
||||
```
|
||||
|
||||
Combining the above, you can easily bypass a requirement based on option choices:
|
||||
|
||||
```python
|
||||
rule = Has("Some Upgrade") | OptionFilter(CombatDifficulty, CombatDifficulty.option_medium, operator="ge")
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
game = "My Game"
|
||||
```
|
||||
|
||||
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead
|
||||
cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||
|
||||
### Item name mapping
|
||||
|
||||
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
|
||||
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps
|
||||
actual item names to real item names so the cache system knows what to invalidate.
|
||||
|
||||
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
|
||||
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical
|
||||
`Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical
|
||||
`Currency`.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
@@ -158,9 +247,13 @@ class MyWorld(CachedRuleBuilderWorld):
|
||||
|
||||
## Defining custom rules
|
||||
|
||||
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide
|
||||
the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and
|
||||
to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||
|
||||
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
|
||||
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically
|
||||
be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more
|
||||
dependencies functions as outlined below.
|
||||
|
||||
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
|
||||
|
||||
@@ -209,7 +302,10 @@ class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
|
||||
|
||||
### Item dependencies
|
||||
|
||||
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
|
||||
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of
|
||||
your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id
|
||||
of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this
|
||||
function even when caching is disabled as more things may use it in the future.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -226,7 +322,10 @@ All of the default `Has*` rules define this function already.
|
||||
|
||||
### Region dependencies
|
||||
|
||||
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of
|
||||
region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
|
||||
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
|
||||
caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -243,7 +342,10 @@ The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules d
|
||||
|
||||
### Location dependencies
|
||||
|
||||
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping
|
||||
of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
|
||||
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
|
||||
caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -260,7 +362,10 @@ The default `CanReachLocation` rule defines this function already.
|
||||
|
||||
### Entrance dependencies
|
||||
|
||||
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping
|
||||
of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These
|
||||
dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the
|
||||
caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
@@ -277,9 +382,13 @@ The default `CanReachEntrance` rule defines this function already.
|
||||
|
||||
### Rule explanations
|
||||
|
||||
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally
|
||||
accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will
|
||||
display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is
|
||||
useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||
|
||||
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
|
||||
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your
|
||||
`Resolved` class:
|
||||
|
||||
```python
|
||||
class MyRule(Rule, game="My Game"):
|
||||
@@ -316,22 +425,35 @@ class MyRule(Rule, game="My Game"):
|
||||
|
||||
### Cache control
|
||||
|
||||
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
|
||||
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two
|
||||
class attributes on the `Resolved` class you can override to change this behavior.
|
||||
|
||||
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
|
||||
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
|
||||
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and
|
||||
always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will
|
||||
cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your
|
||||
rule should be marked as stale.
|
||||
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when
|
||||
being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still
|
||||
define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the
|
||||
overhead of the caching system will slow it down.
|
||||
|
||||
### Caveats
|
||||
|
||||
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
|
||||
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if
|
||||
your world has opted into caching.
|
||||
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
|
||||
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
|
||||
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved`
|
||||
instances directly.
|
||||
|
||||
## Serialization
|
||||
|
||||
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the
|
||||
rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and
|
||||
entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||
|
||||
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
|
||||
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and
|
||||
an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule
|
||||
would look like:
|
||||
|
||||
```python
|
||||
{
|
||||
@@ -344,7 +466,8 @@ The dict contains a `rule` key with the name of the rule, an `options` key with
|
||||
}
|
||||
```
|
||||
|
||||
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
|
||||
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in
|
||||
the same serializable format:
|
||||
|
||||
```python
|
||||
{
|
||||
@@ -428,7 +551,8 @@ class BasicLogicRule(Rule, game="My Game"):
|
||||
}
|
||||
```
|
||||
|
||||
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
|
||||
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it
|
||||
correctly:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
@@ -449,10 +573,14 @@ These are properties and helpers that are available to you in your world.
|
||||
#### Methods
|
||||
|
||||
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
|
||||
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
|
||||
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
|
||||
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the
|
||||
inherited dependencies, gets called automatically after set_rules
|
||||
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given
|
||||
location or entrance
|
||||
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
|
||||
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
|
||||
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`:
|
||||
Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates
|
||||
to `False_()` unless force_creation is `True`
|
||||
|
||||
#### CachedRuleBuilderWorld Properties
|
||||
|
||||
@@ -465,18 +593,27 @@ The following property is only available when inheriting from `CachedRuleBuilder
|
||||
These are properties and helpers that you can use or override for custom rules.
|
||||
|
||||
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
|
||||
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
|
||||
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
|
||||
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's
|
||||
serialization
|
||||
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if
|
||||
you've overridden `to_dict`
|
||||
- `__str__()`: Basic string representation of a rule, useful for debugging
|
||||
|
||||
#### Resolved rule API
|
||||
|
||||
- `player: int`: The slot this rule is resolved for
|
||||
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
|
||||
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
|
||||
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
|
||||
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
|
||||
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
|
||||
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for
|
||||
this rule
|
||||
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item
|
||||
collection
|
||||
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching
|
||||
regions
|
||||
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on
|
||||
reaching locations
|
||||
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on
|
||||
reaching entrances
|
||||
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic
|
||||
(and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is
|
||||
defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
|
||||
|
||||
@@ -78,16 +78,6 @@ first generate the binary distribution and then run `python setup.py bdist_appim
|
||||
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
error if it is required.
|
||||
|
||||
You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases).
|
||||
It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer
|
||||
setting in host.yaml at your Enemizer executable.
|
||||
|
||||
|
||||
## Optional: SNI
|
||||
|
||||
[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
{
|
||||
...
|
||||
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
|
||||
"Factorio":"a699194a9589db3ebc0d821915864b422c782f44",
|
||||
...
|
||||
}
|
||||
|
||||
@@ -327,6 +327,11 @@ reject the placement of an item there.
|
||||
|
||||
### Events (or "generation-only items/locations")
|
||||
|
||||
> **Warning:** If you're trying to tell the Archipelago server that the player has achieved their goal, you want to send
|
||||
a [StatusUpdate packet](network%20protocol.md#statusupdate), or however [your client library](network%20protocol.md)
|
||||
wraps it. Despite the popularity of "victory events" during generation, events have nothing to do with how goals are
|
||||
triggered during gameplay.
|
||||
|
||||
An event item or location is one that only exists during multiworld generation; the server is never made aware of them.
|
||||
Event locations can never be checked by the player, and event items cannot be received during play.
|
||||
|
||||
|
||||
+1
-8
@@ -57,9 +57,8 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
||||
[Files]
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs;
|
||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs;
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
|
||||
[Icons]
|
||||
@@ -83,7 +82,6 @@ Type: files; Name: "{app}\*.exe"
|
||||
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
|
||||
Type: files; Name: "{app}\data\lua\connector_ff1.lua"
|
||||
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
||||
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||
#include "installdelete.iss"
|
||||
|
||||
[Registry]
|
||||
@@ -98,11 +96,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\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: "{#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: "";
|
||||
|
||||
@@ -363,15 +363,16 @@ class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
|
||||
text += "\nPermissions:"
|
||||
for permission_name, permission_data in ctx.permissions.items():
|
||||
text += f"\n {permission_name}: {permission_data}"
|
||||
if ctx.hint_cost is not None and ctx.total_locations:
|
||||
min_cost = int(ctx.server_version >= (0, 3, 9))
|
||||
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
||||
f"For you this means every " \
|
||||
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
||||
"location checks." \
|
||||
f"\nYou currently have {ctx.hint_points} points."
|
||||
elif ctx.hint_cost == 0:
|
||||
text += "\n!hint is free to use."
|
||||
if ctx.total_locations and ctx.hint_cost is not None:
|
||||
if ctx.hint_cost == 0:
|
||||
text += "\n!hint is free to use."
|
||||
else:
|
||||
min_cost = int(ctx.server_version >= (0, 3, 9))
|
||||
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
||||
f"For you this means every " \
|
||||
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
||||
"location checks." \
|
||||
f"\nYou currently have {ctx.hint_points} points."
|
||||
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
|
||||
text += "\nRace mode is enabled." \
|
||||
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
|
||||
|
||||
+17
-17
@@ -1,21 +1,21 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.3
|
||||
jellyfish>=1.2.1
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.8
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.5.0
|
||||
certifi>=2025.11.12
|
||||
cython>=3.2.1
|
||||
cymem>=2.0.13
|
||||
orjson>=3.11.4
|
||||
typing_extensions>=4.15.0
|
||||
pyshortcuts>=1.9.6
|
||||
pathspec>=0.12.1
|
||||
colorama==0.4.6
|
||||
websockets==13.1 # ,<14
|
||||
PyYAML==6.0.3
|
||||
jellyfish==1.2.1
|
||||
jinja2==3.1.6
|
||||
schema==0.7.8
|
||||
kivy==2.3.1
|
||||
bsdiff4==1.2.6
|
||||
platformdirs==4.9.4
|
||||
certifi==2026.2.25
|
||||
cython==3.2.4
|
||||
cymem==2.0.13
|
||||
orjson==3.11.7
|
||||
typing_extensions==4.15.0
|
||||
pyshortcuts==1.9.7
|
||||
pathspec==1.0.4
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
kivymd>=2.0.1.dev0
|
||||
|
||||
# Legacy world dependencies that custom worlds rely on
|
||||
Pymem>=1.13.0
|
||||
Pymem==1.14.0
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+222
-38
@@ -7,6 +7,7 @@ from typing_extensions import TypeVar, dataclass_transform, override
|
||||
from BaseClasses import CollectionState
|
||||
from NetUtils import JSONMessagePart
|
||||
|
||||
from .field_resolvers import FieldResolver, FieldResolverRegister, resolve_field
|
||||
from .options import OptionFilter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,7 +36,7 @@ def _create_hash_fn(resolved_rule_cls: "CustomRuleRegister") -> Callable[..., in
|
||||
class CustomRuleRegister(type):
|
||||
"""A metaclass to contain world custom rules and automatically convert resolved rules to frozen dataclasses"""
|
||||
|
||||
resolved_rules: ClassVar[dict[int, "Rule.Resolved"]] = {}
|
||||
resolved_rules: ClassVar[dict["Rule.Resolved", "Rule.Resolved"]] = {}
|
||||
"""A cached of resolved rules to turn each unique one into a singleton"""
|
||||
|
||||
custom_rules: ClassVar[dict[str, dict[str, type["Rule[Any]"]]]] = {}
|
||||
@@ -63,10 +64,9 @@ class CustomRuleRegister(type):
|
||||
@override
|
||||
def __call__(cls, *args: Any, **kwds: Any) -> Any:
|
||||
rule = super().__call__(*args, **kwds)
|
||||
rule_hash = hash(rule)
|
||||
if rule_hash in cls.resolved_rules:
|
||||
return cls.resolved_rules[rule_hash]
|
||||
cls.resolved_rules[rule_hash] = rule
|
||||
if rule in cls.resolved_rules:
|
||||
return cls.resolved_rules[rule]
|
||||
cls.resolved_rules[rule] = rule
|
||||
return rule
|
||||
|
||||
@classmethod
|
||||
@@ -108,11 +108,14 @@ class Rule(Generic[TWorld]):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Returns a JSON compatible dict representation of this rule"""
|
||||
args = {
|
||||
field.name: getattr(self, field.name, None)
|
||||
for field in dataclasses.fields(self)
|
||||
if field.name not in ("options", "filtered_resolution")
|
||||
}
|
||||
args = {}
|
||||
for field in dataclasses.fields(self):
|
||||
if field.name in ("options", "filtered_resolution"):
|
||||
continue
|
||||
value = getattr(self, field.name, None)
|
||||
if isinstance(value, FieldResolver):
|
||||
value = value.to_dict()
|
||||
args[field.name] = value
|
||||
return {
|
||||
"rule": self.__class__.__qualname__,
|
||||
"options": [o.to_dict() for o in self.options],
|
||||
@@ -124,7 +127,19 @@ class Rule(Generic[TWorld]):
|
||||
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"""
|
||||
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]":
|
||||
"""Combines two rules or a rule and an option filter into an And rule"""
|
||||
@@ -410,13 +425,142 @@ class NestedRule(Rule[TWorld], game="Archipelago"):
|
||||
return combined_deps
|
||||
|
||||
|
||||
class AtLeast(NestedRule[TWorld], game="Archipelago"):
|
||||
"""A rule that returns true when at least N child rules evaluate as true"""
|
||||
|
||||
count: int | FieldResolver
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
count: int | FieldResolver,
|
||||
*children: Rule[TWorld],
|
||||
options: Iterable[OptionFilter] = (),
|
||||
filtered_resolution: bool = False,
|
||||
) -> None:
|
||||
super().__init__(*children, options=options, filtered_resolution=filtered_resolution)
|
||||
self.count = count
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
count = resolve_field(self.count, world, int)
|
||||
if count == 0:
|
||||
return True_().resolve(world)
|
||||
|
||||
children_to_process = [c.resolve(world) for c in self.children]
|
||||
return AtLeast.from_resolved(count, world, children_to_process)
|
||||
|
||||
@classmethod
|
||||
def from_resolved(cls, count: int, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
|
||||
clauses: list[Rule.Resolved] = []
|
||||
|
||||
while children_to_process:
|
||||
child = children_to_process.pop(0)
|
||||
if child.always_true:
|
||||
if count == 1:
|
||||
return child
|
||||
count -= 1
|
||||
continue
|
||||
if child.always_false:
|
||||
# falses can be ignored
|
||||
continue
|
||||
|
||||
clauses.append(child)
|
||||
|
||||
if len(clauses) < count:
|
||||
return False_().resolve(world)
|
||||
if count == 1:
|
||||
# Switch to Or which has more optimized handling
|
||||
return Or.from_resolved(world, clauses)
|
||||
if count == len(clauses):
|
||||
# Switch to And which has more optimized handling
|
||||
return And.from_resolved(world, clauses)
|
||||
return AtLeast.Resolved(
|
||||
tuple(clauses),
|
||||
count=count,
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
|
||||
@override
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
output = super().to_dict()
|
||||
count = self.count
|
||||
output["count"] = count.to_dict() if isinstance(count, FieldResolver) else count
|
||||
return output
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any], world_cls: "type[World]") -> Self:
|
||||
args = cls._parse_field_resolvers(data, world_cls.game)
|
||||
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||
children = [world_cls.rule_from_dict(c) for c in data.get("children", ())]
|
||||
return cls(
|
||||
args.pop("count"),
|
||||
*children,
|
||||
options=options,
|
||||
filtered_resolution=data.get("filtered_resolution", False),
|
||||
)
|
||||
|
||||
class Resolved(NestedRule.Resolved):
|
||||
count: int
|
||||
|
||||
@override
|
||||
def _evaluate(self, state: CollectionState) -> bool:
|
||||
count = self.count
|
||||
for rule in self.children:
|
||||
if rule(state):
|
||||
if count == 1:
|
||||
return True
|
||||
count -= 1
|
||||
return False
|
||||
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
messages: list[JSONMessagePart] = []
|
||||
if state is None:
|
||||
messages = [
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": str(self.count)},
|
||||
{"type": "text", "text": " of ("},
|
||||
]
|
||||
else:
|
||||
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
|
||||
messages = [
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": f"{satisfied_count}/{self.count}"},
|
||||
{"type": "text", "text": " of ("},
|
||||
]
|
||||
for i, child in enumerate(self.children):
|
||||
if i > 0:
|
||||
messages.append({"type": "text", "text": ", "})
|
||||
messages.extend(child.explain_json(state))
|
||||
messages.append({"type": "text", "text": ")"})
|
||||
return messages
|
||||
|
||||
@override
|
||||
def explain_str(self, state: CollectionState | None = None) -> str:
|
||||
clauses = ", ".join([c.explain_str(state) for c in self.children])
|
||||
if state is None:
|
||||
return f"At least {self.count} of ({clauses})"
|
||||
satisfied_count = sum(1 if child(state) else 0 for child in self.children)
|
||||
return f"At least {satisfied_count}/{self.count} of ({clauses})"
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
clauses = ", ".join([str(c) for c in self.children])
|
||||
return f"At least {self.count} of ({clauses})"
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=False)
|
||||
class And(NestedRule[TWorld], game="Archipelago"):
|
||||
"""A rule that only returns true when all child rules evaluate as true"""
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
children_to_process = [c.resolve(world) for c in self.children]
|
||||
return And.from_resolved(world, [c.resolve(world) for c in self.children])
|
||||
|
||||
@classmethod
|
||||
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
|
||||
clauses: list[Rule.Resolved] = []
|
||||
items: dict[str, int] = {}
|
||||
true_rule: Rule.Resolved | None = None
|
||||
@@ -503,7 +647,10 @@ class Or(NestedRule[TWorld], game="Archipelago"):
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
children_to_process = [c.resolve(world) for c in self.children]
|
||||
return Or.from_resolved(world, [c.resolve(world) for c in self.children])
|
||||
|
||||
@classmethod
|
||||
def from_resolved(cls, world: TWorld, children_to_process: list[Rule.Resolved]) -> Rule.Resolved:
|
||||
clauses: list[Rule.Resolved] = []
|
||||
items: dict[str, int] = {}
|
||||
|
||||
@@ -527,7 +674,7 @@ class Or(NestedRule[TWorld], game="Archipelago"):
|
||||
items[item] = 1
|
||||
elif isinstance(child, HasAnyCount.Resolved):
|
||||
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
|
||||
else:
|
||||
clauses.append(child)
|
||||
@@ -688,24 +835,24 @@ class Filtered(WrapperRule[TWorld], game="Archipelago"):
|
||||
class Has(Rule[TWorld], game="Archipelago"):
|
||||
"""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"""
|
||||
|
||||
count: int = 1
|
||||
count: int | FieldResolver = 1
|
||||
"""The count the player is required to have"""
|
||||
|
||||
@override
|
||||
def _instantiate(self, world: TWorld) -> Rule.Resolved:
|
||||
return self.Resolved(
|
||||
self.item_name,
|
||||
self.count,
|
||||
resolve_field(self.item_name, world, str),
|
||||
count=resolve_field(self.count, world, int),
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
|
||||
@override
|
||||
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 ""
|
||||
return f"{self.__class__.__name__}({self.item_name}{count}{options})"
|
||||
|
||||
@@ -991,7 +1138,7 @@ class HasAny(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"""
|
||||
|
||||
item_counts: dict[str, int]
|
||||
item_counts: Mapping[str, int | FieldResolver]
|
||||
"""A mapping of item name to count to check for"""
|
||||
|
||||
@override
|
||||
@@ -1002,12 +1149,30 @@ class HasAllCounts(Rule[TWorld], game="Archipelago"):
|
||||
if len(self.item_counts) == 1:
|
||||
item = next(iter(self.item_counts))
|
||||
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(
|
||||
tuple(self.item_counts.items()),
|
||||
item_counts,
|
||||
player=world.player,
|
||||
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
|
||||
def __str__(self) -> str:
|
||||
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
|
||||
@@ -1096,7 +1261,7 @@ class HasAllCounts(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"""
|
||||
|
||||
item_counts: dict[str, int]
|
||||
item_counts: Mapping[str, int | FieldResolver]
|
||||
"""A mapping of item name to count to check for"""
|
||||
|
||||
@override
|
||||
@@ -1107,12 +1272,30 @@ class HasAnyCount(Rule[TWorld], game="Archipelago"):
|
||||
if len(self.item_counts) == 1:
|
||||
item = next(iter(self.item_counts))
|
||||
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(
|
||||
tuple(self.item_counts.items()),
|
||||
item_counts,
|
||||
player=world.player,
|
||||
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
|
||||
def __str__(self) -> str:
|
||||
items = ", ".join([f"{item} x{count}" for item, count in self.item_counts.items()])
|
||||
@@ -1204,13 +1387,13 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
|
||||
item_names: tuple[str, ...]
|
||||
"""A tuple of item names to check for"""
|
||||
|
||||
count: int = 1
|
||||
count: int | FieldResolver = 1
|
||||
"""The number of items the player needs to have"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*item_names: str,
|
||||
count: int = 1,
|
||||
count: int | FieldResolver = 1,
|
||||
options: Iterable[OptionFilter] = (),
|
||||
filtered_resolution: bool = False,
|
||||
) -> None:
|
||||
@@ -1227,7 +1410,7 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
|
||||
return Has(self.item_names[0], self.count).resolve(world)
|
||||
return self.Resolved(
|
||||
self.item_names,
|
||||
self.count,
|
||||
count=resolve_field(self.count, world, int),
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
@@ -1235,7 +1418,7 @@ class HasFromList(Rule[TWorld], game="Archipelago"):
|
||||
@override
|
||||
@classmethod
|
||||
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", ())
|
||||
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
||||
@@ -1338,13 +1521,13 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
|
||||
item_names: tuple[str, ...]
|
||||
"""A tuple of item names to check for"""
|
||||
|
||||
count: int = 1
|
||||
count: int | FieldResolver = 1
|
||||
"""The number of items the player needs to have"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*item_names: str,
|
||||
count: int = 1,
|
||||
count: int | FieldResolver = 1,
|
||||
options: Iterable[OptionFilter] = (),
|
||||
filtered_resolution: bool = False,
|
||||
) -> None:
|
||||
@@ -1354,14 +1537,15 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
|
||||
|
||||
@override
|
||||
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
|
||||
return False_().resolve(world)
|
||||
if len(self.item_names) == 1:
|
||||
return Has(self.item_names[0]).resolve(world)
|
||||
return self.Resolved(
|
||||
self.item_names,
|
||||
self.count,
|
||||
count,
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
@@ -1369,7 +1553,7 @@ class HasFromListUnique(Rule[TWorld], game="Archipelago"):
|
||||
@override
|
||||
@classmethod
|
||||
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", ())
|
||||
options = OptionFilter.multiple_from_dict(data.get("options", ()))
|
||||
return cls(*item_names, **args, options=options, filtered_resolution=data.get("filtered_resolution", False))
|
||||
@@ -1468,7 +1652,7 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
|
||||
item_name_group: str
|
||||
"""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"""
|
||||
|
||||
@override
|
||||
@@ -1477,14 +1661,14 @@ class HasGroup(Rule[TWorld], game="Archipelago"):
|
||||
return self.Resolved(
|
||||
self.item_name_group,
|
||||
item_names,
|
||||
self.count,
|
||||
count=resolve_field(self.count, world, int),
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
|
||||
@override
|
||||
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 ""
|
||||
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
|
||||
|
||||
@@ -1542,7 +1726,7 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
|
||||
item_name_group: str
|
||||
"""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"""
|
||||
|
||||
@override
|
||||
@@ -1551,14 +1735,14 @@ class HasGroupUnique(Rule[TWorld], game="Archipelago"):
|
||||
return self.Resolved(
|
||||
self.item_name_group,
|
||||
item_names,
|
||||
self.count,
|
||||
count=resolve_field(self.count, world, int),
|
||||
player=world.player,
|
||||
caching_enabled=getattr(world, "rule_caching_enabled", False),
|
||||
)
|
||||
|
||||
@override
|
||||
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 ""
|
||||
return f"{self.__class__.__name__}({self.item_name_group}{count}{options})"
|
||||
|
||||
|
||||
+9
-5
@@ -98,6 +98,8 @@ class Group:
|
||||
self._changed = True
|
||||
attr = new
|
||||
# resolve the path immediately when accessing it
|
||||
if attr.exists():
|
||||
attr.__class__.validate(attr.resolve())
|
||||
return attr.__class__(attr.resolve())
|
||||
return attr
|
||||
|
||||
@@ -633,10 +635,6 @@ class ServerOptions(Group):
|
||||
class GeneratorOptions(Group):
|
||||
"""Options for Generation"""
|
||||
|
||||
class EnemizerPath(LocalFilePath):
|
||||
"""Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases"""
|
||||
is_exe = True
|
||||
|
||||
class PlayerFilesPath(OptionalUserFolderPath):
|
||||
"""Folder from which the player yaml files are pulled from"""
|
||||
# created on demand, so marked as optional
|
||||
@@ -644,6 +642,12 @@ class GeneratorOptions(Group):
|
||||
class Players(int):
|
||||
"""amount of players, 0 to infer from player files"""
|
||||
|
||||
class AllowQuantity(Bool):
|
||||
"""
|
||||
allow players to set an individual quantity for their yaml settings
|
||||
with 'false' any amounts from the players will be ignored and set to 1
|
||||
"""
|
||||
|
||||
class WeightsFilePath(str):
|
||||
"""
|
||||
general weights file, within the stated player_files_path location
|
||||
@@ -687,9 +691,9 @@ class GeneratorOptions(Group):
|
||||
start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
|
||||
"""
|
||||
|
||||
enemizer_path: EnemizerPath = EnemizerPath("EnemizerCLI/EnemizerCLI.Core") # + ".exe" is implied on Windows
|
||||
player_files_path: PlayerFilesPath = PlayerFilesPath("Players")
|
||||
players: Players = Players(0)
|
||||
allow_quantity: AllowQuantity | bool = False
|
||||
weights_file_path: WeightsFilePath = WeightsFilePath("weights.yaml")
|
||||
meta_file_path: MetaFilePath = MetaFilePath("meta.yaml")
|
||||
spoiler: Spoiler = Spoiler(3)
|
||||
|
||||
@@ -71,7 +71,6 @@ non_apworlds: set[str] = {
|
||||
"Ocarina of Time",
|
||||
"Overcooked! 2",
|
||||
"Raft",
|
||||
"Sudoku",
|
||||
"Super Mario 64",
|
||||
"VVVVVV",
|
||||
"Wargroove",
|
||||
@@ -202,7 +201,7 @@ if is_windows:
|
||||
icon=resolve_icon(c.icon),
|
||||
))
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
||||
extra_data = ["LICENSE", "data", "SNI"]
|
||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||
|
||||
|
||||
@@ -457,9 +456,8 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
for world_directory in folders_to_remove)
|
||||
else:
|
||||
# make sure extra programs are executable
|
||||
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
|
||||
sni_exe = self.buildfolder / 'SNI/sni'
|
||||
extra_exes = (enemizer_exe, sni_exe)
|
||||
extra_exes = (sni_exe,)
|
||||
for extra_exe in extra_exes:
|
||||
if extra_exe.is_file():
|
||||
extra_exe.chmod(0o755)
|
||||
@@ -658,7 +656,7 @@ cx_Freeze.setup(
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||
"includes": [],
|
||||
"includes": ["rule_builder.cached_world"],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas"],
|
||||
"zip_includes": [],
|
||||
|
||||
@@ -11,7 +11,7 @@ class TestImplemented(unittest.TestCase):
|
||||
def test_completion_condition(self):
|
||||
"""Ensure a completion condition is set that has requirements."""
|
||||
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):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
||||
@@ -54,12 +54,12 @@ class TestImplemented(unittest.TestCase):
|
||||
|
||||
def test_no_failed_world_loads(self):
|
||||
if failed_world_loads:
|
||||
self.fail(f"The following worlds failed to load: {failed_world_loads}")
|
||||
self.fail(f"The following worlds failed to load: {failed_world_loads.keys()}")
|
||||
|
||||
def test_prefill_items(self):
|
||||
"""Test that every world can reach every location from allstate before pre_fill."""
|
||||
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):
|
||||
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
||||
"set_rules", "connect_entrances", "generate_basic"))
|
||||
|
||||
@@ -109,7 +109,7 @@ class TestOptions(unittest.TestCase):
|
||||
def test_option_set_keys_random(self):
|
||||
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
|
||||
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():
|
||||
if issubclass(option, OptionSet):
|
||||
with self.subTest(game=game_name, option=option_key):
|
||||
|
||||
@@ -6,11 +6,13 @@ from typing_extensions import override
|
||||
|
||||
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
|
||||
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.field_resolvers import FieldResolver, FromOption, FromWorldAttr, resolve_field
|
||||
from rule_builder.options import Operator, OptionFilter
|
||||
from rule_builder.rules import (
|
||||
And,
|
||||
AtLeast,
|
||||
CanReachEntrance,
|
||||
CanReachLocation,
|
||||
CanReachRegion,
|
||||
@@ -59,12 +61,20 @@ class SetOption(OptionSet):
|
||||
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
|
||||
class RuleBuilderOptions(PerGameCommonOptions):
|
||||
toggle_option: ToggleOption
|
||||
choice_option: ChoiceOption
|
||||
text_option: FreeTextOption
|
||||
set_option: SetOption
|
||||
range_option: RangeOption
|
||||
|
||||
|
||||
GAME_NAME = "Rule Builder Test Game"
|
||||
@@ -233,6 +243,48 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase):
|
||||
Or(Has("A"), HasAny("B", "C"), HasAnyCount({"D": 1, "E": 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),
|
||||
),
|
||||
(
|
||||
AtLeast(0, Has("A")),
|
||||
True_.Resolved(player=1),
|
||||
),
|
||||
(
|
||||
AtLeast(3, True_(), Has("A"), Has("B"), Has("C")),
|
||||
AtLeast.Resolved(
|
||||
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
|
||||
),
|
||||
),
|
||||
(
|
||||
AtLeast(2, False_(), Has("A"), Has("B"), Has("C")),
|
||||
AtLeast.Resolved(
|
||||
(Has.Resolved("A", player=1), Has.Resolved("B", player=1), Has.Resolved("C", player=1)), 2, player=1
|
||||
),
|
||||
),
|
||||
(
|
||||
AtLeast(2, True_(), True_(), Has("A")),
|
||||
True_.Resolved(player=1),
|
||||
),
|
||||
(
|
||||
AtLeast(3, Has("A"), Has("B")),
|
||||
False_.Resolved(player=1),
|
||||
),
|
||||
(
|
||||
# This test will fail when Or(Rule, Rule) will be optimized to Rule
|
||||
AtLeast(1, Rule(), Rule()),
|
||||
Or.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
|
||||
),
|
||||
(
|
||||
# This test will fail when And(Rule, Rule) will be optimized to Rule
|
||||
AtLeast(2, Rule(), Rule()),
|
||||
And.Resolved((Rule.Resolved(player=1), Rule.Resolved(player=1)), player=1),
|
||||
),
|
||||
)
|
||||
)
|
||||
class TestSimplify(RuleBuilderTestCase):
|
||||
@@ -399,6 +451,15 @@ class TestHashes(RuleBuilderTestCase):
|
||||
rule2 = HasAll("2", "2", "2", "1")
|
||||
self.assertEqual(hash(rule1.resolve(world)), hash(rule2.resolve(world)))
|
||||
|
||||
def test_hash_collision(self) -> None:
|
||||
multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
|
||||
world = multiworld.worlds[1]
|
||||
rule1 = Has("A", count=1).resolve(world)
|
||||
rule2 = Has("A", count=1 << 61).resolve(world)
|
||||
self.assertEqual(hash(rule1), hash(rule2))
|
||||
self.assertNotEqual(rule1, rule2)
|
||||
self.assertNotEqual(id(rule1), id(rule2))
|
||||
|
||||
|
||||
class TestCaching(CachedRuleBuilderTestCase):
|
||||
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
@@ -605,6 +666,24 @@ class TestRules(RuleBuilderTestCase):
|
||||
self.state.remove(item)
|
||||
self.assertFalse(resolved_rule(self.state))
|
||||
|
||||
def test_at_least(self) -> None:
|
||||
# Has has to be relied on as True_ and False_ would be optimized out
|
||||
rule = AtLeast(2, Has("Item 1"), Has("Item 1"), Has("Item 2"), Has("Item 3"))
|
||||
resolved_rule = rule.resolve(self.world)
|
||||
self.world.register_rule_dependencies(resolved_rule)
|
||||
item1 = self.world.create_item("Item 1")
|
||||
item2 = self.world.create_item("Item 2")
|
||||
item3 = self.world.create_item("Item 3")
|
||||
self.assertFalse(resolved_rule(self.state))
|
||||
self.state.collect(item1)
|
||||
self.assertTrue(resolved_rule(self.state))
|
||||
self.state.collect(item2)
|
||||
self.assertTrue(resolved_rule(self.state))
|
||||
self.state.remove(item1)
|
||||
self.assertFalse(resolved_rule(self.state))
|
||||
self.state.collect(item3)
|
||||
self.assertTrue(resolved_rule(self.state))
|
||||
|
||||
def test_has_all(self) -> None:
|
||||
rule = HasAll("Item 1", "Item 2")
|
||||
resolved_rule = rule.resolve(self.world)
|
||||
@@ -651,14 +730,15 @@ class TestRules(RuleBuilderTestCase):
|
||||
self.assertFalse(resolved_rule(self.state))
|
||||
|
||||
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)
|
||||
resolved_rule = rule.resolve(self.world)
|
||||
self.world.register_rule_dependencies(resolved_rule)
|
||||
|
||||
for item_name, count in item_counts.items():
|
||||
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.state.collect(item)
|
||||
self.assertTrue(resolved_rule(self.state))
|
||||
@@ -755,7 +835,7 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
|
||||
rule: ClassVar[Rule[Any]] = And(
|
||||
Or(
|
||||
Has("i1", count=4),
|
||||
Has("i1", count=FromOption(RangeOption)),
|
||||
HasFromList("i2", "i3", "i4", count=2),
|
||||
HasAnyCount({"i5": 2, "i6": 3}),
|
||||
options=[OptionFilter(ToggleOption, 0)],
|
||||
@@ -763,7 +843,7 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
Or(
|
||||
HasAll("i7", "i8"),
|
||||
HasAllCounts(
|
||||
{"i9": 1, "i10": 5},
|
||||
{"i9": 1, "i10": FromWorldAttr("instance_data.i10_count")},
|
||||
options=[OptionFilter(ToggleOption, 1, operator="ne")],
|
||||
filtered_resolution=True,
|
||||
),
|
||||
@@ -779,8 +859,13 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
OptionFilter(ChoiceOption, ChoiceOption.option_second, "ge"),
|
||||
],
|
||||
),
|
||||
AtLeast(
|
||||
FromWorldAttr("instance_data.at_least_requirement"),
|
||||
Has("i15", count=2),
|
||||
HasGroup("g2", count=3),
|
||||
),
|
||||
CanReachEntrance("e1"),
|
||||
HasGroupUnique("g2", count=5),
|
||||
HasGroupUnique("g3", count=5),
|
||||
)
|
||||
|
||||
rule_dict: ClassVar[dict[str, Any]] = {
|
||||
@@ -803,7 +888,14 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"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",
|
||||
@@ -840,7 +932,12 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
},
|
||||
],
|
||||
"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",
|
||||
@@ -892,6 +989,29 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"rule": "AtLeast",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"count": {"resolver": "FromWorldAttr", "name": "instance_data.at_least_requirement"},
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"args": {
|
||||
"item_name": "i15",
|
||||
"count": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "HasGroup",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"args": {"item_name_group": "g2", "count": 3},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"rule": "CanReachEntrance",
|
||||
"options": [],
|
||||
@@ -902,7 +1022,7 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
"rule": "HasGroupUnique",
|
||||
"options": [],
|
||||
"filtered_resolution": False,
|
||||
"args": {"item_name_group": "g2", "count": 5},
|
||||
"args": {"item_name_group": "g3", "count": 5},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -915,7 +1035,7 @@ class TestSerialization(RuleBuilderTestCase):
|
||||
multiworld = setup_solo_multiworld(self.world_cls, steps=(), seed=0)
|
||||
world = multiworld.worlds[1]
|
||||
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):
|
||||
@@ -934,9 +1054,15 @@ class TestExplain(RuleBuilderTestCase):
|
||||
),
|
||||
player=1,
|
||||
),
|
||||
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
|
||||
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
|
||||
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
|
||||
AtLeast.Resolved(
|
||||
children=(
|
||||
HasAllCounts.Resolved((("Item 6", 1), ("Item 7", 5)), player=1),
|
||||
HasAnyCount.Resolved((("Item 8", 2), ("Item 9", 3)), player=1),
|
||||
HasFromList.Resolved(("Item 10", "Item 11", "Item 12"), count=2, player=1),
|
||||
),
|
||||
count=2,
|
||||
player=1,
|
||||
),
|
||||
HasFromListUnique.Resolved(("Item 13", "Item 14"), player=1),
|
||||
HasGroup.Resolved("Group 1", ("Item 15", "Item 16", "Item 17"), player=1),
|
||||
HasGroupUnique.Resolved("Group 2", ("Item 18", "Item 19"), count=2, player=1),
|
||||
@@ -1001,6 +1127,9 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": "0/2"},
|
||||
{"type": "text", "text": " of ("},
|
||||
{"type": "text", "text": "Missing "},
|
||||
{"type": "color", "color": "cyan", "text": "some"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1011,7 +1140,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "Item 7"},
|
||||
{"type": "text", "text": " x5"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": "Missing "},
|
||||
{"type": "color", "color": "cyan", "text": "all"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1022,7 +1151,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "Item 9"},
|
||||
{"type": "text", "text": " x3"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "salmon", "text": "0/2"},
|
||||
{"type": "text", "text": " items from ("},
|
||||
@@ -1033,6 +1162,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "color", "color": "salmon", "text": "Item 12"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "salmon", "text": "0/1"},
|
||||
@@ -1099,6 +1229,9 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": "3/2"},
|
||||
{"type": "text", "text": " of ("},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "all"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1109,7 +1242,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "green", "text": "Item 7"},
|
||||
{"type": "text", "text": " x5"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "some"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1120,7 +1253,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "green", "text": "Item 9"},
|
||||
{"type": "text", "text": " x3"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "green", "text": "30/2"},
|
||||
{"type": "text", "text": " items from ("},
|
||||
@@ -1131,6 +1264,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "color", "color": "green", "text": "Item 12"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "green", "text": "2/1"},
|
||||
@@ -1165,7 +1299,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "False"},
|
||||
{"type": "text", "text": ")"},
|
||||
]
|
||||
assert self.resolved_rule.explain_json(self.state) == expected
|
||||
self.assertEqual(self.resolved_rule.explain_json(self.state), expected)
|
||||
|
||||
def test_explain_json_without_state(self) -> None:
|
||||
expected: list[JSONMessagePart] = [
|
||||
@@ -1193,6 +1327,9 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "At least "},
|
||||
{"type": "color", "color": "cyan", "text": "2"},
|
||||
{"type": "text", "text": " of ("},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "all"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1202,7 +1339,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "item_name", "flags": 1, "text": "Item 7", "player": 1},
|
||||
{"type": "text", "text": " x5"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "any"},
|
||||
{"type": "text", "text": " of ("},
|
||||
@@ -1212,7 +1349,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "item_name", "flags": 1, "text": "Item 9", "player": 1},
|
||||
{"type": "text", "text": " x3"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "2"},
|
||||
{"type": "text", "text": "x items from ("},
|
||||
@@ -1222,6 +1359,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "text", "text": ", "},
|
||||
{"type": "item_name", "flags": 1, "text": "Item 12", "player": 1},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": ")"},
|
||||
{"type": "text", "text": " & "},
|
||||
{"type": "text", "text": "Has "},
|
||||
{"type": "color", "color": "cyan", "text": "1"},
|
||||
@@ -1255,16 +1393,16 @@ class TestExplain(RuleBuilderTestCase):
|
||||
{"type": "color", "color": "salmon", "text": "False"},
|
||||
{"type": "text", "text": ")"},
|
||||
]
|
||||
assert self.resolved_rule.explain_json() == expected
|
||||
self.assertEqual(self.resolved_rule.explain_json(), expected)
|
||||
|
||||
def test_explain_str_with_state_no_items(self) -> None:
|
||||
expected = (
|
||||
"((Missing 4x Item 1",
|
||||
"| Missing some of (Missing: Item 2, Item 3)",
|
||||
"| Missing all of (Missing: Item 4, Item 5))",
|
||||
"& Missing some of (Missing: Item 6 x1, Item 7 x5)",
|
||||
"& Missing all of (Missing: Item 8 x2, Item 9 x3)",
|
||||
"& Has 0/2 items from (Missing: Item 10, Item 11, Item 12)",
|
||||
"& At least 0/2 of (Missing some of (Missing: Item 6 x1, Item 7 x5),",
|
||||
"Missing all of (Missing: Item 8 x2, Item 9 x3),",
|
||||
"Has 0/2 items from (Missing: Item 10, Item 11, Item 12))",
|
||||
"& Has 0/1 unique items from (Missing: Item 13, Item 14)",
|
||||
"& Has 0/1 items from Group 1",
|
||||
"& Has 0/2 unique items from Group 2",
|
||||
@@ -1274,7 +1412,7 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
|
||||
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
|
||||
|
||||
def test_explain_str_with_state_all_items(self) -> None:
|
||||
self._collect_all()
|
||||
@@ -1283,9 +1421,9 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"((Has 4x Item 1",
|
||||
"| Has all of (Found: Item 2, Item 3)",
|
||||
"| Has some of (Found: Item 4, Item 5))",
|
||||
"& Has all of (Found: Item 6 x1, Item 7 x5)",
|
||||
"& Has some of (Found: Item 8 x2, Item 9 x3)",
|
||||
"& Has 30/2 items from (Found: Item 10, Item 11, Item 12)",
|
||||
"& At least 3/2 of (Has all of (Found: Item 6 x1, Item 7 x5),",
|
||||
"Has some of (Found: Item 8 x2, Item 9 x3),",
|
||||
"Has 30/2 items from (Found: Item 10, Item 11, Item 12))",
|
||||
"& Has 2/1 unique items from (Found: Item 13, Item 14)",
|
||||
"& Has 30/1 items from Group 1",
|
||||
"& Has 2/2 unique items from Group 2",
|
||||
@@ -1295,16 +1433,16 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
assert self.resolved_rule.explain_str(self.state) == " ".join(expected)
|
||||
self.assertEqual(self.resolved_rule.explain_str(self.state), " ".join(expected))
|
||||
|
||||
def test_explain_str_without_state(self) -> None:
|
||||
expected = (
|
||||
"((Has 4x Item 1",
|
||||
"| Has all of (Item 2, Item 3)",
|
||||
"| Has any of (Item 4, Item 5))",
|
||||
"& Has all of (Item 6 x1, Item 7 x5)",
|
||||
"& Has any of (Item 8 x2, Item 9 x3)",
|
||||
"& Has 2x items from (Item 10, Item 11, Item 12)",
|
||||
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
|
||||
"Has any of (Item 8 x2, Item 9 x3),",
|
||||
"Has 2x items from (Item 10, Item 11, Item 12))",
|
||||
"& Has a unique item from (Item 13, Item 14)",
|
||||
"& Has an item from Group 1",
|
||||
"& Has 2x unique items from Group 2",
|
||||
@@ -1314,16 +1452,16 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
assert self.resolved_rule.explain_str() == " ".join(expected)
|
||||
self.assertEqual(self.resolved_rule.explain_str(), " ".join(expected))
|
||||
|
||||
def test_str(self) -> None:
|
||||
expected = (
|
||||
"((Has 4x Item 1",
|
||||
"| Has all of (Item 2, Item 3)",
|
||||
"| Has any of (Item 4, Item 5))",
|
||||
"& Has all of (Item 6 x1, Item 7 x5)",
|
||||
"& Has any of (Item 8 x2, Item 9 x3)",
|
||||
"& Has 2x items from (Item 10, Item 11, Item 12)",
|
||||
"& At least 2 of (Has all of (Item 6 x1, Item 7 x5),",
|
||||
"Has any of (Item 8 x2, Item 9 x3),",
|
||||
"Has 2x items from (Item 10, Item 11, Item 12))",
|
||||
"& Has a unique item from (Item 13, Item 14)",
|
||||
"& Has an item from Group 1",
|
||||
"& Has 2x unique items from Group 2",
|
||||
@@ -1333,4 +1471,33 @@ class TestExplain(RuleBuilderTestCase):
|
||||
"& True",
|
||||
"& False)",
|
||||
)
|
||||
assert str(self.resolved_rule) == " ".join(expected)
|
||||
self.assertEqual(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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
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):
|
||||
@@ -74,3 +76,97 @@ class TestNumericOptions(unittest.TestCase):
|
||||
self.assertTrue(toggle_string)
|
||||
self.assertTrue(toggle_int)
|
||||
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}))
|
||||
|
||||
@@ -33,4 +33,9 @@ class TestBase(unittest.TestCase):
|
||||
cls.app = raw_app
|
||||
|
||||
def setUp(self) -> None:
|
||||
from WebHostLib.models import db
|
||||
from pony.orm import db_session
|
||||
with db_session:
|
||||
for entity in db.entities.values():
|
||||
entity.select().delete(bulk=True)
|
||||
self.client = self.app.test_client()
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
from datetime import timedelta
|
||||
from uuid import UUID, uuid4
|
||||
from pony.orm import db_session, commit
|
||||
|
||||
from Utils import utcnow
|
||||
from WebHostLib.autolauncher import cleanup
|
||||
from WebHostLib.models import Room, Seed, Slot
|
||||
from . import TestBase
|
||||
|
||||
|
||||
class TestCleanup(TestBase):
|
||||
def test_cleanup_unowned(self) -> None:
|
||||
with db_session:
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=UUID(int=0))
|
||||
Room(id=uuid4(), owner=UUID(int=0), seed=s1)
|
||||
|
||||
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4()) # Owned
|
||||
Room(id=uuid4(), owner=UUID(int=0), seed=s2) # Unowned room of owned seed
|
||||
|
||||
Seed(id=uuid4(), multidata=b"", owner=UUID(int=0)) # Unowned seed with no rooms
|
||||
|
||||
commit()
|
||||
|
||||
cleanup({"ROOM_AUTO_DELETE": 0})
|
||||
|
||||
with db_session:
|
||||
self.assertEqual(Room.select().count(), 0) # Both rooms were unowned
|
||||
self.assertEqual(Seed.select().count(), 1) # s2 is owned
|
||||
self.assertIsNotNone(Seed.get(id=s2.id))
|
||||
|
||||
def test_cleanup_auto_delete(self) -> None:
|
||||
now = utcnow()
|
||||
old_time = now - timedelta(days=10)
|
||||
recent_time = now - timedelta(days=2)
|
||||
|
||||
with db_session:
|
||||
# Case 1: Old room, owned
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
|
||||
|
||||
# Case 2: Recent room, owned
|
||||
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r2 = Room(id=uuid4(), owner=uuid4(), seed=s2, last_activity=recent_time)
|
||||
|
||||
# Case 3: Old seed, no rooms, owned
|
||||
s3 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
|
||||
# Case 4: Recent seed, no rooms, owned
|
||||
s4 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=recent_time)
|
||||
|
||||
# Case 5: Old seed with recent room (should not be deleted)
|
||||
s5 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r5 = Room(id=uuid4(), owner=uuid4(), seed=s5, last_activity=recent_time)
|
||||
|
||||
commit()
|
||||
|
||||
# Delete items older than 5 days
|
||||
cleanup({"ROOM_AUTO_DELETE": 5})
|
||||
|
||||
with db_session:
|
||||
self.assertIsNone(Room.get(id=r1.id), "Old room should be deleted")
|
||||
self.assertIsNotNone(Room.get(id=r2.id), "Recent room should NOT be deleted")
|
||||
self.assertIsNone(Seed.get(id=s3.id), "Old seed without rooms should be deleted")
|
||||
self.assertIsNotNone(Seed.get(id=s4.id), "Recent seed without rooms should NOT be deleted")
|
||||
self.assertIsNotNone(Seed.get(id=s5.id), "Old seed with recent room should NOT be deleted")
|
||||
self.assertIsNotNone(Room.get(id=r5.id), "Recent room for old seed should NOT be deleted")
|
||||
|
||||
# Seeds are deleted if they have NO rooms AND are old.
|
||||
# After r1 is deleted, s1 has no rooms. Since it's old, it should be deleted.
|
||||
self.assertIsNone(Seed.get(id=s1.id), "Old seed whose only room was deleted should be deleted")
|
||||
|
||||
def test_cleanup_disabled(self) -> None:
|
||||
now = utcnow()
|
||||
old_time = now - timedelta(days=10)
|
||||
|
||||
with db_session:
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
r1 = Room(id=uuid4(), owner=uuid4(), seed=s1, last_activity=old_time)
|
||||
commit()
|
||||
|
||||
cleanup({"ROOM_AUTO_DELETE": 0})
|
||||
|
||||
with db_session:
|
||||
self.assertIsNotNone(Room.get(id=r1.id), "Room should NOT be deleted when auto-delete is 0")
|
||||
self.assertIsNotNone(Seed.get(id=s1.id), "Seed should NOT be deleted when auto-delete is 0")
|
||||
|
||||
def test_cleanup_slots(self) -> None:
|
||||
now = utcnow()
|
||||
old_time = now - timedelta(days=10)
|
||||
|
||||
with db_session:
|
||||
s1 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=old_time)
|
||||
slot1 = Slot(player_id=1, player_name="P1", seed=s1, game="TestGame")
|
||||
|
||||
s2 = Seed(id=uuid4(), multidata=b"", owner=uuid4(), creation_time=now)
|
||||
slot2 = Slot(player_id=2, player_name="P2", seed=s2, game="TestGame")
|
||||
|
||||
commit()
|
||||
|
||||
# Delete items older than 5 days
|
||||
cleanup({"ROOM_AUTO_DELETE": 5})
|
||||
|
||||
with db_session:
|
||||
self.assertIsNone(Seed.get(id=s1.id), "Old seed should be deleted")
|
||||
self.assertIsNone(Slot.get(id=slot1.id), "Slot of deleted seed should be deleted")
|
||||
self.assertIsNotNone(Seed.get(id=s2.id), "Recent seed should NOT be deleted")
|
||||
self.assertIsNotNone(Slot.get(id=slot2.id), "Slot of recent seed should NOT be deleted")
|
||||
@@ -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)
|
||||
+5
-1
@@ -353,6 +353,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""path it was loaded from"""
|
||||
world_version: ClassVar[Version] = Version(0, 0, 0)
|
||||
"""Optional world version loaded from archipelago.json"""
|
||||
manifest: ClassVar[dict[str, Any]] = {}
|
||||
"""Mapping of the world's archipelago.json manifest. Use game and world_version attrs instead for those values."""
|
||||
|
||||
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||
assert multiworld is not None
|
||||
@@ -512,7 +514,9 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
"""
|
||||
Called when the item pool needs to be filled with additional items to match location count.
|
||||
If core AP removes an item from your item pool, this method is called to choose a replacement item
|
||||
so item count and location count remain equal.
|
||||
For example: plando, item_links and start_inventory_from_pool are features that may cause this.
|
||||
|
||||
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
|
||||
For most worlds this will be one or more of your filler items, but the classification of these items
|
||||
|
||||
@@ -269,8 +269,9 @@ if not is_frozen():
|
||||
from Launcher import open_folder
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser("Build script for APWorlds")
|
||||
parser.add_argument("worlds", type=str, default=(), nargs="*", help="Names of APWorlds to build.")
|
||||
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("--skip_open_folder", action="store_true", help="don't open the output build folder")
|
||||
args = parser.parse_args(launch_args)
|
||||
|
||||
if args.worlds:
|
||||
@@ -320,7 +321,9 @@ if not is_frozen():
|
||||
zf.write(pathlib.Path(world_directory, file), pathlib.Path(file_name, file))
|
||||
|
||||
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,
|
||||
description="Build APWorlds from loose-file world folders."))
|
||||
|
||||
+16
-5
@@ -11,7 +11,7 @@ import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import List, Sequence
|
||||
from zipfile import BadZipFile
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
|
||||
from NetUtils import DataPackage
|
||||
from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox
|
||||
@@ -33,7 +33,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
failed_world_loads: List[str] = []
|
||||
failed_world_loads: dict[str, str] = {}
|
||||
|
||||
|
||||
@dataclasses.dataclass(order=True)
|
||||
@@ -68,8 +68,9 @@ class WorldSource:
|
||||
print(f"Could not load world {self}:", file=file_like)
|
||||
traceback.print_exc(file=file_like)
|
||||
file_like.seek(0)
|
||||
logging.exception(file_like.read())
|
||||
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
reason = file_like.read()
|
||||
logging.exception(reason)
|
||||
failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason
|
||||
return False
|
||||
|
||||
|
||||
@@ -118,6 +119,7 @@ for world_source in world_sources:
|
||||
game = manifest.get("game")
|
||||
if game in AutoWorldRegister.world_types:
|
||||
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
|
||||
AutoWorldRegister.world_types[game].manifest = manifest
|
||||
|
||||
if apworlds:
|
||||
# encapsulation for namespace / gc purposes
|
||||
@@ -128,7 +130,7 @@ if apworlds:
|
||||
|
||||
def fail_world(game_name: str, reason: str, add_as_failed_to_load: bool = True) -> None:
|
||||
if add_as_failed_to_load:
|
||||
failed_world_loads.append(game_name)
|
||||
failed_world_loads[game_name] = reason
|
||||
logging.warning(reason)
|
||||
|
||||
for apworld_source in apworlds:
|
||||
@@ -199,6 +201,15 @@ if apworlds:
|
||||
# world could fail to load at this point
|
||||
if apworld.world_version:
|
||||
AutoWorldRegister.world_types[apworld.game].world_version = apworld.world_version
|
||||
|
||||
assert apworld.path
|
||||
with ZipFile(apworld.path, "r") as zf:
|
||||
manifest = apworld.read_contents(zf)
|
||||
# version/compatible_version shouldn't be needed by world, makes it consistent with folder world
|
||||
manifest.pop("version", None)
|
||||
manifest.pop("compatible_version", None)
|
||||
AutoWorldRegister.world_types[apworld.game].manifest = manifest
|
||||
|
||||
load_apworlds()
|
||||
del load_apworlds
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ from Options import PerGameCommonOptions
|
||||
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):
|
||||
source_region = world.get_region(source, player)
|
||||
target_region = world.get_region(target, player)
|
||||
source_region = multiworld.get_region(source, player)
|
||||
target_region = multiworld.get_region(target, player)
|
||||
|
||||
if name is None:
|
||||
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)
|
||||
connection.connect(target_region)
|
||||
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:
|
||||
|
||||
+25
-25
@@ -3,47 +3,47 @@ from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
world = self.multiworld
|
||||
multiworld = self.multiworld
|
||||
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))
|
||||
set_rule(world.get_entrance("BlackCastlePort", self.player),
|
||||
set_rule(multiworld.get_entrance("BlackCastlePort", 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))
|
||||
|
||||
# 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
|
||||
# use_bat_logic is true
|
||||
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))
|
||||
set_rule(world.get_entrance("WhiteCastlePeekPassage", self.player),
|
||||
set_rule(multiworld.get_entrance("WhiteCastlePeekPassage", self.player),
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
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
|
||||
state.has("Magnet", self.player))
|
||||
|
||||
dragon_slay_check = self.options.dragon_slay_check.value
|
||||
if dragon_slay_check:
|
||||
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
|
||||
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
|
||||
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
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
|
||||
# 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
|
||||
# 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
|
||||
set_rule(world.get_entrance("CreditsWall", self.player),
|
||||
set_rule(multiworld.get_entrance("CreditsWall", self.player),
|
||||
lambda state: state.has("Bridge", self.player) and
|
||||
state.has("Black Key", self.player))
|
||||
|
||||
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))
|
||||
|
||||
# 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
|
||||
if not use_bat_logic:
|
||||
forbid_item(world.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(world.get_location("Credits Right Side", self.player), "Magnet", self.player)
|
||||
forbid_item(multiworld.get_location("Dungeon Vault", self.player), "Magnet", self.player)
|
||||
forbid_item(multiworld.get_location("Red Maze Vault Entrance", 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
|
||||
forbid_item(world.get_location("Inside Yellow Castle", self.player), "Chalice", self.player)
|
||||
overworld = world.get_region("Overworld", self.player)
|
||||
forbid_item(multiworld.get_location("Inside Yellow Castle", self.player), "Chalice", self.player)
|
||||
overworld = multiworld.get_region("Overworld", self.player)
|
||||
|
||||
for loc in overworld.locations:
|
||||
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))
|
||||
|
||||
# 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():
|
||||
# loc = world.random.choice(all_locations)
|
||||
# loc = multiworld.random.choice(all_locations)
|
||||
# if loc.progress_type == LocationProgressType.DEFAULT:
|
||||
# loc.progress_type = LocationProgressType.PRIORITY
|
||||
# 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()}
|
||||
required_client_version: Tuple[int, int, int] = (0, 3, 9)
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
self.rom_name: Optional[bytearray] = bytearray("", "utf8" )
|
||||
self.dragon_rooms: [int] = [0x14, 0x19, 0x4]
|
||||
self.dragon_slay_check: Optional[int] = 0
|
||||
|
||||
@@ -50,16 +50,17 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t
|
||||
|
||||
## FAQ/Common Issues
|
||||
|
||||
### The game is not connecting when starting a new save!
|
||||
For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu
|
||||
(rocket icon) in-game, and re-enable the mod.
|
||||
### The game is crashing on startup repeatedly!
|
||||
This is a common issue on older versions of the game, caused by the game failing to interface with the Steam Workshop.
|
||||
To fix it you can try the following (from least to most effort required)
|
||||
- Subscribe to any random workshop mod, then unsubscribe from it
|
||||
- Restart Steam
|
||||
- Restart your computer
|
||||
- Delete the game's config directory from the files `steamapps/common/HatinTime/HatinTimeGame/Config` then verify the game files
|
||||
- Reinstall the game
|
||||
|
||||
### Why do relics disappear from the stands in the Spaceship after they're completed?
|
||||
This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that
|
||||
a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed
|
||||
after being completed to allow for the placement of more relics without being potentially locked out.
|
||||
The level that the relic set unlocked will stay unlocked.
|
||||
|
||||
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
|
||||
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
|
||||
if you have too many save files. Delete them and it should fix the problem.
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from Utils import pc_to_snes, snes_to_pc
|
||||
from .enemizer_data.base_patch_data import ENEMIZER_BASE_PATCHES
|
||||
from .enemizer_data.symbols import ENEMIZER_SYMBOLS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
from .Rom import LocalRom
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BossPatchData:
|
||||
pointer: tuple[int, int]
|
||||
graphics: int
|
||||
sprite_array: tuple[int, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DungeonBossPatchData:
|
||||
room_id: int
|
||||
sprite_pointer_address: int
|
||||
shell_x: int
|
||||
shell_y: int
|
||||
clear_layer2: bool = False
|
||||
extra_sprites: tuple[int, ...] = ()
|
||||
gt_sprite_write_address: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoomObjectTable:
|
||||
header_byte_0: int
|
||||
header_byte_1: int
|
||||
layer_1_objects: list[bytes] = field(default_factory=list)
|
||||
layer_1_doors: list[bytes] = field(default_factory=list)
|
||||
layer_2_objects: list[bytes] = field(default_factory=list)
|
||||
layer_2_doors: list[bytes] = field(default_factory=list)
|
||||
layer_3_objects: list[bytes] = field(default_factory=list)
|
||||
layer_3_doors: list[bytes] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_rom(cls, rom: "LocalRom", start_address: int) -> "RoomObjectTable":
|
||||
table = cls(rom.read_byte(start_address), rom.read_byte(start_address + 1))
|
||||
layers = (
|
||||
(table.layer_1_objects, table.layer_1_doors),
|
||||
(table.layer_2_objects, table.layer_2_doors),
|
||||
(table.layer_3_objects, table.layer_3_doors),
|
||||
)
|
||||
index = start_address + 2
|
||||
|
||||
for objects, doors in layers:
|
||||
is_door = False
|
||||
while True:
|
||||
if rom.read_bytes(index, 2) == bytearray((0xF0, 0xFF)):
|
||||
is_door = True
|
||||
index += 2
|
||||
continue
|
||||
if rom.read_bytes(index, 2) == bytearray((0xFF, 0xFF)):
|
||||
index += 2
|
||||
break
|
||||
if is_door:
|
||||
doors.append(bytes(rom.read_bytes(index, 2)))
|
||||
index += 2
|
||||
else:
|
||||
objects.append(bytes(rom.read_bytes(index, 3)))
|
||||
index += 3
|
||||
|
||||
return table
|
||||
|
||||
def add_shell(self, x: int, y: int, clear_layer_2: bool, shell_id: int) -> None:
|
||||
self.header_byte_0 = 0xF0
|
||||
if clear_layer_2:
|
||||
self.layer_2_objects.clear()
|
||||
self.layer_2_objects.append(_build_subtype_3_object(x, y, shell_id))
|
||||
|
||||
def remove_shell(self, shell_id: int) -> None:
|
||||
self.layer_2_objects = [obj for obj in self.layer_2_objects if _object_id(obj) != shell_id]
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
output = bytearray((self.header_byte_0, self.header_byte_1))
|
||||
output.extend(self._serialize_layer(self.layer_1_objects, self.layer_1_doors, is_last_layer=False))
|
||||
output.extend(self._serialize_layer(self.layer_2_objects, self.layer_2_doors, is_last_layer=False))
|
||||
output.extend(self._serialize_layer(self.layer_3_objects, self.layer_3_doors, is_last_layer=True))
|
||||
return bytes(output)
|
||||
|
||||
@staticmethod
|
||||
def _serialize_layer(objects: list[bytes], doors: list[bytes], is_last_layer: bool) -> bytes:
|
||||
output = bytearray()
|
||||
for obj in objects:
|
||||
output.extend(obj)
|
||||
if is_last_layer or doors:
|
||||
output.extend((0xF0, 0xFF))
|
||||
for door in doors:
|
||||
output.extend(door)
|
||||
output.extend((0xFF, 0xFF))
|
||||
return bytes(output)
|
||||
|
||||
|
||||
BOSS_PATCH_DATA: dict[str, BossPatchData] = {
|
||||
"Armos": BossPatchData((0x87, 0xE8), 9, (0x05, 0x04, 0x53, 0x05, 0x07, 0x53, 0x05, 0x0A, 0x53,
|
||||
0x08, 0x0A, 0x53, 0x08, 0x07, 0x53, 0x08, 0x04, 0x53,
|
||||
0x08, 0xE7, 0x19)),
|
||||
"Arrghus": BossPatchData((0x97, 0xD9), 20, (0x07, 0x07, 0x8C, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D,
|
||||
0x07, 0x07, 0x8D, 0x07, 0x07, 0x8D)),
|
||||
"Blind": BossPatchData((0x54, 0xE6), 32, (0x05, 0x09, 0xCE)),
|
||||
"Helmasaur": BossPatchData((0x49, 0xE0), 21, (0x06, 0x07, 0x92)),
|
||||
"Kholdstare": BossPatchData((0x01, 0xEA), 22, (0x05, 0x07, 0xA3, 0x05, 0x07, 0xA4, 0x05, 0x07, 0xA2)),
|
||||
"Lanmola": BossPatchData((0xCB, 0xDC), 11, (0x07, 0x06, 0x54, 0x07, 0x09, 0x54, 0x09, 0x07, 0x54)),
|
||||
"Moldorm": BossPatchData((0xC3, 0xD9), 12, (0x09, 0x09, 0x09)),
|
||||
"Mothula": BossPatchData((0x31, 0xDC), 26, (0x06, 0x08, 0x88)),
|
||||
"Trinexx": BossPatchData((0xBA, 0xE5), 23, (0x05, 0x07, 0xCB, 0x05, 0x07, 0xCC, 0x05, 0x07, 0xCD)),
|
||||
"Vitreous": BossPatchData((0x57, 0xE4), 22, (0x05, 0x07, 0xBD)),
|
||||
}
|
||||
|
||||
DUNGEON_BOSS_PATCH_DATA: dict[tuple[str, Optional[str]], DungeonBossPatchData] = {
|
||||
("Eastern Palace", None): DungeonBossPatchData(200, 0x04D7BE, 0x2B, 0x28),
|
||||
("Desert Palace", None): DungeonBossPatchData(51, 0x04D694, 0x0B, 0x28),
|
||||
("Tower of Hera", None): DungeonBossPatchData(7, 0x04D63C, 0x18, 0x16),
|
||||
("Palace of Darkness", None): DungeonBossPatchData(90, 0x04D6E2, 0x2B, 0x28),
|
||||
("Swamp Palace", None): DungeonBossPatchData(6, 0x04D63A, 0x0B, 0x28),
|
||||
("Skull Woods", None): DungeonBossPatchData(41, 0x04D680, 0x2B, 0x28),
|
||||
("Thieves Town", None): DungeonBossPatchData(172, 0x04D786, 0x2B, 0x28, clear_layer2=True),
|
||||
("Ice Palace", None): DungeonBossPatchData(222, 0x04D7EA, 0x2B, 0x08, clear_layer2=True),
|
||||
("Misery Mire", None): DungeonBossPatchData(144, 0x04D74E, 0x0B, 0x28, clear_layer2=True),
|
||||
("Turtle Rock", None): DungeonBossPatchData(164, 0x04D776, 0x0B, 0x28, clear_layer2=True),
|
||||
("Ganons Tower", "bottom"): DungeonBossPatchData(
|
||||
28, 0x04D666, 0x2B, 0x28, extra_sprites=(0x07, 0x07, 0xE3, 0x07, 0x08, 0xE3, 0x08, 0x07, 0xE3, 0x08, 0x08, 0xE3),
|
||||
gt_sprite_write_address=0x04D87E,
|
||||
),
|
||||
("Ganons Tower", "middle"): DungeonBossPatchData(
|
||||
108, 0x04D706, 0x0B, 0x28, extra_sprites=(0x18, 0x17, 0xD1, 0x1C, 0x03, 0xC5), gt_sprite_write_address=0x04D8B6,
|
||||
),
|
||||
("Ganons Tower", "top"): DungeonBossPatchData(77, 0x04D6C8, 0x18, 0x16),
|
||||
}
|
||||
|
||||
TRINEXX_SHELL_OBJECT_ID = 0xFF2
|
||||
KHOLDSTARE_SHELL_OBJECT_ID = 0xF95
|
||||
TRINEXX_VANILLA_ROOM_ID = 164
|
||||
KHOLDSTARE_VANILLA_ROOM_ID = 222
|
||||
ENEMY_HP_TABLE_ADDRESS = 0x6B173
|
||||
ENEMY_DAMAGE_TABLE_ADDRESS = 0x6B266
|
||||
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS = 0xD7BBB
|
||||
DAMAGE_GROUP_TABLE_ADDRESS = 0x3742D
|
||||
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS = 0x301FC
|
||||
RETRO_RUPEE_REPLACEMENT_SPRITE_ID = 0xDA
|
||||
ARROW_REFILL_5_SPRITE_ID = 0xE1
|
||||
THIEF_SPRITE_ID = 0xC4
|
||||
THIEF_DEFAULT_HP = 4
|
||||
VANILLA_HIDDEN_ENEMY_CHANCE_POOL = (
|
||||
0x01, 0x01, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x12,
|
||||
0x10, 0x01, 0x01, 0x01, 0x11, 0x01, 0x01, 0x03,
|
||||
)
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL = (
|
||||
0x01, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x12,
|
||||
0x0F, 0x01, 0x0F, 0x0F, 0x11, 0x0F, 0x0F, 0x03,
|
||||
)
|
||||
EXCLUDED_ENEMY_TABLE_SPRITE_IDS = frozenset({
|
||||
0x09, 0x53, 0x54, 0x70, 0x7A, 0x7B, 0x88, 0x89, 0x8C, 0x8D, 0x92,
|
||||
0xA2, 0xA3, 0xA4, 0xBD, 0xBE, 0xBF, 0xCB, 0xCC, 0xCD, 0xCE, 0xD6, 0xD7,
|
||||
})
|
||||
ENEMY_HEALTH_RANGE_BY_KEY = {
|
||||
"easy": (1, 4),
|
||||
"normal": (2, 15),
|
||||
"hard": (2, 25),
|
||||
"expert": (4, 50),
|
||||
}
|
||||
|
||||
_ENEMIZER_SYMBOLS: Optional[dict[str, int]] = None
|
||||
|
||||
BOSS_GFX_SHEET_INDEXES = {
|
||||
"Agahnim1": 0x8D,
|
||||
"Agahnim2": 0xB5,
|
||||
"Agahnim3": 0xC8,
|
||||
"Agahnim4": 0xB6,
|
||||
"ArmosKnight1": 0x90,
|
||||
"Ganon1": 0x94,
|
||||
"Ganon2": 0xA6,
|
||||
"Ganon3": 0xB4,
|
||||
"Ganon4": 0xB8,
|
||||
"Moldorm1": 0xA3,
|
||||
"Lanmola1": 0xA4,
|
||||
"Arrghus1": 0xAC,
|
||||
"Mothula1": 0xAB,
|
||||
"Helmasaure1": 0xAD,
|
||||
"Helmasaure2": 0xB1,
|
||||
"Blind1": 0xAE,
|
||||
"Kholdstare1": 0xAF,
|
||||
"Vitreous1": 0xB0,
|
||||
"Trinexx1": 0xB2,
|
||||
"Trinexx2": 0xB3,
|
||||
}
|
||||
|
||||
BOSS_GFX_TABLE = {
|
||||
"Agahnim1": (21, 190, 228),
|
||||
"Agahnim2": (22, 255, 135),
|
||||
"Agahnim3": (23, 220, 101),
|
||||
"Agahnim4": (23, 132, 92),
|
||||
"ArmosKnight1": (21, 206, 27),
|
||||
"Ganon1": (21, 227, 160),
|
||||
"Ganon2": (22, 186, 55),
|
||||
"Ganon3": (22, 250, 199),
|
||||
"Ganon4": (23, 142, 33),
|
||||
"Moldorm1": (22, 175, 152),
|
||||
"Lanmola1": (22, 180, 23),
|
||||
"Arrghus1": (22, 214, 147),
|
||||
"Mothula1": (22, 210, 84),
|
||||
"Helmasaure1": (22, 219, 114),
|
||||
"Helmasaure2": (22, 239, 177),
|
||||
"Blind1": (22, 224, 90),
|
||||
"Kholdstare1": (22, 230, 31),
|
||||
"Vitreous1": (22, 235, 9),
|
||||
"Trinexx1": (22, 243, 89),
|
||||
"Trinexx2": (22, 246, 35),
|
||||
}
|
||||
|
||||
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS = 0x04B37E
|
||||
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS = 0xE7A5
|
||||
TILE_TRAP_FLOOR_TILE_ADDRESS = 0xF3BED
|
||||
|
||||
|
||||
def apply_enemizer_base_patch(rom: "LocalRom") -> None:
|
||||
for address, patch_data in _load_enemizer_base_patches():
|
||||
rom.write_bytes(address, patch_data)
|
||||
_apply_trinexx_room_fixes(rom)
|
||||
|
||||
def patch_bosses(world: "ALTTPWorld", rom: "LocalRom") -> None:
|
||||
dungeon_header_base = _get_enemizer_symbol("room_header_table")
|
||||
moved_room_object_base = _get_enemizer_symbol("modified_room_object_table")
|
||||
gt_dungeon_name = "Ganons Tower" if world.options.mode != "inverted" else "Inverted Ganons Tower"
|
||||
gt_dungeon = world.dungeons[gt_dungeon_name]
|
||||
|
||||
placements = (
|
||||
(world.dungeons["Eastern Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]),
|
||||
(world.dungeons["Desert Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Desert Palace", None)]),
|
||||
(world.dungeons["Tower of Hera"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Tower of Hera", None)]),
|
||||
(world.dungeons["Palace of Darkness"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Palace of Darkness", None)]),
|
||||
(world.dungeons["Swamp Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Swamp Palace", None)]),
|
||||
(world.dungeons["Skull Woods"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Skull Woods", None)]),
|
||||
(world.dungeons["Thieves Town"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Thieves Town", None)]),
|
||||
(world.dungeons["Ice Palace"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ice Palace", None)]),
|
||||
(world.dungeons["Misery Mire"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Misery Mire", None)]),
|
||||
(world.dungeons["Turtle Rock"].boss.enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Turtle Rock", None)]),
|
||||
(gt_dungeon.bosses["bottom"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "bottom")]),
|
||||
(gt_dungeon.bosses["middle"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "middle")]),
|
||||
(gt_dungeon.bosses["top"].enemizer_name, DUNGEON_BOSS_PATCH_DATA[("Ganons Tower", "top")]),
|
||||
)
|
||||
|
||||
modified_room_tables: dict[int, RoomObjectTable] = {}
|
||||
|
||||
for boss_name, dungeon_data in placements:
|
||||
boss_data = BOSS_PATCH_DATA[boss_name]
|
||||
rom.write_bytes(dungeon_data.sprite_pointer_address, boss_data.pointer)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 3, boss_data.graphics)
|
||||
|
||||
if boss_name == "Trinexx" and dungeon_data.room_id != TRINEXX_VANILLA_ROOM_ID:
|
||||
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
|
||||
room_table.add_shell(
|
||||
dungeon_data.shell_x,
|
||||
dungeon_data.shell_y - 2,
|
||||
dungeon_data.clear_layer2,
|
||||
TRINEXX_SHELL_OBJECT_ID,
|
||||
)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0x60)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x04)
|
||||
|
||||
if boss_name == "Kholdstare" and dungeon_data.room_id != KHOLDSTARE_VANILLA_ROOM_ID:
|
||||
room_table = _get_room_object_table(rom, modified_room_tables, dungeon_data.room_id)
|
||||
room_table.add_shell(
|
||||
dungeon_data.shell_x,
|
||||
dungeon_data.shell_y,
|
||||
dungeon_data.clear_layer2,
|
||||
KHOLDSTARE_SHELL_OBJECT_ID,
|
||||
)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14), 0xE0)
|
||||
rom.write_byte(dungeon_header_base + (dungeon_data.room_id * 14) + 4, 0x01)
|
||||
|
||||
if boss_name != "Trinexx" and dungeon_data.room_id == TRINEXX_VANILLA_ROOM_ID:
|
||||
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(TRINEXX_SHELL_OBJECT_ID)
|
||||
|
||||
if boss_name != "Kholdstare" and dungeon_data.room_id == KHOLDSTARE_VANILLA_ROOM_ID:
|
||||
_get_room_object_table(rom, modified_room_tables, dungeon_data.room_id).remove_shell(KHOLDSTARE_SHELL_OBJECT_ID)
|
||||
|
||||
if dungeon_data.gt_sprite_write_address is not None:
|
||||
_write_gt_boss_sprite_block(rom, dungeon_data, boss_data)
|
||||
|
||||
write_address = moved_room_object_base
|
||||
for room_id in sorted(modified_room_tables):
|
||||
table_bytes = modified_room_tables[room_id].to_bytes()
|
||||
_write_room_object_pointer(rom, room_id, write_address)
|
||||
rom.write_bytes(write_address, table_bytes)
|
||||
write_address += len(table_bytes)
|
||||
|
||||
rom.write_byte(0x1B0101, 0x01)
|
||||
rom.write_byte(0x04DE81, 0x00)
|
||||
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||
rom.write_byte(0x04DE81, 0x06)
|
||||
rom.write_byte(0x1B0101, 0x00)
|
||||
|
||||
|
||||
def _get_room_object_table(rom: "LocalRom", cache: dict[int, RoomObjectTable], room_id: int) -> RoomObjectTable:
|
||||
room_table = cache.get(room_id)
|
||||
if room_table is not None:
|
||||
return room_table
|
||||
|
||||
pointer_address = 0xF8000 + (room_id * 3)
|
||||
snes_address_bytes = rom.read_bytes(pointer_address, 3)
|
||||
snes_address = (snes_address_bytes[2] << 16) | (snes_address_bytes[1] << 8) | snes_address_bytes[0]
|
||||
room_table = RoomObjectTable.from_rom(rom, snes_to_pc(snes_address))
|
||||
cache[room_id] = room_table
|
||||
return room_table
|
||||
|
||||
|
||||
def _write_gt_boss_sprite_block(rom: "LocalRom", dungeon_data: DungeonBossPatchData, boss_data: BossPatchData) -> None:
|
||||
assert dungeon_data.gt_sprite_write_address is not None
|
||||
rom.write_int16(dungeon_data.sprite_pointer_address, dungeon_data.gt_sprite_write_address)
|
||||
|
||||
sprite_block = bytearray((0x00,))
|
||||
sprite_block.extend(boss_data.sprite_array)
|
||||
if dungeon_data.room_id == 28 and boss_data.pointer == BOSS_PATCH_DATA["Arrghus"].pointer:
|
||||
sprite_block.extend(dungeon_data.extra_sprites[:6])
|
||||
else:
|
||||
sprite_block.extend(dungeon_data.extra_sprites)
|
||||
sprite_block.append(0xFF)
|
||||
rom.write_bytes(dungeon_data.gt_sprite_write_address, sprite_block)
|
||||
|
||||
|
||||
def _write_room_object_pointer(rom: "LocalRom", room_id: int, pc_address: int) -> None:
|
||||
snes_address = pc_to_snes(pc_address)
|
||||
pointer_address = 0xF8000 + (room_id * 3)
|
||||
rom.write_bytes(pointer_address, (
|
||||
snes_address & 0xFF,
|
||||
(snes_address >> 8) & 0xFF,
|
||||
(snes_address >> 16) & 0xFF,
|
||||
))
|
||||
|
||||
|
||||
def _build_subtype_3_object(x: int, y: int, object_id: int) -> bytes:
|
||||
return bytes((
|
||||
((x << 2) & 0xFC) | (object_id & 0x03),
|
||||
((y << 2) & 0xFC) | ((object_id >> 2) & 0x03),
|
||||
0xF0 | ((object_id >> 4) & 0x0F),
|
||||
))
|
||||
|
||||
|
||||
def _object_id(object_bytes: bytes) -> Optional[int]:
|
||||
if len(object_bytes) != 3:
|
||||
return None
|
||||
if object_bytes[0] >= 0xFC:
|
||||
return (object_bytes[2] & 0x3F) + 0x100
|
||||
if object_bytes[2] >= 0xF8:
|
||||
return 0xF00 | ((object_bytes[2] & 0x0F) << 4) | ((object_bytes[1] & 0x03) << 2) | (object_bytes[0] & 0x03)
|
||||
return object_bytes[2]
|
||||
|
||||
|
||||
def _set_enemizer_flag(rom: "LocalRom", symbol_name: str, enabled: bool) -> None:
|
||||
rom.write_byte(_get_enemizer_symbol(symbol_name), 0x01 if enabled else 0x00)
|
||||
|
||||
|
||||
def _apply_killable_thief(rom: "LocalRom") -> None:
|
||||
rom.write_byte(_get_enemizer_symbol("notItemSprite_Mimic") + 4, THIEF_SPRITE_ID)
|
||||
thief_hp_address = ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID
|
||||
if rom.read_byte(thief_hp_address) != 0xFF:
|
||||
rom.write_byte(thief_hp_address, THIEF_DEFAULT_HP)
|
||||
|
||||
|
||||
def _randomize_enemy_health(rom: "LocalRom", rng: random.Random, enemy_health_key: str) -> None:
|
||||
min_hp, max_hp = ENEMY_HEALTH_RANGE_BY_KEY[enemy_health_key]
|
||||
for sprite_id in range(0xF3):
|
||||
hp_address = ENEMY_HP_TABLE_ADDRESS + sprite_id
|
||||
if rom.read_byte(hp_address) == 0xFF or sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
|
||||
continue
|
||||
rom.write_byte(hp_address, rng.randrange(min_hp, max_hp))
|
||||
|
||||
|
||||
def _randomize_enemy_damage(rom: "LocalRom", rng: random.Random, allow_zero_damage: bool) -> None:
|
||||
for sprite_id in range(0xF3):
|
||||
if sprite_id in EXCLUDED_ENEMY_TABLE_SPRITE_IDS:
|
||||
continue
|
||||
new_damage = rng.randrange(8)
|
||||
if not allow_zero_damage and new_damage == 2:
|
||||
continue
|
||||
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + sprite_id, new_damage)
|
||||
|
||||
|
||||
def _shuffle_damage_groups(
|
||||
rom: "LocalRom",
|
||||
rng: random.Random,
|
||||
*,
|
||||
chaos_mode: bool,
|
||||
allow_zero_damage: bool,
|
||||
) -> None:
|
||||
min_damage = 0 if allow_zero_damage else 4
|
||||
max_damage = 64 if chaos_mode else 32
|
||||
|
||||
for group_id in range(10):
|
||||
green_mail_damage = rng.randrange(min_damage, max_damage)
|
||||
if chaos_mode:
|
||||
blue_mail_damage = rng.randrange(min_damage, max_damage)
|
||||
red_mail_damage = rng.randrange(min_damage, max_damage)
|
||||
else:
|
||||
blue_mail_damage = green_mail_damage * 3 // 4
|
||||
red_mail_damage = green_mail_damage * 3 // 8
|
||||
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
|
||||
rom.write_bytes(group_address, (green_mail_damage, blue_mail_damage, red_mail_damage))
|
||||
|
||||
|
||||
def _update_hidden_enemy_item_table_for_retro_mode(rom: "LocalRom") -> None:
|
||||
if rom.read_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS) != RETRO_RUPEE_REPLACEMENT_SPRITE_ID:
|
||||
return
|
||||
|
||||
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
|
||||
for index in range(22):
|
||||
if rom.read_byte(item_table_address + index) == ARROW_REFILL_5_SPRITE_ID:
|
||||
rom.write_byte(item_table_address + index, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
|
||||
|
||||
|
||||
def _apply_trinexx_room_fixes(rom: "LocalRom") -> None:
|
||||
# Match original Enemizer's unconditional Trinexx ice-floor removal so
|
||||
# blue-head projectiles do not create solid walls in non-vanilla rooms.
|
||||
rom.write_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, (0xEA, 0xEA, 0xEA, 0xEA))
|
||||
|
||||
|
||||
def _apply_randomized_tile_trap_floor_tile(rom: "LocalRom") -> None:
|
||||
# Original Enemizer's RandomizeTileTrapFloorTile option changes the tile
|
||||
# left behind by flying floor tile traps. AP does not currently expose or
|
||||
# call this option, so keep the implementation isolated and unused.
|
||||
rom.write_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, (0x88, 0x01))
|
||||
rom.write_byte(TILE_TRAP_FLOOR_TILE_ADDRESS, 0x12)
|
||||
|
||||
|
||||
def _make_native_enemizer_rng(world: "ALTTPWorld") -> random.Random:
|
||||
seed_material = "|".join((
|
||||
str(world.multiworld.seed),
|
||||
world.multiworld.seed_name,
|
||||
str(world.player),
|
||||
_option_key(world.options.enemy_health),
|
||||
_option_key(world.options.enemy_damage),
|
||||
str(int(bool(world.options.enemy_shuffle))),
|
||||
str(int(bool(world.options.bush_shuffle))),
|
||||
str(int(bool(world.options.killable_thieves))),
|
||||
))
|
||||
seed = int.from_bytes(hashlib.sha256(seed_material.encode("utf-8")).digest()[:8], "big")
|
||||
return random.Random(seed)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _load_enemizer_base_patches() -> tuple[tuple[int, bytes], ...]:
|
||||
return tuple(
|
||||
(entry.address, entry.patch_data)
|
||||
for entry in ENEMIZER_BASE_PATCHES
|
||||
)
|
||||
|
||||
|
||||
def _option_key(option: object) -> str:
|
||||
return str(getattr(option, "current_key", option))
|
||||
|
||||
|
||||
def _get_enemizer_symbol(symbol_name: str) -> int:
|
||||
global _ENEMIZER_SYMBOLS
|
||||
if _ENEMIZER_SYMBOLS is None:
|
||||
_ENEMIZER_SYMBOLS = _load_enemizer_symbols()
|
||||
return _ENEMIZER_SYMBOLS[symbol_name]
|
||||
|
||||
|
||||
def _load_enemizer_symbols() -> dict[str, int]:
|
||||
return {
|
||||
name: snes_to_pc(snes_address)
|
||||
for name, snes_address in ENEMIZER_SYMBOLS.items()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
|
||||
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
|
||||
from .Bosses import place_bosses
|
||||
from .Dungeons import get_dungeon_item_pool_player
|
||||
from .EnemyShuffle import generate_enemy_shuffle_state
|
||||
from .EntranceShuffle import connect_entrance
|
||||
from .Items import item_factory, GetBeemizerItem, trap_replaceable, item_name_groups
|
||||
from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode, LTTPBosses
|
||||
@@ -511,6 +512,8 @@ def generate_itempool(world: "ALTTPWorld"):
|
||||
world.options.turtle_rock_medallion.current_key.title())
|
||||
|
||||
place_bosses(world)
|
||||
if world.options.enemy_shuffle:
|
||||
world.enemy_shuffle_state = generate_enemy_shuffle_state(world)
|
||||
|
||||
multiworld.itempool += items
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from Utils import snes_to_pc
|
||||
from .enemizer_data.pot_shuffle_data import POT_ROOMS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ALTTPWorld
|
||||
from .Rom import LocalRom
|
||||
|
||||
|
||||
POT_ITEM_POINTER_TABLE = 0xDB67
|
||||
POT_KEY = 0x08
|
||||
POT_ARROW = 0x09
|
||||
POT_BLUE_RUPEE = 0x07
|
||||
POT_SWITCH = 0x88
|
||||
POT_HOLE = 0x80
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PotData:
|
||||
x: int
|
||||
y: int
|
||||
reserved: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PotRoomData:
|
||||
room_id: int
|
||||
pots: tuple[PotData, ...]
|
||||
items: tuple[int, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FilledPot:
|
||||
x: int
|
||||
y: int
|
||||
item: int
|
||||
|
||||
|
||||
def generate_pot_shuffle(world: "ALTTPWorld") -> dict[int, tuple[FilledPot, ...]]:
|
||||
room_data = _load_pot_room_data()
|
||||
shuffled_pots: dict[int, tuple[FilledPot, ...]] = {}
|
||||
|
||||
for room in room_data:
|
||||
room_items = [item for item in room.items if item != POT_HOLE]
|
||||
if world.options.retro_bow:
|
||||
room_items = [POT_BLUE_RUPEE if item == POT_ARROW else item for item in room_items]
|
||||
|
||||
empty_pots: list[PotData] = []
|
||||
filled_pots: list[FilledPot] = []
|
||||
|
||||
for pot in room.pots:
|
||||
if pot.reserved == 3:
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, POT_HOLE))
|
||||
else:
|
||||
empty_pots.append(pot)
|
||||
|
||||
while POT_KEY in room_items:
|
||||
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 1]
|
||||
if not candidate_indices:
|
||||
break
|
||||
pot_index = world.random.choice(candidate_indices)
|
||||
pot = empty_pots.pop(pot_index)
|
||||
room_items.remove(POT_KEY)
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, POT_KEY))
|
||||
|
||||
while POT_SWITCH in room_items:
|
||||
candidate_indices = [index for index, pot in enumerate(empty_pots) if pot.reserved == 2]
|
||||
if not candidate_indices:
|
||||
break
|
||||
pot_index = world.random.choice(candidate_indices)
|
||||
pot = empty_pots.pop(pot_index)
|
||||
room_items.remove(POT_SWITCH)
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, POT_SWITCH))
|
||||
|
||||
while room_items and empty_pots:
|
||||
pot_index = world.random.randrange(len(empty_pots))
|
||||
item_index = world.random.randrange(len(room_items))
|
||||
pot = empty_pots.pop(pot_index)
|
||||
item = room_items.pop(item_index)
|
||||
filled_pots.append(FilledPot(pot.x, pot.y, item))
|
||||
|
||||
shuffled_pots[room.room_id] = tuple(filled_pots)
|
||||
|
||||
return shuffled_pots
|
||||
|
||||
|
||||
def apply_pot_shuffle(rom: "LocalRom", shuffled_pots: dict[int, tuple[FilledPot, ...]]) -> None:
|
||||
for room_id, pots in shuffled_pots.items():
|
||||
pointer_address = POT_ITEM_POINTER_TABLE + (room_id * 2)
|
||||
snes_address = rom.read_byte(pointer_address) | (rom.read_byte(pointer_address + 1) << 8) | (0x01 << 16)
|
||||
address = snes_to_pc(snes_address)
|
||||
for index, pot in enumerate(pots):
|
||||
rom.write_bytes(address + (index * 3), (pot.x, pot.y, pot.item))
|
||||
|
||||
|
||||
def get_unique_pot_item_position(
|
||||
shuffled_pots: dict[int, tuple[FilledPot, ...]],
|
||||
room_id: int,
|
||||
item: int,
|
||||
) -> tuple[int, int]:
|
||||
positions = [
|
||||
(pot.x, pot.y)
|
||||
for pot in shuffled_pots.get(room_id, ())
|
||||
if pot.item == item
|
||||
]
|
||||
if len(positions) != 1:
|
||||
raise ValueError(
|
||||
f"Expected exactly one pot item {hex(item)} in room {hex(room_id)}, found {len(positions)}"
|
||||
)
|
||||
return positions[0]
|
||||
|
||||
|
||||
def _load_pot_room_data() -> tuple[PotRoomData, ...]:
|
||||
return tuple(
|
||||
PotRoomData(
|
||||
room_id=room.room_id,
|
||||
pots=tuple(PotData(x=pot.x, y=pot.y, reserved=pot.reserved) for pot in room.pots),
|
||||
items=room.items,
|
||||
)
|
||||
for room in POT_ROOMS
|
||||
)
|
||||
+132
-225
@@ -15,7 +15,6 @@ import logging
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
@@ -53,9 +52,6 @@ try:
|
||||
except:
|
||||
xxtea = None
|
||||
|
||||
enemizer_logger = logging.getLogger("Enemizer")
|
||||
|
||||
|
||||
class LocalRom:
|
||||
|
||||
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
|
||||
@@ -179,43 +175,6 @@ class LocalRom:
|
||||
self.write_int32(startaddress + (i * 4), value)
|
||||
|
||||
|
||||
check_lock = threading.Lock()
|
||||
|
||||
|
||||
def check_enemizer(enemizercli):
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
|
||||
raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
|
||||
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
||||
|
||||
with check_lock:
|
||||
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
wanted_version = (7, 1, 0)
|
||||
# version info is saved on the lib, for some reason
|
||||
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
|
||||
with open(library_info) as f:
|
||||
info = json.load(f)
|
||||
|
||||
for lib in info["libraries"]:
|
||||
if lib.startswith("EnemizerLibrary/"):
|
||||
version = lib.split("/")[-1]
|
||||
version = tuple(int(element) for element in version.split("."))
|
||||
enemizer_logger.debug(f"Found Enemizer version {version}")
|
||||
if version < wanted_version:
|
||||
raise Exception(
|
||||
f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
|
||||
f"please update your Enemizer. "
|
||||
f"Such as from https://github.com/Ijwu/Enemizer/releases")
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Could not find Enemizer library version information in {library_info}")
|
||||
|
||||
check_enemizer.done = True
|
||||
|
||||
|
||||
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
|
||||
userandomsprites = False
|
||||
if sprite and not isinstance(sprite, Sprite):
|
||||
@@ -282,174 +241,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
|
||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
|
||||
|
||||
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
||||
player = world.player
|
||||
check_enemizer(enemizercli)
|
||||
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
|
||||
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
|
||||
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
|
||||
|
||||
# write options file for enemizer
|
||||
options = {
|
||||
'RandomizeEnemies': world.options.enemy_shuffle.value,
|
||||
'RandomizeEnemiesType': 3,
|
||||
'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
|
||||
'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
|
||||
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
|
||||
world.options.enemy_health.current_key],
|
||||
'OHKO': False,
|
||||
'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
|
||||
'AllowEnemyZeroDamage': True,
|
||||
'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
|
||||
'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
|
||||
'EasyModeEscape': world.options.mode == "standard",
|
||||
'EnemiesAbsorbable': False,
|
||||
'AbsorbableSpawnRate': 10,
|
||||
'AbsorbableTypes': {
|
||||
'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True,
|
||||
'Key': True,
|
||||
'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True,
|
||||
'RedRupee': True
|
||||
},
|
||||
'BossMadness': False,
|
||||
'RandomizeBosses': True,
|
||||
'RandomizeBossesType': 0,
|
||||
'RandomizeBossHealth': False,
|
||||
'RandomizeBossHealthMinAmount': 0,
|
||||
'RandomizeBossHealthMaxAmount': 300,
|
||||
'RandomizeBossDamage': False,
|
||||
'RandomizeBossDamageMinAmount': 0,
|
||||
'RandomizeBossDamageMaxAmount': 200,
|
||||
'RandomizeBossBehavior': False,
|
||||
'RandomizeDungeonPalettes': False,
|
||||
'SetBlackoutMode': False,
|
||||
'RandomizeOverworldPalettes': False,
|
||||
'RandomizeSpritePalettes': False,
|
||||
'SetAdvancedSpritePalettes': False,
|
||||
'PukeMode': False,
|
||||
'NegativeMode': False,
|
||||
'GrayscaleMode': False,
|
||||
'GenerateSpoilers': False,
|
||||
'RandomizeLinkSpritePalette': False,
|
||||
'RandomizePots': world.options.pot_shuffle.value,
|
||||
'ShuffleMusic': False,
|
||||
'BootlegMagic': True,
|
||||
'CustomBosses': False,
|
||||
'AndyMode': False,
|
||||
'HeartBeepSpeed': 0,
|
||||
'AlternateGfx': False,
|
||||
'ShieldGraphics': "shield_gfx/normal.gfx",
|
||||
'SwordGraphics': "sword_gfx/normal.gfx",
|
||||
'BeeMizer': False,
|
||||
'BeesLevel': 0,
|
||||
'RandomizeTileTrapPattern': False,
|
||||
'RandomizeTileTrapFloorTile': False,
|
||||
'AllowKillableThief': world.options.killable_thieves.value,
|
||||
'RandomizeSpriteOnHit': False,
|
||||
'DebugMode': False,
|
||||
'DebugForceEnemy': False,
|
||||
'DebugForceEnemyId': 0,
|
||||
'DebugForceBoss': False,
|
||||
'DebugForceBossId': 0,
|
||||
'DebugOpenShutterDoors': False,
|
||||
'DebugForceEnemyDamageZero': False,
|
||||
'DebugShowRoomIdInRupeeCounter': False,
|
||||
'UseManualBosses': True,
|
||||
'ManualBosses': {
|
||||
'EasternPalace': world.dungeons["Eastern Palace"].boss.enemizer_name,
|
||||
'DesertPalace': world.dungeons["Desert Palace"].boss.enemizer_name,
|
||||
'TowerOfHera': world.dungeons["Tower of Hera"].boss.enemizer_name,
|
||||
'AgahnimsTower': 'Agahnim',
|
||||
'PalaceOfDarkness': world.dungeons["Palace of Darkness"].boss.enemizer_name,
|
||||
'SwampPalace': world.dungeons["Swamp Palace"].boss.enemizer_name,
|
||||
'SkullWoods': world.dungeons["Skull Woods"].boss.enemizer_name,
|
||||
'ThievesTown': world.dungeons["Thieves Town"].boss.enemizer_name,
|
||||
'IcePalace': world.dungeons["Ice Palace"].boss.enemizer_name,
|
||||
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
|
||||
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
|
||||
'GanonsTower1':
|
||||
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
|
||||
'GanonsTower2':
|
||||
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
|
||||
'GanonsTower3':
|
||||
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
|
||||
'GanonsTower4': 'Agahnim2',
|
||||
'Ganon': 'Ganon',
|
||||
}
|
||||
}
|
||||
|
||||
rom.write_to_file(randopatch_path)
|
||||
|
||||
with open(options_path, 'w') as f:
|
||||
json.dump(options, f)
|
||||
|
||||
max_enemizer_tries = 5
|
||||
for i in range(max_enemizer_tries):
|
||||
enemizer_seed = str(world.random.randint(0, 999999999))
|
||||
enemizer_command = [os.path.abspath(enemizercli),
|
||||
'--rom', randopatch_path,
|
||||
'--seed', enemizer_seed,
|
||||
'--binary',
|
||||
'--enemizer', options_path,
|
||||
'--output', enemizer_output_path]
|
||||
|
||||
p_open = subprocess.Popen(enemizer_command,
|
||||
cwd=os.path.dirname(enemizercli),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True)
|
||||
|
||||
enemizer_logger.debug(
|
||||
f"Enemizer attempt {i + 1} of {max_enemizer_tries} for player {player} using enemizer seed {enemizer_seed}")
|
||||
for stdout_line in iter(p_open.stdout.readline, ""):
|
||||
if i == max_enemizer_tries - 1:
|
||||
enemizer_logger.warning(stdout_line.rstrip())
|
||||
else:
|
||||
enemizer_logger.debug(stdout_line.rstrip())
|
||||
p_open.stdout.close()
|
||||
|
||||
return_code = p_open.wait()
|
||||
if return_code:
|
||||
if i == max_enemizer_tries - 1:
|
||||
raise subprocess.CalledProcessError(return_code, enemizer_command)
|
||||
continue
|
||||
|
||||
for j in range(i + 1, max_enemizer_tries):
|
||||
world.random.randint(0, 999999999)
|
||||
# Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
|
||||
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
|
||||
break
|
||||
|
||||
rom.read_from_file(enemizer_output_path)
|
||||
os.remove(enemizer_output_path)
|
||||
|
||||
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||
rom.write_byte(0x04DE81, 6)
|
||||
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry.
|
||||
|
||||
# Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
|
||||
# Replace them with a Slime enemy if they are placed.
|
||||
if world.options.key_drop_shuffle:
|
||||
key_drop_enemies = {
|
||||
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
|
||||
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
|
||||
}
|
||||
for enemy in key_drop_enemies:
|
||||
if rom.read_byte(enemy) == 0x12:
|
||||
logging.debug(f"Moblin found and replaced at {enemy} in world {player}")
|
||||
rom.write_byte(enemy, 0x8F)
|
||||
|
||||
for used in (randopatch_path, options_path):
|
||||
try:
|
||||
os.remove(used)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
tile_list_lock = threading.Lock()
|
||||
_tile_collection_table = []
|
||||
|
||||
@@ -795,9 +586,13 @@ def get_nonnative_item_sprite(code: int) -> int:
|
||||
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
|
||||
|
||||
|
||||
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int):
|
||||
local_random = multiworld.worlds[player].random
|
||||
local_world = multiworld.worlds[player]
|
||||
enemized = bool(local_world.options.boss_shuffle or local_world.options.enemy_shuffle
|
||||
or local_world.options.enemy_health != 'default' or local_world.options.enemy_damage != 'default'
|
||||
or local_world.options.pot_shuffle or local_world.options.bush_shuffle
|
||||
or local_world.options.killable_thieves)
|
||||
|
||||
# patch items
|
||||
|
||||
@@ -1331,6 +1126,13 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
starting_max_arrows = 30
|
||||
|
||||
startingstate = CollectionState(multiworld)
|
||||
has_blue_shield = False
|
||||
has_red_shield = False
|
||||
has_mirror_shield = False
|
||||
progressive_shields = 0
|
||||
has_blue_mail = False
|
||||
has_red_mail = False
|
||||
progressive_mail = 0
|
||||
|
||||
if startingstate.has('Silver Bow', player):
|
||||
equip[0x340] = 1
|
||||
@@ -1359,18 +1161,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
elif startingstate.has('Fighter Sword', player):
|
||||
equip[0x359] = 1
|
||||
|
||||
if startingstate.has('Mirror Shield', player):
|
||||
equip[0x35A] = 3
|
||||
elif startingstate.has('Red Shield', player):
|
||||
equip[0x35A] = 2
|
||||
elif startingstate.has('Blue Shield', player):
|
||||
equip[0x35A] = 1
|
||||
|
||||
if startingstate.has('Red Mail', player):
|
||||
equip[0x35B] = 2
|
||||
elif startingstate.has('Blue Mail', player):
|
||||
equip[0x35B] = 1
|
||||
|
||||
if startingstate.has('Magic Upgrade (1/4)', player):
|
||||
equip[0x37B] = 2
|
||||
equip[0x36E] = 0x80
|
||||
@@ -1383,8 +1173,6 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
|
||||
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
|
||||
'Red Mail', 'Blue Mail', 'Progressive Mail',
|
||||
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)', 'Triforce Piece'}:
|
||||
continue
|
||||
|
||||
@@ -1489,9 +1277,63 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
|
||||
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
|
||||
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
|
||||
elif item.name == 'Blue Shield':
|
||||
has_blue_shield = True
|
||||
continue
|
||||
elif item.name == 'Red Shield':
|
||||
has_red_shield = True
|
||||
continue
|
||||
elif item.name == 'Mirror Shield':
|
||||
has_mirror_shield = True
|
||||
continue
|
||||
elif item.name == 'Progressive Shield':
|
||||
progressive_shields += 1
|
||||
continue
|
||||
elif item.name == 'Blue Mail':
|
||||
has_blue_mail = True
|
||||
continue
|
||||
elif item.name == 'Red Mail':
|
||||
has_red_mail = True
|
||||
continue
|
||||
elif item.name == 'Progressive Mail':
|
||||
progressive_mail += 1
|
||||
continue
|
||||
else:
|
||||
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
|
||||
|
||||
for _ in range(progressive_shields):
|
||||
if has_mirror_shield:
|
||||
continue
|
||||
if has_red_shield and local_world.difficulty_requirements.progressive_shield_limit >= 3:
|
||||
has_mirror_shield = True
|
||||
continue
|
||||
if has_blue_shield and local_world.difficulty_requirements.progressive_shield_limit >= 2:
|
||||
has_red_shield = True
|
||||
continue
|
||||
if local_world.difficulty_requirements.progressive_shield_limit >= 1:
|
||||
has_blue_shield = True
|
||||
|
||||
for _ in range(progressive_mail):
|
||||
if has_red_mail:
|
||||
continue
|
||||
if has_blue_mail and local_world.difficulty_requirements.progressive_armor_limit >= 2:
|
||||
has_red_mail = True
|
||||
continue
|
||||
if local_world.difficulty_requirements.progressive_armor_limit >= 1:
|
||||
has_blue_mail = True
|
||||
|
||||
if has_mirror_shield:
|
||||
equip[0x35A] = 3
|
||||
elif has_red_shield:
|
||||
equip[0x35A] = 2
|
||||
elif has_blue_shield:
|
||||
equip[0x35A] = 1
|
||||
|
||||
if has_red_mail:
|
||||
equip[0x35B] = 2
|
||||
elif has_blue_mail:
|
||||
equip[0x35B] = 1
|
||||
|
||||
equip[0x343] = min(equip[0x343], starting_max_bombs)
|
||||
rom.write_byte(0x180034, starting_max_bombs)
|
||||
equip[0x377] = min(equip[0x377], starting_max_arrows)
|
||||
@@ -1710,6 +1552,71 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
|
||||
if encoded_players > ROM_PLAYER_LIMIT:
|
||||
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
|
||||
|
||||
if enemized:
|
||||
from . import EnemizerPatches as enemizer_patches
|
||||
from .EnemyShuffle import apply_enemy_shuffle
|
||||
from .PotShuffle import apply_pot_shuffle
|
||||
|
||||
enemizer_patches.apply_enemizer_base_patch(rom)
|
||||
|
||||
enemy_shuffle_enabled = bool(local_world.options.enemy_shuffle)
|
||||
bush_shuffle_enabled = bool(local_world.options.bush_shuffle)
|
||||
enemy_health_key = enemizer_patches._option_key(local_world.options.enemy_health)
|
||||
enemy_damage_key = enemizer_patches._option_key(local_world.options.enemy_damage)
|
||||
|
||||
if enemy_shuffle_enabled or bush_shuffle_enabled:
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
|
||||
hidden_enemy_chance_pool = (
|
||||
enemizer_patches.RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL
|
||||
if bush_shuffle_enabled
|
||||
else enemizer_patches.VANILLA_HIDDEN_ENEMY_CHANCE_POOL
|
||||
)
|
||||
rom.write_bytes(enemizer_patches.HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
|
||||
enemizer_patches._update_hidden_enemy_item_table_for_retro_mode(rom)
|
||||
|
||||
if enemy_shuffle_enabled:
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
|
||||
enemizer_patches._set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
|
||||
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
|
||||
rom.write_byte(0x1F2E5, 0xB0)
|
||||
rom.write_byte(0x1F2EB, 0xD0)
|
||||
|
||||
if local_world.options.killable_thieves:
|
||||
enemizer_patches._apply_killable_thief(rom)
|
||||
|
||||
if enemy_health_key != "default" or enemy_damage_key != "default":
|
||||
rng = enemizer_patches._make_native_enemizer_rng(local_world)
|
||||
else:
|
||||
rng = None
|
||||
|
||||
if enemy_health_key != "default":
|
||||
assert rng is not None
|
||||
enemizer_patches._randomize_enemy_health(rom, rng, enemy_health_key)
|
||||
|
||||
if enemy_damage_key != "default":
|
||||
assert rng is not None
|
||||
enemizer_patches._randomize_enemy_damage(rom, rng, allow_zero_damage=True)
|
||||
enemizer_patches._shuffle_damage_groups(
|
||||
rom,
|
||||
rng,
|
||||
chaos_mode=enemy_damage_key == "chaos",
|
||||
allow_zero_damage=True,
|
||||
)
|
||||
|
||||
enemy_shuffle_state = getattr(local_world, "enemy_shuffle_state", None)
|
||||
if local_world.options.enemy_shuffle and enemy_shuffle_state is not None:
|
||||
apply_enemy_shuffle(rom, enemy_shuffle_state)
|
||||
|
||||
if local_world.options.boss_shuffle:
|
||||
# Boss shuffle must run after enemy shuffle so boss room sprite pointers
|
||||
# and graphics block IDs are not restored to the enemy-shuffled room values.
|
||||
enemizer_patches.patch_bosses(local_world, rom)
|
||||
|
||||
pot_shuffle_state = getattr(local_world, "pot_shuffle_state", None)
|
||||
if local_world.options.pot_shuffle and pot_shuffle_state is not None:
|
||||
apply_pot_shuffle(rom, pot_shuffle_state)
|
||||
|
||||
# Write title screen Code
|
||||
hashint = int(rom.get_hash(), 16)
|
||||
code = [
|
||||
@@ -1860,7 +1767,7 @@ def apply_oof_sfx(rom: LocalRom, oof: str):
|
||||
rom.write_bytes(0x12803A, oof_bytes)
|
||||
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
|
||||
|
||||
# Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
|
||||
# Preserve SPC $3188 instead of writing the unused "WHAT" sound effect there.
|
||||
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
|
||||
from .Options import ALTTPOptions, small_key_shuffle
|
||||
from .PotShuffle import generate_pot_shuffle
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
|
||||
is_main_entrance, key_drop_data
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
from .Rules import set_rules
|
||||
from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name
|
||||
@@ -253,17 +254,6 @@ class ALTTPWorld(World):
|
||||
|
||||
create_items = generate_itempool
|
||||
|
||||
_enemizer_path: typing.ClassVar[typing.Optional[str]] = None
|
||||
|
||||
@property
|
||||
def enemizer_path(self) -> str:
|
||||
# TODO: directly use settings
|
||||
cls = self.__class__
|
||||
if cls._enemizer_path is None:
|
||||
cls._enemizer_path = settings.get_settings().generator.enemizer_path
|
||||
assert isinstance(cls._enemizer_path, str)
|
||||
return cls._enemizer_path
|
||||
|
||||
# custom instance vars
|
||||
dungeon_local_item_names: typing.Set[str]
|
||||
dungeon_specific_item_names: typing.Set[str]
|
||||
@@ -305,6 +295,8 @@ class ALTTPWorld(World):
|
||||
self.required_medallions = ["Ether", "Quake"]
|
||||
self.escape_assist = []
|
||||
self.shops = []
|
||||
self.enemy_shuffle_state = None
|
||||
self.pot_shuffle_state = None
|
||||
self.logical_heart_containers = 10
|
||||
self.logical_heart_pieces = 24
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
@@ -316,10 +308,6 @@ class ALTTPWorld(World):
|
||||
raise FileNotFoundError(rom_file)
|
||||
if multiworld.is_race:
|
||||
import xxtea # noqa
|
||||
for player in multiworld.get_game_players(cls.game):
|
||||
if multiworld.worlds[player].use_enemizer:
|
||||
check_enemizer(multiworld.worlds[player].enemizer_path)
|
||||
break
|
||||
|
||||
def generate_early(self):
|
||||
multiworld = self.multiworld
|
||||
@@ -339,6 +327,9 @@ class ALTTPWorld(World):
|
||||
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||
|
||||
if self.options.pot_shuffle:
|
||||
self.pot_shuffle_state = generate_pot_shuffle(self)
|
||||
|
||||
if self.options.mode == 'standard':
|
||||
if self.options.small_key_shuffle:
|
||||
if (self.options.small_key_shuffle not in
|
||||
@@ -564,13 +555,6 @@ class ALTTPWorld(World):
|
||||
def stage_generate_output(cls, multiworld, output_directory):
|
||||
push_shop_inventories(multiworld)
|
||||
|
||||
@property
|
||||
def use_enemizer(self) -> bool:
|
||||
return bool(self.options.boss_shuffle or self.options.enemy_shuffle
|
||||
or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default'
|
||||
or self.options.pot_shuffle or self.options.bush_shuffle
|
||||
or self.options.killable_thieves)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
@@ -578,14 +562,9 @@ class ALTTPWorld(World):
|
||||
self.pushed_shop_inventories.wait()
|
||||
|
||||
try:
|
||||
use_enemizer = self.use_enemizer
|
||||
|
||||
rom = LocalRom(get_base_rom_path())
|
||||
|
||||
patch_rom(multiworld, rom, player, use_enemizer)
|
||||
|
||||
if use_enemizer:
|
||||
patch_enemizer(self, rom, self.enemizer_path, output_directory)
|
||||
patch_rom(multiworld, rom, player)
|
||||
|
||||
if multiworld.is_race:
|
||||
patch_race_rom(rom, multiworld, player)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
These modules are vendored/generated from the upstream Enemizer compiled release and source that were already present
|
||||
locally in `/home/alchav/PycharmProjects/Archipelago/EnemizerCLI` and `/home/alchav/PycharmProjects/Archipelago/Enemizer`.
|
||||
|
||||
Source details:
|
||||
|
||||
- Upstream project: `Ijwu/Enemizer`
|
||||
- Release family: `7.1`
|
||||
- Library version from `EnemizerCLI/EnemizerCLI.Core.deps.json`: `EnemizerLibrary/7.1.0`
|
||||
|
||||
Vendored data modules:
|
||||
|
||||
- `base_patch_data.py`
|
||||
- `symbols.py`
|
||||
- `enemy_room_metadata.py`
|
||||
- `enemy_sprite_requirements.py`
|
||||
- `overworld_enemy_metadata.py`
|
||||
- `dungeon_sprite_addresses.py`
|
||||
- `pot_shuffle_data.py`
|
||||
|
||||
Purpose:
|
||||
|
||||
- `base_patch_data.py` contains the generated base patch Enemizer applies before feature-specific randomization.
|
||||
- `symbols.py` contains the assembled symbol map consumed by Enemizer's runtime code for ROM addresses.
|
||||
- `enemy_room_metadata.py` and `overworld_enemy_metadata.py` contain room and area grouping/randomization constraints.
|
||||
- `enemy_sprite_requirements.py` contains the sprite metadata used by the native enemy shuffle implementation.
|
||||
- `dungeon_sprite_addresses.py` contains dungeon sprite slot metadata derived from Enemizer's source tables and keyed-enemy address list.
|
||||
- `pot_shuffle_data.py` contains the native pot shuffle room/item source data.
|
||||
@@ -0,0 +1 @@
|
||||
"""Native ALTTP Enemizer data modules."""
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class DungeonSpriteAddressData(NamedTuple):
|
||||
room_id: int
|
||||
sprite_id_addresses: tuple[int, ...]
|
||||
|
||||
DUNGEON_SPRITE_ADDRESSES = (
|
||||
DungeonSpriteAddressData(room_id=2, sprite_id_addresses=(317750, 317753, 317756, 317759, 317762, 317792, 317795)),
|
||||
DungeonSpriteAddressData(room_id=4, sprite_id_addresses=(317803, 317806, 317809, 317812, 317827, 317839, 317842, 317845)),
|
||||
DungeonSpriteAddressData(room_id=9, sprite_id_addresses=(317904, 317907, 317910)),
|
||||
DungeonSpriteAddressData(room_id=10, sprite_id_addresses=(317915, 317918, 317921, 317924, 317930, 317933)),
|
||||
DungeonSpriteAddressData(room_id=11, sprite_id_addresses=(317941, 317944, 317947, 317950, 317953, 317956, 317959, 317962, 317965)),
|
||||
DungeonSpriteAddressData(room_id=14, sprite_id_addresses=(317978, 317981, 317984)),
|
||||
DungeonSpriteAddressData(room_id=17, sprite_id_addresses=(317992, 317995, 317998, 318001, 318004, 318007, 318010, 318013)),
|
||||
DungeonSpriteAddressData(room_id=19, sprite_id_addresses=(318029, 318032, 318035, 318038, 318044, 318056, 318053, 318041)),
|
||||
DungeonSpriteAddressData(room_id=21, sprite_id_addresses=(318105, 318108, 318111, 318114, 318117, 318120)),
|
||||
DungeonSpriteAddressData(room_id=22, sprite_id_addresses=(318125, 318128, 318131, 318134, 318137, 318140, 318143)),
|
||||
DungeonSpriteAddressData(room_id=23, sprite_id_addresses=(318157, 318160, 318163, 318166, 318169, 318172)),
|
||||
DungeonSpriteAddressData(room_id=25, sprite_id_addresses=(318177, 318180, 318183, 318186)),
|
||||
DungeonSpriteAddressData(room_id=26, sprite_id_addresses=(318191, 318194, 318197, 318200, 318203, 318206, 318209, 318212, 318218)),
|
||||
DungeonSpriteAddressData(room_id=27, sprite_id_addresses=(318232, 318235, 318238, 318241)),
|
||||
DungeonSpriteAddressData(room_id=30, sprite_id_addresses=(318284, 318287, 318290, 318293, 318296, 318299)),
|
||||
DungeonSpriteAddressData(room_id=31, sprite_id_addresses=(318304, 318307, 318310, 318313, 318316, 318319, 318322, 318325)),
|
||||
DungeonSpriteAddressData(room_id=33, sprite_id_addresses=(318335, 318341, 318344, 318347, 318350, 318353, 318356, 318359, 318362, 318365, 318368)),
|
||||
DungeonSpriteAddressData(room_id=34, sprite_id_addresses=(318373, 318376, 318379, 318382, 318385, 318388, 318391)),
|
||||
DungeonSpriteAddressData(room_id=36, sprite_id_addresses=(318413, 318416, 318419, 318422, 318425, 318428, 318431)),
|
||||
DungeonSpriteAddressData(room_id=38, sprite_id_addresses=(318471, 318438, 318441, 318444, 318447, 318450, 318453, 318459, 318462, 318465, 318468)),
|
||||
DungeonSpriteAddressData(room_id=39, sprite_id_addresses=(318476, 318479, 318482, 318485, 318488, 318491, 318494)),
|
||||
DungeonSpriteAddressData(room_id=40, sprite_id_addresses=(318511,)),
|
||||
DungeonSpriteAddressData(room_id=42, sprite_id_addresses=(318530, 318533, 318536, 318539, 318542, 318545)),
|
||||
DungeonSpriteAddressData(room_id=43, sprite_id_addresses=(318556, 318559, 318562, 318565, 318568, 318571)),
|
||||
DungeonSpriteAddressData(room_id=46, sprite_id_addresses=(318590, 318593, 318596, 318599, 318602, 318605)),
|
||||
DungeonSpriteAddressData(room_id=49, sprite_id_addresses=(318621, 318624, 318627, 318630, 318633, 318636, 318639, 318642, 318645, 318648)),
|
||||
DungeonSpriteAddressData(room_id=50, sprite_id_addresses=(318653, 318656, 318659, 318662, 318665)),
|
||||
DungeonSpriteAddressData(room_id=52, sprite_id_addresses=(318681, 318684, 318687, 318693, 318696, 318699, 318690)),
|
||||
DungeonSpriteAddressData(room_id=53, sprite_id_addresses=(318710, 318713, 318716, 318719, 318722, 318728, 318731, 318734, 318725)),
|
||||
DungeonSpriteAddressData(room_id=54, sprite_id_addresses=(318742, 318745, 318754, 318760, 318763)),
|
||||
DungeonSpriteAddressData(room_id=55, sprite_id_addresses=(318777, 318780, 318783, 318786, 318792, 318795, 318798, 318801, 318789)),
|
||||
DungeonSpriteAddressData(room_id=56, sprite_id_addresses=(318806, 318809, 318812, 318815, 318818, 318821, 318824)),
|
||||
DungeonSpriteAddressData(room_id=57, sprite_id_addresses=(318829, 318835, 318841, 318844, 318847, 318850)),
|
||||
DungeonSpriteAddressData(room_id=58, sprite_id_addresses=(318855, 318858, 318861, 318864, 318867, 318870)),
|
||||
DungeonSpriteAddressData(room_id=59, sprite_id_addresses=(318875, 318878, 318881, 318884, 318887, 318890, 318893)),
|
||||
DungeonSpriteAddressData(room_id=60, sprite_id_addresses=(318898, 318901, 318904)),
|
||||
DungeonSpriteAddressData(room_id=61, sprite_id_addresses=(318915, 318921, 318924, 318927, 318930, 318933, 318939, 318942, 318945, 318948, 318951)),
|
||||
DungeonSpriteAddressData(room_id=62, sprite_id_addresses=(318959, 318962, 318980, 318983, 318989, 318992)),
|
||||
DungeonSpriteAddressData(room_id=63, sprite_id_addresses=(319000, 319006, 319009)),
|
||||
DungeonSpriteAddressData(room_id=64, sprite_id_addresses=(319014, 319017, 319023, 319026, 319029)),
|
||||
DungeonSpriteAddressData(room_id=65, sprite_id_addresses=(319036, 319039, 319042, 319045)),
|
||||
DungeonSpriteAddressData(room_id=66, sprite_id_addresses=(319050, 319053, 319056, 319059, 319062, 319065)),
|
||||
DungeonSpriteAddressData(room_id=67, sprite_id_addresses=(319070, 319073)),
|
||||
DungeonSpriteAddressData(room_id=68, sprite_id_addresses=(319084, 319087, 319090, 319093, 319096, 319102)),
|
||||
DungeonSpriteAddressData(room_id=69, sprite_id_addresses=(319110, 319116, 319119, 319131, 319134, 319137, 319113, 319122, 319125, 319128)),
|
||||
DungeonSpriteAddressData(room_id=70, sprite_id_addresses=(319142, 319148, 319154)),
|
||||
DungeonSpriteAddressData(room_id=73, sprite_id_addresses=(319161, 319164, 319167, 319170, 319173, 319176, 319182, 319185, 319188, 319191, 319194, 319197)),
|
||||
DungeonSpriteAddressData(room_id=74, sprite_id_addresses=(319205, 319208)),
|
||||
DungeonSpriteAddressData(room_id=75, sprite_id_addresses=(319213, 319216, 319219, 319222, 319225, 319228, 319231, 319234)),
|
||||
DungeonSpriteAddressData(room_id=76, sprite_id_addresses=(319245, 319248, 319251, 319254, 319257, 319260)),
|
||||
DungeonSpriteAddressData(room_id=78, sprite_id_addresses=(319270, 319273, 319276, 319279)),
|
||||
DungeonSpriteAddressData(room_id=80, sprite_id_addresses=(319295, 319298, 319301)),
|
||||
DungeonSpriteAddressData(room_id=81, sprite_id_addresses=(319309, 319312)),
|
||||
DungeonSpriteAddressData(room_id=82, sprite_id_addresses=(319317, 319320, 319323)),
|
||||
DungeonSpriteAddressData(room_id=83, sprite_id_addresses=(319328, 319331, 319334, 319337, 319340, 319343, 319346, 319349, 319352, 319355, 319358, 319361, 319364)),
|
||||
DungeonSpriteAddressData(room_id=84, sprite_id_addresses=(319369, 319372, 319375, 319378, 319381, 319384, 319387, 319390)),
|
||||
DungeonSpriteAddressData(room_id=85, sprite_id_addresses=(319398, 319401)),
|
||||
DungeonSpriteAddressData(room_id=86, sprite_id_addresses=(319415, 319418, 319421, 319424, 319430, 319433, 319436, 319442, 319439)),
|
||||
DungeonSpriteAddressData(room_id=87, sprite_id_addresses=(319447, 319450, 319453, 319456, 319459, 319462, 319468, 319471, 319474, 319477, 319480, 319483, 319486, 319489)),
|
||||
DungeonSpriteAddressData(room_id=88, sprite_id_addresses=(319497, 319500, 319506, 319509, 319515, 319518, 319521)),
|
||||
DungeonSpriteAddressData(room_id=89, sprite_id_addresses=(319526, 319529, 319538, 319544, 319547, 319550, 319553, 319556, 319559, 319541)),
|
||||
DungeonSpriteAddressData(room_id=91, sprite_id_addresses=(319575, 319578, 319581, 319584)),
|
||||
DungeonSpriteAddressData(room_id=93, sprite_id_addresses=(319615, 319618, 319621, 319624, 319627, 319633, 319636, 319639, 319651, 319642, 319645, 319648, 319630)),
|
||||
DungeonSpriteAddressData(room_id=94, sprite_id_addresses=(319659, 319662, 319665, 319668)),
|
||||
DungeonSpriteAddressData(room_id=95, sprite_id_addresses=(319673, 319676, 319679)),
|
||||
DungeonSpriteAddressData(room_id=96, sprite_id_addresses=(319684,)),
|
||||
DungeonSpriteAddressData(room_id=97, sprite_id_addresses=(319689, 319692, 319695)),
|
||||
DungeonSpriteAddressData(room_id=98, sprite_id_addresses=(319700, 319703, 319706)),
|
||||
DungeonSpriteAddressData(room_id=99, sprite_id_addresses=(319714, 319711)),
|
||||
DungeonSpriteAddressData(room_id=100, sprite_id_addresses=(319719, 319725, 319728, 319731, 319734, 319737)),
|
||||
DungeonSpriteAddressData(room_id=101, sprite_id_addresses=(319760, 319763, 319766, 319769, 319772)),
|
||||
DungeonSpriteAddressData(room_id=102, sprite_id_addresses=(319777, 319783, 319786, 319795, 319798, 319801, 319804, 319810)),
|
||||
DungeonSpriteAddressData(room_id=103, sprite_id_addresses=(319818, 319821, 319824, 319827, 319830, 319833, 319836, 319839, 319842)),
|
||||
DungeonSpriteAddressData(room_id=104, sprite_id_addresses=(319859, 319865, 319868)),
|
||||
DungeonSpriteAddressData(room_id=106, sprite_id_addresses=(319873, 319876, 319879, 319882, 319885, 319888)),
|
||||
DungeonSpriteAddressData(room_id=107, sprite_id_addresses=(319899, 319902, 319905, 319911, 319914, 319917, 319920, 319923, 319926, 319929, 319932)),
|
||||
DungeonSpriteAddressData(room_id=109, sprite_id_addresses=(319954, 319957, 319960, 319963, 319966, 319969, 319972, 319975, 319978)),
|
||||
DungeonSpriteAddressData(room_id=110, sprite_id_addresses=(319983, 319986, 319989, 319992, 319995)),
|
||||
DungeonSpriteAddressData(room_id=113, sprite_id_addresses=(320000, 320003)),
|
||||
DungeonSpriteAddressData(room_id=114, sprite_id_addresses=(320011, 320017)),
|
||||
DungeonSpriteAddressData(room_id=115, sprite_id_addresses=(320022, 320025, 320028, 320031, 320034, 320037)),
|
||||
DungeonSpriteAddressData(room_id=116, sprite_id_addresses=(320045, 320048, 320051, 320054, 320057, 320060, 320063, 320066)),
|
||||
DungeonSpriteAddressData(room_id=117, sprite_id_addresses=(320071, 320074, 320077, 320080, 320083, 320086, 320095, 320098)),
|
||||
DungeonSpriteAddressData(room_id=118, sprite_id_addresses=(320106, 320109, 320112, 320115, 320121)),
|
||||
DungeonSpriteAddressData(room_id=119, sprite_id_addresses=(320126, 320138, 320141)),
|
||||
DungeonSpriteAddressData(room_id=123, sprite_id_addresses=(320146, 320149, 320152, 320155, 320158, 320161, 320167, 320170, 320173, 320176)),
|
||||
DungeonSpriteAddressData(room_id=124, sprite_id_addresses=(320181, 320184, 320187, 320190, 320193, 320196)),
|
||||
DungeonSpriteAddressData(room_id=125, sprite_id_addresses=(320216, 320219, 320225, 320228, 320234, 320222, 320231, 320204, 320207, 320210, 320213, 320222, 320231)),
|
||||
DungeonSpriteAddressData(room_id=126, sprite_id_addresses=(320242, 320245, 320254, 320257)),
|
||||
DungeonSpriteAddressData(room_id=128, sprite_id_addresses=(320291, 320294)),
|
||||
DungeonSpriteAddressData(room_id=129, sprite_id_addresses=(320302, 320305)),
|
||||
DungeonSpriteAddressData(room_id=130, sprite_id_addresses=(320310, 320313, 320316)),
|
||||
DungeonSpriteAddressData(room_id=131, sprite_id_addresses=(320321, 320324, 320327, 320330, 320333, 320336, 320339, 320342, 320345, 320348)),
|
||||
DungeonSpriteAddressData(room_id=132, sprite_id_addresses=(320353, 320356, 320359, 320362, 320365, 320368, 320371)),
|
||||
DungeonSpriteAddressData(room_id=133, sprite_id_addresses=(320376, 320379, 320382, 320385, 320388, 320391, 320394, 320397, 320400, 320403)),
|
||||
DungeonSpriteAddressData(room_id=135, sprite_id_addresses=(320410, 320413, 320416, 320419, 320434, 320437, 320440, 320446, 320422)),
|
||||
DungeonSpriteAddressData(room_id=139, sprite_id_addresses=(320468, 320471, 320474, 320477, 320480)),
|
||||
DungeonSpriteAddressData(room_id=140, sprite_id_addresses=(320503, 320506, 320509, 320512, 320518, 320521, 320527, 320524, 320515)),
|
||||
DungeonSpriteAddressData(room_id=141, sprite_id_addresses=(320538, 320541, 320544, 320547, 320550, 320556, 320559, 320562, 320565, 320568, 320571, 320535)),
|
||||
DungeonSpriteAddressData(room_id=142, sprite_id_addresses=(320579, 320582, 320585, 320588, 320591, 320594, 320597)),
|
||||
DungeonSpriteAddressData(room_id=145, sprite_id_addresses=(320610, 320616, 320619, 320622, 320625, 320613)),
|
||||
DungeonSpriteAddressData(room_id=146, sprite_id_addresses=(320636, 320639, 320642, 320645, 320648, 320654, 320657, 320660, 320663)),
|
||||
DungeonSpriteAddressData(room_id=147, sprite_id_addresses=(320668, 320671, 320674, 320677, 320680, 320683, 320686, 320689)),
|
||||
DungeonSpriteAddressData(room_id=149, sprite_id_addresses=(320694, 320697, 320700, 320703)),
|
||||
DungeonSpriteAddressData(room_id=151, sprite_id_addresses=(320728,)),
|
||||
DungeonSpriteAddressData(room_id=152, sprite_id_addresses=(320733, 320736, 320739, 320742, 320745)),
|
||||
DungeonSpriteAddressData(room_id=153, sprite_id_addresses=(320750, 320753, 320756, 320759, 320765, 320768, 320771, 320774, 320777, 320780)),
|
||||
DungeonSpriteAddressData(room_id=155, sprite_id_addresses=(320794, 320797, 320800, 320803, 320806, 320809, 320812, 320815, 320818, 320821)),
|
||||
DungeonSpriteAddressData(room_id=156, sprite_id_addresses=(320826, 320829, 320832, 320835, 320838, 320841)),
|
||||
DungeonSpriteAddressData(room_id=157, sprite_id_addresses=(320852, 320855, 320858, 320861, 320864, 320867, 320870, 320873)),
|
||||
DungeonSpriteAddressData(room_id=158, sprite_id_addresses=(320878, 320881, 320884, 320887)),
|
||||
DungeonSpriteAddressData(room_id=159, sprite_id_addresses=(320907, 320910)),
|
||||
DungeonSpriteAddressData(room_id=160, sprite_id_addresses=(320915, 320918, 320921)),
|
||||
DungeonSpriteAddressData(room_id=161, sprite_id_addresses=(320929, 320932, 320935, 320938, 320941, 320944, 320947, 320950)),
|
||||
DungeonSpriteAddressData(room_id=165, sprite_id_addresses=(320968, 320971, 320974, 320977, 320980, 320983, 320986, 320989, 320998, 321001)),
|
||||
DungeonSpriteAddressData(room_id=167, sprite_id_addresses=(321014, 321017)),
|
||||
DungeonSpriteAddressData(room_id=168, sprite_id_addresses=(321022, 321025, 321028, 321031, 321034)),
|
||||
DungeonSpriteAddressData(room_id=169, sprite_id_addresses=(321039, 321042, 321057, 321060, 321045, 321048, 321051, 321054)),
|
||||
DungeonSpriteAddressData(room_id=170, sprite_id_addresses=(321065, 321068, 321071, 321074, 321077, 321080)),
|
||||
DungeonSpriteAddressData(room_id=171, sprite_id_addresses=(321088, 321091, 321094, 321097, 321100, 321103, 321106)),
|
||||
DungeonSpriteAddressData(room_id=174, sprite_id_addresses=(321116, 321119)),
|
||||
DungeonSpriteAddressData(room_id=176, sprite_id_addresses=(321129, 321132, 321135, 321138, 321141, 321144, 321147, 321150, 321153, 321156, 321159, 321165, 321168)),
|
||||
DungeonSpriteAddressData(room_id=177, sprite_id_addresses=(321173, 321176, 321179, 321182, 321185, 321188, 321191, 321194, 321197, 321200)),
|
||||
DungeonSpriteAddressData(room_id=178, sprite_id_addresses=(321205, 321208, 321211, 321214, 321217, 321220, 321223, 321226, 321229, 321232, 321235, 321238, 321241, 321244)),
|
||||
DungeonSpriteAddressData(room_id=179, sprite_id_addresses=(321249, 321252, 321255, 321258, 321261)),
|
||||
DungeonSpriteAddressData(room_id=182, sprite_id_addresses=(321277, 321280, 321289, 321292, 321301, 321304)),
|
||||
DungeonSpriteAddressData(room_id=183, sprite_id_addresses=(321309, 321312)),
|
||||
DungeonSpriteAddressData(room_id=184, sprite_id_addresses=(321317, 321320, 321323, 321326, 321329, 321332)),
|
||||
DungeonSpriteAddressData(room_id=186, sprite_id_addresses=(321342, 321345, 321348, 321351, 321354, 321357, 321360)),
|
||||
DungeonSpriteAddressData(room_id=187, sprite_id_addresses=(321365, 321368, 321371, 321374, 321377, 321380, 321386, 321389, 321392, 321395, 321383)),
|
||||
DungeonSpriteAddressData(room_id=188, sprite_id_addresses=(321403, 321406, 321409, 321412, 321418, 321421, 321424, 321433, 321400, 321415, 321427, 321430)),
|
||||
DungeonSpriteAddressData(room_id=190, sprite_id_addresses=(321440, 321446, 321449, 321452, 321455, 321458)),
|
||||
DungeonSpriteAddressData(room_id=192, sprite_id_addresses=(321471, 321474, 321477, 321480, 321486, 321489, 321492, 321495)),
|
||||
DungeonSpriteAddressData(room_id=193, sprite_id_addresses=(321503, 321506, 321509, 321512, 321518, 321524, 321527, 321530, 321521, 321515, 321536)),
|
||||
DungeonSpriteAddressData(room_id=194, sprite_id_addresses=(321547, 321550, 321553, 321556, 321562, 321559, 321544, 321541)),
|
||||
DungeonSpriteAddressData(room_id=195, sprite_id_addresses=(321567, 321585, 321588)),
|
||||
DungeonSpriteAddressData(room_id=196, sprite_id_addresses=(321605, 321608, 321611, 321614, 321617, 321620)),
|
||||
DungeonSpriteAddressData(room_id=201, sprite_id_addresses=(321697, 321700, 321703)),
|
||||
DungeonSpriteAddressData(room_id=203, sprite_id_addresses=(321708, 321717, 321720, 321723, 321726, 321729, 321732, 321735, 321738, 321741, 321714, 321711)),
|
||||
DungeonSpriteAddressData(room_id=204, sprite_id_addresses=(321746, 321749, 321755, 321758, 321761, 321770, 321773, 321776, 321779, 321782, 321785, 321752, 321764, 321767)),
|
||||
DungeonSpriteAddressData(room_id=206, sprite_id_addresses=(321790, 321793, 321799, 321802, 321805, 321808, 321811)),
|
||||
DungeonSpriteAddressData(room_id=208, sprite_id_addresses=(321816, 321819, 321822, 321825, 321828, 321831, 321834, 321837, 321840, 321843, 321846)),
|
||||
DungeonSpriteAddressData(room_id=209, sprite_id_addresses=(321851, 321854, 321857, 321860, 321863, 321866, 321869, 321872)),
|
||||
DungeonSpriteAddressData(room_id=210, sprite_id_addresses=(321877, 321880, 321883, 321886, 321889, 321892, 321895, 321898, 321901, 321904)),
|
||||
DungeonSpriteAddressData(room_id=216, sprite_id_addresses=(321937, 321940, 321943, 321946, 321949, 321952, 321955, 321958, 321961, 321964, 321967)),
|
||||
DungeonSpriteAddressData(room_id=217, sprite_id_addresses=(321975, 321978, 321981, 321972)),
|
||||
DungeonSpriteAddressData(room_id=218, sprite_id_addresses=(321986, 321989)),
|
||||
DungeonSpriteAddressData(room_id=219, sprite_id_addresses=(321994, 321997, 322000, 322006, 322003, 322012, 322009)),
|
||||
DungeonSpriteAddressData(room_id=220, sprite_id_addresses=(322020, 322023, 322026, 322029, 322032, 322035, 322047, 322017, 322038, 322041, 322044)),
|
||||
DungeonSpriteAddressData(room_id=223, sprite_id_addresses=(322063, 322066)),
|
||||
DungeonSpriteAddressData(room_id=224, sprite_id_addresses=(322071, 322074, 322077, 322080)),
|
||||
DungeonSpriteAddressData(room_id=232, sprite_id_addresses=(322189, 322192, 322195, 322198)),
|
||||
DungeonSpriteAddressData(room_id=238, sprite_id_addresses=(322213, 322216, 322219, 322222, 322225)),
|
||||
DungeonSpriteAddressData(room_id=239, sprite_id_addresses=(322230, 322233, 322236)),
|
||||
DungeonSpriteAddressData(room_id=249, sprite_id_addresses=(322323, 322326, 322329, 322332)),
|
||||
DungeonSpriteAddressData(room_id=254, sprite_id_addresses=(322378, 322381, 322384, 322387, 322390)),
|
||||
DungeonSpriteAddressData(room_id=263, sprite_id_addresses=(322444, 322447)),
|
||||
DungeonSpriteAddressData(room_id=264, sprite_id_addresses=(322452, 322455, 322458, 322461)),
|
||||
DungeonSpriteAddressData(room_id=267, sprite_id_addresses=(322494,)),
|
||||
DungeonSpriteAddressData(room_id=269, sprite_id_addresses=(322525, 322528)),
|
||||
DungeonSpriteAddressData(room_id=291, sprite_id_addresses=(322671, 322674, 322677, 322680)),
|
||||
)
|
||||
|
||||
KEYED_SPRITE_ID_ADDRESSES = frozenset((317984,
|
||||
318044,
|
||||
318335,
|
||||
318835,
|
||||
318915,
|
||||
318983,
|
||||
320003,
|
||||
320011,
|
||||
320294,
|
||||
320759,
|
||||
321292,
|
||||
321480,
|
||||
321530,
|
||||
320000,
|
||||
321159,
|
||||
321937,
|
||||
321940,
|
||||
321943,
|
||||
321946,
|
||||
321949,
|
||||
321952,
|
||||
321955,
|
||||
321958,
|
||||
321961,
|
||||
321964,
|
||||
321967,
|
||||
321424,
|
||||
321421,
|
||||
321418))
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class RoomGroupRequirementData(NamedTuple):
|
||||
group_id: Optional[int]
|
||||
subgroup_0: Optional[int]
|
||||
subgroup_1: Optional[int]
|
||||
subgroup_2: Optional[int]
|
||||
subgroup_3: Optional[int]
|
||||
rooms: tuple[int, ...]
|
||||
|
||||
SHUTTER_ROOM_IDS = frozenset((184,
|
||||
11,
|
||||
27,
|
||||
75,
|
||||
4,
|
||||
36,
|
||||
182,
|
||||
40,
|
||||
14,
|
||||
46,
|
||||
62,
|
||||
110,
|
||||
49,
|
||||
135,
|
||||
68,
|
||||
69,
|
||||
83,
|
||||
117,
|
||||
133,
|
||||
61,
|
||||
93,
|
||||
107,
|
||||
109,
|
||||
123,
|
||||
125,
|
||||
141,
|
||||
150,
|
||||
165,
|
||||
113,
|
||||
168,
|
||||
216,
|
||||
176,
|
||||
192,
|
||||
224,
|
||||
178,
|
||||
210,
|
||||
239,
|
||||
268,
|
||||
291))
|
||||
WATER_ROOM_IDS = frozenset((22, 40, 52, 54, 56, 70, 102))
|
||||
DONT_RANDOMIZE_ROOM_IDS = frozenset((0, 1, 3, 13, 20, 32, 48, 127))
|
||||
NO_SPECIAL_ENEMIES_STANDARD_ROOM_IDS = frozenset((1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 85, 96, 97, 98, 112, 113, 114, 128, 129, 130))
|
||||
BOSS_ROOM_IDS = frozenset((200, 51, 108, 7, 77, 90, 6, 41, 172, 222, 144, 164, 32, 13, 0))
|
||||
|
||||
ROOM_GROUP_REQUIREMENTS = (
|
||||
RoomGroupRequirementData(group_id=1, subgroup_0=70, subgroup_1=73, subgroup_2=28, subgroup_3=82, rooms=(228, 240)),
|
||||
RoomGroupRequirementData(group_id=5, subgroup_0=75, subgroup_1=77, subgroup_2=74, subgroup_3=90, rooms=(243, 265, 270, 271, 272, 273, 282, 284, 290)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=75, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(255, 274, 287)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, rooms=(289,)),
|
||||
RoomGroupRequirementData(group_id=7, subgroup_0=75, subgroup_1=77, subgroup_2=57, subgroup_3=54, rooms=(8, 44, 276, 277)),
|
||||
RoomGroupRequirementData(group_id=13, subgroup_0=81, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(85, 258, 260)),
|
||||
RoomGroupRequirementData(group_id=14, subgroup_0=71, subgroup_1=73, subgroup_2=76, subgroup_3=80, rooms=(18, 261, 266)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=80, rooms=(264,)),
|
||||
RoomGroupRequirementData(group_id=15, subgroup_0=79, subgroup_1=77, subgroup_2=74, subgroup_3=80, rooms=(244, 245, 257, 259, 262, 280, 281)),
|
||||
RoomGroupRequirementData(group_id=18, subgroup_0=85, subgroup_1=61, subgroup_2=66, subgroup_3=67, rooms=(32, 48)),
|
||||
RoomGroupRequirementData(group_id=24, subgroup_0=85, subgroup_1=26, subgroup_2=66, subgroup_3=67, rooms=(13,)),
|
||||
RoomGroupRequirementData(group_id=34, subgroup_0=33, subgroup_1=65, subgroup_2=69, subgroup_3=51, rooms=(0,)),
|
||||
RoomGroupRequirementData(group_id=40, subgroup_0=14, subgroup_1=None, subgroup_2=74, subgroup_3=80, rooms=(225, 256, 293, 292, 294)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=14, subgroup_1=30, subgroup_2=None, subgroup_3=None, rooms=(291,)),
|
||||
RoomGroupRequirementData(group_id=23, subgroup_0=64, subgroup_1=None, subgroup_2=None, subgroup_3=63, rooms=()),
|
||||
RoomGroupRequirementData(group_id=9, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=29, rooms=(227,)),
|
||||
RoomGroupRequirementData(group_id=11, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=61, rooms=()),
|
||||
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=49, rooms=()),
|
||||
RoomGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=60, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=21, subgroup_0=None, subgroup_1=None, subgroup_2=58, subgroup_3=62, rooms=()),
|
||||
RoomGroupRequirementData(group_id=28, subgroup_0=None, subgroup_1=None, subgroup_2=38, subgroup_3=82, rooms=(14, 126, 142, 158, 190)),
|
||||
RoomGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=48, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=26, subgroup_0=None, subgroup_1=None, subgroup_2=56, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=20, subgroup_0=None, subgroup_1=None, subgroup_2=57, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=32, subgroup_0=None, subgroup_1=44, subgroup_2=59, subgroup_3=None, rooms=()),
|
||||
RoomGroupRequirementData(group_id=3, subgroup_0=93, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(81,)),
|
||||
RoomGroupRequirementData(group_id=42, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(286,)),
|
||||
RoomGroupRequirementData(group_id=10, subgroup_0=47, subgroup_1=None, subgroup_2=46, subgroup_3=None, rooms=(92, 117, 185, 217)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(54, 70, 102, 118)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=32, subgroup_2=None, subgroup_3=None, rooms=(62, 159)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=31, subgroup_1=None, subgroup_2=None, subgroup_3=None, rooms=(127,)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=35, subgroup_3=None, rooms=(57, 73, 86, 87, 104, 141)),
|
||||
RoomGroupRequirementData(group_id=37, subgroup_0=31, subgroup_1=None, subgroup_2=39, subgroup_3=82, rooms=(36, 180, 181, 198, 199, 214)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(23, 42, 68, 76, 86, 88, 89, 103, 104, 126, 139, 235, 251)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(23, 42, 76, 89, 103, 104, 126, 139, 235, 251)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(11, 19, 27, 30, 42, 43, 49, 61, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(11, 19, 27, 30, 42, 43, 49, 53, 62, 91, 107, 119, 135, 139, 145, 146, 155, 157, 161, 171, 182, 191, 193, 196, 239)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(19, 35, 150, 165, 195, 197, 213)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(19, 35, 150, 165, 197, 213)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 38, 43, 64, 74, 87, 107, 123)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(38, 43, 64, 74, 87, 107, 123, 206)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(2, 88, 100, 140, 267)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=82, rooms=(26, 61, 68, 86, 94, 124, 149, 195)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(4, 63, 206)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=83, rooms=(53, 55, 118)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=34, subgroup_3=None, rooms=(40,)),
|
||||
RoomGroupRequirementData(group_id=None, subgroup_0=None, subgroup_1=None, subgroup_2=37, subgroup_3=None, rooms=(151,)),
|
||||
)
|
||||
@@ -0,0 +1,295 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class EnemySpriteRequirementData(NamedTuple):
|
||||
sprite_name: str
|
||||
sprite_id: int
|
||||
boss: bool
|
||||
overlord: bool
|
||||
do_not_randomize: bool
|
||||
killable: bool
|
||||
npc: bool
|
||||
never_use_dungeon: bool
|
||||
never_use_overworld: bool
|
||||
cannot_have_key: bool
|
||||
is_object: bool
|
||||
absorbable: bool
|
||||
is_water_sprite: bool
|
||||
is_enemy_sprite: bool
|
||||
group_ids: tuple[int, ...]
|
||||
subgroup_0: tuple[int, ...]
|
||||
subgroup_1: tuple[int, ...]
|
||||
subgroup_2: tuple[int, ...]
|
||||
subgroup_3: tuple[int, ...]
|
||||
parameters: Optional[int]
|
||||
special_glitched: bool
|
||||
excluded_rooms: tuple[int, ...]
|
||||
dont_randomize_rooms: tuple[int, ...]
|
||||
spawnable_rooms: tuple[int, ...]
|
||||
|
||||
ENEMY_SPRITE_REQUIREMENTS = (
|
||||
EnemySpriteRequirementData(sprite_name='RavenSprite', sprite_id=0, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17, 25), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VultureSprite', sprite_id=1, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EmptySprite', sprite_id=3, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PullSwitch_GoodSprite', sprite_id=4, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PullSwitch_TrapSprite', sprite_id=6, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Octorok_OneWaySprite', sprite_id=8, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MoldormSprite', sprite_id=9, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(48,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Octorok_FourWaySprite', sprite_id=10, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ChickenSprite', sprite_id=11, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21, 80), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BuzzblobSprite', sprite_id=13, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SnapdragonSprite', sprite_id=14, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OctoballoonSprite', sprite_id=15, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OctoballoonHatchlingsSprite', sprite_id=16, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HinoxSprite', sprite_id=17, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MoblinSprite', sprite_id=18, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(23,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MiniHelmasaurSprite', sprite_id=19, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GargoylesDomainGateSprite', sprite_id=20, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AntifairySprite', sprite_id=21, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(64, 210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SahasrahlaAginahSprite', sprite_id=22, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BushHoarderSprite', sprite_id=23, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MiniMoldormSprite', sprite_id=24, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PoeSprite', sprite_id=25, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(14, 21), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DwarvesSprite', sprite_id=26, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowInWall_MaybeSprite', sprite_id=27, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StatueSprite', sprite_id=28, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268, 63), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WeathervaneSprite', sprite_id=29, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CrystalSwitchSprite', sprite_id=30, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BugCatchingKidSprite', sprite_id=31, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(81,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SluggulaSprite', sprite_id=32, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PushSwitchSprite', sprite_id=33, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(83,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RopaSprite', sprite_id=34, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(22,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedBariSprite', sprite_id=35, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueBariSprite', sprite_id=36, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(127,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TalkingTreeSprite', sprite_id=37, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HardhatBeetleSprite', sprite_id=38, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(30,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DeadrockSprite', sprite_id=39, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(127, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StorytellersSprite', sprite_id=40, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlindHideoutAttendantSprite', sprite_id=41, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14, 79), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SweepingLadySprite', sprite_id=42, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MultipurposeSpriteSprite', sprite_id=43, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LumberjacksSprite', sprite_id=44, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TelepathicStones_NoIdeaWhatThisActuallyIsLikelyUnusedSprite', sprite_id=45, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FluteBoysNotesSprite', sprite_id=46, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RaceHPNPCsSprite', sprite_id=47, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Person_MaybeSprite', sprite_id=48, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FortuneTellerSprite', sprite_id=49, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AngryBrothersSprite', sprite_id=50, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PullForRupeesSpriteSprite', sprite_id=51, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ScaredGirl2Sprite', sprite_id=52, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='InnkeeperSprite', sprite_id=53, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WitchSprite', sprite_id=54, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(76,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WaterfallSprite', sprite_id=55, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowTargetSprite', sprite_id=56, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AverageMiddleAgedManSprite', sprite_id=57, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HalfMagicBatSprite', sprite_id=58, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DashItemSprite', sprite_id=59, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VillageKidSprite', sprite_id=60, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Signs_ChickenLadyAlsoShowedUp_ScaredLadiesOutsideHousesSprite', sprite_id=61, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RockHoarderSprite', sprite_id=62, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TutorialSoldierSprite', sprite_id=63, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LightningLockSprite', sprite_id=64, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueSwordSoldier_DetectPlayerSprite', sprite_id=65, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenSwordSoldierSprite', sprite_id=66, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedSpearSoldierSprite', sprite_id=67, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AssaultSwordSoldierSprite', sprite_id=68, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenSpearSoldierSprite', sprite_id=69, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(13, 73), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueArcherSprite', sprite_id=70, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenArcherSprite', sprite_id=71, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(72,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedJavelinSoldierSprite', sprite_id=72, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedJavelinSoldier2Sprite', sprite_id=73, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedBombSoldiersSprite', sprite_id=74, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenSoldierRecruits_HMKnightSprite', sprite_id=75, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(73,), subgroup_2=(19,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GeldmanSprite', sprite_id=76, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RabbitSprite', sprite_id=77, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PopoSprite', sprite_id=78, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Popo2Sprite', sprite_id=79, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CannonBallsSprite', sprite_id=80, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArmosSprite', sprite_id=81, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GiantZoraSprite', sprite_id=82, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArmosKnightsSprite', sprite_id=83, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(29,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LanmolasSprite', sprite_id=84, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(49,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FireballZoraSprite', sprite_id=85, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12, 24), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WalkingZoraSprite', sprite_id=86, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(68,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DesertPalaceBarriersSprite', sprite_id=87, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CrabSprite', sprite_id=88, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(12,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BirdSprite', sprite_id=89, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SquirrelSprite', sprite_id=90, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Spark_LeftToRightSprite', sprite_id=91, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Spark_RightToLeftSprite', sprite_id=92, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Roller_VerticalMovingSprite', sprite_id=93, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Roller_VerticalMoving2Sprite', sprite_id=94, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RollerSprite', sprite_id=95, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Roller_HorizontalMovingSprite', sprite_id=96, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BeamosSprite', sprite_id=97, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MasterSwordSprite', sprite_id=98, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(55,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Devalant_NonShooterSprite', sprite_id=99, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Devalant_ShooterSprite', sprite_id=100, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ShootingGalleryProprietorSprite', sprite_id=101, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_RightSprite', sprite_id=102, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_LeftSprite', sprite_id=103, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_DownSprite', sprite_id=104, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MovingCannonBallShooters_UpSprite', sprite_id=105, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BallNChainTrooperSprite', sprite_id=106, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CannonSoldierSprite', sprite_id=107, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MirrorPortalSprite', sprite_id=108, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RatSprite', sprite_id=109, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RopeSprite', sprite_id=110, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KeeseSprite', sprite_id=111, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LeeverSprite', sprite_id=113, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(47,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ActivatoForThePonds_WhereYouThrowInItemsSprite', sprite_id=114, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='UnclePriestSprite', sprite_id=115, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(71, 81), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RunningManSprite', sprite_id=116, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BottleSalesmanSprite', sprite_id=117, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(6,), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PrincessZeldaSprite', sprite_id=118, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VillageElderSprite', sprite_id=120, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AgahnimSprite', sprite_id=122, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(26, 61), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AgahnimEnergyBallSprite', sprite_id=123, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FloatingStalfosHeadSprite', sprite_id=124, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BigSpikeTrapSprite', sprite_id=125, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GuruguruBar_ClockwiseSprite', sprite_id=126, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GuruguruBar_CounterClockwiseSprite', sprite_id=127, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(181, 150), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WinderSprite', sprite_id=128, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WaterTektiteSprite', sprite_id=129, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(40,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AntifairyCircleSprite', sprite_id=130, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenEyegoreSprite', sprite_id=131, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedEyegoreSprite', sprite_id=132, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KodongosSprite', sprite_id=134, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MothulaSprite', sprite_id=136, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MothulasBeamSprite', sprite_id=137, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(56,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SpikeTrapSprite', sprite_id=138, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(40, 11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GibdoSprite', sprite_id=139, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrghusSprite', sprite_id=140, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrghusSpawnSprite', sprite_id=141, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TerrorpinSprite', sprite_id=142, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(42,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SlimeSprite_JumpsOutOfTheFloor', sprite_id=143, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WallmasterSprite', sprite_id=144, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(1, 2, 17, 33, 34, 50, 65, 66, 80, 81, 82, 96, 97, 98, 112, 113, 114, 128, 129, 130, 137, 153, 168, 169, 170, 184, 185, 186, 200, 201, 216, 217, 218, 51, 67, 83, 99, 115, 116, 117, 131, 132, 133, 7, 23, 39, 49, 119, 135, 167, 32, 48, 64, 176, 192, 208, 224, 9, 10, 11, 25, 26, 27, 42, 43, 58, 59, 74, 75, 90, 106, 6, 22, 38, 40, 52, 53, 54, 55, 56, 70, 84, 102, 118, 41, 57, 73, 86, 87, 88, 89, 103, 104, 68, 69, 100, 101, 171, 172, 187, 188, 203, 204, 219, 220, 14, 30, 31, 46, 62, 63, 78, 79, 94, 95, 110, 126, 127, 142, 158, 159, 174, 175, 190, 191, 206, 222, 144, 145, 146, 147, 151, 152, 160, 161, 162, 163, 177, 178, 179, 193, 194, 195, 209, 210, 4, 19, 20, 21, 35, 36, 164, 180, 181, 182, 183, 196, 197, 198, 199, 213, 214, 12, 13, 28, 29, 61, 76, 77, 91, 92, 93, 107, 108, 109, 123, 124, 125, 139, 140, 141, 149, 150, 155, 156, 157, 165, 166)),
|
||||
EnemySpriteRequirementData(sprite_name='StalfosKnightSprite', sprite_id=145, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HelmasaurKingSprite', sprite_id=146, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(58,), subgroup_3=(62,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BumperSprite', sprite_id=147, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SwimmersEvilSprite', sprite_id=148, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_RightSprite', sprite_id=149, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_LeftSprite', sprite_id=150, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_DownSprite', sprite_id=151, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='EyeLaser_UpSprite', sprite_id=152, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82, 83), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PengatorSprite', sprite_id=153, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KyameronWaterSplashSprite', sprite_id=154, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(268,), dont_randomize_rooms=(40,), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WizzrobeSprite', sprite_id=155, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VerminHorizontalSprite', sprite_id=156, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VerminVerticalSprite', sprite_id=157, boss=False, overlord=False, do_not_randomize=True, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Ostrich_HauntedGroveSprite', sprite_id=158, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FluteSprite', sprite_id=159, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Birds_HauntedGroveSprite', sprite_id=160, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(78,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FreezorSprite', sprite_id=161, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(38,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KholdstareSprite', sprite_id=162, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KholdstaresShellSprite', sprite_id=163, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FallingIceSprite', sprite_id=164, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(60,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueZazakSprite', sprite_id=165, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedZazakSprite', sprite_id=166, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StalfosSprite', sprite_id=167, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworldSprite', sprite_id=168, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BomberFlyingCreaturesFromDarkworld2Sprite', sprite_id=169, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(210, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='PikitSprite', sprite_id=170, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MaidenSprite', sprite_id=171, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AppleSprite', sprite_id=172, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LostOldManSprite', sprite_id=173, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(70,), subgroup_1=(73,), subgroup_2=(28,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DownPipeSprite', sprite_id=174, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='UpPipeSprite', sprite_id=175, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RightPipeSprite', sprite_id=176, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LeftPipeSprite', sprite_id=177, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GoodBee_AgainMaybeSprite', sprite_id=178, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HylianInscriptionSprite', sprite_id=179, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ThiefsChestSprite', sprite_id=180, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(21,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombSalesmanSprite', sprite_id=181, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KikiSprite', sprite_id=182, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MaidenInBlindDungeonSprite', sprite_id=183, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MimicSprite', sprite_id=184, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FeudingFriendsOnDeathMountainSprite', sprite_id=185, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='WhirlpoolSprite', sprite_id=186, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(255, 274, 287)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(271, 272)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(272,)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(280,)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(291, 292)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(14,), subgroup_1=(), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(293,)),
|
||||
EnemySpriteRequirementData(sprite_name='SalesmanChestgameGuy300RupeeGiverGuyChestGameThiefSprite', sprite_id=187, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(21,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=(286,)),
|
||||
EnemySpriteRequirementData(sprite_name='DrunkInTheInnSprite', sprite_id=188, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(79,), subgroup_1=(77,), subgroup_2=(74,), subgroup_3=(80,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Vitreous_LargeEyeballSprite', sprite_id=189, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Vitreous_SmallEyeballSprite', sprite_id=190, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='VitreousLightningSprite', sprite_id=191, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(61,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CatFish_QuakeMedallionSprite', sprite_id=192, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(24,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AgahnimTeleportingZeldaToDarkworldSprite', sprite_id=193, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(85,), subgroup_1=(61,), subgroup_2=(66,), subgroup_3=(67,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BouldersSprite', sprite_id=194, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='Gibo_FloatingBlobSprite', sprite_id=195, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(40,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ThiefSprite', sprite_id=196, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(14, 21), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MedusaSprite', sprite_id=197, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FourWayFireballSpittersSprite', sprite_id=198, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HokkuBokkuSprite', sprite_id=199, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BigFairyWhoHealsYouSprite', sprite_id=200, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(57,), subgroup_3=(54,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TektiteSprite', sprite_id=201, boss=False, overlord=False, do_not_randomize=False, killable=True, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ChainChompSprite', sprite_id=202, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='TrinexxSprite', sprite_id=203, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='AnotherPartOfTrinexxSprite', sprite_id=204, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='YetAnotherPartOfTrinexxSprite', sprite_id=205, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(64,), subgroup_1=(), subgroup_2=(), subgroup_3=(63,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlindTheThiefSprite', sprite_id=206, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(44,), subgroup_2=(59,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SwamolaSprite', sprite_id=207, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=True, is_object=False, absorbable=False, is_water_sprite=True, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(25,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LynelSprite', sprite_id=208, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(20,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BunnyBeamSprite', sprite_id=209, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FloppingFishSprite', sprite_id=210, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='StalSprite', sprite_id=211, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='LandmineSprite', sprite_id=212, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(11, 22, 25, 30, 38, 39, 54, 63, 66, 64, 70, 73, 75, 78, 85, 87, 95, 101, 106, 116, 118, 125, 127, 131, 132, 133, 140, 141, 146, 149, 152, 155, 156, 157, 158, 160, 170, 175, 179, 186, 187, 188, 198, 203, 206, 208, 210, 213, 216, 220, 223, 228, 231, 238, 249, 253, 268), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='DiggingGameProprietorSprite', sprite_id=213, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(42,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GanonSprite', sprite_id=214, boss=True, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(33,), subgroup_1=(65,), subgroup_2=(69,), subgroup_3=(51,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CopyOfGanon_ExceptInvincibleSprite', sprite_id=215, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HeartSprite', sprite_id=216, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='GreenRupeeSprite', sprite_id=217, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BlueRupeeSprite', sprite_id=218, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='RedRupeeSprite', sprite_id=219, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombRefill1Sprite', sprite_id=220, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombRefill4Sprite', sprite_id=221, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BombRefill8Sprite', sprite_id=222, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='SmallMagicRefillSprite', sprite_id=223, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FullMagicRefillSprite', sprite_id=224, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowRefill5Sprite', sprite_id=225, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ArrowRefill10Sprite', sprite_id=226, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FairySprite', sprite_id=227, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='KeySprite', sprite_id=228, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=True, cannot_have_key=True, is_object=False, absorbable=True, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BigKeySprite', sprite_id=229, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='ShieldEaterSprite', sprite_id=230, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(27,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MushroomSprite', sprite_id=231, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='FakeMasterSwordSprite', sprite_id=232, boss=False, overlord=False, do_not_randomize=False, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(17,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MagicShopDude_HisItemsIncludingTheMagicPowderSprite', sprite_id=233, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=True, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(75,), subgroup_1=(), subgroup_2=(), subgroup_3=(90,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HeartContainerSprite', sprite_id=234, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='HeartPieceSprite', sprite_id=235, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='BushesSprite', sprite_id=236, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatformSprite', sprite_id=237, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(39,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MantleSprite', sprite_id=238, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(93,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused1Sprite', sprite_id=239, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused2Sprite', sprite_id=240, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='CaneOfSomariaPlatform_Unused3Sprite', sprite_id=241, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='MedallionTabletSprite', sprite_id=242, boss=False, overlord=False, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=True, absorbable=False, is_water_sprite=False, is_enemy_sprite=False, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(18,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OW_OL_FallingRocks', sprite_id=244, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(16,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EP4Walls', sprite_id=258, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_CanonBalls_EPEntrance', sprite_id=259, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(46,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_StalfosHeadTrap', sprite_id=261, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_BombDrop_RopeTrap', sprite_id=262, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(28, 36), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_MovingFloor', sprite_id=263, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_SlimeDropper', sprite_id=264, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_Wallmaster', sprite_id=265, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(35,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Square', sprite_id=266, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_FloorDrop_Path', sprite_id=267, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_RightEvil_PirogusuSpawner', sprite_id=272, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_LeftEvil_PirogusuSpawner', sprite_id=273, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_DownEvil_PirogusuSpawner', sprite_id=274, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_UpEvil_PirogusuSpawner', sprite_id=275, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(34,), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_FlyingFloorTileTrap', sprite_id=276, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(82,), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_WizzrobeSpawner', sprite_id=277, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=False, never_use_overworld=False, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(37, 41), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_BlackSpawn_Zoro_BombHole', sprite_id=278, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(32,), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_4Skull_Trap_Pot', sprite_id=279, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_Stalfos_Spawn_Trap_EP', sprite_id=280, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(31,), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_ArmosKnight_Trigger', sprite_id=281, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
EnemySpriteRequirementData(sprite_name='OL_BombDrop_BombTrap', sprite_id=282, boss=False, overlord=True, do_not_randomize=True, killable=False, npc=False, never_use_dungeon=True, never_use_overworld=True, cannot_have_key=False, is_object=False, absorbable=False, is_water_sprite=False, is_enemy_sprite=True, group_ids=(), subgroup_0=(), subgroup_1=(), subgroup_2=(), subgroup_3=(), parameters=None, special_glitched=False, excluded_rooms=(), dont_randomize_rooms=(), spawnable_rooms=()),
|
||||
)
|
||||
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
|
||||
class OverworldGroupRequirementData(NamedTuple):
|
||||
group_id: Optional[int]
|
||||
subgroup_0: Optional[int]
|
||||
subgroup_1: Optional[int]
|
||||
subgroup_2: Optional[int]
|
||||
subgroup_3: Optional[int]
|
||||
areas: tuple[int, ...]
|
||||
|
||||
AREA_IDS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207)
|
||||
DO_NOT_RANDOMIZE_AREA_IDS = frozenset((1,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
9,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
25,
|
||||
28,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
35,
|
||||
36,
|
||||
38,
|
||||
39,
|
||||
49,
|
||||
54,
|
||||
56,
|
||||
57,
|
||||
61,
|
||||
62,
|
||||
65,
|
||||
68,
|
||||
70,
|
||||
72,
|
||||
73,
|
||||
75,
|
||||
76,
|
||||
77,
|
||||
78,
|
||||
89,
|
||||
92,
|
||||
95,
|
||||
96,
|
||||
97,
|
||||
99,
|
||||
100,
|
||||
102,
|
||||
103,
|
||||
113,
|
||||
118,
|
||||
120,
|
||||
121,
|
||||
125,
|
||||
126,
|
||||
42,
|
||||
106,
|
||||
130,
|
||||
131,
|
||||
132,
|
||||
133,
|
||||
134,
|
||||
135,
|
||||
136,
|
||||
137,
|
||||
138,
|
||||
139,
|
||||
140,
|
||||
141,
|
||||
142,
|
||||
143,
|
||||
186,
|
||||
250,
|
||||
145,
|
||||
148,
|
||||
150,
|
||||
152,
|
||||
153,
|
||||
155,
|
||||
156,
|
||||
158,
|
||||
169,
|
||||
172,
|
||||
175,
|
||||
176,
|
||||
177,
|
||||
179,
|
||||
180,
|
||||
182,
|
||||
183,
|
||||
193,
|
||||
198,
|
||||
200,
|
||||
274,
|
||||
275,
|
||||
276,
|
||||
277,
|
||||
278,
|
||||
279,
|
||||
281,
|
||||
288))
|
||||
|
||||
FORCED_GROUP_REQUIREMENTS = (
|
||||
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=74, subgroup_3=None, areas=(2,)),
|
||||
OverworldGroupRequirementData(group_id=16, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=16, areas=(3, 147)),
|
||||
OverworldGroupRequirementData(group_id=7, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=17, areas=(10, 154)),
|
||||
OverworldGroupRequirementData(group_id=4, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(15, 159)),
|
||||
OverworldGroupRequirementData(group_id=3, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=14, areas=(20, 164)),
|
||||
OverworldGroupRequirementData(group_id=1, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=63, areas=(27, 171)),
|
||||
OverworldGroupRequirementData(group_id=6, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(34, 40, 178, 184)),
|
||||
OverworldGroupRequirementData(group_id=8, subgroup_0=None, subgroup_1=None, subgroup_2=18, subgroup_3=None, areas=(48, 192)),
|
||||
OverworldGroupRequirementData(group_id=10, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(58, 202)),
|
||||
OverworldGroupRequirementData(group_id=22, subgroup_0=None, subgroup_1=None, subgroup_2=24, subgroup_3=None, areas=(79, 223)),
|
||||
OverworldGroupRequirementData(group_id=21, subgroup_0=21, subgroup_1=None, subgroup_2=None, subgroup_3=21, areas=(98, 242)),
|
||||
OverworldGroupRequirementData(group_id=27, subgroup_0=None, subgroup_1=42, subgroup_2=None, subgroup_3=None, areas=(104, 248)),
|
||||
OverworldGroupRequirementData(group_id=13, subgroup_0=None, subgroup_1=None, subgroup_2=76, subgroup_3=None, areas=(22, 166)),
|
||||
OverworldGroupRequirementData(group_id=29, subgroup_0=None, subgroup_1=77, subgroup_2=None, subgroup_3=21, areas=(105, 249)),
|
||||
OverworldGroupRequirementData(group_id=15, subgroup_0=None, subgroup_1=None, subgroup_2=78, subgroup_3=None, areas=(42, 186)),
|
||||
OverworldGroupRequirementData(group_id=17, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=76, areas=(106, 250)),
|
||||
OverworldGroupRequirementData(group_id=12, subgroup_0=None, subgroup_1=None, subgroup_2=55, subgroup_3=54, areas=(128, 272)),
|
||||
OverworldGroupRequirementData(group_id=14, subgroup_0=None, subgroup_1=None, subgroup_2=12, subgroup_3=68, areas=(129, 273)),
|
||||
OverworldGroupRequirementData(group_id=26, subgroup_0=15, subgroup_1=None, subgroup_2=None, subgroup_3=None, areas=(146,)),
|
||||
OverworldGroupRequirementData(group_id=23, subgroup_0=None, subgroup_1=None, subgroup_2=None, subgroup_3=25, areas=(94, 238)),
|
||||
)
|
||||
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class PotDataRecord(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
reserved: int
|
||||
|
||||
|
||||
class PotRoomDataRecord(NamedTuple):
|
||||
room_id: int
|
||||
pots: tuple[PotDataRecord, ...]
|
||||
items: tuple[int, ...]
|
||||
|
||||
POT_ROOMS = (
|
||||
PotRoomDataRecord(room_id=4, pots=(PotDataRecord(x=162, y=25, reserved=0), PotDataRecord(x=152, y=25, reserved=0), PotDataRecord(x=152, y=22, reserved=0), PotDataRecord(x=162, y=22, reserved=0), PotDataRecord(x=240, y=19, reserved=0), PotDataRecord(x=204, y=19, reserved=0),), items=(10, 10)),
|
||||
PotRoomDataRecord(room_id=9, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(1, 11, 136)),
|
||||
PotRoomDataRecord(room_id=10, pots=(PotDataRecord(x=204, y=11, reserved=0), PotDataRecord(x=156, y=17, reserved=0), PotDataRecord(x=96, y=8, reserved=0), PotDataRecord(x=100, y=7, reserved=0), PotDataRecord(x=160, y=17, reserved=0), PotDataRecord(x=104, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0),), items=(11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=17, pots=(PotDataRecord(x=152, y=19, reserved=0), PotDataRecord(x=152, y=15, reserved=0), PotDataRecord(x=144, y=15, reserved=0), PotDataRecord(x=10, y=15, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=160, y=19, reserved=0),), items=(11, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=21, pots=(PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=100, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0), PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=12, y=6, reserved=0), PotDataRecord(x=16, y=6, reserved=0), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=70, y=11, reserved=0),), items=(1, 7, 9, 9, 10, 11, 12, 12, 13)),
|
||||
PotRoomDataRecord(room_id=22, pots=(PotDataRecord(x=188, y=3, reserved=0), PotDataRecord(x=192, y=3, reserved=0), PotDataRecord(x=188, y=4, reserved=0), PotDataRecord(x=192, y=4, reserved=0), PotDataRecord(x=188, y=5, reserved=0), PotDataRecord(x=192, y=5, reserved=0), PotDataRecord(x=188, y=6, reserved=0), PotDataRecord(x=192, y=6, reserved=0), PotDataRecord(x=240, y=19, reserved=0),), items=(8, 9, 9, 10, 10, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=26, pots=(PotDataRecord(x=232, y=19, reserved=0), PotDataRecord(x=212, y=19, reserved=0), PotDataRecord(x=28, y=5, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(10, 10, 10, 10)),
|
||||
PotRoomDataRecord(room_id=33, pots=(PotDataRecord(x=100, y=28, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=48, y=28, reserved=0), PotDataRecord(x=82, y=28, reserved=0), PotDataRecord(x=160, y=20, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=35, pots=(PotDataRecord(x=86, y=26, reserved=0), PotDataRecord(x=90, y=26, reserved=0), PotDataRecord(x=94, y=26, reserved=0), PotDataRecord(x=98, y=26, reserved=0), PotDataRecord(x=102, y=26, reserved=0),), items=(1, 10, 11)),
|
||||
PotRoomDataRecord(room_id=36, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(1, 11, 12, 7)),
|
||||
PotRoomDataRecord(room_id=38, pots=(PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=150, y=19, reserved=2), PotDataRecord(x=22, y=26, reserved=2), PotDataRecord(x=220, y=26, reserved=0),), items=(7, 9, 10, 12, 136)),
|
||||
PotRoomDataRecord(room_id=39, pots=(PotDataRecord(x=214, y=19, reserved=0), PotDataRecord(x=214, y=20, reserved=0), PotDataRecord(x=166, y=20, reserved=0), PotDataRecord(x=214, y=21, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=102, y=17, reserved=0), PotDataRecord(x=98, y=17, reserved=0), PotDataRecord(x=106, y=17, reserved=0), PotDataRecord(x=166, y=21, reserved=0), PotDataRecord(x=166, y=19, reserved=0), PotDataRecord(x=92, y=12, reserved=0), PotDataRecord(x=160, y=12, reserved=0),), items=(1, 1, 10, 11, 7, 7)),
|
||||
PotRoomDataRecord(room_id=43, pots=(PotDataRecord(x=16, y=5, reserved=2), PotDataRecord(x=44, y=5, reserved=2), PotDataRecord(x=16, y=6, reserved=2), PotDataRecord(x=44, y=6, reserved=2), PotDataRecord(x=16, y=7, reserved=2), PotDataRecord(x=44, y=7, reserved=2), PotDataRecord(x=146, y=21, reserved=0), PotDataRecord(x=170, y=21, reserved=0), PotDataRecord(x=146, y=22, reserved=0), PotDataRecord(x=170, y=22, reserved=0),), items=(9, 9, 10, 10, 10, 10, 11, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=47, pots=(PotDataRecord(x=28, y=7, reserved=0), PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=28, y=9, reserved=0), PotDataRecord(x=32, y=9, reserved=0), PotDataRecord(x=172, y=19, reserved=0), PotDataRecord(x=180, y=19, reserved=0), PotDataRecord(x=104, y=27, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(7, 7, 7, 7, 11, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=53, pots=(PotDataRecord(x=60, y=6, reserved=1), PotDataRecord(x=20, y=8, reserved=0), PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=28, y=8, reserved=0), PotDataRecord(x=32, y=8, reserved=0), PotDataRecord(x=36, y=8, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=76, y=23, reserved=1), PotDataRecord(x=88, y=23, reserved=1), PotDataRecord(x=100, y=27, reserved=1), PotDataRecord(x=242, y=28, reserved=1), PotDataRecord(x=240, y=22, reserved=1), PotDataRecord(x=76, y=28, reserved=1),), items=(7, 7, 7, 7, 7, 8, 11)),
|
||||
PotRoomDataRecord(room_id=54, pots=(PotDataRecord(x=108, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=10, y=16, reserved=0), PotDataRecord(x=114, y=16, reserved=0),), items=(8, 10, 11)),
|
||||
PotRoomDataRecord(room_id=55, pots=(PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=60, y=6, reserved=0),), items=(8,)),
|
||||
PotRoomDataRecord(room_id=56, pots=(PotDataRecord(x=164, y=12, reserved=0), PotDataRecord(x=164, y=13, reserved=0), PotDataRecord(x=164, y=18, reserved=0), PotDataRecord(x=164, y=19, reserved=0),), items=(8, 7, 10, 10)),
|
||||
PotRoomDataRecord(room_id=57, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=100, y=22, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 9, 11, 12)),
|
||||
PotRoomDataRecord(room_id=60, pots=(PotDataRecord(x=24, y=8, reserved=0), PotDataRecord(x=64, y=12, reserved=0), PotDataRecord(x=20, y=14, reserved=0), PotDataRecord(x=68, y=18, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=64, y=20, reserved=0), PotDataRecord(x=64, y=26, reserved=0),), items=(1, 7, 7, 7, 7, 11, 12)),
|
||||
PotRoomDataRecord(room_id=61, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=24, y=22, reserved=0), PotDataRecord(x=40, y=22, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0),), items=(9, 7, 10, 10, 11, 11, 13)),
|
||||
PotRoomDataRecord(room_id=62, pots=(PotDataRecord(x=96, y=6, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=92, y=10, reserved=0),), items=(10, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=63, pots=(PotDataRecord(x=12, y=25, reserved=0), PotDataRecord(x=20, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=12, y=27, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=28, y=23, reserved=0),), items=(1, 1, 8, 10, 10, 11, 136)),
|
||||
PotRoomDataRecord(room_id=65, pots=(PotDataRecord(x=100, y=10, reserved=0), PotDataRecord(x=52, y=15, reserved=0), PotDataRecord(x=52, y=16, reserved=0), PotDataRecord(x=148, y=22, reserved=0),), items=(1, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=67, pots=(PotDataRecord(x=112, y=28, reserved=1), PotDataRecord(x=76, y=28, reserved=1), PotDataRecord(x=76, y=20, reserved=1), PotDataRecord(x=66, y=4, reserved=0), PotDataRecord(x=78, y=4, reserved=0), PotDataRecord(x=66, y=9, reserved=0), PotDataRecord(x=78, y=9, reserved=0), PotDataRecord(x=112, y=20, reserved=1),), items=(8, 9, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=69, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=108, y=11, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=220, y=16, reserved=0), PotDataRecord(x=236, y=16, reserved=0),), items=(9, 9, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=73, pots=(PotDataRecord(x=156, y=27, reserved=0), PotDataRecord(x=172, y=24, reserved=0), PotDataRecord(x=172, y=23, reserved=0), PotDataRecord(x=144, y=20, reserved=0), PotDataRecord(x=104, y=15, reserved=0), PotDataRecord(x=104, y=16, reserved=0), PotDataRecord(x=144, y=19, reserved=0), PotDataRecord(x=172, y=20, reserved=0), PotDataRecord(x=144, y=27, reserved=0), PotDataRecord(x=172, y=28, reserved=0), PotDataRecord(x=160, y=27, reserved=0),), items=(11, 11, 12, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=78, pots=(PotDataRecord(x=48, y=10, reserved=2), PotDataRecord(x=140, y=11, reserved=2), PotDataRecord(x=28, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=0),), items=(136, 11, 12)),
|
||||
PotRoomDataRecord(room_id=83, pots=(PotDataRecord(x=92, y=11, reserved=0), PotDataRecord(x=96, y=11, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=11, reserved=0),), items=(8, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=84, pots=(PotDataRecord(x=186, y=25, reserved=0), PotDataRecord(x=186, y=26, reserved=0), PotDataRecord(x=186, y=27, reserved=0), PotDataRecord(x=186, y=28, reserved=0),), items=(7, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=86, pots=(PotDataRecord(x=100, y=6, reserved=1), PotDataRecord(x=96, y=10, reserved=1), PotDataRecord(x=92, y=10, reserved=1), PotDataRecord(x=48, y=20, reserved=1), PotDataRecord(x=20, y=6, reserved=0), PotDataRecord(x=40, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=36, y=7, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=24, y=9, reserved=0), PotDataRecord(x=36, y=9, reserved=0), PotDataRecord(x=20, y=10, reserved=0), PotDataRecord(x=40, y=10, reserved=0), PotDataRecord(x=12, y=20, reserved=1),), items=(7, 7, 11, 11, 8, 12, 12, 12, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=87, pots=(PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=12, y=20, reserved=2), PotDataRecord(x=92, y=23, reserved=0), PotDataRecord(x=100, y=23, reserved=0), PotDataRecord(x=84, y=25, reserved=0), PotDataRecord(x=76, y=27, reserved=0), PotDataRecord(x=48, y=20, reserved=2), PotDataRecord(x=30, y=22, reserved=2),), items=(7, 10, 11, 12, 12, 12, 13, 136)),
|
||||
PotRoomDataRecord(room_id=88, pots=(PotDataRecord(x=96, y=9, reserved=0), PotDataRecord(x=92, y=8, reserved=0), PotDataRecord(x=108, y=8, reserved=0), PotDataRecord(x=108, y=6, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=92, y=6, reserved=0), PotDataRecord(x=12, y=12, reserved=0), PotDataRecord(x=16, y=7, reserved=0), PotDataRecord(x=96, y=5, reserved=0), PotDataRecord(x=100, y=5, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=92, y=7, reserved=0), PotDataRecord(x=108, y=7, reserved=0), PotDataRecord(x=16, y=8, reserved=0), PotDataRecord(x=100, y=9, reserved=0), PotDataRecord(x=104, y=9, reserved=0),), items=(10, 10, 11, 11, 12, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=91, pots=(PotDataRecord(x=218, y=37, reserved=0), PotDataRecord(x=222, y=37, reserved=0), PotDataRecord(x=226, y=37, reserved=0),), items=(136,)),
|
||||
PotRoomDataRecord(room_id=92, pots=(PotDataRecord(x=228, y=25, reserved=0), PotDataRecord(x=104, y=24, reserved=0), PotDataRecord(x=228, y=22, reserved=0), PotDataRecord(x=216, y=25, reserved=0), PotDataRecord(x=84, y=24, reserved=0), PotDataRecord(x=216, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=94, y=26, reserved=0),), items=(10, 13)),
|
||||
PotRoomDataRecord(room_id=93, pots=(PotDataRecord(x=16, y=5, reserved=0), PotDataRecord(x=44, y=5, reserved=0), PotDataRecord(x=16, y=11, reserved=0), PotDataRecord(x=44, y=11, reserved=0), PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(1, 7, 9, 9, 10, 10, 10, 12)),
|
||||
PotRoomDataRecord(room_id=94, pots=(PotDataRecord(x=92, y=4, reserved=0), PotDataRecord(x=96, y=4, reserved=0), PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=112, y=8, reserved=0),), items=(11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=99, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=12, y=8, reserved=0), PotDataRecord(x=48, y=12, reserved=0), PotDataRecord(x=48, y=8, reserved=0), PotDataRecord(x=12, y=12, reserved=0),), items=(8, 11)),
|
||||
PotRoomDataRecord(room_id=100, pots=(PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=16, y=22, reserved=0), PotDataRecord(x=20, y=22, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(10, 10, 10, 10, 12, 12, 136)),
|
||||
PotRoomDataRecord(room_id=102, pots=(PotDataRecord(x=48, y=37, reserved=0), PotDataRecord(x=52, y=37, reserved=0), PotDataRecord(x=56, y=37, reserved=0), PotDataRecord(x=84, y=5, reserved=0), PotDataRecord(x=104, y=5, reserved=0), PotDataRecord(x=48, y=38, reserved=0), PotDataRecord(x=52, y=38, reserved=0), PotDataRecord(x=56, y=38, reserved=0), PotDataRecord(x=84, y=6, reserved=0), PotDataRecord(x=104, y=6, reserved=0),), items=(7, 7, 9, 9, 9, 10, 10, 10, 11, 11)),
|
||||
PotRoomDataRecord(room_id=103, pots=(PotDataRecord(x=22, y=26, reserved=0), PotDataRecord(x=18, y=22, reserved=0), PotDataRecord(x=92, y=9, reserved=0), PotDataRecord(x=84, y=28, reserved=0), PotDataRecord(x=12, y=7, reserved=0), PotDataRecord(x=48, y=7, reserved=0), PotDataRecord(x=96, y=19, reserved=0), PotDataRecord(x=74, y=20, reserved=0), PotDataRecord(x=18, y=23, reserved=0), PotDataRecord(x=18, y=26, reserved=0), PotDataRecord(x=104, y=28, reserved=0),), items=(9, 11, 11, 11, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=104, pots=(PotDataRecord(x=84, y=14, reserved=0), PotDataRecord(x=84, y=13, reserved=0), PotDataRecord(x=88, y=12, reserved=0), PotDataRecord(x=88, y=6, reserved=0), PotDataRecord(x=88, y=5, reserved=0), PotDataRecord(x=88, y=4, reserved=0), PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=64, y=15, reserved=0), PotDataRecord(x=64, y=7, reserved=0), PotDataRecord(x=88, y=7, reserved=0), PotDataRecord(x=64, y=16, reserved=0), PotDataRecord(x=64, y=24, reserved=0), PotDataRecord(x=64, y=25, reserved=0),), items=(11, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=115, pots=(PotDataRecord(x=154, y=21, reserved=0), PotDataRecord(x=158, y=21, reserved=0), PotDataRecord(x=20, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=144, y=24, reserved=0), PotDataRecord(x=168, y=24, reserved=0), PotDataRecord(x=20, y=26, reserved=0), PotDataRecord(x=36, y=26, reserved=0), PotDataRecord(x=154, y=27, reserved=0), PotDataRecord(x=158, y=27, reserved=0),), items=(1, 1, 11, 11, 7, 7, 9, 9, 12, 136)),
|
||||
PotRoomDataRecord(room_id=116, pots=(PotDataRecord(x=30, y=5, reserved=0), PotDataRecord(x=62, y=5, reserved=0), PotDataRecord(x=94, y=5, reserved=0), PotDataRecord(x=14, y=11, reserved=0), PotDataRecord(x=46, y=11, reserved=0), PotDataRecord(x=78, y=11, reserved=0), PotDataRecord(x=110, y=11, reserved=0),), items=(9, 9, 11, 11, 12, 12, 136)),
|
||||
PotRoomDataRecord(room_id=117, pots=(PotDataRecord(x=148, y=22, reserved=0), PotDataRecord(x=160, y=22, reserved=0), PotDataRecord(x=172, y=22, reserved=0),), items=(9, 11, 12)),
|
||||
PotRoomDataRecord(room_id=123, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=88, y=10, reserved=0), PotDataRecord(x=76, y=7, reserved=0), PotDataRecord(x=60, y=4, reserved=0), PotDataRecord(x=64, y=4, reserved=0),), items=(11, 8)),
|
||||
PotRoomDataRecord(room_id=124, pots=(PotDataRecord(x=36, y=21, reserved=0), PotDataRecord(x=24, y=11, reserved=0), PotDataRecord(x=28, y=4, reserved=0), PotDataRecord(x=32, y=4, reserved=0),), items=(11, 11)),
|
||||
PotRoomDataRecord(room_id=125, pots=(PotDataRecord(x=44, y=12, reserved=0), PotDataRecord(x=44, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=108, y=20, reserved=0), PotDataRecord(x=114, y=20, reserved=0), PotDataRecord(x=76, y=28, reserved=0),), items=(9, 10, 10, 11)),
|
||||
PotRoomDataRecord(room_id=126, pots=(PotDataRecord(x=86, y=15, reserved=0), PotDataRecord(x=82, y=26, reserved=0), PotDataRecord(x=100, y=26, reserved=0), PotDataRecord(x=104, y=26, reserved=0),), items=(11, 12, 136)),
|
||||
PotRoomDataRecord(room_id=130, pots=(PotDataRecord(x=50, y=5, reserved=0), PotDataRecord(x=50, y=10, reserved=0), PotDataRecord(x=76, y=50, reserved=0),), items=(11,)),
|
||||
PotRoomDataRecord(room_id=131, pots=(PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=80, y=4, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=80, y=28, reserved=0),), items=(1, 7, 9, 9)),
|
||||
PotRoomDataRecord(room_id=132, pots=(PotDataRecord(x=64, y=17, reserved=0), PotDataRecord(x=60, y=17, reserved=0), PotDataRecord(x=80, y=14, reserved=0), PotDataRecord(x=44, y=14, reserved=0), PotDataRecord(x=100, y=6, reserved=0), PotDataRecord(x=24, y=6, reserved=0), PotDataRecord(x=24, y=7, reserved=0), PotDataRecord(x=100, y=7, reserved=0),), items=(9, 9)),
|
||||
PotRoomDataRecord(room_id=135, pots=(PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=112, y=20, reserved=0), PotDataRecord(x=16, y=12, reserved=0), PotDataRecord(x=40, y=12, reserved=0), PotDataRecord(x=32, y=12, reserved=0), PotDataRecord(x=24, y=12, reserved=0), PotDataRecord(x=16, y=11, reserved=0),), items=(12, 13)),
|
||||
PotRoomDataRecord(room_id=139, pots=(PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=76, y=12, reserved=1), PotDataRecord(x=32, y=23, reserved=1), PotDataRecord(x=28, y=23, reserved=1), PotDataRecord(x=112, y=12, reserved=1), PotDataRecord(x=32, y=9, reserved=1), PotDataRecord(x=76, y=28, reserved=0),), items=(8, 11, 12)),
|
||||
PotRoomDataRecord(room_id=140, pots=(PotDataRecord(x=76, y=12, reserved=2), PotDataRecord(x=112, y=12, reserved=2), PotDataRecord(x=76, y=20, reserved=0), PotDataRecord(x=92, y=20, reserved=0), PotDataRecord(x=100, y=21, reserved=0), PotDataRecord(x=104, y=26, reserved=0), PotDataRecord(x=88, y=27, reserved=0),), items=(9, 10, 10, 10, 10, 12, 136)),
|
||||
PotRoomDataRecord(room_id=145, pots=(PotDataRecord(x=84, y=4, reserved=0), PotDataRecord(x=104, y=4, reserved=0),), items=(11, 12)),
|
||||
PotRoomDataRecord(room_id=150, pots=(PotDataRecord(x=14, y=18, reserved=0), PotDataRecord(x=32, y=5, reserved=0), PotDataRecord(x=32, y=17, reserved=0), PotDataRecord(x=32, y=24, reserved=0), PotDataRecord(x=76, y=21, reserved=0), PotDataRecord(x=112, y=21, reserved=0), PotDataRecord(x=14, y=24, reserved=0),), items=(11, 12, 12, 13)),
|
||||
PotRoomDataRecord(room_id=155, pots=(PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=48, y=12, reserved=0),), items=(8, 12)),
|
||||
PotRoomDataRecord(room_id=157, pots=(PotDataRecord(x=32, y=7, reserved=0), PotDataRecord(x=40, y=9, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=84, y=4, reserved=0),), items=(10, 12)),
|
||||
PotRoomDataRecord(room_id=159, pots=(PotDataRecord(x=138, y=20, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=138, y=21, reserved=0), PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=138, y=27, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=21, reserved=0), PotDataRecord(x=178, y=20, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=178, y=27, reserved=0), PotDataRecord(x=178, y=26, reserved=0), PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=138, y=26, reserved=0), PotDataRecord(x=20, y=21, reserved=0),), items=(8, 11, 11, 11, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=161, pots=(PotDataRecord(x=96, y=27, reserved=0), PotDataRecord(x=92, y=21, reserved=0), PotDataRecord(x=150, y=6, reserved=0), PotDataRecord(x=100, y=11, reserved=0), PotDataRecord(x=104, y=12, reserved=0), PotDataRecord(x=108, y=13, reserved=0), PotDataRecord(x=112, y=14, reserved=0), PotDataRecord(x=96, y=23, reserved=0), PotDataRecord(x=76, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(8, 11, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=168, pots=(PotDataRecord(x=138, y=28, reserved=0), PotDataRecord(x=178, y=28, reserved=0), PotDataRecord(x=178, y=19, reserved=0), PotDataRecord(x=138, y=19, reserved=0), PotDataRecord(x=30, y=24, reserved=0),), items=(1, 11)),
|
||||
PotRoomDataRecord(room_id=169, pots=(PotDataRecord(x=12, y=19, reserved=0), PotDataRecord(x=112, y=19, reserved=0), PotDataRecord(x=144, y=43, reserved=0), PotDataRecord(x=236, y=43, reserved=0), PotDataRecord(x=144, y=44, reserved=0), PotDataRecord(x=236, y=44, reserved=0), PotDataRecord(x=16, y=20, reserved=0), PotDataRecord(x=108, y=20, reserved=0),), items=(11, 11, 11, 9, 9, 9)),
|
||||
PotRoomDataRecord(room_id=170, pots=(PotDataRecord(x=212, y=10, reserved=2), PotDataRecord(x=232, y=10, reserved=2), PotDataRecord(x=232, y=5, reserved=2), PotDataRecord(x=212, y=5, reserved=2), PotDataRecord(x=94, y=8, reserved=2), PotDataRecord(x=108, y=55, reserved=0), PotDataRecord(x=108, y=56, reserved=0), PotDataRecord(x=108, y=57, reserved=0),), items=(11, 11, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=176, pots=(PotDataRecord(x=20, y=27, reserved=0), PotDataRecord(x=24, y=24, reserved=0), PotDataRecord(x=44, y=25, reserved=0), PotDataRecord(x=20, y=21, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=40, y=21, reserved=0), PotDataRecord(x=16, y=23, reserved=0), PotDataRecord(x=44, y=23, reserved=0), PotDataRecord(x=36, y=24, reserved=0), PotDataRecord(x=16, y=25, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=40, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0),), items=(1, 1, 7, 7, 9, 9, 10, 10, 11, 11)),
|
||||
PotRoomDataRecord(room_id=179, pots=(PotDataRecord(x=12, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(8, 12, 136)),
|
||||
PotRoomDataRecord(room_id=180, pots=(PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(11, 13)),
|
||||
PotRoomDataRecord(room_id=181, pots=(PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=112, y=15, reserved=0), PotDataRecord(x=76, y=16, reserved=0), PotDataRecord(x=112, y=16, reserved=0), PotDataRecord(x=112, y=17, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 10, 11, 11, 13, 136)),
|
||||
PotRoomDataRecord(room_id=184, pots=(PotDataRecord(x=96, y=13, reserved=0), PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=104, y=16, reserved=0),), items=(11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=185, pots=(PotDataRecord(x=92, y=18, reserved=0), PotDataRecord(x=96, y=18, reserved=0), PotDataRecord(x=104, y=18, reserved=0), PotDataRecord(x=108, y=18, reserved=0),), items=(1, 1, 7, 7)),
|
||||
PotRoomDataRecord(room_id=186, pots=(PotDataRecord(x=100, y=8, reserved=0), PotDataRecord(x=88, y=8, reserved=0), PotDataRecord(x=94, y=4, reserved=0), PotDataRecord(x=76, y=6, reserved=0), PotDataRecord(x=112, y=6, reserved=0), PotDataRecord(x=76, y=10, reserved=0), PotDataRecord(x=112, y=10, reserved=0), PotDataRecord(x=94, y=12, reserved=0),), items=(1, 1, 8, 11, 11, 12)),
|
||||
PotRoomDataRecord(room_id=188, pots=(PotDataRecord(x=138, y=3, reserved=2), PotDataRecord(x=178, y=3, reserved=2), PotDataRecord(x=86, y=4, reserved=1), PotDataRecord(x=102, y=4, reserved=1), PotDataRecord(x=138, y=12, reserved=2), PotDataRecord(x=178, y=12, reserved=2), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=28, y=21, reserved=0), PotDataRecord(x=32, y=21, reserved=0), PotDataRecord(x=28, y=27, reserved=0), PotDataRecord(x=32, y=27, reserved=0), PotDataRecord(x=12, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(7, 7, 7, 7, 8, 10, 10, 10, 10, 10, 11, 11, 136)),
|
||||
PotRoomDataRecord(room_id=191, pots=(PotDataRecord(x=40, y=20, reserved=0), PotDataRecord(x=44, y=20, reserved=0), PotDataRecord(x=48, y=20, reserved=0), PotDataRecord(x=40, y=28, reserved=0), PotDataRecord(x=44, y=28, reserved=0), PotDataRecord(x=48, y=28, reserved=0),), items=(9, 10, 11, 12, 12, 12)),
|
||||
PotRoomDataRecord(room_id=192, pots=(PotDataRecord(x=48, y=10, reserved=0), PotDataRecord(x=12, y=14, reserved=0), PotDataRecord(x=12, y=26, reserved=0), PotDataRecord(x=28, y=27, reserved=0),), items=(1, 7, 10, 11)),
|
||||
PotRoomDataRecord(room_id=194, pots=(PotDataRecord(x=180, y=7, reserved=0), PotDataRecord(x=100, y=46, reserved=0), PotDataRecord(x=68, y=48, reserved=0), PotDataRecord(x=64, y=52, reserved=0),), items=(1, 9, 12, 136)),
|
||||
PotRoomDataRecord(room_id=196, pots=(PotDataRecord(x=84, y=9, reserved=0), PotDataRecord(x=24, y=14, reserved=0), PotDataRecord(x=56, y=17, reserved=0), PotDataRecord(x=84, y=17, reserved=0), PotDataRecord(x=12, y=21, reserved=0), PotDataRecord(x=76, y=23, reserved=0), PotDataRecord(x=48, y=25, reserved=0), PotDataRecord(x=12, y=26, reserved=0),), items=(1, 9, 12, 7, 10, 10, 11, 11)),
|
||||
PotRoomDataRecord(room_id=199, pots=(PotDataRecord(x=12, y=10, reserved=0), PotDataRecord(x=12, y=11, reserved=0), PotDataRecord(x=12, y=22, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(9, 12, 11, 13)),
|
||||
PotRoomDataRecord(room_id=201, pots=(PotDataRecord(x=30, y=22, reserved=0), PotDataRecord(x=94, y=22, reserved=0), PotDataRecord(x=60, y=22, reserved=0),), items=(1, 1, 136)),
|
||||
PotRoomDataRecord(room_id=203, pots=(PotDataRecord(x=88, y=16, reserved=0), PotDataRecord(x=88, y=28, reserved=0),), items=(7, 11)),
|
||||
PotRoomDataRecord(room_id=204, pots=(PotDataRecord(x=36, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=36, y=28, reserved=0), PotDataRecord(x=112, y=28, reserved=0),), items=(7, 11, 7, 10)),
|
||||
PotRoomDataRecord(room_id=206, pots=(PotDataRecord(x=76, y=8, reserved=0), PotDataRecord(x=80, y=8, reserved=0), PotDataRecord(x=108, y=12, reserved=0), PotDataRecord(x=112, y=12, reserved=0), PotDataRecord(x=204, y=11, reserved=3),), items=(9, 12, 12, 10, 128)),
|
||||
PotRoomDataRecord(room_id=208, pots=(PotDataRecord(x=158, y=5, reserved=0), PotDataRecord(x=140, y=11, reserved=0), PotDataRecord(x=42, y=13, reserved=0), PotDataRecord(x=48, y=16, reserved=0), PotDataRecord(x=176, y=20, reserved=0), PotDataRecord(x=146, y=23, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(1, 1, 7, 11, 11, 12, 12)),
|
||||
PotRoomDataRecord(room_id=209, pots=(PotDataRecord(x=76, y=12, reserved=0), PotDataRecord(x=48, y=4, reserved=0), PotDataRecord(x=76, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=168, y=7, reserved=0), PotDataRecord(x=112, y=12, reserved=0),), items=(9, 1, 1, 1, 13)),
|
||||
PotRoomDataRecord(room_id=214, pots=(PotDataRecord(x=92, y=22, reserved=0), PotDataRecord(x=96, y=22, reserved=0),), items=(10, 13)),
|
||||
PotRoomDataRecord(room_id=216, pots=(PotDataRecord(x=202, y=8, reserved=0), PotDataRecord(x=242, y=8, reserved=0), PotDataRecord(x=202, y=10, reserved=0), PotDataRecord(x=242, y=10, reserved=0), PotDataRecord(x=202, y=12, reserved=0), PotDataRecord(x=242, y=12, reserved=0), PotDataRecord(x=92, y=24, reserved=0), PotDataRecord(x=96, y=24, reserved=0),), items=(9, 9, 9, 9, 9, 11, 11, 11)),
|
||||
PotRoomDataRecord(room_id=218, pots=(PotDataRecord(x=24, y=23, reserved=0), PotDataRecord(x=36, y=23, reserved=0), PotDataRecord(x=24, y=25, reserved=0), PotDataRecord(x=36, y=25, reserved=0),), items=(9, 9, 11, 136)),
|
||||
PotRoomDataRecord(room_id=219, pots=(PotDataRecord(x=12, y=4, reserved=0), PotDataRecord(x=62, y=19, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=88, y=16, reserved=0),), items=(7, 11)),
|
||||
PotRoomDataRecord(room_id=220, pots=(PotDataRecord(x=56, y=4, reserved=0), PotDataRecord(x=112, y=4, reserved=0), PotDataRecord(x=68, y=16, reserved=0), PotDataRecord(x=12, y=28, reserved=0),), items=(7, 9, 10, 11)),
|
||||
PotRoomDataRecord(room_id=235, pots=(PotDataRecord(x=206, y=8, reserved=0), PotDataRecord(x=210, y=8, reserved=0), PotDataRecord(x=88, y=14, reserved=0), PotDataRecord(x=92, y=14, reserved=0), PotDataRecord(x=96, y=14, reserved=0),), items=(7, 7, 11, 12, 12)),
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
ENEMIZER_SYMBOLS = {
|
||||
':pos_1_0': 0x36975E,
|
||||
':pos_1_1': 0x36976D,
|
||||
':pos_1_10': 0x369819,
|
||||
':pos_1_11': 0x369828,
|
||||
':pos_1_12': 0x369837,
|
||||
':pos_1_13': 0x369845,
|
||||
':pos_1_14': 0x36984B,
|
||||
':pos_1_15': 0x369851,
|
||||
':pos_1_16': 0x369873,
|
||||
':pos_1_17': 0x369898,
|
||||
':pos_1_18': 0x36989E,
|
||||
':pos_1_19': 0x3698A4,
|
||||
':pos_1_2': 0x369787,
|
||||
':pos_1_20': 0x3698C6,
|
||||
':pos_1_21': 0x3698EB,
|
||||
':pos_1_22': 0x3698F1,
|
||||
':pos_1_23': 0x3698F7,
|
||||
':pos_1_24': 0x369919,
|
||||
':pos_1_25': 0x36993E,
|
||||
':pos_1_26': 0x369944,
|
||||
':pos_1_27': 0x36994A,
|
||||
':pos_1_28': 0x36996C,
|
||||
':pos_1_29': 0x369AA5,
|
||||
':pos_1_3': 0x369796,
|
||||
':pos_1_30': 0x36B796,
|
||||
':pos_1_4': 0x3697A5,
|
||||
':pos_1_5': 0x3697B4,
|
||||
':pos_1_6': 0x3697D5,
|
||||
':pos_1_7': 0x3697E8,
|
||||
':pos_1_8': 0x3697F7,
|
||||
':pos_1_9': 0x369806,
|
||||
'CheckIfLinkShouldDie': 0x3699B2,
|
||||
'CheckIfLinkShouldDie_dead': 0x3699BB,
|
||||
'CheckIfLinkShouldDie_done': 0x3699BD,
|
||||
'Check_for_Blind_Fight': 0x1DA085,
|
||||
'CopyShield': 0x36B713,
|
||||
'CopyShield_loop_copy': 0x36B729,
|
||||
'CopyShield_shield_positon_gfx': 0x36B74B,
|
||||
'CopySword': 0x36B6D1,
|
||||
'CopySword_loop_copy': 0x36B6E7,
|
||||
'CopySword_sword_positon_gfx': 0x36B709,
|
||||
'DMAKholdstare': 0x3695A5,
|
||||
'DMATrinexx': 0x369617,
|
||||
'Dungeon_ResetSprites': 0x09C114,
|
||||
'EnemizerCodeStart': 0x3694F5,
|
||||
'EnemizerFlags': 0x368100,
|
||||
'EnemizerFlags_agahnim_fun_balls': 0x368104,
|
||||
'EnemizerFlags_close_blind_door': 0x368101,
|
||||
'EnemizerFlags_enable_mimic_override': 0x368105,
|
||||
'EnemizerFlags_enable_terrorpin_ai_fix': 0x368106,
|
||||
'EnemizerFlags_moldorm_eye_count': 0x368102,
|
||||
'EnemizerFlags_randomize_bushes': 0x368100,
|
||||
'EnemizerFlags_randomize_sprites': 0x368103,
|
||||
'EnemizerTablesStart': 0x368000,
|
||||
'Ext_OnBossDeath': 0x29803C,
|
||||
'Ext_OnDungeonCompleted': 0x29804B,
|
||||
'Ext_OnDungeonEnter': 0x298041,
|
||||
'Ext_OnDungeonExit': 0x298046,
|
||||
'Ext_OnFairyRevive': 0x298019,
|
||||
'Ext_OnFileCreate': 0x298000,
|
||||
'Ext_OnFileLoad': 0x298005,
|
||||
'Ext_OnFileSave': 0x29800A,
|
||||
'Ext_OnIemMenuOpen': 0x298023,
|
||||
'Ext_OnItemChange': 0x29802D,
|
||||
'Ext_OnItemMenuClose': 0x298028,
|
||||
'Ext_OnMapUse': 0x298014,
|
||||
'Ext_OnPlayerAttack': 0x298037,
|
||||
'Ext_OnPlayerDamaged': 0x298032,
|
||||
'Ext_OnPlayerDeath': 0x29800F,
|
||||
'Ext_OnYItemUse': 0x29801E,
|
||||
'Ext_OnZeldaRescued': 0x298050,
|
||||
'FixTerrorpin': 0x36971A,
|
||||
'FixTerrorpin_new': 0x369728,
|
||||
'GFX_Kholdstare_Shell': 0x36C79A,
|
||||
'GFX_Trinexx_Shell': 0x36D79A,
|
||||
'GFX_Trinexx_Shell2': 0x36DF9A,
|
||||
'GetRandomInt': 0x0DBA71,
|
||||
'Initialize_Blind_Fight': 0x1DA090,
|
||||
'Kholdstare_Draw': 0x0DD97F,
|
||||
'LoadFile': 0x369A87,
|
||||
'LoadNewSoundFx': 0x369A9E,
|
||||
'LoadOverworldSprites': 0x36B753,
|
||||
'Module_LoadFile_indoors': 0x028118,
|
||||
'Moldorm_UpdateOamPosition': 0x3699E7,
|
||||
'Moldorm_UpdateOamPosition_more_eyes': 0x3699ED,
|
||||
'NMIHookAction': 0x36956C,
|
||||
'NMIHookAction_loadKholdstare': 0x369584,
|
||||
'NMIHookAction_loadTrinexx': 0x369590,
|
||||
'NMIHookAction_return': 0x36959A,
|
||||
'NMIHookReturn': 0x0080D5,
|
||||
'NewLoadSoundBank': 0x369A98,
|
||||
'NewLoadSoundBank_Intro': 0x369A92,
|
||||
'OnInitFileSelect': 0x3696FA,
|
||||
'OnInitFileSelect_continue': 0x36970F,
|
||||
'Player_Main': 0x078000,
|
||||
'Sound_LoadSongBank': 0x008888,
|
||||
'Sound_SetSfx3PanLong': 0x0DBB8A,
|
||||
'Sound_SetSfxPanWithPlayerCoords': 0x0DBB67,
|
||||
'Spawn_Bees': 0x36B75D,
|
||||
'Spawn_Bees_done': 0x36B779,
|
||||
'SpritePrep_Eyegore': 0x1EC6FA,
|
||||
'SpritePrep_EyegoreNew': 0x369A1A,
|
||||
'SpritePrep_EyegoreNew_mimic': 0x369A31,
|
||||
'SpritePrep_EyegoreNew_new': 0x369A25,
|
||||
'Sprite_ResetAll': 0x09C44E,
|
||||
'Sprite_SpawnDynamically': 0x1DF65D,
|
||||
'VitreousKeyReset': 0x36B77A,
|
||||
'boss_move': 0x369743,
|
||||
'boss_move_loop_bottom_left': 0x369935,
|
||||
'boss_move_loop_bottom_left2': 0x369963,
|
||||
'boss_move_loop_bottom_right': 0x3698E2,
|
||||
'boss_move_loop_bottom_right2': 0x369910,
|
||||
'boss_move_loop_middle': 0x36983C,
|
||||
'boss_move_loop_middle2': 0x36986A,
|
||||
'boss_move_loop_top_right': 0x36988F,
|
||||
'boss_move_loop_top_right2': 0x3698BD,
|
||||
'boss_move_move_to_bottom_left': 0x369933,
|
||||
'boss_move_move_to_bottom_right': 0x3698E0,
|
||||
'boss_move_move_to_middle': 0x36983A,
|
||||
'boss_move_move_to_top_right': 0x36988D,
|
||||
'boss_move_no_blind_door': 0x3697D2,
|
||||
'boss_move_no_change': 0x369863,
|
||||
'boss_move_no_change2': 0x3698B6,
|
||||
'boss_move_no_change3': 0x369909,
|
||||
'boss_move_no_change4': 0x36995C,
|
||||
'boss_move_no_change_ov': 0x369885,
|
||||
'boss_move_no_change_ov2': 0x3698D8,
|
||||
'boss_move_no_change_ov3': 0x36992B,
|
||||
'boss_move_no_change_ov4': 0x36997E,
|
||||
'boss_move_return': 0x369986,
|
||||
'change_heartcontainer_position': 0x3699BE,
|
||||
'change_heartcontainer_position_not_moldorm_room': 0x3699E1,
|
||||
'check_blind_boss_room': 0x36B782,
|
||||
'check_special_action': 0x369731,
|
||||
'check_special_action_no_special_action': 0x36973E,
|
||||
'enemizer_info_table': 0x368000,
|
||||
'linkIsDead': 0x0780D5,
|
||||
'linkNotDead': 0x0780F7,
|
||||
'modified_room_object_table': 0x36B79A,
|
||||
'moved_room_header_bank_value_address': 0x368374,
|
||||
'newKodongoCollision': 0x369A02,
|
||||
'newKodongoCollision_continue': 0x369A19,
|
||||
'new_kholdstare_code': 0x369987,
|
||||
'new_kholdstare_code_already_iced': 0x369997,
|
||||
'new_trinexx_code': 0x36999C,
|
||||
'new_trinexx_code_already_rocked': 0x3699AC,
|
||||
'notItemSprite_Mimic': 0x369A66,
|
||||
'notItemSprite_Mimic_changeSpriteId': 0x369A7A,
|
||||
'notItemSprite_Mimic_continue': 0x369A82,
|
||||
'notItemSprite_Mimic_reloadSpriteIdAndSkipMimic': 0x369A7F,
|
||||
'resetSprite_Mimic': 0x369A4E,
|
||||
'resetSprite_Mimic_notMimic': 0x369A60,
|
||||
'room_header_table': 0x368375,
|
||||
'shieldgfx': 0x36AAD1,
|
||||
'sprite_bush_spawn': 0x3694F5,
|
||||
'sprite_bush_spawn_continue': 0x36950F,
|
||||
'sprite_bush_spawn_dontGoPhase2': 0x369565,
|
||||
'sprite_bush_spawn_item_table': 0x369525,
|
||||
'sprite_bush_spawn_newSpriteSpawn': 0x36954C,
|
||||
'sprite_bush_spawn_not_random': 0x36953B,
|
||||
'sprite_bush_spawn_not_random_old': 0x36950B,
|
||||
'sprite_bush_spawn_return': 0x369568,
|
||||
'sprite_bush_spawn_table': 0x368120,
|
||||
'sprite_bush_spawn_table_dungeons': 0x368248,
|
||||
'sprite_bush_spawn_table_overworld': 0x368120,
|
||||
'sprite_bush_spawn_table_random_sprites': 0x368370,
|
||||
'swordgfx': 0x369AD1,
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
maseya-z3pr>=1.0.0rc1
|
||||
xxtea>=3.0.0
|
||||
maseya-z3pr==1.0.0rc1
|
||||
xxtea==3.7.0
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from worlds.alttp.EnemizerPatches import (
|
||||
ARROW_REFILL_5_SPRITE_ID,
|
||||
BOSS_GFX_SHEET_INDEXES,
|
||||
BOSS_PATCH_DATA,
|
||||
DAMAGE_GROUP_TABLE_ADDRESS,
|
||||
DUNGEON_BOSS_PATCH_DATA,
|
||||
ENEMY_DAMAGE_TABLE_ADDRESS,
|
||||
ENEMY_HP_TABLE_ADDRESS,
|
||||
EXCLUDED_ENEMY_TABLE_SPRITE_IDS,
|
||||
HIDDEN_ENEMY_CHANCE_POOL_ADDRESS,
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS,
|
||||
RETRO_RUPEE_REPLACEMENT_SPRITE_ID,
|
||||
THIEF_DEFAULT_HP,
|
||||
THIEF_SPRITE_ID,
|
||||
TILE_TRAP_FLOOR_TILE_ADDRESS,
|
||||
TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS,
|
||||
TRINEXX_ICE_PROJECTILE_TILE_ADDRESS,
|
||||
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
_apply_killable_thief,
|
||||
_apply_randomized_tile_trap_floor_tile,
|
||||
_get_enemizer_symbol,
|
||||
_make_native_enemizer_rng,
|
||||
_option_key,
|
||||
patch_bosses,
|
||||
_randomize_enemy_damage,
|
||||
_randomize_enemy_health,
|
||||
_set_enemizer_flag,
|
||||
_shuffle_damage_groups,
|
||||
_update_hidden_enemy_item_table_for_retro_mode,
|
||||
apply_enemizer_base_patch,
|
||||
)
|
||||
|
||||
|
||||
class FakeRom:
|
||||
def __init__(self, size: int = 0x400000) -> None:
|
||||
self.buffer = bytearray(size)
|
||||
|
||||
def read_byte(self, address: int) -> int:
|
||||
return self.buffer[address]
|
||||
|
||||
def read_bytes(self, startaddress: int, length: int) -> bytearray:
|
||||
return self.buffer[startaddress:startaddress + length]
|
||||
|
||||
def write_byte(self, address: int, value: int) -> None:
|
||||
self.buffer[address] = value
|
||||
|
||||
def write_bytes(self, startaddress: int, values) -> None:
|
||||
self.buffer[startaddress:startaddress + len(values)] = values
|
||||
|
||||
def write_int16(self, address: int, value: int) -> None:
|
||||
self.write_bytes(address, (value & 0xFF, (value >> 8) & 0xFF))
|
||||
|
||||
|
||||
class TestEnemizerPatches(unittest.TestCase):
|
||||
def test_enemizer_base_patch_applies_mimic_hooks(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
apply_enemizer_base_patch(rom)
|
||||
|
||||
self.assertEqual(tuple(rom.read_bytes(0x307CB, 2)), (0xB6, 0x91))
|
||||
self.assertEqual(tuple(rom.read_bytes(0x311B6, 4)), (0x22, 0x1A, 0x9A, 0x36))
|
||||
self.assertEqual(tuple(rom.read_bytes(0x36C08, 5)), (0x22, 0x4E, 0x9A, 0x36, 0xEA))
|
||||
self.assertEqual(tuple(rom.read_bytes(0x36DA6, 4)), (0x22, 0x66, 0x9A, 0x36))
|
||||
self.assertEqual(tuple(rom.read_bytes(0xF0BB1, 2)), (0x95, 0xC7))
|
||||
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_FLOOR_ROUTINE_ADDRESS, 4)), (0xEA, 0xEA, 0xEA, 0xEA))
|
||||
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x00, 0x00))
|
||||
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x00)
|
||||
|
||||
def test_randomized_tile_trap_floor_tile_patch_is_separate(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
_apply_randomized_tile_trap_floor_tile(rom)
|
||||
|
||||
self.assertEqual(tuple(rom.read_bytes(TRINEXX_ICE_PROJECTILE_TILE_ADDRESS, 2)), (0x88, 0x01))
|
||||
self.assertEqual(rom.read_byte(TILE_TRAP_FLOOR_TILE_ADDRESS), 0x12)
|
||||
|
||||
def test_enemy_shuffle_enables_hidden_enemy_and_mimic_support(self) -> None:
|
||||
rom = FakeRom()
|
||||
world = self._build_world(enemy_shuffle=True, bush_shuffle=False)
|
||||
|
||||
self._apply_native_enemizer_features(world, rom)
|
||||
|
||||
self.assertEqual(
|
||||
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(VANILLA_HIDDEN_ENEMY_CHANCE_POOL))),
|
||||
VANILLA_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x01)
|
||||
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x54, 0x9C))
|
||||
self.assertEqual(rom.read_byte(0x1F2E5), 0xB0)
|
||||
self.assertEqual(rom.read_byte(0x1F2EB), 0xD0)
|
||||
|
||||
def test_bush_shuffle_and_remaining_tables_are_patched_natively(self) -> None:
|
||||
rom = FakeRom()
|
||||
item_table_address = _get_enemizer_symbol("sprite_bush_spawn_item_table")
|
||||
not_item_sprite_address = _get_enemizer_symbol("notItemSprite_Mimic")
|
||||
rom.write_byte(RETRO_ARROW_REPLACEMENT_CHECK_ADDRESS, RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
|
||||
rom.write_byte(item_table_address + 5, ARROW_REFILL_5_SPRITE_ID)
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
|
||||
|
||||
included_hp_sprite_id = 0x01
|
||||
included_damage_sprite_id = 0x02
|
||||
excluded_sprite_id = min(EXCLUDED_ENEMY_TABLE_SPRITE_IDS)
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id, 0x06)
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id, 0x07)
|
||||
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id, 0x06)
|
||||
rom.write_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id, 0x05)
|
||||
|
||||
world = self._build_world(
|
||||
bush_shuffle=True,
|
||||
killable_thieves=True,
|
||||
enemy_health="hard",
|
||||
enemy_damage="chaos",
|
||||
)
|
||||
|
||||
self._apply_native_enemizer_features(world, rom)
|
||||
|
||||
self.assertEqual(
|
||||
tuple(rom.read_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, len(RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL))),
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL,
|
||||
)
|
||||
self.assertEqual(rom.read_byte(item_table_address + 5), RETRO_RUPEE_REPLACEMENT_SPRITE_ID)
|
||||
self.assertEqual(rom.read_byte(not_item_sprite_address + 4), THIEF_SPRITE_ID)
|
||||
self.assertNotEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 0x08)
|
||||
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 2)
|
||||
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), 25)
|
||||
self.assertGreaterEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 2)
|
||||
self.assertLess(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + included_hp_sprite_id), 25)
|
||||
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + excluded_sprite_id), 0x07)
|
||||
self.assertIn(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + included_damage_sprite_id), range(8))
|
||||
self.assertEqual(rom.read_byte(ENEMY_DAMAGE_TABLE_ADDRESS + excluded_sprite_id), 0x05)
|
||||
for group_id in range(10):
|
||||
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
|
||||
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
|
||||
self.assertIn(green_mail, range(64))
|
||||
self.assertIn(blue_mail, range(64))
|
||||
self.assertIn(red_mail, range(64))
|
||||
|
||||
def test_killable_thief_sets_default_hp_without_enemy_health_shuffle(self) -> None:
|
||||
rom = FakeRom()
|
||||
rom.write_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID, 0x08)
|
||||
|
||||
world = self._build_world(killable_thieves=True)
|
||||
|
||||
self._apply_native_enemizer_features(world, rom)
|
||||
|
||||
self.assertEqual(rom.read_byte(ENEMY_HP_TABLE_ADDRESS + THIEF_SPRITE_ID), THIEF_DEFAULT_HP)
|
||||
|
||||
def test_bush_shuffle_without_enemy_shuffle_does_not_enable_sprite_randomization_flags(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
self._apply_native_enemizer_features(self._build_world(bush_shuffle=True), rom)
|
||||
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_bushes")), 0x01)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_randomize_sprites")), 0x00)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_mimic_override")), 0x00)
|
||||
self.assertEqual(rom.read_byte(_get_enemizer_symbol("EnemizerFlags_enable_terrorpin_ai_fix")), 0x00)
|
||||
self.assertEqual(tuple(rom.read_bytes(0x1F2D5, 2)), (0x00, 0x00))
|
||||
self.assertEqual(rom.read_byte(0x1F2E5), 0x00)
|
||||
self.assertEqual(rom.read_byte(0x1F2EB), 0x00)
|
||||
|
||||
def test_non_chaos_enemy_damage_uses_expected_mail_scaling(self) -> None:
|
||||
rom = FakeRom()
|
||||
|
||||
self._apply_native_enemizer_features(self._build_world(enemy_damage="hard"), rom)
|
||||
|
||||
for group_id in range(10):
|
||||
group_address = DAMAGE_GROUP_TABLE_ADDRESS + (group_id * 3)
|
||||
green_mail, blue_mail, red_mail = rom.read_bytes(group_address, 3)
|
||||
self.assertEqual(blue_mail, green_mail * 3 // 4)
|
||||
self.assertEqual(red_mail, green_mail * 3 // 8)
|
||||
|
||||
def test_patch_bosses_overwrites_enemy_shuffle_boss_room_graphics(self) -> None:
|
||||
rom = FakeRom()
|
||||
dungeon_header_base = _get_enemizer_symbol("room_header_table")
|
||||
eastern_dungeon_data = DUNGEON_BOSS_PATCH_DATA[("Eastern Palace", None)]
|
||||
rom.write_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3, BOSS_PATCH_DATA["Armos"].graphics)
|
||||
|
||||
for table_index in BOSS_GFX_SHEET_INDEXES.values():
|
||||
rom.write_byte(0x4FC0 + table_index, 0xAA)
|
||||
rom.write_byte(0x509F + table_index, 0xBB)
|
||||
rom.write_byte(0x517E + table_index, 0xCC)
|
||||
|
||||
patch_bosses(self._build_boss_world({"Eastern Palace": "Vitreous"}), rom)
|
||||
|
||||
eastern_boss_data = BOSS_PATCH_DATA["Vitreous"]
|
||||
self.assertEqual(
|
||||
tuple(rom.read_bytes(eastern_dungeon_data.sprite_pointer_address, 2)),
|
||||
eastern_boss_data.pointer,
|
||||
)
|
||||
self.assertEqual(
|
||||
rom.read_byte(dungeon_header_base + (eastern_dungeon_data.room_id * 14) + 3),
|
||||
eastern_boss_data.graphics,
|
||||
)
|
||||
|
||||
for table_index in BOSS_GFX_SHEET_INDEXES.values():
|
||||
self.assertEqual(rom.read_byte(0x4FC0 + table_index), 0xAA)
|
||||
self.assertEqual(rom.read_byte(0x509F + table_index), 0xBB)
|
||||
self.assertEqual(rom.read_byte(0x517E + table_index), 0xCC)
|
||||
|
||||
def test_native_enemizer_rng_is_deterministic_for_same_world_settings(self) -> None:
|
||||
world = self._build_world(enemy_health="hard", enemy_damage="chaos", bush_shuffle=True)
|
||||
|
||||
rng_a = _make_native_enemizer_rng(world)
|
||||
rng_b = _make_native_enemizer_rng(world)
|
||||
|
||||
self.assertEqual([rng_a.randrange(256) for _ in range(8)], [rng_b.randrange(256) for _ in range(8)])
|
||||
|
||||
@staticmethod
|
||||
def _apply_native_enemizer_features(world: SimpleNamespace, rom: FakeRom) -> None:
|
||||
enemy_shuffle_enabled = bool(world.options.enemy_shuffle)
|
||||
bush_shuffle_enabled = bool(world.options.bush_shuffle)
|
||||
enemy_health_key = _option_key(world.options.enemy_health)
|
||||
enemy_damage_key = _option_key(world.options.enemy_damage)
|
||||
|
||||
if enemy_shuffle_enabled or bush_shuffle_enabled:
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_randomize_bushes", True)
|
||||
hidden_enemy_chance_pool = (
|
||||
RANDOMIZED_HIDDEN_ENEMY_CHANCE_POOL if bush_shuffle_enabled else VANILLA_HIDDEN_ENEMY_CHANCE_POOL
|
||||
)
|
||||
rom.write_bytes(HIDDEN_ENEMY_CHANCE_POOL_ADDRESS, hidden_enemy_chance_pool)
|
||||
_update_hidden_enemy_item_table_for_retro_mode(rom)
|
||||
|
||||
if enemy_shuffle_enabled:
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_randomize_sprites", True)
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_enable_mimic_override", True)
|
||||
_set_enemizer_flag(rom, "EnemizerFlags_enable_terrorpin_ai_fix", True)
|
||||
rom.write_bytes(0x1F2D5, (0x54, 0x9C))
|
||||
rom.write_byte(0x1F2E5, 0xB0)
|
||||
rom.write_byte(0x1F2EB, 0xD0)
|
||||
|
||||
if world.options.killable_thieves:
|
||||
_apply_killable_thief(rom)
|
||||
|
||||
if enemy_health_key != "default" or enemy_damage_key != "default":
|
||||
rng = _make_native_enemizer_rng(world)
|
||||
else:
|
||||
rng = None
|
||||
|
||||
if enemy_health_key != "default":
|
||||
assert rng is not None
|
||||
_randomize_enemy_health(rom, rng, enemy_health_key)
|
||||
|
||||
if enemy_damage_key != "default":
|
||||
assert rng is not None
|
||||
_randomize_enemy_damage(rom, rng, allow_zero_damage=True)
|
||||
_shuffle_damage_groups(rom, rng, chaos_mode=enemy_damage_key == "chaos", allow_zero_damage=True)
|
||||
|
||||
@staticmethod
|
||||
def _build_world(
|
||||
*,
|
||||
enemy_shuffle: bool = False,
|
||||
bush_shuffle: bool = False,
|
||||
killable_thieves: bool = False,
|
||||
enemy_health: str = "default",
|
||||
enemy_damage: str = "default",
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
player=1,
|
||||
multiworld=SimpleNamespace(seed=12345, seed_name="native-enemizer-test"),
|
||||
options=SimpleNamespace(
|
||||
enemy_shuffle=enemy_shuffle,
|
||||
bush_shuffle=bush_shuffle,
|
||||
killable_thieves=killable_thieves,
|
||||
enemy_health=SimpleNamespace(current_key=enemy_health),
|
||||
enemy_damage=SimpleNamespace(current_key=enemy_damage),
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
|
||||
boss_overrides = boss_overrides or {}
|
||||
|
||||
def boss(name: str) -> SimpleNamespace:
|
||||
return SimpleNamespace(enemizer_name=name)
|
||||
|
||||
return SimpleNamespace(
|
||||
options=SimpleNamespace(mode="open"),
|
||||
dungeons={
|
||||
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
|
||||
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
|
||||
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
|
||||
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
|
||||
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
|
||||
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
|
||||
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
|
||||
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
|
||||
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
|
||||
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
|
||||
"Ganons Tower": SimpleNamespace(
|
||||
bosses={
|
||||
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
|
||||
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
|
||||
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,834 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
import random
|
||||
|
||||
from worlds.alttp.EnemyShuffle import (
|
||||
DungeonEnemyRoom,
|
||||
DungeonEnemySprite,
|
||||
DungeonSpriteGroup,
|
||||
EnemyShuffleState,
|
||||
EnemySpriteRequirement,
|
||||
OverworldEnemyArea,
|
||||
OverworldEnemySprite,
|
||||
RandomizedDungeonEnemyRoom,
|
||||
RandomizedDungeonEnemySprite,
|
||||
RandomizedOverworldEnemyArea,
|
||||
RandomizedOverworldEnemySprite,
|
||||
WALLMASTER_SPRITE_ID,
|
||||
_load_dungeon_sprite_metadata,
|
||||
_read_room_sprites,
|
||||
get_possible_dungeon_sprite_groups,
|
||||
_get_requirements_for_usable_dungeon_enemies,
|
||||
_get_requirements_for_usable_overworld_enemies,
|
||||
_get_randomizable_sprites_in_room,
|
||||
_apply_selected_boss_group_requirements,
|
||||
_randomize_overworld_groups,
|
||||
_randomize_room_sprites,
|
||||
_setup_required_overworld_groups,
|
||||
can_spawn_in_room,
|
||||
validate_enemy_shuffle_state,
|
||||
)
|
||||
|
||||
|
||||
class TestEnemyShuffleValidation(unittest.TestCase):
|
||||
def test_curated_room_sprite_addresses_exclude_hera_basement_key_slot(self) -> None:
|
||||
room_id = 135
|
||||
sprite_table_address = 0x4E397
|
||||
rom_bytes = bytearray(0x4E3C0)
|
||||
rom_bytes[sprite_table_address] = 0
|
||||
room_135_sprite_records = (
|
||||
(0x4E398, 0x05, 0x14, 0x18),
|
||||
(0x4E39B, 0x07, 0x1A, 0x18),
|
||||
(0x4E39E, 0x0B, 0x13, 0x18),
|
||||
(0x4E3A1, 0x19, 0x06, 0x18),
|
||||
(0x4E3A4, 0x08, 0xE7, 0x14),
|
||||
(0x4E3A7, 0x04, 0x17, 0x1E),
|
||||
(0x4E3AA, 0x0C, 0x03, 0x1E),
|
||||
(0x4E3AD, 0x15, 0x04, 0x1E),
|
||||
(0x4E3B0, 0x17, 0x0B, 0xA7),
|
||||
(0x4E3B3, 0x18, 0x19, 0xA7),
|
||||
(0x4E3B6, 0x19, 0x04, 0xA7),
|
||||
(0x4E3B9, 0x1A, 0x08, 0xE4),
|
||||
(0x4E3BC, 0x1C, 0x15, 0xA7),
|
||||
)
|
||||
for address, byte_0, byte_1, sprite_id in room_135_sprite_records:
|
||||
rom_bytes[address] = byte_0
|
||||
rom_bytes[address + 1] = byte_1
|
||||
rom_bytes[address + 2] = sprite_id
|
||||
rom_bytes[0x4E3BF] = 0xFF
|
||||
|
||||
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, _load_dungeon_sprite_metadata())
|
||||
sprite_addresses = {sprite.address for sprite in sprites}
|
||||
|
||||
self.assertNotIn(0x4E3B9, sprite_addresses)
|
||||
self.assertIn(0x4E3B6, sprite_addresses)
|
||||
self.assertFalse(any(sprite.has_key for sprite in sprites))
|
||||
|
||||
def test_curated_room_sprite_addresses_deduplicate_duplicate_slots(self) -> None:
|
||||
room_id = 125
|
||||
sprite_table_address = 0x4E2CA
|
||||
metadata = _load_dungeon_sprite_metadata()
|
||||
max_sprite_id_address = max(metadata["room_sprite_id_addresses"][room_id])
|
||||
rom_bytes = bytearray(max_sprite_id_address + 2)
|
||||
rom_bytes[sprite_table_address] = 0
|
||||
for offset, sprite_id_address in enumerate(metadata["room_sprite_id_addresses"][room_id]):
|
||||
address = sprite_id_address - 2
|
||||
sprite_id = 0x80 if offset % 2 == 0 else 0x81
|
||||
rom_bytes[address] = 0
|
||||
rom_bytes[address + 1] = 0
|
||||
rom_bytes[address + 2] = sprite_id
|
||||
|
||||
sprites = _read_room_sprites(rom_bytes, room_id, sprite_table_address, metadata)
|
||||
sprite_addresses = [sprite.address for sprite in sprites]
|
||||
|
||||
self.assertEqual(len(sprite_addresses), len(set(sprite_addresses)))
|
||||
|
||||
def test_rejects_non_killable_shutter_room(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x10, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
randomized_dungeon_rooms={
|
||||
1: RandomizedDungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
RandomizedDungeonEnemySprite(
|
||||
address=0x1000,
|
||||
byte_0=0,
|
||||
byte_1=0,
|
||||
original_sprite_id=0x10,
|
||||
sprite_id=0x11,
|
||||
is_overlord=False,
|
||||
has_key=False,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x10, killable=True, subgroup_0=(1,)),
|
||||
self._requirement(0x11, killable=False, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_rejects_water_enemy_in_non_water_room(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={165: room},
|
||||
randomized_dungeon_rooms={
|
||||
165: RandomizedDungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
RandomizedDungeonEnemySprite(
|
||||
address=0x1000,
|
||||
byte_0=0,
|
||||
byte_1=0,
|
||||
original_sprite_id=0x20,
|
||||
sprite_id=0x81,
|
||||
is_overlord=False,
|
||||
has_key=False,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, killable=True, subgroup_0=(1,)),
|
||||
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "water enemy"):
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_rejects_multiple_flopping_fish(self) -> None:
|
||||
area = OverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
graphics_block_id=1,
|
||||
bush_sprite_id=0x20,
|
||||
sprites=(
|
||||
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
|
||||
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
|
||||
),
|
||||
do_not_randomize=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
overworld_areas={0x10: area},
|
||||
randomized_overworld_areas={
|
||||
0x10: RandomizedOverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
original_bush_sprite_id=0x20,
|
||||
bush_sprite_id=0xD2,
|
||||
sprites=(
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2000,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x20,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2003,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x21,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, group_ids=(1,)),
|
||||
self._requirement(0x21, group_ids=(1,)),
|
||||
self._requirement(0x22, group_ids=(1,)),
|
||||
self._requirement(0xD2, group_ids=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_allows_multiple_flopping_fish_when_no_other_sprite_is_possible(self) -> None:
|
||||
area = OverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
graphics_block_id=1,
|
||||
bush_sprite_id=0x20,
|
||||
sprites=(
|
||||
OverworldEnemySprite(address=0x2000, y_coord=0, x_coord=0, sprite_id=0x20),
|
||||
OverworldEnemySprite(address=0x2003, y_coord=0, x_coord=0, sprite_id=0x21),
|
||||
),
|
||||
do_not_randomize=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
overworld_areas={0x10: area},
|
||||
randomized_overworld_areas={
|
||||
0x10: RandomizedOverworldEnemyArea(
|
||||
area_id=0x10,
|
||||
sprite_table_address=0,
|
||||
graphics_block_address=0,
|
||||
original_graphics_block_id=1,
|
||||
graphics_block_id=1,
|
||||
original_bush_sprite_id=0x20,
|
||||
bush_sprite_id=0xD2,
|
||||
sprites=(
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2000,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x20,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
RandomizedOverworldEnemySprite(
|
||||
address=0x2003,
|
||||
y_coord=0,
|
||||
x_coord=0,
|
||||
original_sprite_id=0x21,
|
||||
sprite_id=0xD2,
|
||||
),
|
||||
),
|
||||
skipped_randomization=False,
|
||||
)
|
||||
},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, group_ids=(2,)),
|
||||
self._requirement(0x21, group_ids=(2,)),
|
||||
self._requirement(0xD2, group_ids=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
validate_enemy_shuffle_state(state, is_standard_mode=False)
|
||||
|
||||
def test_excludes_absorbables_from_usable_enemy_pools(self) -> None:
|
||||
state = self._build_state(
|
||||
sprite_requirements=(
|
||||
self._requirement(0x10, subgroup_0=(1,)),
|
||||
self._requirement(0xE3, subgroup_0=(1,), absorbable=True),
|
||||
self._requirement(0x20, subgroup_0=(1,), never_use_dungeon=True),
|
||||
self._requirement(0x21, subgroup_0=(1,), never_use_overworld=True),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[requirement.sprite_id for requirement in _get_requirements_for_usable_dungeon_enemies(state)],
|
||||
[0x10, 0x21],
|
||||
)
|
||||
self.assertEqual(
|
||||
[requirement.sprite_id for requirement in _get_requirements_for_usable_overworld_enemies(state)],
|
||||
[0x10, 0x20],
|
||||
)
|
||||
|
||||
def test_key_enemy_replacements_exclude_moblins(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x12, is_overlord=False, has_key=True),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x12, killable=True, subgroup_0=(1,), cannot_have_key=True),
|
||||
self._requirement(0x13, killable=True, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
selected_group = state.sprite_groups[0x41]
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(0)),
|
||||
state,
|
||||
room,
|
||||
selected_group,
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x13)
|
||||
|
||||
def test_shutter_water_room_prefers_killable_water_enemy(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=40,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x8A, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=True,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={40: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x8A, killable=False, subgroup_2=(34,)),
|
||||
self._requirement(0x81, killable=True, subgroup_2=(34,), is_water_sprite=True),
|
||||
self._requirement(0x9A, killable=False, subgroup_2=(34,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
selected_group = state.sprite_groups[0x41]
|
||||
selected_group.subgroup_2 = 34
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(0)),
|
||||
state,
|
||||
room,
|
||||
selected_group,
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x81)
|
||||
|
||||
def test_non_water_shutter_room_replacements_exclude_water_enemies(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={165: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, killable=False, subgroup_0=(1,)),
|
||||
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
|
||||
self._requirement(0x22, killable=True, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(1)),
|
||||
state,
|
||||
room,
|
||||
state.sprite_groups[0x41],
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertEqual(randomized_room.sprites[0].sprite_id, 0x22)
|
||||
|
||||
def test_non_water_shutter_group_selection_requires_non_water_killable_enemy(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=165,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=True,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={165: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, killable=False, subgroup_0=(1,)),
|
||||
self._requirement(0x81, killable=True, subgroup_0=(1,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(get_possible_dungeon_sprite_groups(state, room), tuple())
|
||||
|
||||
def test_wallmaster_cannot_spawn_in_high_room_ids(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=0x100,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=tuple(),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
|
||||
self.assertFalse(can_spawn_in_room(self._requirement(WALLMASTER_SPRITE_ID), room))
|
||||
|
||||
def test_room_specific_do_not_randomize_sprites_are_not_updated(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=7,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x30, is_overlord=False, has_key=False),
|
||||
DungeonEnemySprite(address=0x1003, byte_0=0, byte_1=0, sprite_id=0x31, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={7: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x30, subgroup_0=(1,), dont_randomize_rooms=(7,)),
|
||||
self._requirement(0x31, subgroup_0=(1,)),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[sprite.sprite_id for sprite in _get_randomizable_sprites_in_room(state, room)],
|
||||
[0x31],
|
||||
)
|
||||
|
||||
def test_water_rooms_only_use_water_enemies(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=True,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, subgroup_0=(1,)),
|
||||
self._requirement(0x21, subgroup_0=(1,), is_water_sprite=True),
|
||||
self._requirement(0x22, subgroup_0=(1,), is_water_sprite=True),
|
||||
),
|
||||
)
|
||||
|
||||
randomized_room = _randomize_room_sprites(
|
||||
SimpleNamespace(random=random.Random(0)),
|
||||
state,
|
||||
room,
|
||||
state.sprite_groups[0x41],
|
||||
False,
|
||||
)
|
||||
|
||||
self.assertIn(randomized_room.sprites[0].sprite_id, {0x21, 0x22})
|
||||
|
||||
def test_dungeon_group_selection_excludes_groups_without_enemy_requirements(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=1,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=False),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={1: room},
|
||||
sprite_requirements=(self._requirement(0x20, subgroup_0=(1,)),),
|
||||
)
|
||||
state.sprite_groups[0x42] = DungeonSpriteGroup(
|
||||
group_id=0x42,
|
||||
dungeon_group_id=2,
|
||||
subgroup_0=0,
|
||||
subgroup_1=0,
|
||||
subgroup_2=0,
|
||||
subgroup_3=0,
|
||||
)
|
||||
|
||||
possible_groups = get_possible_dungeon_sprite_groups(state, room)
|
||||
|
||||
self.assertEqual([group.group_id for group in possible_groups], [0x41])
|
||||
|
||||
def test_key_room_group_selection_excludes_groups_without_room_spawnable_key_enemies(self) -> None:
|
||||
room = DungeonEnemyRoom(
|
||||
room_id=61,
|
||||
room_header_address=0,
|
||||
sprite_table_address=0,
|
||||
graphics_block_id=1,
|
||||
tag_1=0,
|
||||
tag_2=0,
|
||||
sort_sprites_value=0,
|
||||
sprites=(
|
||||
DungeonEnemySprite(address=0x1000, byte_0=0, byte_1=0, sprite_id=0x20, is_overlord=False, has_key=True),
|
||||
),
|
||||
required_group_id=None,
|
||||
required_subgroup_0=tuple(),
|
||||
required_subgroup_1=tuple(),
|
||||
required_subgroup_2=tuple(),
|
||||
required_subgroup_3=tuple(),
|
||||
is_shutter_room=False,
|
||||
is_water_room=False,
|
||||
do_not_randomize=False,
|
||||
no_special_enemies_standard=False,
|
||||
)
|
||||
state = self._build_state(
|
||||
dungeon_rooms={61: room},
|
||||
sprite_requirements=(
|
||||
self._requirement(0x20, subgroup_0=(1,)),
|
||||
self._requirement(0x50, killable=True, subgroup_1=(32,), excluded_rooms=(61,)),
|
||||
self._requirement(0x9C, killable=True, subgroup_1=(32,), cannot_have_key=True),
|
||||
self._requirement(0x51, killable=True, subgroup_1=(33,)),
|
||||
),
|
||||
)
|
||||
state.sprite_groups[0x41] = DungeonSpriteGroup(
|
||||
group_id=0x41,
|
||||
dungeon_group_id=1,
|
||||
subgroup_0=1,
|
||||
subgroup_1=32,
|
||||
subgroup_2=1,
|
||||
subgroup_3=1,
|
||||
)
|
||||
state.sprite_groups[0x42] = DungeonSpriteGroup(
|
||||
group_id=0x42,
|
||||
dungeon_group_id=2,
|
||||
subgroup_0=1,
|
||||
subgroup_1=33,
|
||||
subgroup_2=1,
|
||||
subgroup_3=1,
|
||||
)
|
||||
|
||||
possible_groups = get_possible_dungeon_sprite_groups(state, room)
|
||||
|
||||
self.assertEqual([group.group_id for group in possible_groups], [0x42])
|
||||
|
||||
def test_overworld_group_randomization_preserves_forced_subgroups(self) -> None:
|
||||
sprite_groups = {
|
||||
7: DungeonSpriteGroup(group_id=7, dungeon_group_id=-57, subgroup_0=1, subgroup_1=2, subgroup_2=3, subgroup_3=4),
|
||||
}
|
||||
|
||||
_setup_required_overworld_groups(
|
||||
sprite_groups,
|
||||
(
|
||||
SimpleNamespace(
|
||||
group_id=7,
|
||||
subgroup_0=None,
|
||||
subgroup_1=None,
|
||||
subgroup_2=None,
|
||||
subgroup_3=17,
|
||||
areas=(0x02,),
|
||||
),
|
||||
),
|
||||
)
|
||||
_randomize_overworld_groups(SimpleNamespace(random=random.Random(0)), sprite_groups)
|
||||
|
||||
group = sprite_groups[7]
|
||||
self.assertEqual(group.subgroup_3, 17)
|
||||
self.assertIn(group.subgroup_0, {22, 31, 47, 14})
|
||||
self.assertIn(group.subgroup_1, {44, 30, 32})
|
||||
self.assertIn(group.subgroup_2, {12, 18, 23, 24, 28, 46, 34, 35, 39, 40, 38, 41, 36, 37, 42})
|
||||
|
||||
def test_selected_boss_group_requirements_override_shared_boss_graphics_group(self) -> None:
|
||||
sprite_groups = {
|
||||
0x56: DungeonSpriteGroup(
|
||||
group_id=0x56,
|
||||
dungeon_group_id=22,
|
||||
subgroup_0=1,
|
||||
subgroup_1=1,
|
||||
subgroup_2=60,
|
||||
subgroup_3=49,
|
||||
),
|
||||
}
|
||||
sprite_requirements = (
|
||||
self._requirement(162, subgroup_2=(60,)),
|
||||
self._requirement(189, subgroup_3=(61,)),
|
||||
)
|
||||
|
||||
_apply_selected_boss_group_requirements(
|
||||
self._build_boss_world({"Eastern Palace": "Vitreous"}),
|
||||
sprite_groups,
|
||||
sprite_requirements,
|
||||
)
|
||||
|
||||
group = sprite_groups[0x56]
|
||||
self.assertEqual(group.subgroup_2, 60)
|
||||
self.assertEqual(group.subgroup_3, 61)
|
||||
self.assertTrue(group.preserve_subgroup_2)
|
||||
self.assertTrue(group.preserve_subgroup_3)
|
||||
|
||||
@staticmethod
|
||||
def _requirement(
|
||||
sprite_id: int,
|
||||
*,
|
||||
killable: bool = False,
|
||||
subgroup_0: tuple[int, ...] = tuple(),
|
||||
subgroup_1: tuple[int, ...] = tuple(),
|
||||
subgroup_2: tuple[int, ...] = tuple(),
|
||||
subgroup_3: tuple[int, ...] = tuple(),
|
||||
group_ids: tuple[int, ...] = tuple(),
|
||||
absorbable: bool = False,
|
||||
never_use_dungeon: bool = False,
|
||||
never_use_overworld: bool = False,
|
||||
cannot_have_key: bool = False,
|
||||
is_water_sprite: bool = False,
|
||||
excluded_rooms: tuple[int, ...] = tuple(),
|
||||
dont_randomize_rooms: tuple[int, ...] = tuple(),
|
||||
) -> EnemySpriteRequirement:
|
||||
return EnemySpriteRequirement(
|
||||
sprite_name=f"sprite_{sprite_id:02x}",
|
||||
sprite_id=sprite_id,
|
||||
boss=False,
|
||||
overlord=False,
|
||||
do_not_randomize=False,
|
||||
killable=killable,
|
||||
npc=False,
|
||||
never_use_dungeon=never_use_dungeon,
|
||||
never_use_overworld=never_use_overworld,
|
||||
cannot_have_key=cannot_have_key,
|
||||
is_object=False,
|
||||
absorbable=absorbable,
|
||||
is_water_sprite=is_water_sprite,
|
||||
is_enemy_sprite=True,
|
||||
group_ids=group_ids,
|
||||
subgroup_0=subgroup_0,
|
||||
subgroup_1=subgroup_1,
|
||||
subgroup_2=subgroup_2,
|
||||
subgroup_3=subgroup_3,
|
||||
parameters=None,
|
||||
special_glitched=False,
|
||||
excluded_rooms=excluded_rooms,
|
||||
dont_randomize_rooms=dont_randomize_rooms,
|
||||
spawnable_rooms=tuple(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_state(
|
||||
*,
|
||||
dungeon_rooms=None,
|
||||
overworld_areas=None,
|
||||
randomized_dungeon_rooms=None,
|
||||
randomized_overworld_areas=None,
|
||||
sprite_requirements=tuple(),
|
||||
) -> EnemyShuffleState:
|
||||
sprite_groups = {
|
||||
1: DungeonSpriteGroup(group_id=1, dungeon_group_id=-63, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
|
||||
0x41: DungeonSpriteGroup(group_id=0x41, dungeon_group_id=1, subgroup_0=1, subgroup_1=1, subgroup_2=1, subgroup_3=1),
|
||||
}
|
||||
return EnemyShuffleState(
|
||||
dungeon_rooms=dungeon_rooms or {},
|
||||
overworld_areas=overworld_areas or {},
|
||||
sprite_groups=sprite_groups,
|
||||
sprite_requirements=sprite_requirements,
|
||||
room_group_requirements=tuple(),
|
||||
overworld_group_requirements=tuple(),
|
||||
shutter_room_ids=frozenset(),
|
||||
water_room_ids=frozenset(),
|
||||
dont_randomize_room_ids=frozenset(),
|
||||
no_special_enemies_standard_room_ids=frozenset(),
|
||||
boss_room_ids=frozenset(),
|
||||
dont_randomize_overworld_area_ids=frozenset(),
|
||||
randomized_dungeon_rooms=randomized_dungeon_rooms or {},
|
||||
randomized_overworld_areas=randomized_overworld_areas or {},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_boss_world(boss_overrides: dict[str, str] | None = None) -> SimpleNamespace:
|
||||
boss_overrides = boss_overrides or {}
|
||||
|
||||
def boss(name: str) -> SimpleNamespace:
|
||||
return SimpleNamespace(enemizer_name=name)
|
||||
|
||||
return SimpleNamespace(
|
||||
options=SimpleNamespace(mode="open"),
|
||||
dungeons={
|
||||
"Eastern Palace": SimpleNamespace(boss=boss(boss_overrides.get("Eastern Palace", "Armos"))),
|
||||
"Desert Palace": SimpleNamespace(boss=boss(boss_overrides.get("Desert Palace", "Lanmola"))),
|
||||
"Tower of Hera": SimpleNamespace(boss=boss(boss_overrides.get("Tower of Hera", "Moldorm"))),
|
||||
"Palace of Darkness": SimpleNamespace(boss=boss(boss_overrides.get("Palace of Darkness", "Helmasaur"))),
|
||||
"Swamp Palace": SimpleNamespace(boss=boss(boss_overrides.get("Swamp Palace", "Arrghus"))),
|
||||
"Skull Woods": SimpleNamespace(boss=boss(boss_overrides.get("Skull Woods", "Mothula"))),
|
||||
"Thieves Town": SimpleNamespace(boss=boss(boss_overrides.get("Thieves Town", "Blind"))),
|
||||
"Ice Palace": SimpleNamespace(boss=boss(boss_overrides.get("Ice Palace", "Kholdstare"))),
|
||||
"Misery Mire": SimpleNamespace(boss=boss(boss_overrides.get("Misery Mire", "Vitreous"))),
|
||||
"Turtle Rock": SimpleNamespace(boss=boss(boss_overrides.get("Turtle Rock", "Trinexx"))),
|
||||
"Ganons Tower": SimpleNamespace(
|
||||
bosses={
|
||||
"bottom": boss(boss_overrides.get("Ganons Tower Bottom", "Armos")),
|
||||
"middle": boss(boss_overrides.get("Ganons Tower Middle", "Lanmola")),
|
||||
"top": boss(boss_overrides.get("Ganons Tower Top", "Moldorm")),
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,56 @@
|
||||
import random
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from worlds.alttp.PotShuffle import (
|
||||
POT_KEY,
|
||||
POT_HOLE,
|
||||
generate_pot_shuffle,
|
||||
get_unique_pot_item_position,
|
||||
)
|
||||
|
||||
|
||||
class TestPotShuffle(unittest.TestCase):
|
||||
def test_reserved_key_rooms_only_place_actual_keys(self) -> None:
|
||||
for seed in range(10):
|
||||
world = SimpleNamespace(
|
||||
random=random.Random(seed),
|
||||
options=SimpleNamespace(retro_bow=False),
|
||||
)
|
||||
shuffled_pots = generate_pot_shuffle(world)
|
||||
conveyor_cross_keys = [
|
||||
pot for pot in shuffled_pots[0x8B]
|
||||
if pot.item == POT_KEY
|
||||
]
|
||||
self.assertEqual(len(conveyor_cross_keys), 1)
|
||||
|
||||
def test_get_unique_pot_item_position_returns_single_match(self) -> None:
|
||||
world = SimpleNamespace(
|
||||
random=random.Random(0),
|
||||
options=SimpleNamespace(retro_bow=False),
|
||||
)
|
||||
shuffled_pots = generate_pot_shuffle(world)
|
||||
|
||||
self.assertEqual(
|
||||
get_unique_pot_item_position(shuffled_pots, 0x36, POT_KEY),
|
||||
(114, 16),
|
||||
)
|
||||
|
||||
def test_reserved_hole_room_keeps_hole_fixed(self) -> None:
|
||||
for seed in range(25):
|
||||
world = SimpleNamespace(
|
||||
random=random.Random(seed),
|
||||
options=SimpleNamespace(retro_bow=False),
|
||||
)
|
||||
shuffled_pots = generate_pot_shuffle(world)
|
||||
hole_positions = [
|
||||
(pot.x, pot.y)
|
||||
for pot in shuffled_pots[206]
|
||||
if pot.item == POT_HOLE
|
||||
]
|
||||
|
||||
self.assertEqual(hole_positions, [(204, 11)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"game": "APQuest",
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "1.0.1",
|
||||
"minimum_ap_version": "0.6.7",
|
||||
"world_version": "2.0.0",
|
||||
"authors": ["NewSoupVi"]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
C to fire available Confetti Cannons
|
||||
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>:
|
||||
orientation: "horizontal"
|
||||
|
||||
@@ -4,8 +4,9 @@ from argparse import Namespace
|
||||
from enum import Enum
|
||||
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 Utils import gui_enabled
|
||||
|
||||
from ..game.events import ConfettiFired, LocationClearedEvent, MathProblemSolved, MathProblemStarted, VictoryEvent
|
||||
from ..game.game import Game
|
||||
@@ -41,6 +42,16 @@ class ConnectionStatus(Enum):
|
||||
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):
|
||||
game = "APQuest"
|
||||
items_handling = 0b111 # full remote
|
||||
@@ -65,6 +76,7 @@ class APQuestContext(CommonContext):
|
||||
delay_intro_song: bool
|
||||
|
||||
ui: APQuestManager
|
||||
command_processor = APQuestClientCommandProcessor
|
||||
|
||||
def __init__(
|
||||
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
|
||||
self.ap_quest_game.gameboard.fill_remote_location_content(remote_item_graphic_overrides)
|
||||
self.render()
|
||||
self.ui.game_view.bind_keyboard()
|
||||
|
||||
self.connection_status = ConnectionStatus.GAME_RUNNING
|
||||
self.ui.game_started()
|
||||
@@ -187,7 +198,7 @@ class APQuestContext(CommonContext):
|
||||
if self.ap_quest_game is None:
|
||||
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()
|
||||
|
||||
def location_checked_side_effects(self, location: int) -> None:
|
||||
@@ -244,6 +255,59 @@ class APQuestContext(CommonContext):
|
||||
self.ap_quest_game.input(input_key)
|
||||
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]":
|
||||
self.load_kv()
|
||||
return APQuestManager
|
||||
|
||||
@@ -4,29 +4,26 @@ from math import sqrt
|
||||
from random import choice, random
|
||||
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.instructions import Canvas
|
||||
from kivy.input import MotionEvent
|
||||
from kivy.uix.behaviors import ButtonBehavior
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
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 CommonClient import logger
|
||||
|
||||
from ..game.inputs import Input
|
||||
|
||||
|
||||
INPUT_MAP = {
|
||||
"up": Input.UP,
|
||||
INPUT_MAP_STR = {
|
||||
"w": Input.UP,
|
||||
"down": Input.DOWN,
|
||||
"s": Input.DOWN,
|
||||
"right": Input.RIGHT,
|
||||
"d": Input.RIGHT,
|
||||
"left": Input.LEFT,
|
||||
"a": Input.LEFT,
|
||||
"spacebar": Input.ACTION,
|
||||
" ": Input.ACTION,
|
||||
"c": Input.CONFETTI,
|
||||
"0": Input.ZERO,
|
||||
"1": Input.ONE,
|
||||
@@ -38,38 +35,52 @@ INPUT_MAP = {
|
||||
"7": Input.SEVEN,
|
||||
"8": Input.EIGHT,
|
||||
"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):
|
||||
_keyboard: Keyboard | None = None
|
||||
focused: int = 1
|
||||
input_function: Callable[[Input], None]
|
||||
|
||||
def __init__(self, input_function: Callable[[Input], None], **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
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:
|
||||
self.bind_keyboard()
|
||||
|
||||
def bind_keyboard(self) -> None:
|
||||
if self._keyboard is not None:
|
||||
def check_focus(self, _, touch, *args, **kwargs) -> None:
|
||||
if self.parent.collide_point(*touch.pos):
|
||||
self.focused += 1
|
||||
self.opacity = 1
|
||||
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:
|
||||
if self._keyboard is None:
|
||||
return
|
||||
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
|
||||
self._keyboard = None
|
||||
self.focused = 0
|
||||
self.opacity = 0.5
|
||||
|
||||
def _on_keyboard_down(self, _: Any, keycode: tuple[int, str], _1: Any, _2: Any) -> bool:
|
||||
if keycode[1] in INPUT_MAP:
|
||||
self.input_function(INPUT_MAP[keycode[1]])
|
||||
return True
|
||||
def force_focus(self) -> None:
|
||||
Window.release_keyboard()
|
||||
self.focused = 1
|
||||
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):
|
||||
@@ -77,7 +88,7 @@ class APQuestGrid(GridLayout):
|
||||
parent_width, parent_height = self.parent.size
|
||||
|
||||
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:
|
||||
self.size = parent_width, self_height_according_to_parent_width
|
||||
@@ -203,13 +214,23 @@ class Confetti:
|
||||
return True
|
||||
|
||||
|
||||
class ConfettiView(MDRecycleView):
|
||||
class ConfettiView(Widget):
|
||||
confetti: list[Confetti]
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
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:
|
||||
parent_width, parent_height = self.parent.size
|
||||
|
||||
@@ -254,3 +275,32 @@ class VolumeSliderView(BoxLayout):
|
||||
|
||||
class APQuestControlsView(BoxLayout):
|
||||
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
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from kivy._clock import ClockEvent
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.image import Image
|
||||
@@ -13,7 +14,16 @@ from kivy.uix.layout import Layout
|
||||
from kivymd.uix.recycleview import MDRecycleView
|
||||
|
||||
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 .sounds import SoundManager
|
||||
|
||||
@@ -28,15 +38,17 @@ class APQuestManager(GameManager):
|
||||
lower_game_grid: GridLayout
|
||||
upper_game_grid: GridLayout
|
||||
|
||||
game_view: MDRecycleView
|
||||
game_view: MDRecycleView | None = None
|
||||
game_view_tab: MDNavigationItemBase
|
||||
|
||||
sound_manager: SoundManager
|
||||
|
||||
bottom_image_grid: list[list[Image]]
|
||||
top_image_grid: list[list[Image]]
|
||||
top_image_grid: list[list[TapImage]]
|
||||
confetti_view: ConfettiView
|
||||
|
||||
move_event: ClockEvent | None
|
||||
|
||||
bottom_grid_is_grass: bool
|
||||
|
||||
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.top_image_grid = []
|
||||
self.bottom_image_grid = []
|
||||
self.move_event = None
|
||||
self.bottom_grid_is_grass = False
|
||||
|
||||
def allow_intro_song(self) -> None:
|
||||
@@ -71,25 +84,27 @@ class APQuestManager(GameManager):
|
||||
|
||||
def game_started(self) -> None:
|
||||
self.switch_to_game_tab()
|
||||
if self.game_view is not None:
|
||||
self.game_view.force_focus()
|
||||
self.sound_manager.game_started = True
|
||||
|
||||
def render(self, game: Game, player_sprite: PlayerSprite) -> None:
|
||||
self.setup_game_grid_if_not_setup(game.gameboard.size)
|
||||
def render(self, game: Game, player_sprite: PlayerSprite, hard_mode: bool) -> None:
|
||||
self.setup_game_grid_if_not_setup(game)
|
||||
|
||||
# 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
|
||||
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.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()
|
||||
|
||||
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):
|
||||
texture = get_texture(graphic, player_sprite)
|
||||
texture = get_texture(graphic, player_sprite, hard_mode)
|
||||
|
||||
if texture is None:
|
||||
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):
|
||||
image = image_row[-1]
|
||||
|
||||
image.is_confetti_cannon = item_graphic == Graphic.CONFETTI_CANNON
|
||||
|
||||
texture = get_texture(item_graphic)
|
||||
if texture is None:
|
||||
image.opacity = 0
|
||||
@@ -136,23 +153,25 @@ class APQuestManager(GameManager):
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
self.top_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.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))
|
||||
self.lower_game_grid.add_widget(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.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))
|
||||
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.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:
|
||||
container = super().build()
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import pkgutil
|
||||
from collections.abc import Buffer
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
from typing import Literal, NamedTuple, Protocol, cast
|
||||
|
||||
from kivy.uix.image import CoreImage
|
||||
from typing_extensions import Buffer
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
@@ -29,6 +29,7 @@ class RelatedTexture(NamedTuple):
|
||||
|
||||
|
||||
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
||||
# Inanimates
|
||||
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
|
||||
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 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_ACTIVATED: RelatedTexture("inanimates.png", 16, 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_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 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_3_HEALTH: RelatedTexture("boss.png", 32, 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),
|
||||
|
||||
# Items
|
||||
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
|
||||
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
|
||||
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
|
||||
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
|
||||
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
|
||||
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
|
||||
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
|
||||
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.ITEMS_TEXT: "items_text.png",
|
||||
|
||||
# Numbers
|
||||
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
|
||||
Graphic.ONE: RelatedTexture("numbers.png", 16, 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.EIGHT: RelatedTexture("numbers.png", 48, 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_E: RelatedTexture("letters.png", 16, 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_M: RelatedTexture("letters.png", 16, 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.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
|
||||
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
|
||||
Graphic.PLUS: RelatedTexture("symbols.png", 0, 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.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
|
||||
}
|
||||
|
||||
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
|
||||
|
||||
EASY_MODE_BOSS_2_HEALTH = RelatedTexture("boss.png", 16, 0, 16, 16)
|
||||
|
||||
|
||||
class PlayerSprite(Enum):
|
||||
HUMAN = 0
|
||||
@@ -160,13 +160,18 @@ def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Textu
|
||||
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:
|
||||
return None
|
||||
|
||||
if graphic == "Grass":
|
||||
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:
|
||||
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import asyncio
|
||||
import pkgutil
|
||||
from asyncio import Task
|
||||
from collections.abc import Buffer
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from kivy import Config
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
from typing_extensions import Buffer
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
@@ -85,7 +85,7 @@ class SoundManager:
|
||||
|
||||
def ensure_config(self) -> None:
|
||||
Config.adddefaultsection("APQuest")
|
||||
Config.setdefault("APQuest", "volume", 50)
|
||||
Config.setdefault("APQuest", "volume", 30)
|
||||
self.set_volume_percentage(Config.getint("APQuest", "volume"))
|
||||
|
||||
async def sound_manager_loop(self) -> None:
|
||||
@@ -149,6 +149,7 @@ class SoundManager:
|
||||
continue
|
||||
|
||||
if sound_name == audio_filename:
|
||||
sound.volume = self.volume_percentage / 100
|
||||
sound.play()
|
||||
self.update_background_music()
|
||||
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.
|
||||
# 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":
|
||||
song.volume = self.current_background_music_volume * self.volume_percentage / 100
|
||||
song.play()
|
||||
song.seek(0)
|
||||
continue
|
||||
@@ -228,6 +230,7 @@ class SoundManager:
|
||||
|
||||
if self.current_background_music_volume != 0:
|
||||
if song.state == "stop":
|
||||
song.volume = self.current_background_music_volume * self.volume_percentage / 100
|
||||
song.play()
|
||||
song.seek(0)
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||
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
|
||||
|
||||
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,
|
||||
|
||||
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),
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
auto_move_attempt_passing_through = False
|
||||
|
||||
@abstractmethod
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
@@ -89,15 +91,16 @@ class Chest(Entity, InteractableMixin, LocationMixin):
|
||||
self.is_open = True
|
||||
self.update_solidity()
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
if self.has_given_content:
|
||||
return
|
||||
return False
|
||||
|
||||
if self.is_open:
|
||||
self.give_content(player)
|
||||
return
|
||||
return True
|
||||
|
||||
self.open()
|
||||
return True
|
||||
|
||||
def content_success(self) -> None:
|
||||
self.update_solidity()
|
||||
@@ -135,47 +138,59 @@ class Door(Entity):
|
||||
|
||||
|
||||
class KeyDoor(Door, InteractableMixin):
|
||||
auto_move_attempt_passing_through = True
|
||||
|
||||
closed_graphic = Graphic.KEY_DOOR
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
if self.is_open:
|
||||
return
|
||||
return False
|
||||
|
||||
if not player.has_item(Item.KEY):
|
||||
return
|
||||
return False
|
||||
|
||||
player.remove_item(Item.KEY)
|
||||
|
||||
self.open()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BreakableBlock(Door, InteractableMixin):
|
||||
auto_move_attempt_passing_through = True
|
||||
|
||||
closed_graphic = Graphic.BREAKABLE_BLOCK
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
if self.is_open:
|
||||
return
|
||||
return False
|
||||
|
||||
if not player.has_item(Item.HAMMER):
|
||||
return
|
||||
return False
|
||||
|
||||
player.remove_item(Item.HAMMER)
|
||||
|
||||
self.open()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Bush(Door, InteractableMixin):
|
||||
auto_move_attempt_passing_through = True
|
||||
|
||||
closed_graphic = Graphic.BUSH
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
if self.is_open:
|
||||
return
|
||||
return False
|
||||
|
||||
if not player.has_item(Item.SWORD):
|
||||
return
|
||||
return False
|
||||
|
||||
self.open()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Button(Entity, InteractableMixin):
|
||||
solid = True
|
||||
@@ -186,12 +201,13 @@ class Button(Entity, InteractableMixin):
|
||||
def __init__(self, activates: ActivatableMixin) -> None:
|
||||
self.activates = activates
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
if self.activated:
|
||||
return
|
||||
return False
|
||||
|
||||
self.activated = True
|
||||
self.activates.activate(player)
|
||||
return True
|
||||
|
||||
@property
|
||||
def graphic(self) -> Graphic:
|
||||
@@ -240,9 +256,9 @@ class Enemy(Entity, InteractableMixin):
|
||||
return
|
||||
self.current_health = self.max_health
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
if self.dead:
|
||||
return
|
||||
return False
|
||||
|
||||
if player.has_item(Item.SWORD):
|
||||
self.current_health = max(0, self.current_health - 1)
|
||||
@@ -250,9 +266,10 @@ class Enemy(Entity, InteractableMixin):
|
||||
if self.current_health == 0:
|
||||
if not self.dead:
|
||||
self.die()
|
||||
return
|
||||
return True
|
||||
|
||||
player.damage(2)
|
||||
return True
|
||||
|
||||
@property
|
||||
def graphic(self) -> Graphic:
|
||||
@@ -270,13 +287,15 @@ class EnemyWithLoot(Enemy, LocationMixin):
|
||||
self.dead = True
|
||||
self.solid = not self.has_given_content
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
if self.dead:
|
||||
if not self.has_given_content:
|
||||
self.give_content(player)
|
||||
return
|
||||
return True
|
||||
return False
|
||||
|
||||
super().interact(player)
|
||||
return True
|
||||
|
||||
@property
|
||||
def graphic(self) -> Graphic:
|
||||
@@ -303,10 +322,12 @@ class FinalBoss(Enemy):
|
||||
}
|
||||
enemy_default_graphic = Graphic.BOSS_1_HEALTH
|
||||
|
||||
def interact(self, player: Player) -> None:
|
||||
def interact(self, player: Player) -> bool:
|
||||
dead_before = self.dead
|
||||
|
||||
super().interact(player)
|
||||
changed = super().interact(player)
|
||||
|
||||
if not dead_before and self.dead:
|
||||
player.victory()
|
||||
|
||||
return changed
|
||||
|
||||
+74
-10
@@ -23,6 +23,8 @@ class Game:
|
||||
active_math_problem: MathProblem | None
|
||||
active_math_problem_input: list[int] | None
|
||||
|
||||
auto_target_path: list[tuple[int, int]] = []
|
||||
|
||||
remotely_received_items: set[tuple[int, int, int]]
|
||||
|
||||
def __init__(
|
||||
@@ -32,6 +34,7 @@ class Game:
|
||||
self.gameboard = create_gameboard(hard_mode, hammer_exists, extra_chest)
|
||||
self.player = Player(self.gameboard, self.queued_events.append)
|
||||
self.active_math_problem = None
|
||||
self.active_math_problem_input = None
|
||||
self.remotely_received_items = set()
|
||||
|
||||
if random_object is None:
|
||||
@@ -94,29 +97,40 @@ class Game:
|
||||
|
||||
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
|
||||
|
||||
delta_x, delta_y = direction.value
|
||||
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:
|
||||
self.player.current_x = new_x
|
||||
self.player.current_y = new_y
|
||||
if self.gameboard.get_entity_at(new_x, new_y).solid:
|
||||
return False
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
if isinstance(entity, InteractableMixin):
|
||||
entity.interact(self.player)
|
||||
return entity.interact(self.player)
|
||||
|
||||
def attempt_fire_confetti_cannon(self) -> None:
|
||||
if self.player.has_item(Item.CONFETTI_CANNON):
|
||||
self.player.remove_item(Item.CONFETTI_CANNON)
|
||||
self.queued_events.append(ConfettiFired(self.player.current_x, self.player.current_y))
|
||||
return False
|
||||
|
||||
def attempt_fire_confetti_cannon(self) -> bool:
|
||||
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:
|
||||
self.active_math_problem = None
|
||||
@@ -154,6 +168,12 @@ class Game:
|
||||
self.active_math_problem_input.pop()
|
||||
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:
|
||||
if not self.gameboard.ready:
|
||||
return
|
||||
@@ -201,3 +221,47 @@ class Game:
|
||||
def force_clear_location(self, location_id: int) -> None:
|
||||
location = Location(location_id)
|
||||
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,
|
||||
Entity,
|
||||
FinalBoss,
|
||||
InteractableMixin,
|
||||
KeyDoor,
|
||||
LocationMixin,
|
||||
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 .items import Item
|
||||
from .locations import DEFAULT_CONTENT, Location
|
||||
from .path_finding import find_path_or_closest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .player import Player
|
||||
@@ -107,6 +109,21 @@ class Gameboard:
|
||||
|
||||
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(
|
||||
self, problem: MathProblem, current_input_digits: list[int], current_input_int: int | None
|
||||
) -> tuple[tuple[Graphic, ...], ...]:
|
||||
@@ -186,6 +203,23 @@ class Gameboard:
|
||||
entity = self.remote_entity_by_location_id[location]
|
||||
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
|
||||
def ready(self) -> bool:
|
||||
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()
|
||||
|
||||
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 = (
|
||||
(Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty(), Wall(), Empty(), Empty(), Empty()),
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import NamedTuple
|
||||
|
||||
_random = random.Random()
|
||||
|
||||
|
||||
class NumberChoiceConstraints(NamedTuple):
|
||||
num_1_min: int
|
||||
num_1_max: int
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 754 B |
@@ -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 []
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user