Compare commits

..

50 Commits

Author SHA1 Message Date
ubufugu 316444cc5a fix merge conflict with docker.yml 2026-04-09 11:04:26 -07:00
PoryGone b7c4fcb4c6 DKC3: Delete DKC3 (#6097) 2026-04-08 21:35:43 +02:00
williamronn bcd3f9a74c WebHost: 2025 to 2026 (#6094) 2026-04-07 22:16:39 +02:00
Trevor L 6262235161 Bomb Rush Cyberfunk: Fix logic requirement increasing after collecting item (#6105) 2026-04-07 22:15:47 +02:00
Silvris 030cb4b578 Various Worlds: purge world: MultiWorld (#5973) 2026-04-07 22:13:31 +02:00
agilbert1412 36bab6f52a Stardew Valley: 0.6.7 Day 1 fixes (#6098)
- The Shane and Sebastian Portrait filler items werent tagged properly.
- The Beach Farm adds one secretsanity check, so it needs to be in the allsanity preset
- The Allsanity preset is renamed to "Maxsanity" to encourage better defined terminology and Allsanity means something else
- The "All Random" preset has been removed entirely. It has been the cause of too many beginner footguns over the years. People can still achieve the effect manually, but at least they'll have to try a little bit to ruin their own experience.
2026-04-07 21:38:04 +02:00
Duck e0cfef3407 APWorld Builder: Add option to skip opening output folder (#5847) 2026-04-05 19:50:17 +02:00
Duck bb2a775c05 WebHost: Fix hosting with invalid worlds installed (#5648) 2026-04-04 07:52:27 +02:00
ubufugu 7971961166 add schedule I, sonic 1/frontiers/heroes, spirit island 2026-04-02 23:46:36 -07:00
palex00 427b147818 APQuest: Add Link to Poptracker Pack to Setup Guide (#6089)
* Update setup_en.md

* Update setup_de.md
2026-04-01 22:43:14 +02:00
NewSoupVi 3f3c343fb3 APQuest: Fix focus issues (#6090)
* My best attempt at fixing focus issues on Android

* didn't mean to remove that
2026-04-01 22:42:08 +02:00
Bryce Wilson debe4cf035 Pokemon Emerald: Bump version (#6083) 2026-04-01 19:18:42 +02:00
Silvris 68f25f4642 MM3: Bump world version (#6088) 2026-04-01 19:18:11 +02:00
NewSoupVi 3c4af8f432 APQuest: Tap to move (#6082)
* Tap to move

* inputs

* cleanup

* oops
2026-03-31 20:55:52 +02:00
NewSoupVi 5360b6bb37 The Witness: (Unbeatable seed) Ensure Desert Laser Redirection is required when the box is rotated (#5889)
* Unbeatable seed: 11 lasers + redirect when the box is rotated

* naming
2026-03-31 00:31:05 +02:00
black-sliver 2ee20a3ac4 CI: set permissions, update and pin actions, CodeQL for actions (#6073)
* CI: reduce default permissions to minimum

* CI: update pin actions

Most of them. CodeQL and action-gh-release is untouched for now.
Immutable actions and actions/* are pinned to version,
other actions are pinned to hash.

* CI: make use of archive: false in upload-artifact

also set compression level and error behavior for scan-build upload.

* CI: update codeql and enable scanning actions
2026-03-30 21:46:43 +02:00
Ian Robinson c640d2fa24 Rule Builder: Add field resolvers (#5919) 2026-03-30 18:19:10 +02:00
James White 58a6407040 SMW: Prevent receiving your own traps while aliased (#5763) 2026-03-30 17:27:10 +02:00
EdricY ba7ca0bd23 Options Creator: bind free text set_value to text instead of on_text_validate (#5915) 2026-03-30 17:25:25 +02:00
Louis M bdbf72f148 Aquaria: Fixing bug where Urchin Costume is not a progression damaging item (#5998) 2026-03-30 01:40:05 +02:00
Jarno 2b46df90b4 Satisfactory: Fixed buildings missing from goal check (#5772) 2026-03-30 00:46:01 +02:00
NewSoupVi 88dc135960 APQuest: Various fixes (#6079)
* Import Buffer from typing_extensions instead of collections.abc for 3.11 compat

* always re-set sound volumes before playing

* fix game window scaling if parent is vertical

* make default volume lower
2026-03-30 00:32:06 +02:00
Duck 95f696c04f WebHost: Remove space before comma separators in tutorial authors (#5999)
* Remove space before comma

* Factorio authors update

* Simplify template
2026-03-30 00:19:54 +02:00
el-u 96277fe9be lufia2ac: update CollectionRule import (#5936) 2026-03-29 23:37:53 +02:00
XxDERProjectxX a7a7879df4 Satisfactory: bug fix in __init__.py (#5930)
Solved indentation error to return to intended functionality
2026-03-29 23:34:21 +02:00
Alchav 773f3c4f08 Super Mario Land 2: Fix Space Zone 2 Logic (#6025) 2026-03-29 23:25:46 +02:00
agilbert1412 139856a573 Stardew Valley: Fixed an issue where some specific option combinations could create more items than locations (#6012)
* - Improved the dynamic locations count algorithm to take into account the nature of various heavy settings in both directions

* - Fixes from Code Review

* - We're only testing for sunday locations, might as well only take sunday locations in the list to test

* - One more slight optimization

* - Added consideration for bundles per room in filler locations counting

* - Registered some more IDs to handle items up to 10
2026-03-29 23:21:29 +02:00
Noa Aarts a1ed804267 Stardew Valley: trimmed lucky purple shorts need gold to make (#6034)
The current logic only requires the shorts and a sewing machine, but a
gold bar is also necessary
2026-03-29 23:20:24 +02:00
agilbert1412 2d58e7953c Stardew valley: Four small fixes (#6055)
* - Fixed the Dr Seuss Bundle asking for tigerseye (mineral) instead of tiger trout (fish)

* - Made blue grass starter more consistent

* - Fragments of the past does not rely on ginger island

* - Removed legacy hard coded strange bun recipe that messed with chefsanity logic
2026-03-29 23:20:00 +02:00
Flit 393ed51203 Messenger: Require Wingsuit to traverse Dark Cave (#6059) 2026-03-29 23:16:34 +02:00
Mysteryem 03c9d0717b Muse Dash: Fix nondeterministic generation with include_songs (#6040)
The include_songs option is an OptionSet, whose value is a set, but was being iterated to produce self.included_songs. Sets are unordered and may have a different iteration order each time a python process is run. This meant that the order of the elements in self.included_songs could differ even when generating with a fixed seed.

This caused nondeterministic generation with the same seed because create_song_pool() deterministically randomly picks songs from self.included_songs, which could be in a different order each time, so different songs could be picked.
2026-03-29 23:12:25 +02:00
Bryce Wilson 5ca50cd8d3 Pokemon Emerald: Fix Latios KeyError (#6056) 2026-03-29 23:10:16 +02:00
Sebastian 36cf86f2e8 Docs: update macOS setup instructions for more specificity on Python version (#6078) 2026-03-29 21:18:03 +02:00
Duck 1705620c4f Launcher: Add konsole to terminal list and rework launch dialog (#5684)
* Make component launching indicate if no terminal window, add konsole

* Attempt to spell better and remove whitespace

* Update terminal priority

* Make helper for clearing LD_LIBRARY_PATH

* Add handling to linux launch

* Hopefully fix setter

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2026-03-29 20:07:55 +02:00
black-sliver ffe4c6dd15 Core, Webhost: update and pin dependency versions (#6075) 2026-03-29 19:44:29 +02:00
black-sliver cf47cc67c0 Clients: remove datapackage from persistent_storage ... (#6074)
... next time it gets written to.
This makes loading peristent_storage faster in the future.
2026-03-29 19:43:26 +02:00
Ian Robinson 645f25a94e setup.py: add rule_builder.cached_world to included list (#6070) 2026-03-29 18:29:37 +02:00
qwint 74f41e3733 Core: Make Generate.main only init logging on __main__ (#6069) 2026-03-28 00:58:36 +01:00
Phaneros 4276c6d6b0 sc2: Fixing random fill errors in unit tests (#6045) 2026-03-27 22:45:38 +01:00
Justus Lind 116ab2286a Muse Dash: Add support for Wuthering Waves Pioneer Podcast and Ay-Aye Horse (#6071) 2026-03-27 18:36:36 +01:00
ubufugu 9246bd9541 merge upstream 2026-03-18 16:58:13 -07:00
Ian Robinson fb45a2f87e Rule Builder: Fix count resolution when Oring HasAnyCount (#6048) 2026-03-18 18:54:17 +01:00
Fabian Dill 2e5356ad05 Core: other resources guide (#6043)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com>
2026-03-18 03:30:22 +01:00
lepideble 8457ff3e4b Factorio: only show fluid boxes on assembling machine 1 when the selected recipe needs fluids (#5412) 2026-03-16 15:17:54 +01:00
Fabian Dill 70fc3e05fb Webhost: port reuse fix & configurable max room timeout (#6033)
* WebHost: make autolauncher max room timeout configurable

* WebHost: launch rooms with assigned port first
2026-03-12 02:48:45 +01:00
Duck d01c9577ab CommonClient: Add explicit message for connection timeout (#5842)
* Change timeout and add timeout-specific message

* Revert open_timeout
2026-03-11 23:46:59 +01:00
qwint 260bae359d Core: Update .gitignore to include an exe setup.py downloads (#6031) 2026-03-11 21:37:00 +01:00
Mysteryem 3016379b85 KH2: Fix nondeterministic generation when CasualBounties is enabled (#5967)
When CasualBounties was enabled, the location names in
`exclusion_table["HitlistCasual"]` would be iterated into
`self.random_super_boss_list` in `generate_early`, but
`exclusion_table["HitlistCasual"]` was a `set`, so its iteration order
would vary on each generation, even with same seed.

Random location names would be picked from `self.random_super_boss_list`
to place Bounty items at, so different locations could be picked on each
generation with the same seed.

`exclusion_table["Hitlist"]` is similar and was already a `list`,
avoiding the issue of nondeterministic iteration order, so
`exclusion_table["HitlistCasual"]` has been changed to a `list` to
match.
2026-03-10 23:06:44 +01:00
ubufugu 30fa0658b0 remove attestation from docker workflow
I don't know what this is or what value this adds, so removing it for now as it doesn't work on gitea
2026-02-23 19:04:15 -08:00
ubufugu 44a0c44036 update docker.yml to create and publish a docker image to dockerhub 2026-02-23 18:54:19 -08:00
2577 changed files with 1075455 additions and 6556 deletions
-1
View File
@@ -51,7 +51,6 @@ EnemizerCLI/
/SNI/
/sni-*/
/appimagetool*
/host.yaml
/options.yaml
/config.yaml
/logs/
+1
View File
@@ -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",
+4 -2
View File
@@ -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
View File
@@ -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
+16 -8
View File
@@ -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
+3 -1
View File
@@ -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
+21 -122
View File
@@ -11,144 +11,43 @@ on:
- "!.github/workflows/**"
- ".github/workflows/docker.yml"
branches:
- "main"
- "dock-dev"
tags:
- "v?[0-9]+.[0-9]+.[0-9]*"
workflow_dispatch:
env:
REGISTRY: ghcr.io
jobs:
prepare:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
outputs:
image-name: ${{ steps.image.outputs.name }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
package-name: ${{ steps.package.outputs.name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set lowercase image name
id: image
run: |
echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
- name: Set package name
id: package
run: |
echo "name=$(basename ${GITHUB_REPOSITORY,,})" >> $GITHUB_OUTPUT
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
tags: |
type=ref,event=branch,enable={{is_not_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=nightly,enable={{is_default_branch}}
- name: Compute final tags
id: final-tags
run: |
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
if [[ "${{ github.ref_type }}" == "tag" ]]; then
tag="${{ github.ref_name }}"
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
full_latest="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:latest"
# Check if latest is already in tags to avoid duplicates
if ! printf '%s\n' "${tags[@]}" | grep -q "^$full_latest$"; then
tags+=("$full_latest")
fi
fi
fi
# Set multiline output
echo "tags<<EOF" >> $GITHUB_OUTPUT
printf '%s\n' "${tags[@]}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
build:
needs: prepare
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- platform: amd64
runner: ubuntu-latest
suffix: amd64
cache-scope: amd64
- platform: arm64
runner: ubuntu-24.04-arm
suffix: arm64
cache-scope: arm64
contents: read
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check out the repo
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Compute suffixed tags
id: tags
run: |
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
suffixed=()
for t in "${tags[@]}"; do
suffixed+=("$t-${{ matrix.suffix }}")
done
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ubufugu/dockipelago
- name: Build and push Docker image
uses: docker/build-push-action@v5
id: push
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
file: ./Dockerfile
platforms: linux/${{ matrix.platform }}
push: true
tags: ${{ steps.tags.outputs.tags }}
labels: ${{ needs.prepare.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.cache-scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
provenance: false
manifest:
needs: [prepare, build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push multi-arch manifest
run: |
readarray -t tag_array <<< "${{ needs.prepare.outputs.tags }}"
for tag in "${tag_array[@]}"; do
docker manifest create "$tag" \
"$tag-amd64" \
"$tag-arm64"
docker manifest push "$tag"
done
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1 -1
View File
@@ -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:
+6 -6
View File
@@ -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
@@ -114,14 +114,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 +157,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
+7 -3
View File
@@ -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
+4 -2
View File
@@ -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"
+6 -4
View File
@@ -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
+4
View File
@@ -45,7 +45,11 @@ EnemizerCLI/
/SNI/
/sni-*/
/appimagetool*
<<<<<<< Updated upstream
/VC_redist.x64.exe
/host.yaml
=======
>>>>>>> Stashed changes
/options.yaml
/config.yaml
/logs/
+3 -1
View File
@@ -773,7 +773,7 @@ class CommonContext:
if len(parts) == 1:
parts = title.split(', ', 1)
if len(parts) > 1:
text = parts[1] + '\n\n' + text
text = f"{parts[1]}\n\n{text}" if text else parts[1]
title = parts[0]
# display error
self._messagebox = MessageBox(title, text, error=True)
@@ -896,6 +896,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
"May not be running Archipelago on that address or port.")
except websockets.InvalidURI:
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
except asyncio.TimeoutError:
ctx.handle_connection_loss("Failed to connect to the multiworld server. Connection timed out.")
except OSError:
ctx.handle_connection_loss("Failed to connect to the multiworld server")
except Exception:
+6
View File
@@ -97,4 +97,10 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
# Ensure no runtime ModuleUpdate.
ENV SKIP_REQUIREMENTS_UPDATE=true
# Port range for Archipelago rooms. I choose only ports 49152-49162
ARG MAX_PORT=49162
RUN sed -i "s/65535/${MAX_PORT}/" WebHostLib/customserver.py
EXPOSE 80
ENTRYPOINT [ "python", "WebHost.py" ]
+2 -1
View File
@@ -87,7 +87,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)
+29 -20
View File
@@ -29,8 +29,8 @@ 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')
@@ -52,10 +52,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 +103,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 +196,32 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
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:
@@ -406,12 +410,17 @@ 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()
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. """
+89 -94
View File
@@ -44,9 +44,8 @@ import NetUtils
import Utils
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType, LocationStore, MultiData, Hint, HintStatus, GamesPackage
SlotType, LocationStore, MultiData, Hint, HintStatus
from BaseClasses import ItemClassification
from apmw.multiserver.gamespackagecache import GamesPackageCache
min_client_version = Version(0, 5, 0)
@@ -242,38 +241,21 @@ class Context:
slot_info: typing.Dict[int, NetworkSlot]
generator_version = Version(0, 0, 0)
checksums: typing.Dict[str, str]
played_games: set[str]
item_names: typing.Dict[str, typing.Dict[int, str]]
item_name_groups: typing.Dict[str, typing.Dict[str, list[str]]]
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
location_names: typing.Dict[str, typing.Dict[int, str]]
location_name_groups: typing.Dict[str, typing.Dict[str, list[str]]]
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
non_hintable_names: typing.Dict[str, typing.AbstractSet[str]]
spheres: typing.List[typing.Dict[int, typing.Set[int]]]
""" each sphere is { player: { location_id, ... } } """
games_package_cache: GamesPackageCache
logger: logging.Logger
def __init__(
self,
host: str,
port: int,
server_password: str,
password: str,
location_check_points: int,
hint_cost: int,
item_cheat: bool,
release_mode: str = "disabled",
collect_mode="disabled",
countdown_mode: str = "auto",
remaining_mode: str = "disabled",
auto_shutdown: typing.SupportsFloat = 0,
compatibility: int = 2,
log_network: bool = False,
games_package_cache: GamesPackageCache | None = None,
logger: logging.Logger = logging.getLogger(),
) -> None:
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
countdown_mode: str = "auto", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0,
compatibility: int = 2, log_network: bool = False, logger: logging.Logger = logging.getLogger()):
self.logger = logger
super(Context, self).__init__()
self.slot_info = {}
@@ -324,7 +306,6 @@ class Context:
self.save_dirty = False
self.tags = ['AP']
self.games: typing.Dict[int, str] = {}
self.played_games = set()
self.minimum_client_versions: typing.Dict[int, Version] = {}
self.seed_name = ""
self.groups = {}
@@ -334,10 +315,9 @@ class Context:
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
self.read_data = {}
self.spheres = []
self.games_package_cache = games_package_cache or GamesPackageCache()
# init empty to satisfy linter, I suppose
self.reduced_games_package = {}
self.gamespackage = {}
self.checksums = {}
self.item_name_groups = {}
self.location_name_groups = {}
@@ -349,11 +329,50 @@ class Context:
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))
self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data()
# Data package retrieval
def _load_game_data(self):
import worlds
self.gamespackage = worlds.network_data_package["games"]
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
self.location_name_groups = {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.non_hintable_names[world_name] = world.hint_blacklist
for game_package in self.gamespackage.values():
# remove groups from data sent to clients
del game_package["item_name_groups"]
del game_package["location_name_groups"]
def _init_game_data(self):
for game_name, game_package in self.gamespackage.items():
if "checksum" in game_package:
self.checksums[game_name] = game_package["checksum"]
for item_name, item_id in game_package["item_name_to_id"].items():
self.item_names[game_name][item_id] = item_name
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[game_name][location_id] = location_name
self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
self.all_location_and_group_names[game_name] = \
set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, []))
archipelago_item_names = self.item_names["Archipelago"]
archipelago_location_names = self.location_names["Archipelago"]
for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]:
# Add Archipelago items and locations to each data package.
self.item_names[game].update(archipelago_item_names)
self.location_names[game].update(archipelago_location_names)
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.reduced_games_package[game]["item_name_to_id"] if game in self.reduced_games_package else None
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
return self.reduced_games_package[game]["location_name_to_id"] if game in self.reduced_games_package else None
return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None
# General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
@@ -463,17 +482,19 @@ class Context:
with open(multidatapath, 'rb') as f:
data = f.read()
self._load(self.decompress(data), use_embedded_server_options)
self._load(self.decompress(data), {}, use_embedded_server_options)
self.data_filename = multidatapath
@staticmethod
def decompress(data: bytes) -> typing.Any:
def decompress(data: bytes) -> dict:
format_version = data[0]
if format_version > 3:
raise Utils.VersionException("Incompatible multidata.")
return restricted_loads(zlib.decompress(data[1:]))
def _load(self, decoded_obj: MultiData, use_embedded_server_options: bool) -> None:
def _load(self, decoded_obj: MultiData, game_data_packages: typing.Dict[str, typing.Any],
use_embedded_server_options: bool):
self.read_data = {}
# there might be a better place to put this.
race_mode = decoded_obj.get("race_mode", 0)
@@ -494,7 +515,6 @@ class Context:
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.played_games = {"Archipelago"} | {self.games[x] for x in range(1, len(self.games) + 1)}
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
if slot_info.type == SlotType.group}
@@ -539,11 +559,18 @@ class Context:
server_options = decoded_obj.get("server_options", {})
self._set_options(server_options)
# load and apply world data and (embedded) data package
self._load_world_data()
self._load_data_package(decoded_obj.get("datapackage", {}))
# embedded data package
for game_name, data in decoded_obj.get("datapackage", {}).items():
if game_name in game_data_packages:
data = game_data_packages[game_name]
self.logger.info(f"Loading embedded data package for game {game_name}")
self.gamespackage[game_name] = data
self.item_name_groups[game_name] = data["item_name_groups"]
if "location_name_groups" in data:
self.location_name_groups[game_name] = data["location_name_groups"]
del data["location_name_groups"]
del data["item_name_groups"] # remove from data package, but keep in self.item_name_groups
self._init_game_data()
for game_name, data in self.item_name_groups.items():
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
for game_name, data in self.location_name_groups.items():
@@ -552,55 +579,6 @@ class Context:
# sorted access spheres
self.spheres = decoded_obj.get("spheres", [])
def _load_world_data(self) -> None:
import worlds
for world_name, world in worlds.AutoWorldRegister.world_types.items():
# TODO: move hint_blacklist into GamesPackage?
self.non_hintable_names[world_name] = world.hint_blacklist
def _load_data_package(self, data_package: dict[str, GamesPackage]) -> None:
"""Populates reduced_games_package, item_name_groups, location_name_groups from static data and data_package"""
# NOTE: for worlds loaded from db, only checksum is set in GamesPackage, but this is handled by cache
for game_name in sorted(self.played_games):
if game_name in data_package:
self.logger.info(f"Loading embedded data package for game {game_name}")
data = self.games_package_cache.get(game_name, data_package[game_name])
else:
# NOTE: we still allow uploading a game without datapackage. Once that is changed, we could drop this.
data = self.games_package_cache.get_static(game_name)
(
self.reduced_games_package[game_name],
self.item_name_groups[game_name],
self.location_name_groups[game_name],
) = data
del self.games_package_cache # Not used past this point. Free memory.
def _init_game_data(self) -> None:
"""Update internal values from previously loaded data packages"""
for game_name, game_package in self.reduced_games_package.items():
if game_name not in self.played_games:
continue
if "checksum" in game_package:
self.checksums[game_name] = game_package["checksum"]
# NOTE: we could save more memory by moving the stuff below to data package cache as well
for item_name, item_id in game_package["item_name_to_id"].items():
self.item_names[game_name][item_id] = item_name
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[game_name][location_id] = location_name
self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
self.all_location_and_group_names[game_name] = \
set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, []))
archipelago_item_names = self.item_names["Archipelago"]
archipelago_location_names = self.location_names["Archipelago"]
for game in [game_name for game_name in self.reduced_games_package if game_name != "Archipelago"]:
# Add Archipelago items and locations to each data package.
self.item_names[game].update(archipelago_item_names)
self.location_names[game].update(archipelago_location_names)
# saving
def save(self, now=False) -> bool:
@@ -941,10 +919,12 @@ async def server(websocket: "ServerConnection", path: str = "/", ctx: Context =
async def on_client_connected(ctx: Context, client: Client):
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
games.add("Archipelago")
await ctx.send_msgs(client, [{
'cmd': 'RoomInfo',
'password': bool(ctx.password),
'games': sorted(ctx.played_games),
'games': games,
# tags are for additional features in the communication.
# Name them by feature or fork, as you feel is appropriate.
'tags': ctx.tags,
@@ -953,7 +933,8 @@ async def on_client_connected(ctx: Context, client: Client):
'permissions': get_permissions(ctx),
'hint_cost': ctx.hint_cost,
'location_check_points': ctx.location_check_points,
'datapackage_checksums': ctx.checksums,
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
in ctx.gamespackage.items() if game in games and "checksum" in game_data},
'seed_name': ctx.seed_name,
'time': time.time(),
}])
@@ -1959,11 +1940,25 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
await ctx.send_msgs(client, reply)
elif cmd == "GetDataPackage":
games = {
name: game_data for name, game_data in ctx.reduced_games_package.items()
if name in set(args.get("games", []))
}
await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": {"games": games}}])
exclusions = args.get("exclusions", [])
if "games" in args:
games = {name: game_data for name, game_data in ctx.gamespackage.items()
if name in set(args.get("games", []))}
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": {"games": games}}])
# TODO: remove exclusions behaviour around 0.5.0
elif exclusions:
exclusions = set(exclusions)
games = {name: game_data for name, game_data in ctx.gamespackage.items()
if name not in exclusions}
package = {"games": games}
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": package}])
else:
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": {"games": ctx.gamespackage}}])
elif client.auth:
if cmd == "ConnectUpdate":
+4 -3
View File
@@ -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):
-1
View File
@@ -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
+19 -14
View File
@@ -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
@@ -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 {}
@@ -758,6 +753,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 +778,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
View File
@@ -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"]:
+2 -1
View File
@@ -42,11 +42,12 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["GAME_PORTS"] = ["49152-65535", 0]
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
# maximum time in seconds since last activity for a room to be hosted
app.config["MAX_ROOM_TIMEOUT"] = 259200
# memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
+4 -4
View File
@@ -9,7 +9,7 @@ from threading import Event, Thread
from typing import Any
from uuid import UUID
from pony.orm import db_session, select, commit, PrimaryKey
from pony.orm import db_session, select, commit, PrimaryKey, desc
from Utils import restricted_loads, utcnow
from .locker import Locker, AlreadyRunningException
@@ -129,7 +129,8 @@ def autohost(config: dict):
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= utcnow() - timedelta(days=3))
room.last_activity >= utcnow() - timedelta(
seconds=config["MAX_ROOM_TIMEOUT"])).order_by(desc(Room.last_port))
for room in rooms:
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
@@ -187,7 +188,6 @@ class MultiworldInstance():
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.game_ports = config["GAME_PORTS"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
@@ -198,7 +198,7 @@ class MultiworldInstance():
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host, self.game_ports,
self.cert, self.key, self.host,
self.rooms_to_start, self.rooms_shutting_down),
name=self.name)
process.start()
+88 -178
View File
@@ -4,7 +4,6 @@ import asyncio
import collections
import datetime
import functools
import itertools
import logging
import multiprocessing
import pickle
@@ -14,9 +13,7 @@ import threading
import time
import typing
import sys
from asyncio import AbstractEventLoop
import psutil
import websockets
from pony.orm import commit, db_session, select
@@ -27,10 +24,8 @@ from MultiServer import (
server_per_message_deflate_factory,
)
from Utils import restricted_loads, cache_argsless
from NetUtils import GamesPackage
from apmw.webhost.customserver.gamespackagecache import DBGamesPackageCache
from .locker import Locker
from .models import Command, Room, db
from .models import Command, GameDataPackage, Room, db
class CustomClientMessageProcessor(ClientMessageProcessor):
@@ -67,39 +62,18 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context):
room_id: int
video: dict[tuple[int, int], tuple[str, str]]
main_loop: AbstractEventLoop
static_server_data: StaticServerData
def __init__(
self,
static_server_data: StaticServerData,
games_package_cache: DBGamesPackageCache,
logger: logging.Logger,
) -> None:
def __init__(self, static_server_data: dict, logger: logging.Logger):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
super(WebHostContext, self).__init__(
"",
0,
"",
"",
1,
40,
True,
"enabled",
"enabled",
"enabled",
0,
2,
games_package_cache=games_package_cache,
logger=logger,
)
self.tags = ["AP", "WebHost"]
self.video = {}
self.main_loop = asyncio.get_running_loop()
self.static_server_data = static_server_data
self.games_package_cache = games_package_cache
super(WebHostContext, self).__init__("", 0, "", "", 1,
40, True, "enabled", "enabled",
"enabled", 0, 2, logger=logger)
del self.static_server_data
self.main_loop = asyncio.get_running_loop()
self.video = {}
self.tags = ["AP", "WebHost"]
def __del__(self):
try:
@@ -109,6 +83,12 @@ class WebHostContext(Context):
except ImportError:
self.logger.debug("Context destroyed")
def _load_game_data(self):
for key, value in self.static_server_data.items():
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
setattr(self, key, value)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
async def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)
@@ -135,17 +115,45 @@ class WebHostContext(Context):
if room.last_port:
self.port = room.last_port
else:
self.port = 0
self.port = get_random_port()
multidata = self.decompress(room.seed.multidata)
return self._load(multidata, True)
game_data_packages = {}
def _load_world_data(self):
# Use static_server_data, but skip static data package since that is in cache anyway.
# Also NOT importing worlds here!
# FIXME: does this copy the non_hintable_names (also for games not part of the room)?
self.non_hintable_names = collections.defaultdict(frozenset, self.static_server_data["non_hintable_names"])
del self.static_server_data # Not used past this point. Free memory.
static_gamespackage = self.gamespackage # this is shared across all rooms
static_item_name_groups = self.item_name_groups
static_location_name_groups = self.location_name_groups
self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load
self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})}
self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})}
missing_checksum = False
for game in list(multidata.get("datapackage", {})):
game_data = multidata["datapackage"][game]
if "checksum" in game_data:
if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
# non-custom. remove from multidata and use static data
# games package could be dropped from static data once all rooms embed data package
del multidata["datapackage"][game]
else:
row = GameDataPackage.get(checksum=game_data["checksum"])
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete
game_data_packages[game] = restricted_loads(row.data)
continue
else:
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
else:
missing_checksum = True # Game rolled on old AP and will load data package from multidata
self.gamespackage[game] = static_gamespackage.get(game, {})
self.item_name_groups[game] = static_item_name_groups.get(game, {})
self.location_name_groups[game] = static_location_name_groups.get(game, {})
if not game_data_packages and not missing_checksum:
# all static -> use the static dicts directly
self.gamespackage = static_gamespackage
self.item_name_groups = static_item_name_groups
self.location_name_groups = static_location_name_groups
return self._load(multidata, game_data_packages, True)
def init_save(self, enabled: bool = True):
self.saving = enabled
@@ -173,117 +181,38 @@ class WebHostContext(Context):
return d
class GameRangePorts(typing.NamedTuple):
parsed_ports: list[range]
weights: list[int]
ephemeral_allowed: bool
@functools.cache
def parse_game_ports(game_ports: tuple[str | int, ...]) -> GameRangePorts:
parsed_ports: list[range] = []
weights: list[int] = []
ephemeral_allowed = False
total_length = 0
for item in game_ports:
if isinstance(item, str) and "-" in item:
start, end = map(int, item.split("-"))
x = range(start, end + 1)
total_length += len(x)
weights.append(total_length)
parsed_ports.append(x)
elif int(item) == 0:
ephemeral_allowed = True
else:
total_length += 1
weights.append(total_length)
num = int(item)
parsed_ports.append(range(num, num + 1))
return GameRangePorts(parsed_ports, weights, ephemeral_allowed)
def weighted_random(ranges: list[range], cum_weights: list[int]) -> int:
[picked] = random.choices(ranges, cum_weights=cum_weights)
return random.randrange(picked.start, picked.stop, picked.step)
def create_random_port_socket(game_ports: tuple[str | int, ...], host: str) -> socket.socket:
parsed_ports, weights, ephemeral_allowed = parse_game_ports(game_ports)
used_ports = get_used_ports()
i = 1024 if len(parsed_ports) > 0 else 0
while i > 0:
port_num = weighted_random(parsed_ports, weights)
if port_num in used_ports:
used_ports = get_used_ports()
continue
i -= 0
try:
return socket.create_server((host, port_num))
except OSError:
pass
if ephemeral_allowed:
return socket.create_server((host, 0))
raise OSError(98, "No available ports")
def try_conns_per_process(p: psutil.Process) -> typing.Iterable[int]:
try:
return (c.laddr.port for c in p.net_connections("tcp4"))
except psutil.AccessDenied:
return ()
def get_active_net_connections() -> typing.Iterable[int]:
# Don't even try to check if system using AIX
if psutil.AIX:
return ()
try:
return (c.laddr.port for c in psutil.net_connections("tcp4"))
# raises AccessDenied when done on macOS
except psutil.AccessDenied:
# flatten the list of iterables
return itertools.chain.from_iterable(map(
# get the net connections of the process and then map its ports
try_conns_per_process,
# this method has caching handled by psutil
psutil.process_iter(["net_connections"])
))
def get_used_ports():
last_used_ports: tuple[frozenset[int], float] | None = getattr(get_used_ports, "last", None)
t_hash = round(time.time() / 90) # cache for 90 seconds
if last_used_ports is None or last_used_ports[1] != t_hash:
last_used_ports = (frozenset(get_active_net_connections()), t_hash)
setattr(get_used_ports, "last", last_used_ports)
return last_used_ports[0]
class StaticServerData(typing.TypedDict, total=True):
non_hintable_names: dict[str, typing.AbstractSet[str]]
games_package: dict[str, GamesPackage]
def get_random_port():
return random.randint(49152, 65535)
@cache_argsless
def get_static_server_data() -> StaticServerData:
def get_static_server_data() -> dict:
import worlds
return {
data = {
"non_hintable_names": {
world_name: world.hint_blacklist
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
"games_package": worlds.network_data_package["games"]
"gamespackage": {
world_name: {
key: value
for key, value in game_package.items()
if key not in ("item_name_groups", "location_name_groups")
}
for world_name, game_package in worlds.network_data_package["games"].items()
},
"item_name_groups": {
world_name: world.item_name_groups
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
"location_name_groups": {
world_name: world.location_name_groups
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
}
return data
def set_up_logging(room_id) -> logging.Logger:
import os
@@ -316,19 +245,9 @@ def tear_down_logging(room_id):
del logging.Logger.manager.loggerDict[logger_name]
def run_server_process(
name: str,
ponyconfig: dict[str, typing.Any],
static_server_data: StaticServerData,
cert_file: typing.Optional[str],
cert_key_file: typing.Optional[str],
host: str,
game_ports: typing.Iterable[str | int],
rooms_to_run: multiprocessing.Queue,
rooms_shutting_down: multiprocessing.Queue,
) -> None:
import gc
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
from setproctitle import setproctitle
setproctitle(name)
@@ -344,11 +263,6 @@ def run_server_process(
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
del resource, file_limit
# prime the data package cache with static data
games_package_cache = DBGamesPackageCache(static_server_data["games_package"])
# convert to tuple because its hashable
game_ports = tuple(game_ports)
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
@@ -356,6 +270,8 @@ def run_server_process(
if "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded in the custom server.")
import gc
if not cert_file:
def get_ssl_context():
return None
@@ -380,30 +296,24 @@ def run_server_process(
with Locker(f"RoomLocker {room_id}"):
try:
logger = set_up_logging(room_id)
ctx = WebHostContext(static_server_data, games_package_cache, logger)
ctx = WebHostContext(static_server_data, logger)
ctx.load(room_id)
ctx.init_save()
assert ctx.server is None
if ctx.port != 0:
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
ctx.host,
ctx.port,
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
await ctx.server
except OSError:
ctx.port = 0
if ctx.port == 0:
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
sock=create_random_port_socket(game_ports, ctx.host),
ctx.host,
ctx.port,
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
@@ -478,7 +388,7 @@ def run_server_process(
def run(self):
while 1:
next_room = rooms_to_run.get(block=True, timeout=None)
next_room = rooms_to_run.get(block=True, timeout=None)
gc.collect()
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
self._tasks.append(task)
+12 -12
View File
@@ -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.0
docutils==0.22.4
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

-11
View File
@@ -33,17 +33,6 @@ html{
z-index: 10;
}
#landing-header h5 {
color: #ffffff;
font-style: italic;
font-size: 28px;
margin-top: 15px;
margin-bottom: -43px;
text-shadow: 1px 1px 7px #000000;
font-kerning: none;
z-index: 10;
}
#landing-links{
margin-left: auto;
margin-right: auto;
+1 -1
View File
@@ -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>
-
+2 -3
View File
@@ -11,7 +11,7 @@
<div id="landing-wrapper">
<div id="landing-header">
<img id="landing-logo" src="static/static/branding/landing-logo.png" alt="Archipelago Logo" />
<h4>multiworld multi-game randomizer</h4><h5>beta</h5>
<h4>multiworld multi-game randomizer</h4>
</div>
<div id="landing-links">
<a href="/games" id="far-left-button">Supported<br />Games</a>
@@ -35,8 +35,7 @@
</div>
<div id="landing" class="grass-island">
<div id="landing-body">
<p id="first-line">Welcome to Archipelago Beta!</p>
<p>For the stable version, visit <a href="//archipelago.gg">Archipelago.gg</a>!</p>
<p id="first-line">Welcome to Archipelago!</p>
<p>
This is a cross-game modification system which randomizes different games, then uses the result to
build a single unified multi-player game. Items from one game may be present in another, and
-1
View File
@@ -21,7 +21,6 @@
</div>
{% endif %}
{% endwith %}
<div class="user-message">This is the beta site! For the stable version, visit <a href="https://archipelago.gg">Archipelago.gg</a>!</div>
{% block body %}
{% endblock %}
+3 -1
View File
@@ -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">
+1 -5
View File
@@ -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>
-96
View File
@@ -1,96 +0,0 @@
import typing as t
from weakref import WeakValueDictionary
from NetUtils import GamesPackage
GameAndChecksum = tuple[str, str | None]
ItemNameGroups = dict[str, list[str]]
LocationNameGroups = dict[str, list[str]]
K = t.TypeVar("K")
V = t.TypeVar("V")
class DictLike(dict[K, V]):
__slots__ = ("__weakref__",)
class GamesPackageCache:
# NOTE: this uses 3 separate collections because unpacking the get() result would end the container lifetime
_reduced_games_packages: WeakValueDictionary[GameAndChecksum, GamesPackage]
"""Does not include item_name_groups nor location_name_groups"""
_item_name_groups: WeakValueDictionary[GameAndChecksum, dict[str, list[str]]]
_location_name_groups: WeakValueDictionary[GameAndChecksum, dict[str, list[str]]]
def __init__(self) -> None:
self._reduced_games_packages = WeakValueDictionary()
self._item_name_groups = WeakValueDictionary()
self._location_name_groups = WeakValueDictionary()
def _get(
self,
cache_key: GameAndChecksum,
) -> tuple[GamesPackage | None, ItemNameGroups | None, LocationNameGroups | None]:
if cache_key[1] is None:
return None, None, None
return (
self._reduced_games_packages.get(cache_key, None),
self._item_name_groups.get(cache_key, None),
self._location_name_groups.get(cache_key, None),
)
def get(
self,
game: str,
full_games_package: GamesPackage,
) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
"""Loads and caches embedded data package provided by multidata"""
cache_key = (game, full_games_package.get("checksum", None))
cached_reduced_games_package, cached_item_name_groups, cached_location_name_groups = self._get(cache_key)
if cached_reduced_games_package is None:
cached_reduced_games_package = t.cast(
t.Any,
DictLike(
{
"item_name_to_id": full_games_package["item_name_to_id"],
"location_name_to_id": full_games_package["location_name_to_id"],
"checksum": full_games_package.get("checksum", None),
}
),
)
if cache_key[1] is not None: # only cache if checksum is available
self._reduced_games_packages[cache_key] = cached_reduced_games_package
if cached_item_name_groups is None:
# optimize strings to be references instead of copies
item_names = {name: name for name in cached_reduced_games_package["item_name_to_id"].keys()}
cached_item_name_groups = DictLike(
{
group_name: [item_names.get(item_name, item_name) for item_name in group_items]
for group_name, group_items in full_games_package["item_name_groups"].items()
}
)
if cache_key[1] is not None: # only cache if checksum is available
self._item_name_groups[cache_key] = cached_item_name_groups
if cached_location_name_groups is None:
# optimize strings to be references instead of copies
location_names = {name: name for name in cached_reduced_games_package["location_name_to_id"].keys()}
cached_location_name_groups = DictLike(
{
group_name: [location_names.get(location_name, location_name) for location_name in group_locations]
for group_name, group_locations in full_games_package.get("location_name_groups", {}).items()
}
)
if cache_key[1] is not None: # only cache if checksum is available
self._location_name_groups[cache_key] = cached_location_name_groups
return cached_reduced_games_package, cached_item_name_groups, cached_location_name_groups
def get_static(self, game: str) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
"""Loads legacy data package from installed worlds"""
import worlds
return self.get(game, worlds.network_data_package["games"][game])
@@ -1,42 +0,0 @@
from typing_extensions import override
from NetUtils import GamesPackage
from Utils import restricted_loads
from apmw.multiserver.gamespackagecache import GamesPackageCache, ItemNameGroups, LocationNameGroups
class DBGamesPackageCache(GamesPackageCache):
_static: dict[str, tuple[GamesPackage, ItemNameGroups, LocationNameGroups]]
def __init__(self, static_games_package: dict[str, GamesPackage]) -> None:
super().__init__()
self._static = {
game: GamesPackageCache.get(self, game, games_package)
for game, games_package in static_games_package.items()
}
@override
def get(
self,
game: str,
full_games_package: GamesPackage,
) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
# for games started on webhost, full_games_package is likely unpopulated and only has the checksum field
cache_key = (game, full_games_package.get("checksum", None))
cached = self._get(cache_key)
if any(value is None for value in cached):
if "checksum" not in full_games_package:
return super().get(game, full_games_package) # no checksum, assume fully populated
from WebHostLib.models import GameDataPackage
row: GameDataPackage | None = GameDataPackage.get(checksum=full_games_package["checksum"])
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8 ...
return super().get(game, restricted_loads(row.data))
return super().get(game, full_games_package) # ... in which case full_games_package should be populated
return cached # type: ignore # mypy doesn't understand any value is None
@override
def get_static(self, game: str) -> tuple[GamesPackage, ItemNameGroups, LocationNameGroups]:
return self._static[game]
-5
View File
@@ -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
-6
View File
@@ -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]
+36
View File
@@ -129,6 +129,42 @@ common_rule_only_on_easy = common_rule & easy_filter
common_rule_skipped_on_easy = common_rule | easy_filter
```
### Field resolvers
When creating rules you may sometimes need to set a field to a value that depends on the world instance. You can use a `FieldResolver` to define how to populate that field when the rule is being resolved.
There are two build-in field resolvers:
- `FromOption`: Resolves to the value of the given option
- `FromWorldAttr`: Resolves to the value of the given world instance attribute, can specify a dotted path `a.b.c` to get a nested attribute or dict item
```python
world.options.mcguffin_count = 5
world.precalculated_value = 99
rule = (
Has("A", count=FromOption(McguffinCount))
| HasGroup("Important items", count=FromWorldAttr("precalculated_value"))
)
# Results in Has("A", count=5) | HasGroup("Important items", count=99)
```
You can define your own resolvers by creating a class that inherits from `FieldResolver`, provides your game name, and implements a `resolve` function:
```python
@dataclasses.dataclass(frozen=True)
class FromCustomResolution(FieldResolver, game="MyGame"):
modifier: str
@override
def resolve(self, world: "World") -> Any:
return some_math_calculation(world, self.modifier)
rule = Has("Combat Level", count=FromCustomResolution("combat"))
```
If you want to support rule serialization and your resolver contains non-serializable properties you may need to override `to_dict` or `from_dict`.
## Enabling caching
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
-1
View File
@@ -108,7 +108,6 @@ Example:
```json
{
...
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
"Factorio":"a699194a9589db3ebc0d821915864b422c782f44",
...
}
-6
View File
@@ -17,12 +17,6 @@
# Web hosting port
#PORT: 80
# Ports used for game hosting. Values can be specific ports, port ranges or both. Default is: [49152-65535, 0]
# Zero means it will use a random free port if there is no port in the next 1024 randomly chosen ports from the range
# Examples of valid values: [40000-41000, 49152-65535]
# If ports within the range(s) are already in use, the WebHost will fallback to the default [49152-65535, 0] range.
#GAME_PORTS: [49152-65535, 0]
# Place where uploads go.
#UPLOAD_FOLDER: uploads
+586
View File
@@ -0,0 +1,586 @@
general_options:
# Where to place output files
output_path: "output"
# Options for MultiServer
# Null means nothing, for the server this means to default the value
# These overwrite command line arguments!
server_options:
host: null
port: 38281
password: null
multidata: null
savefile: null
disable_save: false
loglevel: "info"
logtime: false
# Allows for clients to log on and manage the server. If this is null, no remote administration is possible.
server_password: null
# Disallow !getitem
disable_item_cheat: false
# Client hint system
# Points given to a player for each acquired item in their world
location_check_points: 1
# Relative point cost to receive a hint via !hint for players
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint,
# for a total of 5
hint_cost: 10
# Release modes
# A Release sends out the remaining items *from* a world that releases
# "disabled" -> clients can't release,
# "enabled" -> clients can always release
# "auto" -> automatic release on goal completion
# "auto-enabled" -> automatic release on goal completion and manual release is also enabled
# "goal" -> release is allowed after goal completion
release_mode: "auto"
# Collect modes
# A Collect sends the remaining items *to* a world that collects
# "disabled" -> clients can't collect,
# "enabled" -> clients can always collect
# "auto" -> automatic collect on goal completion
# "auto-enabled" -> automatic collect on goal completion and manual collect is also enabled
# "goal" -> collect is allowed after goal completion
collect_mode: "auto"
# Remaining modes
# !remaining handling, that tells a client which items remain in their pool
# "enabled" -> Client can always ask for remaining items
# "disabled" -> Client can never ask for remaining items
# "goal" -> Client can ask for remaining items after goal completion
remaining_mode: "goal"
# Countdown modes
# Determines whether or not a player can initiate a countdown with !countdown
# Note that /countdown is always available to the host.
# "enabled" -> Client can always initiate a countdown with !countdown.
# "disabled" -> Client can never initiate a countdown with !countdown.
# "auto" -> !countdown will be available for any room with less than 30 slots.
countdown_mode: "auto"
# Automatically shut down the server after this many seconds without new location checks, 0 to keep running
auto_shutdown: 0
# Compatibility handling
# 2 -> Recommended for casual/cooperative play, attempt to be compatible with everything across all versions
# 1 -> No longer in use, kept reserved in case of future use
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
compatibility: 2
# log all server traffic, mostly for dev use
log_network: 0
# Options for Generation
generator:
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
enemizer_path: "EnemizerCLI/EnemizerCLI.Core"
# Folder from which the player yaml files are pulled from
player_files_path: "Players"
# amount of players, 0 to infer from player files
players: 0
# general weights file, within the stated player_files_path location
# gets used if players is higher than the amount of per-player files found to fill remaining slots
weights_file_path: "weights.yaml"
# Meta file name, within the stated player_files_path location
meta_file_path: "meta.yaml"
# Create a spoiler file
# 0 -> None
# 1 -> Spoiler without playthrough or paths to playthrough required items
# 2 -> Spoiler with playthrough (viable solution to goals)
# 3 -> Spoiler with playthrough and traversal paths towards items
spoiler: 3
# Create encrypted race roms and flag games as race mode
race: 0
# List of options that can be plando'd. Can be combined, for example "bosses, items"
# Available options: bosses, items, texts, connections
plando_options: "bosses, connections, texts"
# What to do if the current item placements appear unsolvable.
# raise -> Raise an exception and abort.
# swap -> Attempt to fix it by swapping prior placements around. (Default)
# start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
panic_method: "swap"
loglevel: "info"
logtime: false
sni_options:
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
sni_path: "SNI"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
snes_rom_start: true
bizhawkclient_options:
# The location of the EmuHawk you want to auto launch patched ROMs with
emuhawk_path: "None"
# Set this to true to autostart a patched ROM in BizHawk with the connector script,
# to false to never open the patched rom automatically,
# or to a path to an external program to open the ROM file with that instead.
rom_start: true
adventure_options:
# File name of the standard NTSC Adventure rom.
# The licensed "The 80 Classic Games" CD-ROM contains this.
# It may also have a .a26 extension
rom_file: "roms/ADVNTURE.BIN"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program for '.a26'
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
rom_start: true
# Optional, additional args passed into rom_start before the .bin file
# For example, this can be used to autoload the connector script in BizHawk
# (see BizHawk --lua= option)
# Windows example:
# rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"
rom_args: " "
# Set this to true to display item received messages in EmuHawk
display_msgs: true
ape_escape_3_options:
# Preferences for game session management.
# > save_state_on_room_transition: Automatically create a save state when transitioning between rooms.
# > save_state_on_item_received: Automatically create a save state when receiving a new progressive item.
# > save_state_on_location_check: Automatically create a save state when checking a new location.
# > load_state_on_connect: Load a state automatically after connecting to the multiworld if the client
# is already connected to the game and that the last save is from a save state and not a normal game save.
save_state_on_room_transition: false
save_state_on_item_received: true
save_state_on_location_check: false
load_state_on_connect: false
# Preferences for game/client-enforcement behavior
# > auto-equip : Automatically assign received gadgets to a face button
auto_equip: true
# Preferences for game generation. Only relevant for world generation and not the setup of or during play.
# > whitelist_pgc_bypass: Allow Ape Escape 3 players to enable "PGC Bypass" as a possible outcome for
# Lucky Ticket Consolation Prize.
# > whitelist_instant_goal: Allow Ape Escape 3 players to enable "Instant Goal" as a possible outcome for
# Lucky Ticket Consolation Prize.
whitelist_pgc_bypass: false
whitelist_instant_goal: false
banjo_tooie_options:
# File path of the Banjo-Tooie (USA) ROM.
rom_path: ""
# Folder path of where to save the patched ROM.
patch_path: ""
# File path of the program to automatically run.
# Leave blank to disable.
program_path: ""
# Arguments to pass to the automatically run program.
# Leave blank to disable.
# Set to "--lua=" to automatically use the correct path for the lua connector.
program_args: "--lua="
# No idea
clair_obscur_options:
{}
cv64_options:
# File name of the CV64 US 1.0 rom
rom_file: "roms/Castlevania (USA).z64"
cv_dos_options:
# File name of the Castlevania: Dawn of Sorrow ROM file.
rom_file: "roms/CASTLEVANIA1_ACVEA4_00.nds"
cvcotm_options:
# File name of the Castlevania CotM US rom
rom_file: "roms/Castlevania - Circle of the Moon (USA).gba"
cvhodis_options:
# File name of the Castlevania HoD US rom
rom_file: "roms/Castlevania - Harmony of Dissonance (USA).gba"
cvlod_options:
# File name of the CVLoD US rom
rom_file: "Castlevania - Legacy of Darkness (USA).z64"
# Settings for the DK64 randomizer.
dk64_options:
# Choose the release version of the DK64 randomizer to use.
# By setting it to master (Default) you will always pull the latest stable version.
# By setting it to dev you will pull the latest development version.
# If you want a specific version, you can set it to a AP version number eg: v1.0.45
release_branch: "master"
dkc2_options:
# File name of the Donkey Kong Country 2 US v1.1 ROM
rom_file: "roms/Donkey Kong Country 2 - Diddy's Kong Quest (USA).sfc"
# Path to the user's Donkey Kong Country 2 Poptracker Pack.
ut_poptracker_path: ""
# Folder path of the trivia database
# Preferably point it to /data/trivia/dkc2/
trivia_path: "data/trivia/dkc2"
dkc3_options:
# File name of the DKC3 US rom
rom_file: "roms/Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
earthbound_options:
# File name of the EarthBound US ROM
rom_file: "roms/EarthBound.sfc"
factorio_options:
executable: "factorio/bin/x64/factorio"
# by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used.
# server_settings: "factorio\\data\\server-settings.json"
server_settings: null
# Whether to filter item send messages displayed in-game to only those that involve you.
filter_item_sends: false
# Whether to filter connection changes displayed in-game.
filter_connection_changes: false
# Whether to send chat messages from players on the Factorio server to Archipelago.
bridge_chat_out: true
fe8_settings:
# File name of your Fire Emblem: The Sacred Stones (U) ROM
rom_file: "roms/Fire Emblem The Sacred Stones (U).gba"
ffr_options:
display_msgs: true
gauntletlegends_options:
# The location of your Retroarch folder
retroarch_path: "None"
# File name of the GL US rom
rom_file: "roms/Gauntlet Legends (U) [!].z64"
rom_start: true
glover_options:
# File path of the Glover (USA) ROM.
rom_path: ""
# Folder path of where to save the patched ROM.
patch_path: ""
# File path of the program to automatically run.
# Leave blank to disable.
program_path: ""
# Arguments to pass to the automatically run program.
# Leave blank to disable.
# Set to "--lua=" to automatically use the correct path for the lua connector.
program_args: "--lua="
gstla_options:
# File name of the GS TLA UE Rom
rom_file: "roms/Golden Sun - The Lost Age (UE) [!].gba"
hades_options:
# Path to the StyxScribe install
styx_scribe_path: "C:/Program Files/Steam/steamapps/common/Hades/StyxScribe.py"
hk_options:
# Disallows the APMapMod from showing spoiler placements.
disable_spoilers: false
jakanddaxter_options:
# Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe).
# Ensure this path contains forward slashes (/) only. This setting only applies if
# Auto Detect Root Directory is set to false.
root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal"
# Attempt to find the OpenGOAL installation and the mod executables (gk.exe and goalc.exe)
# automatically. If set to true, the ArchipelaGOAL Root Directory setting is ignored.
auto_detect_root_directory: true
# Enforce friendly player options in both single and multiplayer seeds. Disabling this allows for
# more disruptive and challenging options, but may impact seed generation. Use at your own risk!
enforce_friendly_options: true
k64_options:
# File name of the K64 EN rom
rom_file: "roms/Kirby 64 - The Crystal Shards (USA).z64"
kdl3_options:
# File name of the KDL3 JP or EN rom
rom_file: "roms/Kirby's Dream Land 3.sfc"
ladx_options:
# File name of the Link's Awakening DX rom
rom_file: "roms/Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
# Set this to false to never autostart a rom (such as after patching)
# true for operating system default program
# Alternatively, a path to a program to open the .gbc file with
# Examples:
# Retroarch:
# rom_start: "C:/RetroArch-Win64/retroarch.exe -L sameboy"
# BizHawk:
# rom_start: "C:/BizHawk-2.9-win-x64/EmuHawk.exe --lua=data/lua/connector_ladx_bizhawk.lua"
rom_start: true
# Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx
# Only .bin or .bdiff files
# The same directory will be checked for a matching text modification file
gfx_mod_file: ""
lttp_options:
# File name of the v1.0 J rom
rom_file: "roms/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
lufia2ac_options:
# File name of the US rom
rom_file: "roms/Lufia II - Rise of the Sinistrals (USA).sfc"
messenger_settings:
game_path: "TheMessenger.exe"
metroidzeromission_options:
# File name of the Metroid: Zero Mission ROM.
rom_file: "roms/Metroid - Zero Mission (USA).gba"
# Set this to false to never autostart a rom (such as after patching),
# Set it to true to have the operating system default program open the rom
# Alternatively, set it to a path to a program to open the .gba file with
rom_start: true
mk64_options:
# File name of the MK64 ROM
rom_file: "roms/Mario Kart 64 (U) [!].z64"
metroidfusion_options:
# File name of the Metroid Fusion ROM
rom_file: "roms/Metroid Fusion (USA).gba"
rom_start: true
display_location_found_messages: true
mlss_options:
# File name of the MLSS US rom
rom_file: "roms/Mario & Luigi - Superstar Saga (U).gba"
rom_start: true
mm2_options:
# File name of the MM2 EN rom
rom_file: "roms/Mega Man 2 (USA).nes"
mmbn3_options:
# File name of the MMBN3 Blue US rom
rom_file: "roms/Mega Man Battle Network 3 - Blue Version (USA).gba"
# Set this to false to never autostart a rom (such as after patching),
# true for operating system default program
# Alternatively, a path to a program to open the .gba file with
rom_start: true
mzm_options:
rom_file: "roms/Metroid - Zero Mission (USA).gba"
rom_start: true
oot_options:
# File name of the OoT v1.0 ROM
rom_file: "roms/The Legend of Zelda - Ocarina of Time.z64"
# Set this to false to never autostart a rom (such as after patching),
# true for operating system default program
# Alternatively, a path to a program to open the .z64 file with
rom_start: true
paper_mario_settings:
# File name of the Paper Mario USA ROM
rom_file: "roms/Paper Mario (USA).z64"
# Set this to false to never autostart a rom (such as after patching),
# true for operating system default program
# Alternatively, a path to a program to open the .z64 file with
rom_start: true
papermariottyd_options:
# The location of the Dolphin you want to auto launch patched ROMs with
dolphin_path: "None"
# File name of the TTYD US iso
rom_file: "roms/Paper Mario - The Thousand-Year Door (USA).iso"
rom_start: true
pmd_eos_options:
# File name of the EoS EU rom
rom_file: "roms/POKEDUN_SORA_C2SP01_00.nds"
rom_start: true
pokemon_bw_settings:
# File name of your Pokémon Black Version ROM
black_rom: "PokemonBlack.nds"
# File name of your Pokémon White Version ROM
white_rom: "PokemonWhite.nds"
# Toggles whether Encounter Plando is enabled for players in generation.
# If disabled, yamls that use Encounter Plando do not raise OptionErrors, but display a warning.
enable_encounter_plando: true
# If enabled, files inside the rom that are changed as part of the patching process (except for base patches)
# will be dumped into a zip file next to the patched rom (for debug purposes).
dump_patched_files: false
pokemon_crystal_settings:
rom_file: "roms/Pokemon - Crystal Version (UE) [C][!].gbc"
pokemon_emerald_settings:
# File name of your English Pokemon Emerald ROM
rom_file: "roms/Pokemon - Emerald Version (USA, Europe).gba"
pokemon_frlg_settings:
# File name of your English Pokémon FireRed ROM
firered_rom_file: "roms/Pokemon - FireRed Version (USA, Europe).gba"
# File name of your English Pokémon LeafGreen ROM
leafgreen_rom_file: "roms/Pokemon - LeafGreen Version (USA, Europe).gba"
ut_poptracker_path: ""
pokemon_platinum_settings:
rom_file: "roms/pokeplatinum.nds"
pokemon_rb_options:
# File names of the Pokemon Red and Blue roms
red_rom_file: "roms/Pokemon Red (UE) [S][!].gb"
blue_rom_file: "roms/Pokemon Blue (UE) [S][!].gb"
pokepinball_settings:
# File name of the Pokemon Pinball Color US rom
rom_file: "roms/PokemonPinball.gbc"
portal2_options:
# The file path of the extras.txt file (used to generate the menu in game)
menu_file: "C:\\Program Files (x86)\\Steam\\steamapps\\sourcemods\\Portal2Archipelago\\scripts\\extras.txt"
# The port set in the portal 2 launch options e.g. 3000
default_portal2_port: 3000
saving_princess_settings:
# Path to the game executable from which files are extracted
exe_path: "Saving Princess.exe"
# Path to the mod installation folder
install_folder: "Saving Princess"
# Set this to false to never autostart the game
launch_game: true
# The console command that will be used to launch the game
# The command will be executed with the installation folder as the current directory
launch_command: "wine \"Saving Princess v0_8.exe\""
sc2_options:
# The starting width the client window in pixels
window_width: 1080
# The starting height the client window in pixels
window_height: 720
# Controls whether the game should start in windowed mode
game_windowed_mode: false
# If set to true, in-client scouting will show traps as distinct from filler
show_traps: false
# Overrides the disable forced-camera slot option. Possible values: `true`, `false`, `default`. Default uses slot value
disable_forced_camera: "default"
# Overrides the skip cutscenes slot option. Possible values: `true`, `false`, `default`. Default uses slot value
skip_cutscenes: "default"
# Overrides the slot's difficulty setting. Possible values: `casual`, `normal`, `hard`, `brutal`, `default`. Default uses slot value
game_difficulty: "default"
# Overrides the slot's gamespeed setting. Possible values: `slower`, `slow`, `normal`, `fast`, `faster`, `default`. Default uses slot value
game_speed: "default"
# Defines the colour of terran mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)
terran_button_color:
- 0.0838
- 0.2898
- 0.2346
# Defines the colour of zerg mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)
zerg_button_color:
- 0.345
- 0.22425
- 0.12765
# Defines the colour of protoss mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)
protoss_button_color:
- 0.18975
- 0.2415
- 0.345
sf64_options:
# File path of the Star Fox 64 v1.1 ROM.
rom_path: ""
# Folder path of where to save the patched ROM.
patch_path: ""
# File path of the program to automatically run.
# Leave blank to disable.
program_path: ""
# Arguments to pass to the automatically run program.
# Leave blank to disable.
program_args: "--lua=\\\\wsl.localhost\\Ubuntu\\home\\ubufu\\ap-cm-1dd91ec\\Archipelago-main\\data\\lua\\connector_sf64_bizhawk.lua"
# Whether to enable the built in logic Tracker.
# If enabled, the 'Tracker' tab will show all unchecked locations in logic.
enable_tracker: true
sm_options:
# File name of the v1.0 J rom
rom_file: "roms/Super Metroid (JU).sfc"
sml2_options:
# File name of the Super Mario Land 2 1.0 ROM
rom_file: "roms/Super Mario Land 2 - 6 Golden Coins (USA, Europe).gb"
sms_options:
iso_file: "roms/sms_us_2002.iso"
smw_options:
# File name of the SMW US rom
rom_file: "roms/Super Mario World (USA).sfc"
soe_options:
# File name of the SoE US ROM
rom_file: "roms/Secret of Evermore (USA).sfc"
spyro2_options:
# Permits full gemsanity options for multiplayer games.
# Full gemsanity adds 2546 locations and an equal number of progression items.
# These items may be local-only or spread across the multiworld.
allow_full_gemsanity: false
stadium_options:
# File name of the Pokemon Stadium (US, 1.0) ROM
rom_file: "roms/Pokemon Stadium (US, 1.0).z64"
stardew_valley_options:
# Allow players to pick the goal 'Allsanity'. If disallowed, generation will fail.
allow_allsanity: true
# Allow players to pick the goal 'Perfection'. If disallowed, generation will fail.
allow_perfection: true
# Allow players to pick the option 'Bundle Price: Maximum'. If disallowed, it will be replaced with 'Very Expensive'
allow_max_bundles: true
# Allow players to pick the option 'Entrance Randomization: Chaos'. If disallowed, it will be replaced with 'Buildings'
allow_chaos_er: false
# Allow players to pick the option 'Shipsanity: Everything'. If disallowed, it will be replaced with 'Full Shipment With Fish'
allow_shipsanity_everything: true
# Allow players to pick the option 'Hatsanity: Near Perfection OR Post Perfection'. If disallowed, it will be replaced with 'Difficult'
allow_hatsanity_perfection: true
# Allow players to toggle on Custom logic flags. If disallowed, it will be disabled
allow_custom_logic: true
# Allow players to enable Jojapocalypse. If disallowed, it will be disabled
allow_jojapocalypse: false
tcg_card_shop_simulator_options:
# This limits goals to a reasonable number and sets all excessive settings to local_fill or Excluded for better sync experiences.
limit_checks_for_syncs: false
# Card Sanity adds pure randomness to card checks. This option disables this sanity in your multiworlds
allow_card_sanity: true
tloz_ooa_options:
# File path of the OOA US rom
rom_file: "roms/Legend of Zelda, The - Oracle of Ages (USA).gbc"
# A factor applied to the infamous heart beep sound interval.
# Valid values are: "vanilla", "half", "quarter", "disabled"
heart_beep_interval: "vanilla"
# The name of the sprite file to use (from "data/sprites/oos_ooa/").
# Putting "link" as a value uses the default game sprite.
# Putting "random" as a value randomly picks a sprite from your sprites directory for each generated ROM.
character_sprite: "link"
# The color palette used for character sprite throughout the game.
# Valid values are: "green", "red", "blue", "orange", and "random"
character_palette: "green"
# Defines if you don't want to spam the buttons to swim with the mermaid suit.
qol_mermaid_suit: true
# When enabled, playing the flute and the harp will immobilize you during a very small amount of time compared to vanilla game.
qol_quick_flute: true
# Defines if you want to skip the small dance that tokkay does
skip_tokkey_dance: false
# Defines if you want to skip the joke you tell to the sad boi
skip_boi_joke: false
tloz_oos_options:
# File name of the Oracle of Seasons US ROM
rom_file: "roms/Legend of Zelda, The - Oracle of Seasons (USA).gbc"
# File name of the Oracle of Ages US ROM (only needed for cross items)
ages_rom_file: "roms/Legend of Zelda, The - Oracle of Ages (USA).gbc"
rom_start: true
# The name of the sprite file to use (from "data/sprites/oos_ooa/").
# Putting "link" as a value uses the default game sprite.
# Putting "random" as a value randomly picks a sprite from your sprites directory for each generated ROM.
# If you want some weighted result, you can arrange the options like in your option yaml.
character_sprite: "link"
# The color palette used for character sprite throughout the game.
# Valid values are: "green", "red", "blue", "orange", and "random"
# If you want some weighted result, you can arrange the options like in your option yaml.
# If you want a color weight to only apply to a specific sprite, you can write color|sprite: weight.
# For example, red|link: 1 would add red in the possible palettes with a weight of 1 only if link is the selected sprite
character_palette: "green"
# If enabled, hidden digging spots in Subrosia are revealed as diggable tiles.
reveal_hidden_subrosia_digging_spots: true
# A factor applied to the infamous heart beep sound interval.
# Valid values are: "vanilla", "half", "quarter", "disabled"
heart_beep_interval: "vanilla"
# If true, no music will be played in the game while sound effects remain untouched
remove_music: false
tloz_options:
# File name of the Zelda 1
rom_file: "roms/Legend of Zelda, The (U) (PRG0) [!].nes"
# Set this to false to never autostart a rom (such as after patching)
# true for operating system default program
# Alternatively, a path to a program to open the .nes file with
rom_start: true
# Display message inside of Bizhawk
display_msgs: true
tloz_ph_options:
# For use with universal tracker.
# Toggles if universal tracker can use unlocked shortcuts and map warps to find shorter paths for /get_logical_path.
ut_get_logical_path_shortcuts: true
tloz_st_options:
# Train speed for each of the 4 gears, from lowest (reverse) to highest.
# defaults are -143, 0, 115, 193
train_speed:
- -143
- 0
- 115
- 193
# The train will instantly switch to the new speed when changing gears, no acceleration required.
# Does not apply to your stop gear.
train_snap_speed: true
# Allows entering stations immediately on the stop gear, no matter your speed.
train_quick_station: true
ttyd_options:
# The location of the Dolphin you want to auto launch patched ROMs with
dolphin_path: "None"
# File name of the TTYD US iso
rom_file: "roms/Paper Mario - The Thousand-Year Door (USA).iso"
rom_start: true
tunic_options:
# Disallows the TUNIC client from creating a local spoiler log.
disable_local_spoiler: false
# Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95.
limit_grass_rando: true
# Path to the user's TUNIC Poptracker Pack.
ut_poptracker_path: ""
vampire_survivors_options:
# Allow the use of unfair characters
allow_unfair_characters: false
voltorb_flip_settings:
# Allows the **experimental** choice in the **Artificial Logic** option.
allow_experimental_logic: false
wargroove_options:
# Locates the Wargroove root directory on your system.
# This is used by the Wargroove client, so it knows where to send communication files to.
root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
# Locates the Wargroove save file directory on your system.
# This is used by the Wargroove client, so it knows where to send mod and save files to.
save_directory: "%APPDATA%"
yoshisisland_options:
# File name of the Yoshi's Island 1.0 US rom
rom_file: "roms/Super Mario World 2 - Yoshi's Island (U).sfc"
yugioh06_settings:
# File name of your Yu-Gi-Oh 2006 ROM
rom_file: "roms/YuGiOh06.gba"
zillion_options:
# File name of the Zillion US rom
rom_file: "roms/Zillion (UE) [!].sms"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
# RetroArch doesn't make it easy to launch a game from the command line.
# You have to know the path to the emulator core library on the user's computer.
rom_start: "retroarch"
-5
View File
@@ -98,11 +98,6 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}smpatch\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: "";
+17 -17
View File
@@ -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
+162
View File
@@ -0,0 +1,162 @@
import dataclasses
import importlib
from abc import ABC, abstractmethod
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, ClassVar, Self, TypeVar, cast, overload
from typing_extensions import override
from Options import Option
if TYPE_CHECKING:
from worlds.AutoWorld import World
class FieldResolverRegister:
"""A container class to contain world custom resolvers"""
custom_resolvers: ClassVar[dict[str, dict[str, type["FieldResolver"]]]] = {}
"""
A mapping of game name to mapping of resolver name to resolver class
to hold custom resolvers implemented by worlds
"""
@classmethod
def get_resolver_cls(cls, game_name: str, resolver_name: str) -> type["FieldResolver"]:
"""Returns the world-registered or default resolver with the given name"""
custom_resolver_classes = cls.custom_resolvers.get(game_name, {})
if resolver_name not in DEFAULT_RESOLVERS and resolver_name not in custom_resolver_classes:
raise ValueError(f"Resolver '{resolver_name}' for game '{game_name}' not found")
return custom_resolver_classes.get(resolver_name) or DEFAULT_RESOLVERS[resolver_name]
@dataclasses.dataclass(frozen=True)
class FieldResolver(ABC):
@abstractmethod
def resolve(self, world: "World") -> Any: ...
def to_dict(self) -> dict[str, Any]:
"""Returns a JSON compatible dict representation of this resolver"""
fields = {field.name: getattr(self, field.name, None) for field in dataclasses.fields(self)}
return {
"resolver": self.__class__.__name__,
**fields,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> Self:
"""Returns a new instance of this resolver from a serialized dict representation"""
assert data.get("resolver", None) == cls.__name__
return cls(**{k: v for k, v in data.items() if k != "resolver"})
@override
def __str__(self) -> str:
return self.__class__.__name__
@classmethod
def __init_subclass__(cls, /, game: str) -> None:
if game != "Archipelago":
custom_resolvers = FieldResolverRegister.custom_resolvers.setdefault(game, {})
if cls.__qualname__ in custom_resolvers:
raise TypeError(f"Resolver {cls.__qualname__} has already been registered for game {game}")
custom_resolvers[cls.__qualname__] = cls
elif cls.__module__ != "rule_builder.field_resolvers":
raise TypeError("You cannot define custom resolvers for the base Archipelago world")
@dataclasses.dataclass(frozen=True)
class FromOption(FieldResolver, game="Archipelago"):
option: type[Option[Any]]
field: str = "value"
@override
def resolve(self, world: "World") -> Any:
option_name = next(
(name for name, cls in world.options.__class__.type_hints.items() if cls is self.option),
None,
)
if option_name is None:
raise ValueError(
f"Cannot find option {self.option.__name__} in options class {world.options.__class__.__name__}"
)
opt = cast(Option[Any] | None, getattr(world.options, option_name, None))
if opt is None:
raise ValueError(f"Invalid option: {option_name}")
return getattr(opt, self.field)
@override
def to_dict(self) -> dict[str, Any]:
return {
"resolver": "FromOption",
"option": f"{self.option.__module__}.{self.option.__name__}",
"field": self.field,
}
@override
@classmethod
def from_dict(cls, data: dict[str, Any]) -> Self:
if "option" not in data:
raise ValueError("Missing required option")
option_path = data["option"]
try:
option_mod_name, option_cls_name = option_path.rsplit(".", 1)
option_module = importlib.import_module(option_mod_name)
option = getattr(option_module, option_cls_name, None)
except (ValueError, ImportError) as e:
raise ValueError(f"Cannot parse option '{option_path}'") from e
if option is None or not issubclass(option, Option):
raise ValueError(f"Invalid option '{option_path}' returns type '{option}' instead of Option subclass")
return cls(cast(type[Option[Any]], option), data.get("field", "value"))
@override
def __str__(self) -> str:
field = f".{self.field}" if self.field != "value" else ""
return f"FromOption({self.option.__name__}{field})"
@dataclasses.dataclass(frozen=True)
class FromWorldAttr(FieldResolver, game="Archipelago"):
name: str
@override
def resolve(self, world: "World") -> Any:
obj: Any = world
for field in self.name.split("."):
if obj is None:
return None
if isinstance(obj, Mapping):
obj = obj.get(field, None) # pyright: ignore[reportUnknownMemberType]
else:
obj = getattr(obj, field, None)
return obj
@override
def __str__(self) -> str:
return f"FromWorldAttr({self.name})"
T = TypeVar("T")
@overload
def resolve_field(field: Any, world: "World", expected_type: type[T]) -> T: ...
@overload
def resolve_field(field: Any, world: "World", expected_type: None = None) -> Any: ...
def resolve_field(field: Any, world: "World", expected_type: type[T] | None = None) -> T | Any:
if isinstance(field, FieldResolver):
field = field.resolve(world)
if expected_type:
assert isinstance(field, expected_type), f"Expected type {expected_type} but got {type(field)}"
return field
DEFAULT_RESOLVERS = {
resolver_name: resolver_class
for resolver_name, resolver_class in locals().items()
if isinstance(resolver_class, type)
and issubclass(resolver_class, FieldResolver)
and resolver_class is not FieldResolver
}
+84 -31
View File
@@ -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:
@@ -108,11 +109,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 +128,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"""
@@ -527,7 +543,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 +704,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 +1007,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 +1018,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 +1130,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 +1141,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 +1256,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 +1279,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 +1287,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 +1390,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 +1406,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 +1422,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 +1521,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 +1530,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 +1595,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 +1604,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})"
+1 -2
View File
@@ -71,7 +71,6 @@ non_apworlds: set[str] = {
"Ocarina of Time",
"Overcooked! 2",
"Raft",
"Sudoku",
"Super Mario 64",
"VVVVVV",
"Wargroove",
@@ -658,7 +657,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": [],
+2 -2
View File
@@ -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))
@@ -59,7 +59,7 @@ class TestImplemented(unittest.TestCase):
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"))
+1 -1
View File
@@ -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):
+67 -8
View File
@@ -6,8 +6,9 @@ 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,
@@ -59,12 +60,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 +242,14 @@ 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),
),
)
)
class TestSimplify(RuleBuilderTestCase):
@@ -651,14 +668,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 +773,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 +781,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,
),
@@ -803,7 +821,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 +865,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",
@@ -915,7 +945,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):
@@ -1334,3 +1364,32 @@ class TestExplain(RuleBuilderTestCase):
"& False)",
)
assert str(self.resolved_rule) == " ".join(expected)
@classvar_matrix(
rules=(
(
Has("A", FromOption(RangeOption)),
Has.Resolved("A", count=5, player=1),
),
(
Has("B", FromWorldAttr("pre_calculated")),
Has.Resolved("B", count=3, player=1),
),
(
Has("C", FromWorldAttr("instance_data.key")),
Has.Resolved("C", count=7, player=1),
),
)
)
class TestFieldResolvers(RuleBuilderTestCase):
rules: ClassVar[tuple[Rule[Any], Rule.Resolved]]
def test_simplify(self) -> None:
multiworld = setup_solo_multiworld(self.world_cls, steps=("generate_early",), seed=0)
world = multiworld.worlds[1]
world.pre_calculated = 3 # pyright: ignore[reportAttributeAccessIssue]
world.instance_data = {"key": 7} # pyright: ignore[reportAttributeAccessIssue]
rule, expected = self.rules
resolved_rule = rule.resolve(world)
self.assertEqual(resolved_rule, expected, f"\n{resolved_rule}\n{expected}")
-132
View File
@@ -1,132 +0,0 @@
import typing as t
from copy import deepcopy
from unittest import TestCase
from typing_extensions import override
import NetUtils
from NetUtils import GamesPackage
from apmw.multiserver.gamespackagecache import GamesPackageCache
class GamesPackageCacheTest(TestCase):
cache: GamesPackageCache
any_game: t.ClassVar[str] = "APQuest"
example_games_package: GamesPackage = {
"item_name_to_id": {"Item 1": 1},
"item_name_groups": {"Everything": ["Item 1"]},
"location_name_to_id": {"Location 1": 1},
"location_name_groups": {"Everywhere": ["Location 1"]},
"checksum": "1234",
}
@override
def setUp(self) -> None:
self.cache = GamesPackageCache()
def test_get_static_is_same(self) -> None:
"""Tests that get_static returns the same objects twice"""
reduced_games_package1, item_name_groups1, location_name_groups1 = self.cache.get_static(self.any_game)
reduced_games_package2, item_name_groups2, location_name_groups2 = self.cache.get_static(self.any_game)
self.assertIs(reduced_games_package1, reduced_games_package2)
self.assertIs(item_name_groups1, item_name_groups2)
self.assertIs(location_name_groups1, location_name_groups2)
def test_get_static_data_format(self) -> None:
"""Tests that get_static returns data in the correct format"""
reduced_games_package, item_name_groups, location_name_groups = self.cache.get_static(self.any_game)
self.assertTrue(reduced_games_package["checksum"])
self.assertTrue(reduced_games_package["item_name_to_id"])
self.assertTrue(reduced_games_package["location_name_to_id"])
self.assertNotIn("item_name_groups", reduced_games_package)
self.assertNotIn("location_name_groups", reduced_games_package)
self.assertTrue(item_name_groups["Everything"])
self.assertTrue(location_name_groups["Everywhere"])
def test_get_static_is_serializable(self) -> None:
"""Tests that get_static returns data that can be serialized"""
NetUtils.encode(self.cache.get_static(self.any_game))
def test_get_static_missing_raises(self) -> None:
"""Tests that get_static raises KeyError if the world is missing"""
with self.assertRaises(KeyError):
_ = self.cache.get_static("Does not exist")
def test_eviction(self) -> None:
"""Tests that unused items get evicted from cache"""
game_name = "Test"
before_add = len(self.cache._reduced_games_packages)
data = self.cache.get(game_name, self.example_games_package)
self.assertTrue(data)
self.assertEqual(before_add + 1, len(self.cache._reduced_games_packages))
del data
if len(self.cache._reduced_games_packages) != before_add: # gc.collect() may not even be required
import gc
gc.collect()
self.assertEqual(before_add, len(self.cache._reduced_games_packages))
def test_get_required_field(self) -> None:
"""Tests that missing required field raises a KeyError"""
for field in ("item_name_to_id", "location_name_to_id", "item_name_groups"):
with self.subTest(field=field):
games_package = deepcopy(self.example_games_package)
del games_package[field] # type: ignore
with self.assertRaises(KeyError):
_ = self.cache.get(self.any_game, games_package)
def test_get_optional_properties(self) -> None:
"""Tests that missing optional field works"""
for field in ("checksum", "location_name_groups"):
with self.subTest(field=field):
games_package = deepcopy(self.example_games_package)
del games_package[field] # type: ignore
_, item_name_groups, location_name_groups = self.cache.get(self.any_game, games_package)
self.assertTrue(item_name_groups)
self.assertEqual(field != "location_name_groups", bool(location_name_groups))
def test_item_name_deduplication(self) -> None:
n = 1
s1 = f"Item {n}"
s2 = f"Item {n}"
# check if the deduplication is actually gonna do anything
self.assertIsNot(s1, s2)
self.assertEqual(s1, s2)
# do the thing
game_name = "Test"
games_package: GamesPackage = {
"item_name_to_id": {s1: n},
"item_name_groups": {"Everything": [s2]},
"location_name_to_id": {},
"location_name_groups": {},
"checksum": "1234",
}
reduced_games_package, item_name_groups, location_name_groups = self.cache.get(game_name, games_package)
self.assertIs(
next(iter(reduced_games_package["item_name_to_id"].keys())),
item_name_groups["Everything"][0],
)
def test_location_name_deduplication(self) -> None:
n = 1
s1 = f"Location {n}"
s2 = f"Location {n}"
# check if the deduplication is actually gonna do anything
self.assertIsNot(s1, s2)
self.assertEqual(s1, s2)
# do the thing
game_name = "Test"
games_package: GamesPackage = {
"item_name_to_id": {},
"item_name_groups": {},
"location_name_to_id": {s1: n},
"location_name_groups": {"Everywhere": [s2]},
"checksum": "1234",
}
reduced_games_package, item_name_groups, location_name_groups = self.cache.get(game_name, games_package)
self.assertIs(
next(iter(reduced_games_package["location_name_to_id"].keys())),
location_name_groups["Everywhere"][0],
)
-86
View File
@@ -1,86 +0,0 @@
import os
import unittest
from Utils import is_macos
from WebHostLib.customserver import parse_game_ports, create_random_port_socket, get_used_ports
ci = bool(os.environ.get("CI"))
class TestPortAllocating(unittest.TestCase):
def test_parse_game_ports(self) -> None:
"""Ensure that game ports with ranges are parsed correctly"""
val = parse_game_ports(("1000-2000", "2000-5000", "1000-2000", 20, 40, "20", "0"))
self.assertListEqual(val.parsed_ports,
[range(1000, 2001), range(2000, 5001), range(1000, 2001), range(20, 21), range(40, 41),
range(20, 21)], "The parsed game ports are not the expected values")
self.assertTrue(val.ephemeral_allowed, "The ephemeral allowed flag is not set even though it was passed")
self.assertListEqual(val.weights, [1001, 4002, 5003, 5004, 5005, 5006],
"Cumulative weights are not the expected value")
val = parse_game_ports(())
self.assertListEqual(val.parsed_ports, [], "Empty list of game port returned something")
self.assertFalse(val.ephemeral_allowed, "Empty list returned that ephemeral is allowed")
val = parse_game_ports((0,))
self.assertListEqual(val.parsed_ports, [], "Empty list of ranges returned something")
self.assertTrue(val.ephemeral_allowed, "List with just 0 is not allowing ephemeral ports")
val = parse_game_ports((1,))
self.assertEqual(val.parsed_ports, [range(1, 2)], "Parsed ports doesn't contain the expected values")
self.assertFalse(val.ephemeral_allowed, "List with just single port returned that ephemeral is allowed")
def test_parse_game_port_errors(self) -> None:
"""Ensure that game ports with incorrect values raise the expected error"""
with self.assertRaises(ValueError, msg="Negative numbers didn't get interpreted as an invalid range"):
parse_game_ports(tuple("-50215"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number"):
parse_game_ports(tuple("dwafawg"))
with self.assertRaises(
ValueError,
msg="A range with an extra dash at the end didn't get interpreted as an invalid number because of it's end dash"
):
parse_game_ports(tuple("20-21215-"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number for the start of a range"):
parse_game_ports(tuple("f-21215"))
def test_random_port_socket_edge_cases(self) -> None:
"""Verify if edge cases on creation of random port socket is working fine"""
# Try giving an empty tuple and fail over it
with self.assertRaises(OSError) as err:
create_random_port_socket(tuple(), "127.0.0.1")
self.assertEqual(err.exception.errno, 98, "Raised an unexpected error code")
self.assertEqual(err.exception.strerror, "No available ports", "Raised an unexpected error string")
# Try only having ephemeral ports enabled
try:
create_random_port_socket(("0",), "127.0.0.1").close()
except OSError as err:
self.assertEqual(err.errno, 98, "Raised an unexpected error code")
# If it returns our error string that means something is wrong with our code
self.assertNotEqual(err.strerror, "No available ports",
"Raised an unexpected error string")
@unittest.skipUnless(ci, "can't guarantee free ports outside of CI")
def test_random_port_socket(self) -> None:
"""Verify if returned sockets use the correct port ranges"""
sockets = []
for _ in range(6):
socket = create_random_port_socket(("8080-8085",), "127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(8080, 8086), "Port of socket was not inside the expected range")
for s in sockets:
s.close()
sockets.clear()
length = 5_000 if is_macos else (30_000 - len(get_used_ports()))
for _ in range(length):
socket = create_random_port_socket(("30000-65535",), "127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(30_000, 65536), "Port of socket was not inside the expected range")
for s in sockets:
s.close()
@@ -1,147 +0,0 @@
import typing as t
from copy import deepcopy
from typing_extensions import override
from test.multiserver.test_gamespackage_cache import GamesPackageCacheTest
import Utils
from NetUtils import GamesPackage
from apmw.webhost.customserver.gamespackagecache import DBGamesPackageCache
class FakeGameDataPackage:
_rows: "t.ClassVar[dict[str, FakeGameDataPackage]]" = {}
data: bytes
@classmethod
def get(cls, checksum: str) -> "FakeGameDataPackage | None":
return cls._rows.get(checksum, None)
@classmethod
def add(cls, checksum: str, full_games_package: GamesPackage) -> None:
row = FakeGameDataPackage()
row.data = Utils.restricted_dumps(full_games_package)
cls._rows[checksum] = row
class DBGamesPackageCacheTest(GamesPackageCacheTest):
cache: DBGamesPackageCache
any_game: t.ClassVar[str] = "My Game"
static_data: t.ClassVar[dict[str, GamesPackage]] = { # noqa: pycharm doesn't understand this
"My Game": {
"item_name_to_id": {"Item 1": 1},
"location_name_to_id": {"Location 1": 1},
"item_name_groups": {"Everything": ["Item 1"]},
"location_name_groups": {"Everywhere": ["Location 1"]},
"checksum": "2345",
}
}
orig_db_type: t.ClassVar[type]
@override
@classmethod
def setUpClass(cls) -> None:
import WebHostLib.models
cls.orig_db_type = WebHostLib.models.GameDataPackage
WebHostLib.models.GameDataPackage = FakeGameDataPackage # type: ignore
@override
def setUp(self) -> None:
self.cache = DBGamesPackageCache(self.static_data)
@override
@classmethod
def tearDownClass(cls) -> None:
import WebHostLib.models
WebHostLib.models.GameDataPackage = cls.orig_db_type # type: ignore
def assert_conversion(
self,
full_games_package: GamesPackage,
reduced_games_package: dict[str, t.Any],
item_name_groups: dict[str, t.Any],
location_name_groups: dict[str, t.Any],
) -> None:
for key in ("item_name_to_id", "location_name_to_id", "checksum"):
if key in full_games_package:
self.assertEqual(reduced_games_package[key], full_games_package[key]) # noqa: pycharm
self.assertEqual(item_name_groups, full_games_package["item_name_groups"])
self.assertEqual(location_name_groups, full_games_package["location_name_groups"])
def assert_static_conversion(
self,
full_games_package: GamesPackage,
reduced_games_package: dict[str, t.Any],
item_name_groups: dict[str, t.Any],
location_name_groups: dict[str, t.Any],
) -> None:
self.assert_conversion(full_games_package, reduced_games_package, item_name_groups, location_name_groups)
for key in ("item_name_to_id", "location_name_to_id", "checksum"):
self.assertIs(reduced_games_package[key], full_games_package[key]) # noqa: pycharm
def test_get_static_contents(self) -> None:
"""Tests that get_static returns the correct data"""
reduced_games_package, item_name_groups, location_name_groups = self.cache.get_static(self.any_game)
for key in ("item_name_to_id", "location_name_to_id", "checksum"):
self.assertIs(reduced_games_package[key], self.static_data[self.any_game][key]) # noqa: pycharm
self.assertEqual(item_name_groups, self.static_data[self.any_game]["item_name_groups"])
self.assertEqual(location_name_groups, self.static_data[self.any_game]["location_name_groups"])
def test_static_not_evicted(self) -> None:
"""Tests that static data is not evicted from cache during gc"""
import gc
game_name = next(iter(self.static_data.keys()))
ids = [id(o) for o in self.cache.get_static(game_name)]
gc.collect()
self.assertEqual(ids, [id(o) for o in self.cache.get_static(game_name)])
def test_get_is_static(self) -> None:
"""Tests that a get with correct checksum return the static items"""
# NOTE: this is only true for the DB cache, not the "regular" one, since we want to avoid loading worlds there
cks: GamesPackage = {"checksum": self.static_data[self.any_game]["checksum"]} # noqa: pycharm doesn't like this
reduced_games_package1, item_name_groups1, location_name_groups1 = self.cache.get(self.any_game, cks)
reduced_games_package2, item_name_groups2, location_name_groups2 = self.cache.get_static(self.any_game)
self.assertIs(reduced_games_package1, reduced_games_package2)
self.assertEqual(location_name_groups1, location_name_groups2)
self.assertEqual(item_name_groups1, item_name_groups2)
def test_get_from_db(self) -> None:
"""Tests that a get with only checksum will load the full data from db and is cached"""
game_name = "Another Game"
full_games_package = deepcopy(self.static_data[self.any_game])
full_games_package["checksum"] = "3456"
cks: GamesPackage = {"checksum": full_games_package["checksum"]} # noqa: pycharm doesn't like this
FakeGameDataPackage.add(full_games_package["checksum"], full_games_package)
before_add = len(self.cache._reduced_games_packages)
data = self.cache.get(game_name, cks)
self.assert_conversion(full_games_package, *data) # type: ignore
self.assertEqual(before_add + 1, len(self.cache._reduced_games_packages))
def test_get_missing_from_db_uses_full_games_package(self) -> None:
"""Tests that a get with full data (missing from db) will use the full data and is cached"""
game_name = "Yet Another Game"
full_games_package = deepcopy(self.static_data[self.any_game])
full_games_package["checksum"] = "4567"
before_add = len(self.cache._reduced_games_packages)
data = self.cache.get(game_name, full_games_package)
self.assert_conversion(full_games_package, *data) # type: ignore
self.assertEqual(before_add + 1, len(self.cache._reduced_games_packages))
def test_get_without_checksum_uses_full_games_package(self) -> None:
"""Tests that a get with full data and no checksum will use the full data and is not cached"""
game_name = "Yet Another Game"
full_games_package = deepcopy(self.static_data[self.any_game])
del full_games_package["checksum"]
before_add = len(self.cache._reduced_games_packages)
data = self.cache.get(game_name, full_games_package)
self.assert_conversion(full_games_package, *data) # type: ignore
self.assertEqual(before_add, len(self.cache._reduced_games_packages))
def test_get_missing_from_db_raises(self) -> None:
"""Tests that a get that requires a row to exist raise an exception if it doesn't"""
with self.assertRaises(Exception):
_ = self.cache.get("Does not exist", {"checksum": "0000"})
+6 -3
View File
@@ -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."))
+429
View File
@@ -0,0 +1,429 @@
import logging
from typing import TYPE_CHECKING
from .Items import pokemon_stadium_items, gym_badge_codes, box_upgrade_items, cup_tier_upgrade_items
from .Locations import pokemon_stadium_locations, event_locations
from NetUtils import ClientStatus
from .Types import LocData
import Utils
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
logger = logging.getLogger('Client')
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
class PokemonStadiumClient(BizHawkClient):
game = 'Pokemon Stadium'
system = 'N64'
patch_suffix = '.apstadium'
def __init__(self):
super().__init__()
self.local_checked_locations = set()
self.glc_loaded = False
self.cups_loaded = False
self.minigame_index = None
self.minigame_done = False
self.minigame_check_sent = False
async def validate_rom(self, ctx: 'BizHawkClientContext') -> bool:
try:
# Check ROM name
rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 15, 'ROM')]))[0]).decode('ascii')
if rom_name != 'POKEMON STADIUM':
logger.info('Invalid ROM for Pokemon Stadium AP World')
return False
except bizhawk.RequestFailedError:
return False
ctx.game = self.game
ctx.items_handling = 0b111
ctx.want_slot_data = True
return True
async def game_watcher(self, ctx: 'BizHawkClientContext') -> None:
item_codes = {net_item.item for net_item in ctx.items_received}
flags = await bizhawk.read(ctx.bizhawk_ctx, [
(0x420000, 4, 'RDRAM'), # GLC Flag
(0x420010, 4, 'RDRAM'), # Entered Battle Flag
(0x148AC8, 12, 'RDRAM'), # Beat Rival Flag
(0x12FC1C, 4, 'RDRAM'), # Minigame being played
(0x124860, 4, 'RDRAM'), # Minigame results
(0xAE77F, 1, 'RDRAM'), # Enemy team HP slot 1
(0xAE7D3, 1, 'RDRAM'), # Enemy team HP slot 2
(0xAE827, 1, 'RDRAM'), # Enemy team HP slot 3
(0x220C19, 3, 'RDRAM'), # GLC Rentals address
(0x221D99, 3, 'RDRAM'), # GLC Registration table address
(0x218CE9, 3, 'RDRAM'), # Poke Cup Rentals address
(0x219E69, 3, 'RDRAM'), # Poke Cup Registration table address
(0x218CB9, 3, 'RDRAM'), # Prime Cup Rentals address
(0x219E39, 3, 'RDRAM'), # Prime Cup Registration table address
(0x218C99, 3, 'RDRAM'), # Petit Cup Rentals address
(0x219E19, 3, 'RDRAM'), # Petit Cup Registration table address
(0x218CA9, 3, 'RDRAM'), # Pika Cup Rentals address
(0x219E29, 3, 'RDRAM'), # Pika Cup Registration table address
(0x420020, 4, 'RDRAM'), # Picking a Cup tier
]
)
player_has_battled = flags[1] != b'\x00\x00\x00\x00'
battle_info = await bizhawk.read(ctx.bizhawk_ctx, [(0x0AE540, 4, 'RDRAM')])
mode = int(battle_info[0].hex()[:2])
gym_info = battle_info[0].hex()[4:]
gym_number = int(battle_info[0].hex()[4:6])
trainer_index = int(battle_info[0].hex()[6:])
if player_has_battled:
player_won = all(x == b'\x00' for x in flags[5:8])
if player_won:
ap_code = 20000000 + (mode * 100) + (gym_number * 10) + trainer_index
# If a Gym Leader was beaten or the last trainer for a Cup was beaten an additional check must be sent
if mode == 7 and trainer_index == 4:
locations_to_check = set([ap_code, ap_code + 1])
elif trainer_index == 8:
locations_to_check = set([ap_code, ap_code - trainer_index, ap_code + 1])
else:
locations_to_check = set([ap_code])
try:
await ctx.check_locations(locations_to_check)
await bizhawk.write(ctx.bizhawk_ctx, [(0x420010, [0x00, 0x00, 0x00, 0x00], 'RDRAM')])
self.glc_loaded = False
except:
pass
glc_flag = int.from_bytes(flags[0], byteorder='big')
if glc_flag == 2 and not self.glc_loaded:
self.glc_loaded = True
self.GLC_UNLOCK_FLAGS = [
0x147B70, # Pewter
0x147B98, # Cerulean
0x147BC0, # Vermilion
0x147BE8, # Celadon
0x147C10, # Fuchsia
0x147C38, # Saffron
0x147C60, # Cinnabar
0x147C88, # Viridian
0x147CB1, # E4 entrance
0x147CD9, # E4 exit
0x147D01, # E4
]
# UUDDLLRR
self.GLC_CURSOR_TARGETS = [
0x147B84, # Brock, 00000002
0x147BAC, # Misty, 03000103
0x147BD4, # Surge, 04020200
0x147BFC, # Erika, 05030500
0x147C24, # Koga, 06040604
0x147C4C, # Sabrina, 07050007
0x147C74, # Blaine, 00080608
0x147C9C, # Giovanni, 07000709
]
gym_codes = [
pokemon_stadium_items['Pewter City Key'].ap_code,
pokemon_stadium_items['Cerulean City Key'].ap_code,
pokemon_stadium_items['Vermillion City Key'].ap_code,
pokemon_stadium_items['Celadon City Key'].ap_code,
pokemon_stadium_items['Fuchsia City Key'].ap_code,
pokemon_stadium_items['Saffron City Key'].ap_code,
pokemon_stadium_items['Cinnabar Island Key'].ap_code,
pokemon_stadium_items['Viridian City Key'].ap_code,
]
self.unlocked_gyms = [i + 1 for i, code in enumerate(gym_codes) if code in item_codes]
victory_road_open = set(gym_badge_codes).issubset(item_codes)
if victory_road_open:
self.unlocked_gyms.append(9)
if gym_codes[0] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[0], [0x00, 0x01], 'RDRAM')])
await self.update_brock_cursor(ctx)
if gym_codes[1] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[1], [0x00, 0x01], 'RDRAM')])
await self.update_misty_cursor(ctx)
if gym_codes[2] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[2], [0x00, 0x01], 'RDRAM')])
await self.update_surge_cursor(ctx)
if gym_codes[3] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[3], [0x00, 0x01], 'RDRAM')])
await self.update_erika_cursor(ctx)
if gym_codes[4] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[4], [0x00, 0x01], 'RDRAM')])
await self.update_koga_cursor(ctx)
if gym_codes[5] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[5], [0x00, 0x01], 'RDRAM')])
await self.update_sabrina_cursor(ctx)
if gym_codes[6] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[6], [0x00, 0x01], 'RDRAM')])
await self.update_blaine_cursor(ctx)
if gym_codes[7] in item_codes:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[7], [0x00, 0x01], 'RDRAM')])
await self.update_giovanni_cursor(ctx, item_codes)
if victory_road_open:
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[8], [0x01], 'RDRAM')])
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[9], [0x01], 'RDRAM')])
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[10], [0x01], 'RDRAM')])
if len(self.unlocked_gyms) > 0 and gym_info != '0804':
first_gym = self.unlocked_gyms[0] - 1
await bizhawk.write(ctx.bizhawk_ctx, [(0x147D50, [0x00, first_gym], 'RDRAM')])
await bizhawk.write(ctx.bizhawk_ctx, [(0x146F38, [0x52, 0x61, 0xFF, 0x82], 'RDRAM')])
elif glc_flag != 2 and self.glc_loaded:
self.glc_loaded = False
text = flags[2].decode("ascii", errors="ignore")
if text == 'Magnificent!':
await ctx.check_locations(set([event_locations['Beat Rival'].ap_code]))
await bizhawk.write(ctx.bizhawk_ctx, [(0x420010, [0x00, 0x00, 0x00, 0x00], 'RDRAM')])
cups_flag = int.from_bytes(flags[18], byteorder='big')
if cups_flag != 0 and not self.cups_loaded:
self.cups_loaded = True
if mode == 3:
cup_tier_item = cup_tier_upgrade_items['Poké Cup - Tier Upgrade'].ap_code
else:
cup_tier_item = cup_tier_upgrade_items['Prime Cup - Tier Upgrade'].ap_code
cup_tier = sum(1 for net_item in ctx.items_received if net_item.item == cup_tier_item)
await bizhawk.write(ctx.bizhawk_ctx, [(0x147018, [0x00, 0x00, 0x00, cup_tier], 'RDRAM')])
elif cups_flag == 0:
self.cups_loaded = False
# GLC Boxes
selecting_team = flags[8] == b'\x22\x0E\x20'
registering_team = flags[9] == b'\x22\x1F\xA0'
if selecting_team or registering_team:
address = 0x220E23 if selecting_team else 0x221FA3
item = box_upgrade_items['GLC PC Box Upgrade'].ap_code
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
table_size = 29 + 20 * box_count
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
# Poke Boxes
selecting_team = flags[10] == b'\x21\x8F\x10'
registering_team = flags[11] == b'\x21\xA0\x90'
if selecting_team or registering_team:
address = 0x218F13 if selecting_team else 0x21A093
item = box_upgrade_items['Poke Cup PC Box Upgrade'].ap_code
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
table_size = 29 + 20 * box_count
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
# Prime Boxes
selecting_team = flags[12] == b'\x21\x8F\x10'
registering_team = flags[13] == b'\x21\xA0\x90'
if selecting_team or registering_team:
address = 0x218F13 if selecting_team else 0x21A093
item = box_upgrade_items['Prime Cup PC Box Upgrade'].ap_code
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
table_size = 29 + 20 * box_count
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
# Minigames
if flags[3].startswith(b'\x00\x03\x00') and flags[3][3] in range(9):
self.minigame_index = flags[3][3]
if self.minigame_index != None and flags[4] == b'\x00\x00\x00\x00':
self.minigame_done = False
if self.minigame_index != None and not self.minigame_done and flags[4] == b'\x01\x00\x00\x00':
self.minigame_done = True
self.minigame_check_sent = False
if self.minigame_done and self.minigame_index != None and not self.minigame_check_sent:
minigame_ap_acode = 20000100 + self.minigame_index
await ctx.check_locations([minigame_ap_acode])
self.minigame_check_sent = True
# Send game clear
if not ctx.finished_game and pokemon_stadium_items['Victory'].ap_code in item_codes:
ctx.finished_game = True
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL,
}])
def lowest_unlocked_from(self, lower_bound):
for i in range(lower_bound, 9):
if i in self.unlocked_gyms:
return i
return 0
def highest_unlocked_from(self, upper_bound):
for i in range(upper_bound, 0, -1):
if i in self.unlocked_gyms:
return i
return 0
async def update_brock_cursor(self, ctx):
# Determine UP: lowest unlocked gym from 4 to 9
up = self.lowest_unlocked_from(4)
# Determine RIGHT: lowest of 2 or 3 or 4 if any are unlocked
right = 0
misty_unlocked = 2 in self.unlocked_gyms
surge_unlocked = 3 in self.unlocked_gyms
erika_unlocked = 4 in self.unlocked_gyms
if misty_unlocked:
right = 2
elif surge_unlocked:
right = 3
elif erika_unlocked:
right = 4
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[0], [up, 0x00, 0x00, right], 'RDRAM')])
async def update_misty_cursor(self, ctx):
# Determine UP: lowest unlocked gym from 4 to 9
up = self.lowest_unlocked_from(4)
# Determine LEFT: is Brock unlocked
left = 1 if 1 in self.unlocked_gyms else 0
# Determine RIGHT: is Surge unlocked
right = 3 if 3 in self.unlocked_gyms else 0
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[1], [up, 0x00, left, right], 'RDRAM')])
async def update_surge_cursor(self, ctx):
# Determine UP: lowest unlocked gym from 4 to 9
up = self.lowest_unlocked_from(4)
# Determine DOWN: is Misty unlocked
down = 2 if 2 in self.unlocked_gyms else 0
# Determine LEFT: is Misty or Brock unlocked
left = 0
misty_unlocked = 2 if 2 in self.unlocked_gyms else 0
brock_unlocked = 1 if 1 in self.unlocked_gyms else 0
if misty_unlocked:
left = 2
elif brock_unlocked:
left = 1
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[2], [up, down, left, 0x00], 'RDRAM')])
async def update_erika_cursor(self, ctx):
# Determine UP: lowest unlocked gym from 5 to 9
up = self.lowest_unlocked_from(5)
# Determine DOWN: highest unlocked gym from 3 to 1
down = self.highest_unlocked_from(3)
# Determine LEFT: is Koga or Sabrina unlocked
left = 0
koga_unlocked = 5 if 5 in self.unlocked_gyms else 0
sabrina_unlocked = 6 if 6 in self.unlocked_gyms else 0
if koga_unlocked:
left = 5
elif sabrina_unlocked:
left = 6
# Determine RIGHT: is Surge unlocked
right = 3 if 3 in self.unlocked_gyms else 0
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[3], [up, down, left, right], 'RDRAM')])
async def update_koga_cursor(self, ctx):
# Determine UP: lowest unlocked gym from 6 to 9
up = self.lowest_unlocked_from(6)
# Determine DOWN: highest unlocked gym from 2 to 1
down = self.highest_unlocked_from(2)
# Determine LEFT: is Sabrina unlocked
left = 6 if 6 in self.unlocked_gyms else 0
# Determine RIGHT: is Erika or Surge unlocked
right = 0
erika_unlocked = 4 if 4 in self.unlocked_gyms else 0
surge_unlocked = 3 if 3 in self.unlocked_gyms else 0
if erika_unlocked:
right = 4
elif surge_unlocked:
right = 3
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[4], [up, down, left, right], 'RDRAM')])
async def update_sabrina_cursor(self, ctx):
# Determine DOWN: highest unlocked gym from 5 to 1
down = self.highest_unlocked_from(5)
# Determine RIGHT: is Blaine or Giovanni unlocked
right = 0
blaine_unlocked = 7 if 7 in self.unlocked_gyms else 0
giovanni_unlocked = 8 if 8 in self.unlocked_gyms else 0
if blaine_unlocked:
right = 7
elif giovanni_unlocked:
right = 8
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[5], [0x00, down, 0x00, right], 'RDRAM')])
async def update_blaine_cursor(self, ctx):
# Determine DOWN: highest unlocked gym from 5 to 1
down = self.highest_unlocked_from(5)
# Determine LEFT: is Sabrina unlocked
left = 6 if 6 in self.unlocked_gyms else 0
# Determine RIGHT: is Giovanni unlocked or do you have all badges needed
if 8 in self.unlocked_gyms:
right = 8
elif 9 in self.unlocked_gyms:
right = 9
else:
right = 0
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[6], [0x00, down, left, right], 'RDRAM')])
async def update_giovanni_cursor(self, ctx, item_codes):
# Determine UP: All badges obtained?
up = 9 if set(gym_badge_codes).issubset(item_codes) else 0
# Determine DOWN: highest unlocked gym from 5 to 1
down = self.highest_unlocked_from(5)
# Determine LEFT: is Blaine or Sabrina unlocked
left = 0
blaine_unlocked = 7 if 7 in self.unlocked_gyms else 0
sabrina_unlocked = 6 if 6 in self.unlocked_gyms else 0
if blaine_unlocked:
left = 7
elif sabrina_unlocked:
left = 6
# Determine RIGHT: All badges obtained?
right = up
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[7], [up, down, left, right], 'RDRAM')])
+132
View File
@@ -0,0 +1,132 @@
import logging
import random
from BaseClasses import Item, ItemClassification
from .Types import ItemData, PokemonStadiumItem
from .Locations import get_total_locations
from typing import List, Dict, TYPE_CHECKING
if TYPE_CHECKING:
from . import PokemonStadiumWorld
def create_itempool(world: 'PokemonStadiumWorld') -> List[Item]:
item_pool: List[Item] = []
# This is a good place to grab anything you need from options
for name in pokemon_stadium_items:
if name != 'Victory' and name not in world.starting_gym_keys:
item_pool.append(create_item(world, name))
victory = create_item(world, 'Victory')
world.multiworld.get_location('Beat Rival', world.player).place_locked_item(victory)
item_pool += create_multiple_items(world, 'Poké Cup - Tier Upgrade', 3, ItemClassification.progression)
item_pool += create_multiple_items(world, 'Prime Cup - Tier Upgrade', 3, ItemClassification.progression)
item_pool += create_multiple_items(world, 'GLC PC Box Upgrade', 6, ItemClassification.useful)
item_pool += create_multiple_items(world, 'Poke Cup PC Box Upgrade', 6, ItemClassification.useful)
item_pool += create_multiple_items(world, 'Prime Cup PC Box Upgrade', 6, ItemClassification.useful)
item_pool += create_junk_items(world, get_total_locations(world) - len(item_pool) - 1)
return item_pool
def create_item(world: 'PokemonStadiumWorld', name: str) -> Item:
data = item_table[name]
return PokemonStadiumItem(name, data.classification, data.ap_code, world.player)
def create_multiple_items(world: "PokemonStadiumWorld", name: str, count: int, item_type: ItemClassification = ItemClassification.progression) -> List[Item]:
data = item_table[name]
itemlist: List[Item] = []
for _ in range(count):
itemlist += [PokemonStadiumItem(name, item_type, data.ap_code, world.player)]
return itemlist
def create_junk_items(world: 'PokemonStadiumWorld', count: int) -> List[Item]:
junk_pool: List[Item] = []
junk_list: Dict[str, int] = {}
for name in item_table.keys():
ic = item_table[name].classification
if ic == ItemClassification.filler:
junk_list[name] = junk_weights.get(name)
for _ in range(count):
junk_pool.append(world.create_item(world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0]))
return junk_pool
pokemon_stadium_items = {
# Progression items
'Pewter City Key': ItemData(10000001, ItemClassification.progression),
'Boulder Badge': ItemData(10000002, ItemClassification.progression),
'Cerulean City Key': ItemData(10000003, ItemClassification.progression),
'Cascade Badge': ItemData(10000004, ItemClassification.progression),
'Vermillion City Key': ItemData(10000005, ItemClassification.progression),
'Thunder Badge': ItemData(10000006, ItemClassification.progression),
'Celadon City Key': ItemData(10000007, ItemClassification.progression),
'Rainbow Badge': ItemData(10000008, ItemClassification.progression),
'Fuchsia City Key': ItemData(10000009, ItemClassification.progression),
'Soul Badge': ItemData(10000010, ItemClassification.progression),
'Saffron City Key': ItemData(10000011, ItemClassification.progression),
'Marsh Badge': ItemData(10000012, ItemClassification.progression),
'Cinnabar Island Key': ItemData(10000013, ItemClassification.progression),
'Volcano Badge': ItemData(10000014, ItemClassification.progression),
'Viridian City Key': ItemData(10000015, ItemClassification.progression),
'Earth Badge': ItemData(10000016, ItemClassification.progression),
# Victory is added here since in this organization it needs to be in the default item pool
'Victory': ItemData(10000000, ItemClassification.progression)
}
gym_keys = [
'Pewter City Key',
'Cerulean City Key',
'Vermillion City Key',
'Celadon City Key',
'Fuchsia City Key',
'Saffron City Key',
'Cinnabar Island Key',
'Viridian City Key',
]
gym_badge_codes = [
10000002,
10000004,
10000006,
10000008,
10000010,
10000012,
10000014,
10000016,
]
cup_tier_upgrade_items = {
'Poké Cup - Tier Upgrade': ItemData(10000017, ItemClassification.progression),
'Prime Cup - Tier Upgrade': ItemData(10000018, ItemClassification.progression),
}
box_upgrade_items = {
'GLC PC Box Upgrade': ItemData(10000101, ItemClassification.useful),
'Poke Cup PC Box Upgrade' : ItemData(10000102, ItemClassification.useful),
'Prime Cup PC Box Upgrade' : ItemData(10000103, ItemClassification.useful),
}
junk_items = {
"Pokedoll": ItemData(10000200, ItemClassification.filler, 0),
}
junk_weights = {
"Pokedoll": 40,
}
item_table = {
**pokemon_stadium_items,
**cup_tier_upgrade_items,
**box_upgrade_items,
**junk_items,
}
+193
View File
@@ -0,0 +1,193 @@
from typing import Dict, TYPE_CHECKING
import logging
from .Types import LocData
if TYPE_CHECKING:
from . import PokemonStadiumWorld
def get_total_locations(world: 'PokemonStadiumWorld') -> int:
if world.options.Trainersanity.value == 1:
location_table.update(trainersanity_locations)
return len(location_table)
def get_location_names() -> Dict[str, int]:
temp_loc_table = location_table.copy()
temp_loc_table.update(trainersanity_locations)
names = {name: data.ap_code for name, data in temp_loc_table.items()}
return names
def is_valid_location(world: 'PokemonStadiumWorld', name) -> bool:
return True
pokemon_stadium_locations = {
'Magikarp\'s Splash': LocData(20000100, 'Kids Club'),
'Clefairy Says': LocData(20000101, 'Kids Club'),
'Run, Rattata, Run': LocData(20000102, 'Kids Club'),
'Snore War': LocData(20000103, 'Kids Club'),
'Thundering Dynamo': LocData(20000104, 'Kids Club'),
'Sushi-Go-Round': LocData(20000105, 'Kids Club'),
'Ekans\'s Hoop Hurl': LocData(20000106, 'Kids Club'),
'Rock Harden': LocData(20000107, 'Kids Club'),
'Dig! Dig! Dig!': LocData(20000108, 'Kids Club'),
'Poké Cup - Poké Ball - Prize': LocData(20000300, 'Poké Cup'),
'Poké Cup - Poké Ball - Tier Upgrade': LocData(20000309, 'Poké Cup'),
'Poké Cup - Great Ball - Prize': LocData(20000310, 'Poké Cup'),
'Poké Cup - Great Ball - Tier Upgrade': LocData(20000319, 'Poké Cup'),
'Poké Cup - Ultra Ball - Prize': LocData(20000320, 'Poké Cup'),
'Poké Cup - Ultra Ball - Tier Upgrade': LocData(20000329, 'Poké Cup'),
'Poké Cup - Master Ball - Prize': LocData(20000330, 'Poké Cup'),
'Petit Cup Prize': LocData(20000400, 'Petit Cup'),
'Pika Cup Prize': LocData(20000500, 'Pika Cup'),
'Prime Cup - Poké Ball - Prize': LocData(20000600, 'Prime Cup'),
'Prime Cup - Poké Ball - Tier Upgrade': LocData(20000609, 'Prime Cup'),
'Prime Cup - Great Ball - Prize': LocData(20000610, 'Prime Cup'),
'Prime Cup - Great Ball - Tier Upgrade': LocData(20000619, 'Prime Cup'),
'Prime Cup - Ultra Ball - Prize': LocData(20000620, 'Prime Cup'),
'Prime Cup - Ultra Ball - Tier Upgrade': LocData(20000629, 'Prime Cup'),
'Prime Cup - Master Ball - Prize': LocData(20000630, 'Prime Cup'),
'BROCK': LocData(20000704, 'Gym Leader Castle'),
'Pewter Gym': LocData(20000705, 'Gym Leader Castle'),
'MISTY': LocData(20000714, 'Gym Leader Castle'),
'Cerulean Gym': LocData(20000715, 'Gym Leader Castle'),
'SURGE': LocData(20000724, 'Gym Leader Castle'),
'Vermillion Gym': LocData(20000725, 'Gym Leader Castle'),
'ERIKA': LocData(20000734, 'Gym Leader Castle'),
'Celadon Gym': LocData(20000735, 'Gym Leader Castle'),
'KOGA': LocData(20000744, 'Gym Leader Castle'),
'Fuchsia Gym': LocData(20000745, 'Gym Leader Castle'),
'SABRINA': LocData(20000754, 'Gym Leader Castle'),
'Saffron Gym': LocData(20000755, 'Gym Leader Castle'),
'BLAINE': LocData(20000764, 'Gym Leader Castle'),
'Cinnabar Gym': LocData(20000765, 'Gym Leader Castle'),
'GIOVANNI': LocData(20000774, 'Gym Leader Castle'),
'Viridian Gym': LocData(20000775, 'Gym Leader Castle'),
}
event_locations = {
'Beat Rival': LocData(20000000, 'Hall of Fame')
}
trainersanity_locations = {
'Poké Cup - Poké Ball - Bug Boy': LocData(20000301, 'Poké Cup'),
'Poké Cup - Poké Ball - Lad': LocData(20000302, 'Poké Cup'),
'Poké Cup - Poké Ball - Nerd': LocData(20000303, 'Poké Cup'),
'Poké Cup - Poké Ball - Sailor': LocData(20000304, 'Poké Cup'),
'Poké Cup - Poké Ball - Jr(F)': LocData(20000305, 'Poké Cup'),
'Poké Cup - Poké Ball - Jr(M)': LocData(20000306, 'Poké Cup'),
'Poké Cup - Poké Ball - Lass': LocData(20000307, 'Poké Cup'),
'Poké Cup - Poké Ball - Pokémaniac': LocData(20000308, 'Poké Cup'),
'Poké Cup - Great Ball - Bug Boy': LocData(20000311, 'Poké Cup'),
'Poké Cup - Great Ball - Lad': LocData(20000312, 'Poké Cup'),
'Poké Cup - Great Ball - Nerd': LocData(20000313, 'Poké Cup'),
'Poké Cup - Great Ball - Sailor': LocData(20000314, 'Poké Cup'),
'Poké Cup - Great Ball - Jr(F)': LocData(20000315, 'Poké Cup'),
'Poké Cup - Great Ball - Jr(M)': LocData(20000316, 'Poké Cup'),
'Poké Cup - Great Ball - Lass': LocData(20000317, 'Poké Cup'),
'Poké Cup - Great Ball - Pokémaniac': LocData(20000318, 'Poké Cup'),
'Poké Cup - Ultra Ball - Bug Boy': LocData(20000321, 'Poké Cup'),
'Poké Cup - Ultra Ball - Lad': LocData(20000322, 'Poké Cup'),
'Poké Cup - Ultra Ball - Nerd': LocData(20000323, 'Poké Cup'),
'Poké Cup - Ultra Ball - Sailor': LocData(20000324, 'Poké Cup'),
'Poké Cup - Ultra Ball - Jr(F)': LocData(20000325, 'Poké Cup'),
'Poké Cup - Ultra Ball - Jr(M)': LocData(20000326, 'Poké Cup'),
'Poké Cup - Ultra Ball - Lass': LocData(20000327, 'Poké Cup'),
'Poké Cup - Ultra Ball - Pokémaniac': LocData(20000328, 'Poké Cup'),
'Poké Cup - Master Ball - Bug Boy': LocData(20000331, 'Poké Cup'),
'Poké Cup - Master Ball - Lad': LocData(20000332, 'Poké Cup'),
'Poké Cup - Master Ball - Nerd': LocData(20000333, 'Poké Cup'),
'Poké Cup - Master Ball - Sailor': LocData(20000334, 'Poké Cup'),
'Poké Cup - Master Ball - Jr(F)': LocData(20000335, 'Poké Cup'),
'Poké Cup - Master Ball - Jr(M)': LocData(20000336, 'Poké Cup'),
'Poké Cup - Master Ball - Lass': LocData(20000337, 'Poké Cup'),
'Poké Cup - Master Ball - Pokémaniac': LocData(20000338, 'Poké Cup'),
'Petit Cup - Bug Boy': LocData(20000401, 'Petit Cup'),
'Petit Cup - Lad': LocData(20000402, 'Petit Cup'),
'Petit Cup - Nerd': LocData(20000403, 'Petit Cup'),
'Petit Cup - Sailor': LocData(20000404, 'Petit Cup'),
'Petit Cup - Jr(F)': LocData(20000405, 'Petit Cup'),
'Petit Cup - Jr(M)': LocData(20000406, 'Petit Cup'),
'Petit Cup - Lass': LocData(20000407, 'Petit Cup'),
'Petit Cup - Pokémaniac': LocData(20000408, 'Petit Cup'),
'Pika Cup - Bug Boy': LocData(20000501, 'Pika Cup'),
'Pika Cup - Lad': LocData(20000502, 'Pika Cup'),
'Pika Cup - Swimmer': LocData(20000503, 'Pika Cup'),
'Pika Cup - Burglar': LocData(20000504, 'Pika Cup'),
'Pika Cup - Mr. Fix': LocData(20000505, 'Pika Cup'),
'Pika Cup - Hiker': LocData(20000506, 'Pika Cup'),
'Pika Cup - Lass': LocData(20000507, 'Pika Cup'),
'Pika Cup - Fisher': LocData(20000508, 'Pika Cup'),
'Prime Cup - Poké Ball - Cue Ball': LocData(20000601, 'Prime Cup'),
'Prime Cup - Poké Ball - Rocket': LocData(20000602, 'Prime Cup'),
'Prime Cup - Poké Ball - Judoboy': LocData(20000603, 'Prime Cup'),
'Prime Cup - Poké Ball - Gambler': LocData(20000604, 'Prime Cup'),
'Prime Cup - Poké Ball - Cool(F)': LocData(20000605, 'Prime Cup'),
'Prime Cup - Poké Ball - Bird Boy': LocData(20000606, 'Prime Cup'),
'Prime Cup - Poké Ball - Lab Man': LocData(20000607, 'Prime Cup'),
'Prime Cup - Poké Ball - Cool(M)': LocData(20000608, 'Prime Cup'),
'Prime Cup - Great Ball - Cue Ball': LocData(20000611, 'Prime Cup'),
'Prime Cup - Great Ball - Rocket': LocData(20000612, 'Prime Cup'),
'Prime Cup - Great Ball - Judoboy': LocData(20000613, 'Prime Cup'),
'Prime Cup - Great Ball - Gambler': LocData(20000614, 'Prime Cup'),
'Prime Cup - Great Ball - Cool(F)': LocData(20000615, 'Prime Cup'),
'Prime Cup - Great Ball - Bird Boy': LocData(20000616, 'Prime Cup'),
'Prime Cup - Great Ball - Lab Man': LocData(20000617, 'Prime Cup'),
'Prime Cup - Great Ball - Cool(M)': LocData(20000618, 'Prime Cup'),
'Prime Cup - Ultra Ball - Cue Ball': LocData(20000621, 'Prime Cup'),
'Prime Cup - Ultra Ball - Rocket': LocData(20000622, 'Prime Cup'),
'Prime Cup - Ultra Ball - Judoboy': LocData(20000623, 'Prime Cup'),
'Prime Cup - Ultra Ball - Gambler': LocData(20000624, 'Prime Cup'),
'Prime Cup - Ultra Ball - Cool(F)': LocData(20000625, 'Prime Cup'),
'Prime Cup - Ultra Ball - Bird Boy': LocData(20000626, 'Prime Cup'),
'Prime Cup - Ultra Ball - Lab Man': LocData(20000627, 'Prime Cup'),
'Prime Cup - Ultra Ball - Cool(M)': LocData(20000628, 'Prime Cup'),
'Prime Cup - Master Ball - Cue Ball': LocData(20000631, 'Prime Cup'),
'Prime Cup - Master Ball - Rocket': LocData(20000632, 'Prime Cup'),
'Prime Cup - Master Ball - Judoboy': LocData(20000633, 'Prime Cup'),
'Prime Cup - Master Ball - Gambler': LocData(20000634, 'Prime Cup'),
'Prime Cup - Master Ball - Cool(F)': LocData(20000635, 'Prime Cup'),
'Prime Cup - Master Ball - Bird Boy': LocData(20000636, 'Prime Cup'),
'Prime Cup - Master Ball - Lab Man': LocData(20000637, 'Prime Cup'),
'Prime Cup - Master Ball - Cool(M)': LocData(20000638, 'Prime Cup'),
'Pewter Gym - Bug Boy': LocData(20000701, 'Gym Leader Castle'),
'Pewter Gym - Lad': LocData(20000702, 'Gym Leader Castle'),
'Pewter Gym - Jr(M)': LocData(20000703, 'Gym Leader Castle'),
'Cerulean Gym - Fisher': LocData(20000711, 'Gym Leader Castle'),
'Cerulean Gym - Jr(F)': LocData(20000712, 'Gym Leader Castle'),
'Cerulean Gym - Swimmer': LocData(20000713, 'Gym Leader Castle'),
'Vermillion Gym - Sailor': LocData(20000721, 'Gym Leader Castle'),
'Vermillion Gym - Rocker': LocData(20000722, 'Gym Leader Castle'),
'Vermillion Gym - Old Man': LocData(20000723, 'Gym Leader Castle'),
'Celadon Gym - Lass': LocData(20000731, 'Gym Leader Castle'),
'Celadon Gym - Beauty': LocData(20000732, 'Gym Leader Castle'),
'Celadon Gym - Cool(F)': LocData(20000733, 'Gym Leader Castle'),
'Fuchsia Gym - Biker': LocData(20000741, 'Gym Leader Castle'),
'Fuchsia Gym - Tamer': LocData(20000742, 'Gym Leader Castle'),
'Fuchsia Gym - Juggler': LocData(20000743, 'Gym Leader Castle'),
'Saffron Gym - Cue Ball': LocData(20000751, 'Gym Leader Castle'),
'Saffron Gym - Burglar': LocData(20000752, 'Gym Leader Castle'),
'Saffron Gym - Medium': LocData(20000753, 'Gym Leader Castle'),
'Cinnabar Gym - Judoboy': LocData(20000761, 'Gym Leader Castle'),
'Cinnabar Gym - Psychic': LocData(20000762, 'Gym Leader Castle'),
'Cinnabar Gym - Nerd': LocData(20000763, 'Gym Leader Castle'),
'Viridian Gym - Rocket': LocData(20000771, 'Gym Leader Castle'),
'Viridian Gym - Lab Man': LocData(20000772, 'Gym Leader Castle'),
'Viridian Gym - Cool(M)': LocData(20000773, 'Gym Leader Castle'),
}
location_table = {
**pokemon_stadium_locations,
**event_locations
}
+342
View File
@@ -0,0 +1,342 @@
from typing import List, Dict, Any
from dataclasses import dataclass
from worlds.AutoWorld import PerGameCommonOptions
from Options import Choice, OptionGroup, Toggle, Range
def create_option_groups() -> List[OptionGroup]:
option_group_list: List[OptionGroup] = []
for name, options in pokemon_stadium_option_groups.items():
option_group_list.append(OptionGroup(name=name, options=options))
return option_group_list
class VictoryCondition(Choice):
"""
Choose victory condition
"""
display_name = "Victory Condition"
option_defeat_rival = 1
option_clear_master_ball_cup = 2
default = 1
class BaseStatTotalRandomness(Choice):
"""
Controls the level of randomness for Pokemon BST. Stat distribution per Pokemon will follow a randomly selected distribution curve.
The higher the selection, the more extreme a curve you may see used.
Stat changes are universal. Rental Pokemon and enemy trainer team Pokemon use the same BSTs.
Vanilla - No change
Low - 3 distribution types
Medium - 4 distribution types
High - 5 distribution types
"""
display_name = "BST Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class Trainersanity(Toggle):
"""
Toggle on to make all Trainers into checks. This option is off by default.
"""
display_name = 'Trainersanity'
option_off = 0
option_on = 1
default = 0
class GymCastleTrainerRandomness(Choice):
"""
Controls the level of randomness for the enemy team and movesets in Gym Leader Castle.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Gym Castle Trainer Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PokeCupTrainerRandomness(Choice):
"""
Controls the level of randomness for the enemy team and movesets in Poke Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Poke Cup Trainer Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PrimeCupTrainerRandomness(Choice):
"""
Controls the level of randomness for the enemy team and movesets in Prime Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Prime Cup Trainer Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PetitCupTrainerRandomness(Choice):
"""
Controls the level of randomness for the enemy team and movesets in Petit Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Petit Cup Trainer Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PikaCupTrainerRandomness(Choice):
"""
Controls the level of randomness for the enemy team and movesets in Pika Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Pika Cup Trainer Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class GymCastleRentalRandomness(Choice):
"""
Controls the level of randomness for the rental Pokemon moves in Gym Leader Castle.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Gym Castle Rental Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PokeCupRentalRandomness(Choice):
"""
Controls the level of randomness for the rental Pokemon moves in the Poke Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Poke Cup Rental Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PrimeCupRentalRandomness(Choice):
"""
Controls the level of randomness for the rental Pokemon moves in the Prime Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Prime Cup Rental Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PetitCupRentalRandomness(Choice):
"""
Controls the level of randomness for the rental Pokemon moves in the Petit Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Petit Cup Rental Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class PikaCupRentalRandomness(Choice):
"""
Controls the level of randomness for the rental Pokemon moves in the Pika Cup.
Vanilla - No change
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
"""
display_name = "Pika Cup Rental Randomness"
option_vanilla = 1
option_low = 2
option_medium = 3
option_high = 4
default = 1
class RentalListShuffle(Choice):
"""
Controls whether the rental pokemon list is randomized or not
Instead of going in dex order, the rental tables will be shuffled
Off - No change
On - All tables shuffled
Manual: Select which tables are shuffled
"""
display_name = "Rental List Shuffle"
option_off = 1
option_on = 2
option_manual = 3
default = 1
class RentalListShuffleGLC(Choice):
"""
Controls whether the rental pokemon list for the Gym Leader Castle is randomized or not
Instead of going in dex order, the rental tables will be shuffled
This option only matters if RentalListShuffle is set to Manual mode.
Default is set to On
Off - No change
On - All tables shuffled
"""
display_name = "RLS Manual: Gym Leader Castle"
option_off = 1
option_on = 2
default = 2
class RentalListShufflePokeCup(Choice):
"""
Controls whether the rental pokemon list for the Poke Cup is randomized or not
Instead of going in dex order, the rental tables will be shuffled
This option only matters if RentalListShuffle is set to Manual mode.
Default is set to On
Off - No change
On - All tables shuffled
"""
display_name = "RLS Manual: Poke Cup"
option_off = 1
option_on = 2
default = 2
class RentalListShufflePrimeCup(Choice):
"""
Controls whether the rental pokemon list for the Prime Cup is randomized or not
Instead of going in dex order, the rental tables will be shuffled
This option only matters if RentalListShuffle is set to Manual mode.
Default is set to On
Off - No change
On - All tables shuffled
"""
display_name = "RLS Manual: Prime Cup"
option_off = 1
option_on = 2
default = 2
class RentalListShufflePetitCup(Choice):
"""
Controls whether the rental pokemon list for the Petit Cup is randomized or not
Instead of going in dex order, the rental tables will be shuffled
This option only matters if RentalListShuffle is set to Manual mode.
Default is set to On
Off - No change
On - All tables shuffled
"""
display_name = "RLS Manual: Petit Cup"
option_off = 1
option_on = 2
default = 2
class RentalListShufflePikaCup(Choice):
"""
Controls whether the rental pokemon list for the Pika Cup is randomized or not
Instead of going in dex order, the rental tables will be shuffled
This option only matters if RentalListShuffle is set to Manual mode.
Default is set to On
Off - No change
On - All tables shuffled
"""
display_name = "RLS Manual: Pika Cup"
option_off = 1
option_on = 2
default = 2
@dataclass
class PokemonStadiumOptions(PerGameCommonOptions):
VictoryCondition: VictoryCondition
BaseStatTotalRandomness: BaseStatTotalRandomness
Trainersanity: Trainersanity
GymCastleTrainerRandomness: GymCastleTrainerRandomness
PokeCupTrainerRandomness: PokeCupTrainerRandomness
PrimeCupTrainerRandomness: PrimeCupTrainerRandomness
PetitCupTrainerRandomness: PetitCupTrainerRandomness
PikaCupTrainerRandomness: PikaCupTrainerRandomness
GymCastleRentalRandomness: GymCastleRentalRandomness
PokeCupRentalRandomness: PokeCupRentalRandomness
PrimeCupRentalRandomness: PrimeCupRentalRandomness
PetitCupRentalRandomness: PetitCupRentalRandomness
PikaCupRentalRandomness: PikaCupRentalRandomness
RentalListShuffle: RentalListShuffle
RentalListShuffleGLC: RentalListShuffleGLC
RentalListShufflePokeCup: RentalListShufflePokeCup
RentalListShufflePrimeCup: RentalListShufflePrimeCup
RentalListShufflePetitCup: RentalListShufflePetitCup
RentalListShufflePikaCup: RentalListShufflePikaCup
# This is where you organize your options
# Its entirely up to you how you want to organize it
pokemon_stadium_option_groups: Dict[str, List[Any]] = {
"General Options": [
VictoryCondition,
BaseStatTotalRandomness,
Trainersanity,
],
"Enemy Trainer Pokemon Options": [
GymCastleTrainerRandomness,
PokeCupTrainerRandomness,
PrimeCupTrainerRandomness,
PetitCupTrainerRandomness,
PikaCupTrainerRandomness,
],
"Rental Pokemon Options":
[
GymCastleRentalRandomness,
PokeCupRentalRandomness,
PrimeCupRentalRandomness,
PetitCupRentalRandomness,
PikaCupRentalRandomness,
],
"Shuffling Options":
[ RentalListShuffle,
RentalListShuffleGLC,
RentalListShufflePokeCup,
RentalListShufflePrimeCup,
RentalListShufflePetitCup,
RentalListShufflePikaCup],
}
+49
View File
@@ -0,0 +1,49 @@
from BaseClasses import Region
from .Types import PokemonStadiumLocation
from .Locations import location_table, trainersanity_locations, is_valid_location
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import PokemonStadiumWorld
def create_regions(world: "PokemonStadiumWorld"):
menu = create_region(world, "Menu")
# ---------------------------------- Gym Leader Castle ----------------------------------
gym_leader_castle = create_region_and_connect(world, "Gym Leader Castle", "Menu -> Gym Leader Castle", menu)
create_region_and_connect(world, "Elite Four", "Gym Leader Castle -> Elite Four", gym_leader_castle)
create_region_and_connect(world, "Rival", "Elite Four -> Rival", gym_leader_castle)
create_region_and_connect(world, "Hall of Fame", "Rival -> Hall of Fame", gym_leader_castle)
create_region_and_connect(world, "Beat Rival", "Hall of Fame -> Beat Rival", gym_leader_castle)
# -------------------------------------- Kids Club --------------------------------------
create_region_and_connect(world, "Kids Club", "Menu -> Kids Club", menu)
# --------------------------------------- Stadium ---------------------------------------
stadium = create_region_and_connect(world, "Stadium", "Menu -> Stadium", menu)
create_region_and_connect(world, "Poké Cup", "Stadium -> Poké Cup", stadium)
create_region_and_connect(world, "Petit Cup", "Stadium -> Petit Cup", stadium)
create_region_and_connect(world, "Pika Cup", "Stadium -> Pika Cup", stadium)
create_region_and_connect(world, "Prime Cup", "Stadium -> Prime Cup", stadium)
def create_region(world: "PokemonStadiumWorld", name: str) -> Region:
reg = Region(name, world.player, world.multiworld)
if world.options.Trainersanity.value == 1:
location_table.update(trainersanity_locations)
for (key, data) in location_table.items():
if data.region == name:
if not is_valid_location(world, key):
continue
location = PokemonStadiumLocation(world.player, key, data.ap_code, reg)
reg.locations.append(location)
world.multiworld.regions.append(reg)
return reg
def create_region_and_connect(world: "PokemonStadiumWorld", name: str, entrancename: str, connected_region: Region) -> Region:
reg: Region = create_region(world, name)
connected_region.connect(reg, entrancename)
return reg
+136
View File
@@ -0,0 +1,136 @@
import hashlib
import os
from settings import get_settings
import Utils
from worlds.AutoWorld import World
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
from .randomizer import stadium_randomizer
NOP = bytes([0x00,0x00,0x00,0x00])
MD5Hash = "ed1378bc12115f71209a77844965ba50"
class PokemonStadiumProcedurePatch(APProcedurePatch, APTokenMixin):
game = "Pokemon Stadium"
hash = MD5Hash
patch_file_ending = ".apstadium"
result_file_ending = ".z64"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def get_base_rom_bytes() -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path()
base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
md5hash = basemd5.hexdigest()
if MD5Hash !=md5hash:
raise Exception("Supplied Rom does not match known MD5 for Pokemon Stadium")
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path():
file_name = get_settings()["stadium_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
def write_tokens(world:World, patch:PokemonStadiumProcedurePatch):
# version = settings['ROMVersion']
bst_factor = world.options.BaseStatTotalRandomness.value
glc_trainer_factor = world.options.GymCastleTrainerRandomness.value
pokecup_trainer_factor = world.options.PokeCupTrainerRandomness.value
primecup_trainer_factor = world.options.PrimeCupTrainerRandomness.value
petitcup_trainer_factor = world.options.PetitCupTrainerRandomness.value
pikacup_trainer_factor = world.options.PikaCupTrainerRandomness.value
glc_rental_factor = world.options.GymCastleRentalRandomness.value
pokecup_rental_factor = world.options.PokeCupRentalRandomness.value
primecup_rental_factor = world.options.PrimeCupRentalRandomness.value
petitcup_rental_factor = world.options.PetitCupRentalRandomness.value
pikacup_rental_factor = world.options.PikaCupRentalRandomness.value
rental_list_shuffle_factor = world.options.RentalListShuffle.value
rental_list_shuffle_glc_factor = world.options.RentalListShuffleGLC.value
rental_list_shuffle_poke_cup_factor = world.options.RentalListShufflePokeCup.value
rental_list_shuffle_prime_cup_factor = world.options.RentalListShufflePrimeCup.value
rental_list_shuffle_petit_cup_factor = world.options.RentalListShufflePetitCup.value
rental_list_shuffle_pika_cup_factor = world.options.RentalListShufflePikaCup.value
randomizer = stadium_randomizer.Randomizer('US_1.0', bst_factor, glc_trainer_factor, pokecup_trainer_factor, primecup_trainer_factor, petitcup_trainer_factor,
pikacup_trainer_factor, glc_rental_factor, pokecup_rental_factor, primecup_rental_factor,petitcup_rental_factor, pikacup_rental_factor,
rental_list_shuffle_factor, rental_list_shuffle_glc_factor, rental_list_shuffle_poke_cup_factor, rental_list_shuffle_prime_cup_factor,
rental_list_shuffle_petit_cup_factor, rental_list_shuffle_pika_cup_factor)
# Bypass CIC
randomizer.disable_checksum(patch)
if bst_factor > 1:
randomizer.randomize_base_stats(patch)
if glc_trainer_factor > 1:
randomizer.randomize_glc_trainer_pokemon_round1(patch)
if pokecup_trainer_factor > 1:
randomizer.randomize_pokecup_trainer_pokemon_round1(patch)
if primecup_trainer_factor > 1:
randomizer.randomize_primecup_trainer_pokemon_round1(patch)
if petitcup_trainer_factor > 1:
randomizer.randomize_petitcup_trainer_pokemon_round1(patch)
if pikacup_trainer_factor > 1:
randomizer.randomize_pikacup_trainer_pokemon_round1(patch)
if glc_rental_factor > 1:
randomizer.randomize_glc_rentals_round1(patch)
if pokecup_rental_factor > 1:
randomizer.randomize_pokecup_rentals(patch)
if primecup_rental_factor > 1:
randomizer.randomize_primecup_rentals_round1(patch)
if petitcup_rental_factor > 1:
randomizer.randomize_petitcup_rentals(patch)
if pikacup_rental_factor > 1:
randomizer.randomize_pikacup_rentals(patch)
if rental_list_shuffle_factor > 1:
if rental_list_shuffle_factor != 3: #Not in manual mode
randomizer.shuffle_rentals(patch)
else:
if rental_list_shuffle_glc_factor > 1:
randomizer.shuffle_glc(patch)
if rental_list_shuffle_poke_cup_factor > 1:
randomizer.shuffle_poke(patch)
if rental_list_shuffle_prime_cup_factor > 1:
randomizer.shuffle_prime(patch)
if rental_list_shuffle_petit_cup_factor > 1:
randomizer.shuffle_petit(patch)
if rental_list_shuffle_pika_cup_factor > 1:
randomizer.shuffle_pika(patch)
# Set GP Register to 80420000
patch.write_token(APTokenTypes.WRITE, 0x202B8, bytes([0x3C, 0x1C, 0x80, 0x42]))
# Set 'Starting Battle' flag
patch.write_token(APTokenTypes.WRITE, 0x855C, bytes([0xAF, 0x81, 0x00, 0x10]))
# Clear 'Starting Battle' flag
patch.write_token(APTokenTypes.WRITE, 0x396D08, bytes([0xAF, 0x80, 0x00, 0x10]))
# Turn off A and B button on GLC select screen
patch.write_token(APTokenTypes.WRITE, 0x3B4DA8, bytes([0x50, 0x21, 0xFF, 0x82]))
# First instruction to set flag for GLC selection screen
patch.write_token(APTokenTypes.WRITE, 0x3B5548, bytes([0xAF, 0x84, 0x00, 0x00]))
# Second instruction to set flag for GLC selection screen
patch.write_token(APTokenTypes.WRITE, 0x3B55F4, bytes([0xAF, 0x82, 0x00, 0x00]))
# Set selecting Poke Cup tier flag
patch.write_token(APTokenTypes.WRITE, 0x2D6A20, bytes([0xAF, 0x93, 0x00, 0x20]))
# Clear selecting Poke Cup tier flag
patch.write_token(APTokenTypes.WRITE, 0x2D6DB0, bytes([0xAF, 0x80, 0x00, 0x20]))
# Stop game from activating unlocked gyms
patch.write_token(APTokenTypes.WRITE, 0x3B5728, bytes([0xA3, 0x20, 0x00, 0x01]))
# Write patch file
patch.write_file("token_data.bin", patch.get_token_binary())
+132
View File
@@ -0,0 +1,132 @@
from worlds.generic.Rules import set_rule, add_item_rule
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import PokemonStadiumWorld
def set_rules(world: "PokemonStadiumWorld"):
player = world.player
options = world.options
# Gym Access
set_rule(world.multiworld.get_location("Pewter Gym", player), lambda state: state.has("Pewter City Key", player))
set_rule(world.multiworld.get_location("Cerulean Gym", player), lambda state: state.has("Cerulean City Key", player))
set_rule(world.multiworld.get_location("Vermillion Gym", player), lambda state: state.has("Vermillion City Key", player))
set_rule(world.multiworld.get_location("Celadon Gym", player), lambda state: state.has("Celadon City Key", player))
set_rule(world.multiworld.get_location("Fuchsia Gym", player), lambda state: state.has("Fuchsia City Key", player))
set_rule(world.multiworld.get_location("Saffron Gym", player), lambda state: state.has("Saffron City Key", player))
set_rule(world.multiworld.get_location("Cinnabar Gym", player), lambda state: state.has("Cinnabar Island Key", player))
set_rule(world.multiworld.get_location("Viridian Gym", player), lambda state: state.has("Viridian City Key", player))
set_rule(world.multiworld.get_location("BROCK", player), lambda state: state.has("Pewter City Key", player))
set_rule(world.multiworld.get_location("MISTY", player), lambda state: state.has("Cerulean City Key", player))
set_rule(world.multiworld.get_location("SURGE", player), lambda state: state.has("Vermillion City Key", player))
set_rule(world.multiworld.get_location("ERIKA", player), lambda state: state.has("Celadon City Key", player))
set_rule(world.multiworld.get_location("KOGA", player), lambda state: state.has("Fuchsia City Key", player))
set_rule(world.multiworld.get_location("SABRINA", player), lambda state: state.has("Saffron City Key", player))
set_rule(world.multiworld.get_location("BLAINE", player), lambda state: state.has("Cinnabar Island Key", player))
set_rule(world.multiworld.get_location("GIOVANNI", player), lambda state: state.has("Viridian City Key", player))
# Cup Access
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Prize", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Prize", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Prize", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Prize", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Prize", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Prize", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
#Trainersanity All
if world.options.Trainersanity.value == 1:
set_rule(world.multiworld.get_location("Pewter Gym - Bug Boy", player), lambda state: state.has("Pewter City Key", player))
set_rule(world.multiworld.get_location("Pewter Gym - Lad", player), lambda state: state.has("Pewter City Key", player))
set_rule(world.multiworld.get_location("Pewter Gym - Jr(M)", player), lambda state: state.has("Pewter City Key", player))
set_rule(world.multiworld.get_location("Cerulean Gym - Fisher", player), lambda state: state.has("Cerulean City Key", player))
set_rule(world.multiworld.get_location("Cerulean Gym - Jr(F)", player), lambda state: state.has("Cerulean City Key", player))
set_rule(world.multiworld.get_location("Cerulean Gym - Swimmer", player), lambda state: state.has("Cerulean City Key", player))
set_rule(world.multiworld.get_location("Vermillion Gym - Sailor", player), lambda state: state.has("Vermillion City Key", player))
set_rule(world.multiworld.get_location("Vermillion Gym - Rocker", player), lambda state: state.has("Vermillion City Key", player))
set_rule(world.multiworld.get_location("Vermillion Gym - Old Man", player), lambda state: state.has("Vermillion City Key", player))
set_rule(world.multiworld.get_location("Celadon Gym - Lass", player), lambda state: state.has("Celadon City Key", player))
set_rule(world.multiworld.get_location("Celadon Gym - Beauty", player), lambda state: state.has("Celadon City Key", player))
set_rule(world.multiworld.get_location("Celadon Gym - Cool(F)", player), lambda state: state.has("Celadon City Key", player))
set_rule(world.multiworld.get_location("Fuchsia Gym - Biker", player), lambda state: state.has("Fuchsia City Key", player))
set_rule(world.multiworld.get_location("Fuchsia Gym - Tamer", player), lambda state: state.has("Fuchsia City Key", player))
set_rule(world.multiworld.get_location("Fuchsia Gym - Juggler", player), lambda state: state.has("Fuchsia City Key", player))
set_rule(world.multiworld.get_location("Saffron Gym - Cue Ball", player), lambda state: state.has("Saffron City Key", player))
set_rule(world.multiworld.get_location("Saffron Gym - Burglar", player), lambda state: state.has("Saffron City Key", player))
set_rule(world.multiworld.get_location("Saffron Gym - Medium", player), lambda state: state.has("Saffron City Key", player))
set_rule(world.multiworld.get_location("Cinnabar Gym - Judoboy", player), lambda state: state.has("Cinnabar Island Key", player))
set_rule(world.multiworld.get_location("Cinnabar Gym - Psychic", player), lambda state: state.has("Cinnabar Island Key", player))
set_rule(world.multiworld.get_location("Cinnabar Gym - Nerd", player), lambda state: state.has("Cinnabar Island Key", player))
set_rule(world.multiworld.get_location("Viridian Gym - Rocket", player), lambda state: state.has("Viridian City Key", player))
set_rule(world.multiworld.get_location("Viridian Gym - Lab Man", player), lambda state: state.has("Viridian City Key", player))
set_rule(world.multiworld.get_location("Viridian Gym - Cool(M)", player), lambda state: state.has("Viridian City Key", player))
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Bug Boy", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Lad", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Nerd", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Sailor", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Jr(F)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Jr(M)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Lass", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Pokémaniac", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Bug Boy", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Lad", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Nerd", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Sailor", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Jr(F)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Jr(M)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Lass", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Pokémaniac", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Bug Boy", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Lad", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Nerd", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Sailor", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Jr(F)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Jr(M)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Lass", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Pokémaniac", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Cue Ball", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Rocket", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Judoboy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Gambler", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Cool(F)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Bird Boy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Lab Man", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Cool(M)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Cue Ball", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Rocket", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Judoboy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Gambler", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Cool(F)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Bird Boy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Lab Man", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Cool(M)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Cue Ball", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Rocket", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Judoboy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Gambler", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Cool(F)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Bird Boy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Lab Man", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Cool(M)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
# Beat Rival Rule
badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge", "Volcano Badge", "Earth Badge"]
set_rule(world.multiworld.get_location("Beat Rival", player), lambda state: state.has_all(badges, player))
# Victory condition rule!
world.multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
+18
View File
@@ -0,0 +1,18 @@
from enum import IntEnum
from typing import NamedTuple, Optional
from BaseClasses import Location, Item, ItemClassification
class PokemonStadiumLocation(Location):
game = 'PokemonStadium'
class PokemonStadiumItem(Item):
game = 'PokemonStadium'
class ItemData(NamedTuple):
ap_code: Optional[int]
classification: ItemClassification
count: Optional[int] = 1
class LocData(NamedTuple):
ap_code: Optional[int]
region: Optional[str]
+148
View File
@@ -0,0 +1,148 @@
import hashlib
import logging
import os
import pkgutil
import random
from BaseClasses import MultiWorld, Item, Tutorial
import settings
from typing import Dict
import Utils
from worlds.AutoWorld import World, CollectionState, WebWorld
from .Client import PokemonStadiumClient # Unused, but required to register with BizHawkClient
from .Items import create_item, create_itempool, gym_keys, item_table
from .Locations import get_location_names, get_total_locations
from .Options import PokemonStadiumOptions
from .Regions import create_regions
from .Rom import MD5Hash, PokemonStadiumProcedurePatch, write_tokens
from .Rom import get_base_rom_path as get_base_rom_path
from .Rules import set_rules
class PokemonStadiumSettings(settings.Group):
class PokemonStadiumRomFile(settings.UserFilePath):
"""File name of the Pokemon Stadium (US, 1.0) ROM"""
description = "Pokemon Stadium (US, 1.0) ROM File"
copy_to = "Pokemon Stadium (US, 1.0).z64"
md5s = [PokemonStadiumProcedurePatch.hash]
rom_file: PokemonStadiumRomFile = PokemonStadiumRomFile(PokemonStadiumRomFile.copy_to)
class PokemonStadiumWeb(WebWorld):
theme = "Party"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up (the game you are randomizing) for Archipelago. "
"This guide covers single-player, multiworld, and related software.",
"English",
"setup_en.md",
"setup/en",
["JCIII"]
)]
class PokemonStadiumWorld(World):
game = "Pokemon Stadium"
settings_key = "stadium_options"
settings: PokemonStadiumSettings
item_name_to_id = {name: data.ap_code for name, data in item_table.items()}
location_name_to_id = get_location_names()
options_dataclass = PokemonStadiumOptions
options = PokemonStadiumOptions
web = PokemonStadiumWeb()
starting_gym_keys = random.sample(gym_keys, 3)
def __init__(self, multiworld: "MultiWorld", player: int):
super().__init__(multiworld, player)
def generate_early(self):
for key in self.starting_gym_keys:
self.multiworld.push_precollected(self.create_item(key))
def create_regions(self):
create_regions(self)
def create_items(self):
self.multiworld.itempool += create_itempool(self)
def create_item(self, name: str) -> Item:
return create_item(self, name)
def set_rules(self):
set_rules(self)
def fill_slot_data(self) -> Dict[str, object]:
slot_data: Dict[str, object] = {
"options": {
"VictoryCondition": self.options.VictoryCondition.value,
"BaseStatTotalRandomness": self.options.BaseStatTotalRandomness.value,
"Trainersanity": self.options.Trainersanity.value,
"GymCastleTrainerRandomness": self.options.GymCastleTrainerRandomness.value,
"PokeCupTrainerRandomness": self.options.PokeCupTrainerRandomness.value,
"PrimeCupTrainerRandomness": self.options.PrimeCupTrainerRandomness.value,
"PetitupTrainerRandomness": self.options.PetitCupTrainerRandomness.value,
"PikaCupTrainerRandomness": self.options.PikaCupTrainerRandomness.value,
"GymCastleRentalRandomness": self.options.GymCastleRentalRandomness.value,
"PokeCupRentalRandomness": self.options.PokeCupRentalRandomness.value,
"PrimeCupRentalRandomness": self.options.PrimeCupRentalRandomness.value,
"PetitCupRentalRandomness": self.options.PetitCupRentalRandomness.value,
"RentalListShuffle": self.options.RentalListShuffle.value,
"RentalListShuffleGLC": self.options.RentalListShuffleGLC.value,
"RentalListShufflePokeCup": self.options.RentalListShufflePokeCup.value,
"RentalListShufflePrimeCup": self.options.RentalListShufflePrimeCup.value,
"RentalListShufflePetitCup": self.options.RentalListShufflePetitCup.value,
"RentalListShufflePikaCup": self.options.RentalListShufflePikaCup.value,
},
"Seed": self.multiworld.seed_name, # to verify the server's multiworld
"Slot": self.multiworld.player_name[self.player], # to connect to server
"TotalLocations": get_total_locations(self) # get_total_locations(self) comes from Locations.py
}
return slot_data
def generate_output(self, output_directory: str) -> None:
# === Step 1: Build ROM and player metadata ===
outfilepname = f"_P{self.player}_"
outfilepname += f"{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}"
# ROM name metadata (embedded in ROM for client/UI)
self.rom_name_text = f'PokemonStadium{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:011}\0'
self.romName = bytearray(self.rom_name_text, "utf8")[:0x20]
self.romName.extend([0] * (0x20 - len(self.romName))) # pad to 0x20
self.rom_name = self.romName
# Player name metadata
self.playerName = bytearray(self.multiworld.player_name[self.player], "utf8")[:0x20]
self.playerName.extend([0] * (0x20 - len(self.playerName)))
# === Step 3: Create procedure patch object ===
patch = PokemonStadiumProcedurePatch(
player=self.player,
player_name=self.multiworld.player_name[self.player]
)
# === Step 4: Apply token modifications directly ===
write_tokens(self, patch)
procedure = [("apply_tokens", ["token_data.bin"])]
# === Step 6: Finalize procedure ===
patch.procedure = procedure
# Generate output file path
out_file_name = self.multiworld.get_out_file_name_base(self.player)
patch_file_path = os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}")
# Write the final patch file (.bps)
patch.write(patch_file_path)
def collect(self, state: "CollectionState", item: "Item") -> bool:
return super().collect(state, item)
def remove(self, state: "CollectionState", item: "Item") -> bool:
return super().remove(state, item)
+1
View File
@@ -0,0 +1 @@
stop making me do this
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
@@ -0,0 +1,18 @@
# Pokemon Stadium Randomizer
https://stadiumrando.com
## Built-in Rental Rando
- Press START on the rental team selection screen to fill your team with random Pokemon
## Currently Randomizing
- Gym Leader Castle
- Player Rentals
- Base stats
- EVs and IVs
- Moves
- Enemies
- Pokemon
- Base stats
- EVs and IVs
- Moves
@@ -0,0 +1,898 @@
rom_offsets = {
"US_1.0" : {
"CheckSum1" : 0x63C,
"CheckSum2" : 0x648,
"SetBattleStartFlag": 34140,
"BaseStats" : 465825,
"SetGPRegister": 131768,
"SetPokeCupFlag": 2976288,
"ClearPokeCupFlag": 2977200,
"Rental_Table_Input_Routine" : 3023512,
"DefeatedNonLeaderFlag": 3761116,
"LostToTrainerFlag": 3763336,
"SetGLCFlag1": 3888456,
"SetGLCFlag2": 3888628,
"GymCastle_Round1": 9057228,
"PokeCup_Round1": 9039244, #This starts at pokeball cup
"PokeCup_Round2": 9159120, #Needs adjustment
"PrimeCup_Round1": 9021260, #This starts at pokeball cup
"PrimeCup_Round2": 9141136, #Needs adjustment
"PetitCup_Round1": 9012268, #Starts at first pokemon first trainer
"PetitCup_Round2": 9132144, #Needs adjustment
"PikaCup_Round1": 9016764, #Starts at first pokemon first trainer
"PikaCup_Round2": 9136640, #Needs adjustment
"Mewtwo_Round1": 9081408, #Needs adjustment
"Mewtwo_Round2": 9201344, #Needs adjustment
"Rentals_GymCastle_Round1" : 9119616,
"Rentals_PokeCup" : 9105952,
"Rentals_PrimeCup_Round1" : 9093424,
"Rentals_PrimeCup_Round2" : 9201920,
"Rentals_PetitCup" : 9081984,
"Rentals_PikaCup" : 9085776,
},
"PAL_1.1" : {
"CheckSum1" : 1596,
"CheckSum2" : 1608,
"BaseStats" : 466337,
"Rental_Table_Input_Routine" : 2967864,
"Rental_Table_Header" : 7882439,
"Rental_GymCastle_Round1_Pointer" : 8872432,
"GymCastle_Round1": 8917964,
"EmptyRomSpace" : 33301456,
"EmptyRomSpaceForTables" : 33302224,
"OffsetToNewTable" : "0174C6D000003200"
}
}
kanto_dex_names = [
{"name": "BULBASAUR", "type": "1603", "exp": "117360", 'bst': [45, 49, 49, 45, 65 ], "gr" : "mediumslow"},
{"name": "IVYSAUR", "type": "1603", "exp": "117360", 'bst': [60, 62, 63, 60, 80 ], "gr" : "mediumslow"},
{"name": "VENUSAUR", "type": "1603", "exp": "117360", 'bst': [80, 82, 83, 80, 100], "gr" : "mediumslow"},
{"name": "CHARMANDER", "type": "1414", "exp": "117360", 'bst': [39, 52, 43, 65, 50 ], "gr" : "mediumslow"},
{"name": "CHARMELEON", "type": "1414", "exp": "117360", 'bst': [58, 64, 58, 80, 65 ], "gr" : "mediumslow"},
{"name": "CHARIZARD", "type": "1402", "exp": "117360", 'bst': [78, 84, 78, 100, 85 ], "gr" : "mediumslow"},
{"name": "SQUIRTLE", "type": "1515", "exp": "117360", 'bst': [44, 48, 65, 43, 50 ], "gr" : "mediumslow"},
{"name": "WARTORTLE", "type": "1515", "exp": "117360", 'bst': [59, 63, 80, 58, 65 ], "gr" : "mediumslow"},
{"name": "BLASTOISE", "type": "1515", "exp": "117360", 'bst': [79, 83, 100, 78, 85 ], "gr" : "mediumslow"},
{"name": "CATERPIE", "type": "0707", "exp": "125000", 'bst': [45, 30, 35, 45, 20 ], "gr" : "mediumfast"},
{"name": "METAPOD", "type": "0707", "exp": "125000", 'bst': [50, 20, 55, 30, 25 ], "gr" : "mediumfast"},
{"name": "BUTTERFREE", "type": "0702", "exp": "125000", 'bst': [60, 45, 50, 70, 80 ], "gr" : "mediumfast"},
{"name": "WEEDLE", "type": "0703", "exp": "125000", 'bst': [40, 35, 30, 50, 20 ], "gr" : "mediumfast"},
{"name": "KAKUNA", "type": "0703", "exp": "125000", 'bst': [45, 25, 50, 35, 25 ], "gr" : "mediumfast"},
{"name": "BEEDRILL", "type": "0703", "exp": "125000", 'bst': [65, 80, 40, 75, 45 ], "gr" : "mediumfast"},
{"name": "PIDGEY", "type": "0002", "exp": "117360", 'bst': [40, 45, 40, 56, 35 ], "gr" : "mediumslow"},
{"name": "PIDGEOTTO", "type": "0002", "exp": "117360", 'bst': [63, 60, 55, 71, 50 ], "gr" : "mediumslow"},
{"name": "PIDGEOT", "type": "0002", "exp": "117360", 'bst': [83, 80, 75, 91, 70 ], "gr" : "mediumslow"},
{"name": "RATTATA", "type": "0000", "exp": "125000", 'bst': [30, 56, 35, 72, 25 ], "gr" : "mediumfast"},
{"name": "RATICATE", "type": "0000", "exp": "125000", 'bst': [55, 81, 60, 97, 50 ], "gr" : "mediumfast"},
{"name": "SPEAROW", "type": "0002", "exp": "125000", 'bst': [40, 60, 30, 70, 31 ], "gr" : "mediumfast"},
{"name": "FEAROW", "type": "0002", "exp": "125000", 'bst': [65, 90, 65, 100, 61 ], "gr" : "mediumfast"},
{"name": "EKANS", "type": "0303", "exp": "125000", 'bst': [35, 60, 44, 55, 40 ], "gr" : "mediumfast"},
{"name": "ARBOK", "type": "0303", "exp": "125000", 'bst': [60, 85, 69, 80, 65 ], "gr" : "mediumfast"},
{"name": "PIKACHU", "type": "1717", "exp": "125000", 'bst': [35, 55, 30, 90, 50 ], "gr" : "mediumfast"},
{"name": "RAICHU", "type": "1717", "exp": "125000", 'bst': [60, 90, 55, 100, 90 ], "gr" : "mediumfast"},
{"name": "SANDSHREW", "type": "0404", "exp": "125000", 'bst': [50, 75, 85, 40, 30 ], "gr" : "mediumfast"},
{"name": "SANDSLASH", "type": "0404", "exp": "125000", 'bst': [75, 100, 110, 65, 55 ], "gr" : "mediumfast"},
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [55, 47, 52, 41, 40 ], "gr" : "mediumslow"},
{"name": "NIDORINA", "type": "0303", "exp": "117360", 'bst': [70, 62, 67, 56, 55 ], "gr" : "mediumslow"},
{"name": "NIDOQUEEN", "type": "0304", "exp": "117360", 'bst': [90, 82, 87, 76, 75 ], "gr" : "mediumslow"},
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [46, 57, 40, 50, 40 ], "gr" : "mediumslow"},
{"name": "NIDORINO", "type": "0303", "exp": "117360", 'bst': [61, 72, 57, 65, 55 ], "gr" : "mediumslow"},
{"name": "NIDOKING", "type": "0304", "exp": "117360", 'bst': [81, 92, 77, 85, 75 ], "gr" : "mediumslow"},
{"name": "CLEFAIRY", "type": "0000", "exp": "100000", 'bst': [70, 45, 48, 35, 60 ], "gr" : "fast"},
{"name": "CLEFABLE", "type": "0000", "exp": "100000", 'bst': [95, 70, 73, 60, 85 ], "gr" : "fast"},
{"name": "VULPIX", "type": "1414", "exp": "125000", 'bst': [38, 41, 40, 65, 65 ], "gr" : "mediumfast"},
{"name": "NINETALES", "type": "1414", "exp": "125000", 'bst': [73, 76, 75, 100, 100], "gr" : "mediumfast"},
{"name": "JIGGLYPUFF", "type": "0000", "exp": "100000", 'bst': [115, 45, 20, 20, 25 ], "gr" : "fast"},
{"name": "WIGGLYTUFF", "type": "0000", "exp": "100000", 'bst': [140, 70, 45, 45, 50 ], "gr" : "fast"},
{"name": "ZUBAT", "type": "0302", "exp": "125000", 'bst': [40, 45, 35, 55, 40 ], "gr" : "mediumfast"},
{"name": "GOLBAT", "type": "0302", "exp": "125000", 'bst': [75, 80, 70, 90, 75 ], "gr" : "mediumfast"},
{"name": "ODDISH", "type": "1603", "exp": "117360", 'bst': [45, 50, 55, 30, 75 ], "gr" : "mediumslow"},
{"name": "GLOOM", "type": "1603", "exp": "117360", 'bst': [60, 65, 70, 40, 85 ], "gr" : "mediumslow"},
{"name": "VILEPLUME", "type": "1603", "exp": "117360", 'bst': [75, 80, 85, 50, 100], "gr" : "mediumslow"},
{"name": "PARAS", "type": "0716", "exp": "125000", 'bst': [35, 70, 55, 25, 55 ], "gr" : "mediumfast"},
{"name": "PARASECT", "type": "0716", "exp": "125000", 'bst': [60, 95, 80, 30, 80 ], "gr" : "mediumfast"},
{"name": "VENONAT", "type": "0703", "exp": "125000", 'bst': [60, 55, 50, 45, 40 ], "gr" : "mediumfast"},
{"name": "VENOMOTH", "type": "0703", "exp": "125000", 'bst': [70, 65, 60, 90, 90 ], "gr" : "mediumfast"},
{"name": "DIGLETT", "type": "0404", "exp": "125000", 'bst': [10, 55, 25, 95, 45 ], "gr" : "mediumfast"},
{"name": "DUGTRIO", "type": "0404", "exp": "125000", 'bst': [35, 80, 50, 120, 70 ], "gr" : "mediumfast"},
{"name": "MEOWTH", "type": "0000", "exp": "125000", 'bst': [40, 45, 35, 90, 40 ], "gr" : "mediumfast"},
{"name": "PERSIAN", "type": "0000", "exp": "125000", 'bst': [65, 70, 60, 115, 65 ], "gr" : "mediumfast"},
{"name": "PSYDUCK", "type": "1515", "exp": "125000", 'bst': [50, 52, 48, 55, 50 ], "gr" : "mediumfast"},
{"name": "GOLDUCK", "type": "1515", "exp": "125000", 'bst': [80, 82, 78, 85, 80 ], "gr" : "mediumfast"},
{"name": "MANKEY", "type": "0101", "exp": "125000", 'bst': [40, 80, 35, 70, 35 ], "gr" : "mediumfast"},
{"name": "PRIMEAPE", "type": "0101", "exp": "125000", 'bst': [65, 105, 60, 95, 60 ], "gr" : "mediumfast"},
{"name": "GROWLITHE", "type": "1414", "exp": "156250", 'bst': [55, 70, 45, 60, 50 ], "gr" : "slow"},
{"name": "ARCANINE", "type": "1414", "exp": "156250", 'bst': [90, 110, 80, 95, 80 ], "gr" : "slow"},
{"name": "POLIWAG", "type": "1515", "exp": "117360", 'bst': [40, 50, 40, 90, 40 ], "gr" : "mediumslow"},
{"name": "POLIWHIRL", "type": "1515", "exp": "117360", 'bst': [65, 65, 65, 90, 50 ], "gr" : "mediumslow"},
{"name": "POLIWRATH", "type": "1501", "exp": "117360", 'bst': [90, 85, 95, 70, 70 ], "gr" : "mediumslow"},
{"name": "ABRA", "type": "1818", "exp": "117360", 'bst': [25, 20, 15, 90, 105], "gr" : "mediumslow"},
{"name": "KADABRA", "type": "1818", "exp": "117360", 'bst': [40, 35, 30, 105, 120], "gr" : "mediumslow"},
{"name": "ALAKAZAM", "type": "1818", "exp": "117360", 'bst': [55, 50, 45, 120, 135], "gr" : "mediumslow"},
{"name": "MACHOP", "type": "0101", "exp": "117360", 'bst': [70, 80, 50, 35, 35 ], "gr" : "mediumslow"},
{"name": "MACHOKE", "type": "0101", "exp": "117360", 'bst': [80, 100, 70, 45, 50 ], "gr" : "mediumslow"},
{"name": "MACHAMP", "type": "0101", "exp": "117360", 'bst': [90, 130, 80, 55, 65 ], "gr" : "mediumslow"},
{"name": "BELLSPROUT", "type": "1603", "exp": "117360", 'bst': [50, 75, 35, 40, 70 ], "gr" : "mediumslow"},
{"name": "WEEPINBELL", "type": "1603", "exp": "117360", 'bst': [65, 90, 50, 55, 85 ], "gr" : "mediumslow"},
{"name": "VICTREEBEL", "type": "1603", "exp": "117360", 'bst': [80, 105, 65, 70, 100], "gr" : "mediumslow"},
{"name": "TENTACOOL", "type": "1503", "exp": "156250", 'bst': [40, 40, 35, 70, 100], "gr" : "slow"},
{"name": "TENTACRUEL", "type": "1503", "exp": "156250", 'bst': [80, 70, 65, 100, 120], "gr" : "slow"},
{"name": "GEODUDE", "type": "0504", "exp": "117360", 'bst': [40, 80, 100, 20, 30 ], "gr" : "mediumslow"},
{"name": "GRAVELER", "type": "0504", "exp": "117360", 'bst': [55, 95, 115, 35, 45 ], "gr" : "mediumslow"},
{"name": "GOLEM", "type": "0504", "exp": "117360", 'bst': [80, 110, 130, 45, 55 ], "gr" : "mediumslow"},
{"name": "PONYTA", "type": "1414", "exp": "125000", 'bst': [50, 85, 55, 90, 65 ], "gr" : "mediumfast"},
{"name": "RAPIDASH", "type": "1414", "exp": "125000", 'bst': [65, 100, 70, 105, 80 ], "gr" : "mediumfast"},
{"name": "SLOWPOKE", "type": "1518", "exp": "125000", 'bst': [90, 65, 65, 15, 40 ], "gr" : "mediumfast"},
{"name": "SLOWBRO", "type": "1518", "exp": "125000", 'bst': [95, 75, 110, 30, 80 ], "gr" : "mediumfast"},
{"name": "MAGNEMITE", "type": "1717", "exp": "125000", 'bst': [25, 35, 70, 45, 95 ], "gr" : "mediumfast"},
{"name": "MAGNETON", "type": "1717", "exp": "125000", 'bst': [50, 60, 95, 70, 120], "gr" : "mediumfast"},
{"name": "FARFETCH'D", "type": "0002", "exp": "125000", 'bst': [52, 65, 55, 60, 58 ], "gr" : "mediumfast"},
{"name": "DODUO", "type": "0002", "exp": "125000", 'bst': [35, 85, 45, 75, 35 ], "gr" : "mediumfast"},
{"name": "DODRIO", "type": "0002", "exp": "125000", 'bst': [60, 110, 70, 100, 60 ], "gr" : "mediumfast"},
{"name": "SEEL", "type": "1515", "exp": "125000", 'bst': [65, 45, 55, 45, 70 ], "gr" : "mediumfast"},
{"name": "DEWGONG", "type": "1519", "exp": "125000", 'bst': [90, 70, 80, 70, 95 ], "gr" : "mediumfast"},
{"name": "GRIMER", "type": "0303", "exp": "125000", 'bst': [80, 80, 50, 25, 40 ], "gr" : "mediumfast"},
{"name": "MUK", "type": "0303", "exp": "125000", 'bst': [105, 105, 75, 50, 65 ], "gr" : "mediumfast"},
{"name": "SHELLDER", "type": "1515", "exp": "156250", 'bst': [30, 65, 100, 40, 45 ], "gr" : "slow"},
{"name": "CLOYSTER", "type": "1519", "exp": "156250", 'bst': [50, 95, 180, 70, 85 ], "gr" : "slow"},
{"name": "GASTLY", "type": "0803", "exp": "117360", 'bst': [30, 35, 30, 80, 100], "gr" : "mediumslow"},
{"name": "HAUNTER", "type": "0803", "exp": "117360", 'bst': [45, 50, 45, 95, 115], "gr" : "mediumslow"},
{"name": "GENGAR", "type": "0803", "exp": "117360", 'bst': [60, 65, 60, 110, 130], "gr" : "mediumslow"},
{"name": "ONIX", "type": "0504", "exp": "125000", 'bst': [35, 45, 160, 70, 30 ], "gr" : "mediumfast"},
{"name": "DROWZEE", "type": "1818", "exp": "125000", 'bst': [60, 48, 45, 42, 90 ], "gr" : "mediumfast"},
{"name": "HYPNO", "type": "1818", "exp": "125000", 'bst': [85, 73, 70, 67, 115], "gr" : "mediumfast"},
{"name": "KRABBY", "type": "1515", "exp": "125000", 'bst': [30, 105, 90, 50, 25 ], "gr" : "mediumfast"},
{"name": "KINGLER", "type": "1515", "exp": "125000", 'bst': [55, 130, 115, 75, 50 ], "gr" : "mediumfast"},
{"name": "VOLTORB", "type": "1717", "exp": "125000", 'bst': [40, 30, 50, 100, 55 ], "gr" : "mediumfast"},
{"name": "ELECTRODE", "type": "1717", "exp": "125000", 'bst': [60, 50, 70, 140, 80 ], "gr" : "mediumfast"},
{"name": "EXEGGCUTE", "type": "1618", "exp": "156250", 'bst': [60, 40, 80, 40, 60 ], "gr" : "slow"},
{"name": "EXEGGUTOR", "type": "1618", "exp": "156250", 'bst': [95, 95, 85, 55, 125], "gr" : "slow"},
{"name": "CUBONE", "type": "0404", "exp": "125000", 'bst': [50, 50, 95, 35, 40 ], "gr" : "mediumfast"},
{"name": "MAROWAK", "type": "0404", "exp": "125000", 'bst': [60, 80, 110, 45, 50 ], "gr" : "mediumfast"},
{"name": "HITMONLEE", "type": "0101", "exp": "125000", 'bst': [50, 120, 53, 87, 35 ], "gr" : "mediumfast"},
{"name": "HITMONCHAN", "type": "0101", "exp": "125000", 'bst': [50, 105, 79, 76, 35 ], "gr" : "mediumfast"},
{"name": "LICKITUNG", "type": "0000", "exp": "125000", 'bst': [90, 55, 75, 30, 60 ], "gr" : "mediumfast"},
{"name": "KOFFING", "type": "0303", "exp": "125000", 'bst': [40, 65, 95, 35, 60 ], "gr" : "mediumfast"},
{"name": "WEEZING", "type": "0303", "exp": "125000", 'bst': [65, 90, 120, 60, 85 ], "gr" : "mediumfast"},
{"name": "RHYHORN", "type": "0405", "exp": "156250", 'bst': [80, 85, 95, 25, 30 ], "gr" : "slow"},
{"name": "RHYDON", "type": "0405", "exp": "156250", 'bst': [105, 130, 120, 40, 45 ], "gr" : "slow"},
{"name": "CHANSEY", "type": "0000", "exp": "100000", 'bst': [250, 5, 5, 50, 105], "gr" : "fast"},
{"name": "TANGELA", "type": "1616", "exp": "125000", 'bst': [65, 55, 115, 60, 100], "gr" : "mediumfast"},
{"name": "KANGASKHAN", "type": "0000", "exp": "125000", 'bst': [105, 95, 80, 90, 40 ], "gr" : "mediumfast"},
{"name": "HORSEA", "type": "1515", "exp": "125000", 'bst': [30, 40, 70, 60, 70 ], "gr" : "mediumfast"},
{"name": "SEADRA", "type": "1515", "exp": "125000", 'bst': [55, 65, 95, 85, 95 ], "gr" : "mediumfast"},
{"name": "GOLDEEN", "type": "1515", "exp": "125000", 'bst': [45, 67, 60, 63, 50 ], "gr" : "mediumfast"},
{"name": "SEAKING", "type": "1515", "exp": "125000", 'bst': [80, 92, 65, 68, 80 ], "gr" : "mediumfast"},
{"name": "STARYU", "type": "1515", "exp": "156250", 'bst': [30, 45, 55, 85, 70 ], "gr" : "slow"},
{"name": "STARMIE", "type": "1518", "exp": "156250", 'bst': [60, 75, 85, 115, 100], "gr" : "slow"},
{"name": "MR. MIME", "type": "1818", "exp": "125000", 'bst': [40, 45, 65, 90, 100], "gr" : "mediumfast"},
{"name": "SCYTHER", "type": "0702", "exp": "125000", 'bst': [70, 110, 80, 105, 55 ], "gr" : "mediumfast"},
{"name": "JYNX", "type": "1918", "exp": "125000", 'bst': [65, 50, 35, 95, 95 ], "gr" : "mediumfast"},
{"name": "ELECTABUZZ", "type": "1717", "exp": "125000", 'bst': [65, 83, 57, 105, 85 ], "gr" : "mediumfast"},
{"name": "MAGMAR", "type": "1414", "exp": "125000", 'bst': [65, 95, 57, 93, 85 ], "gr" : "mediumfast"},
{"name": "PINSIR", "type": "0707", "exp": "156250", 'bst': [65, 125, 100, 85, 55 ], "gr" : "slow"},
{"name": "TAUROS", "type": "0000", "exp": "156250", 'bst': [75, 100, 95, 110, 70 ], "gr" : "slow"},
{"name": "MAGIKARP", "type": "1515", "exp": "156250", 'bst': [20, 10, 55, 80, 20 ], "gr" : "slow"},
{"name": "GYARADOS", "type": "1502", "exp": "156250", 'bst': [95, 125, 79, 81, 100], "gr" : "slow"},
{"name": "LAPRAS", "type": "1519", "exp": "156250", 'bst': [130, 85, 80, 60, 95 ], "gr" : "slow"},
{"name": "DITTO", "type": "0000", "exp": "125000", 'bst': [48, 48, 48, 48, 48 ], "gr" : "mediumfast"},
{"name": "EEVEE", "type": "0000", "exp": "125000", 'bst': [55, 55, 50, 55, 65 ], "gr" : "mediumfast"},
{"name": "VAPOREON", "type": "1515", "exp": "125000", 'bst': [130, 65, 60, 65, 110], "gr" : "mediumfast"},
{"name": "JOLTEON", "type": "1717", "exp": "125000", 'bst': [65, 65, 60, 130, 110], "gr" : "mediumfast"},
{"name": "FLAREON", "type": "1414", "exp": "125000", 'bst': [65, 130, 60, 65, 110], "gr" : "mediumfast"},
{"name": "PORYGON", "type": "0000", "exp": "125000", 'bst': [65, 60, 70, 40, 75 ], "gr" : "mediumfast"},
{"name": "OMANYTE", "type": "0515", "exp": "125000", 'bst': [35, 40, 100, 35, 90 ], "gr" : "mediumfast"},
{"name": "OMASTAR", "type": "0515", "exp": "125000", 'bst': [70, 60, 125, 55, 115], "gr" : "mediumfast"},
{"name": "KABUTO", "type": "0515", "exp": "125000", 'bst': [30, 80, 90, 55, 45 ], "gr" : "mediumfast"},
{"name": "KABUTOPS", "type": "0515", "exp": "125000", 'bst': [60, 115, 105, 80, 70 ], "gr" : "mediumfast"},
{"name": "AERODACTYL", "type": "0502", "exp": "156250", 'bst': [80, 105, 65, 130, 60 ], "gr" : "slow"},
{"name": "SNORLAX", "type": "0000", "exp": "156250", 'bst': [160, 110, 65, 30, 65 ], "gr" : "slow"},
{"name": "ARTICUNO", "type": "1902", "exp": "156250", 'bst': [90, 85, 100, 85, 125], "gr" : "slow"},
{"name": "ZAPDOS", "type": "1702", "exp": "156250", 'bst': [90, 90, 85, 100, 125], "gr" : "slow"},
{"name": "MOLTRES", "type": "1402", "exp": "156250", 'bst': [90, 100, 90, 90, 125], "gr" : "slow"},
{"name": "DRATINI", "type": "1A1A", "exp": "156250", 'bst': [41, 64, 45, 50, 50 ], "gr" : "slow"},
{"name": "DRAGONAIR", "type": "1A1A", "exp": "156250", 'bst': [61, 84, 65, 70, 70 ], "gr" : "slow"},
{"name": "DRAGONITE", "type": "1A02", "exp": "156250", 'bst': [91, 134, 95, 80, 100], "gr" : "slow"},
{"name": "MEWTWO", "type": "1818", "exp": "156250", 'bst': [106, 110, 90, 130, 154], "gr" : "slow"},
{"name": "MEW", "type": "1818", "exp": "117360", 'bst': [100, 100, 100, 100, 100], "gr" : "slow"}
]
GLC_list = [
{"name": "BULBASAUR", "type": "1603", "exp": "117360", 'bst': [45, 49, 49, 45, 65 ], "Moveset": [73, 92, 34, 75]},
{"name": "IVYSAUR", "type": "1603", "exp": "117360", 'bst': [60, 62, 63, 60, 80 ], "Moveset": [75, 79, 72, 38]},
{"name": "VENUSAUR", "type": "1603", "exp": "117360", 'bst': [80, 82, 83, 80, 100], "Moveset": [73, 77, 76, 36]},
{"name": "CHARMANDER", "type": "1414", "exp": "117360", 'bst': [39, 52, 43, 65, 50 ], "Moveset": [53, 163, 69, 91]},
{"name": "CHARMELEON", "type": "1414", "exp": "117360", 'bst': [58, 64, 58, 80, 65 ], "Moveset": [126, 68, 82, 163]},
{"name": "CHARIZARD", "type": "1402", "exp": "117360", 'bst': [78, 84, 78, 100, 85 ], "Moveset": [19, 91, 83, 102]},
{"name": "SQUIRTLE", "type": "1515", "exp": "117360", 'bst': [44, 48, 65, 43, 50 ], "Moveset": [57, 59, 91, 69]},
{"name": "WARTORTLE", "type": "1515", "exp": "117360", 'bst': [59, 63, 80, 58, 65 ], "Moveset": [57, 68, 66, 58]},
{"name": "BLASTOISE", "type": "1515", "exp": "117360", 'bst': [79, 83, 100, 78, 85 ], "Moveset": [56, 117, 70, 110]},
{"name": "CATERPIE", "type": "0707", "exp": "125000", 'bst': [45, 30, 35, 45, 20 ], "Moveset": [33, 81, 0, 0]},
{"name": "METAPOD", "type": "0707", "exp": "125000", 'bst': [50, 20, 55, 30, 25 ], "Moveset": [33, 81, 0, 0]},
{"name": "BUTTERFREE", "type": "0702", "exp": "125000", 'bst': [60, 45, 50, 70, 80 ], "Moveset": [94, 48, 63, 72]},
{"name": "WEEDLE", "type": "0703", "exp": "125000", 'bst': [40, 35, 30, 50, 20 ], "Moveset": [81, 40, 0, 0]},
{"name": "KAKUNA", "type": "0703", "exp": "125000", 'bst': [45, 25, 50, 35, 25 ], "Moveset": [81, 40, 0, 0]},
{"name": "BEEDRILL", "type": "0703", "exp": "125000", 'bst': [65, 80, 40, 75, 45 ], "Moveset": [41, 116, 38, 72]},
{"name": "PIDGEY", "type": "0002", "exp": "117360", 'bst': [40, 45, 40, 56, 35 ], "Moveset": [19, 38, 92, 104]},
{"name": "PIDGEOTTO", "type": "0002", "exp": "117360", 'bst': [63, 60, 55, 71, 50 ], "Moveset": [19, 97, 28, 36]},
{"name": "PIDGEOT", "type": "0002", "exp": "117360", 'bst': [83, 80, 75, 91, 70 ], "Moveset": [119, 19, 98, 63]},
{"name": "RATTATA", "type": "0000", "exp": "125000", 'bst': [30, 56, 35, 72, 25 ], "Moveset": [162, 158, 59, 91]},
{"name": "RATICATE", "type": "0000", "exp": "125000", 'bst': [55, 81, 60, 97, 50 ], "Moveset": [158, 61, 116, 91]},
{"name": "SPEAROW", "type": "0002", "exp": "125000", 'bst': [40, 60, 30, 70, 31 ], "Moveset": [65, 119, 104, 38]},
{"name": "FEAROW", "type": "0002", "exp": "125000", 'bst': [65, 90, 65, 100, 61 ], "Moveset": [97, 104, 19, 129]},
{"name": "EKANS", "type": "0303", "exp": "125000", 'bst': [35, 60, 44, 55, 40 ], "Moveset": [89, 70, 137, 51]},
{"name": "ARBOK", "type": "0303", "exp": "125000", 'bst': [60, 85, 69, 80, 65 ], "Moveset": [137, 157, 51, 91]},
{"name": "PIKACHU", "type": "1717", "exp": "125000", 'bst': [35, 55, 30, 90, 50 ], "Moveset": [85, 69, 86, 148]},
{"name": "RAICHU", "type": "1717", "exp": "125000", 'bst': [60, 90, 55, 100, 90 ], "Moveset": [87, 86, 45, 25]},
{"name": "SANDSHREW", "type": "0404", "exp": "125000", 'bst': [50, 75, 85, 40, 30 ], "Moveset": [89, 163, 69, 28]},
{"name": "SANDSLASH", "type": "0404", "exp": "125000", 'bst': [75, 100, 110, 65, 55 ], "Moveset": [91, 157, 28, 154]},
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [55, 47, 52, 41, 40 ], "Moveset": [34, 92, 85, 59]},
{"name": "NIDORINA", "type": "0303", "exp": "117360", 'bst': [70, 62, 67, 56, 55 ], "Moveset": [87, 58, 92, 34]},
{"name": "NIDOQUEEN", "type": "0304", "exp": "117360", 'bst': [90, 82, 87, 76, 75 ], "Moveset": [24, 92, 34, 87]},
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [46, 57, 40, 50, 40 ], "Moveset": [32, 92, 85, 59]},
{"name": "NIDORINO", "type": "0303", "exp": "117360", 'bst': [61, 72, 57, 65, 55 ], "Moveset": [92, 32, 58, 38]},
{"name": "NIDOKING", "type": "0304", "exp": "117360", 'bst': [81, 92, 77, 85, 75 ], "Moveset": [89, 32, 24, 40]},
{"name": "CLEFAIRY", "type": "0000", "exp": "100000", 'bst': [70, 45, 48, 35, 60 ], "Moveset": [85, 59, 34, 118]},
{"name": "CLEFABLE", "type": "0000", "exp": "100000", 'bst': [95, 70, 73, 60, 85 ], "Moveset": [47, 118, 161, 58]},
{"name": "VULPIX", "type": "1414", "exp": "125000", 'bst': [38, 41, 40, 65, 65 ], "Moveset": [53, 115, 109, 91]},
{"name": "NINETALES", "type": "1414", "exp": "125000", 'bst': [73, 76, 75, 100, 100], "Moveset": [109, 91, 83, 117]},
{"name": "JIGGLYPUFF", "type": "0000", "exp": "100000", 'bst': [115, 45, 20, 20, 25 ], "Moveset": [47, 34, 69, 94]},
{"name": "WIGGLYTUFF", "type": "0000", "exp": "100000", 'bst': [140, 70, 45, 45, 50 ], "Moveset": [47, 70, 50, 94]},
{"name": "ZUBAT", "type": "0302", "exp": "125000", 'bst': [40, 45, 35, 55, 40 ], "Moveset": [109, 72, 92, 38]},
{"name": "GOLBAT", "type": "0302", "exp": "125000", 'bst': [75, 80, 70, 90, 75 ], "Moveset": [109, 72, 63, 114]},
{"name": "ODDISH", "type": "1603", "exp": "117360", 'bst': [45, 50, 55, 30, 75 ], "Moveset": [78, 80, 72, 38]},
{"name": "GLOOM", "type": "1603", "exp": "117360", 'bst': [60, 65, 70, 40, 85 ], "Moveset": [78, 80, 51, 36]},
{"name": "VILEPLUME", "type": "1603", "exp": "117360", 'bst': [75, 80, 85, 50, 100], "Moveset": [80, 51, 15, 78]},
{"name": "PARAS", "type": "0716", "exp": "125000", 'bst': [35, 70, 55, 25, 55 ], "Moveset": [147, 163, 91, 72]},
{"name": "PARASECT", "type": "0716", "exp": "125000", 'bst': [60, 95, 80, 30, 80 ], "Moveset": [147, 91, 74, 72]},
{"name": "VENONAT", "type": "0703", "exp": "125000", 'bst': [60, 55, 50, 45, 40 ], "Moveset": [94, 72, 38, 92]},
{"name": "VENOMOTH", "type": "0703", "exp": "125000", 'bst': [70, 65, 60, 90, 90 ], "Moveset": [94, 48, 129, 92]},
{"name": "DIGLETT", "type": "0404", "exp": "125000", 'bst': [10, 55, 25, 95, 45 ], "Moveset": [89, 163, 90, 157]},
{"name": "DUGTRIO", "type": "0404", "exp": "125000", 'bst': [35, 80, 50, 120, 70 ], "Moveset": [91, 28, 157, 164]},
{"name": "MEOWTH", "type": "0000", "exp": "125000", 'bst': [40, 45, 35, 90, 40 ], "Moveset": [163, 85, 61, 104]},
{"name": "PERSIAN", "type": "0000", "exp": "125000", 'bst': [65, 70, 60, 115, 65 ], "Moveset": [85, 38, 117, 103]},
{"name": "PSYDUCK", "type": "1515", "exp": "125000", 'bst': [50, 52, 48, 55, 50 ], "Moveset": [57, 69, 91, 59]},
{"name": "GOLDUCK", "type": "1515", "exp": "125000", 'bst': [80, 82, 78, 85, 80 ], "Moveset": [50, 57, 93, 25]},
{"name": "MANKEY", "type": "0101", "exp": "125000", 'bst': [40, 80, 35, 70, 35 ], "Moveset": [66, 91, 69, 70]},
{"name": "PRIMEAPE", "type": "0101", "exp": "125000", 'bst': [65, 105, 60, 95, 60 ], "Moveset": [69, 103, 5, 67]},
{"name": "GROWLITHE", "type": "1414", "exp": "156250", 'bst': [55, 70, 45, 60, 50 ], "Moveset": [53, 34, 115, 91]},
{"name": "ARCANINE", "type": "1414", "exp": "156250", 'bst': [90, 110, 80, 95, 80 ], "Moveset": [126, 36, 43, 97]},
{"name": "POLIWAG", "type": "1515", "exp": "117360", 'bst': [40, 50, 40, 90, 40 ], "Moveset": [34, 59, 57, 133]},
{"name": "POLIWHIRL", "type": "1515", "exp": "117360", 'bst': [65, 65, 65, 90, 50 ], "Moveset": [95, 56, 70, 89]},
{"name": "POLIWRATH", "type": "1501", "exp": "117360", 'bst': [90, 85, 95, 70, 70 ], "Moveset": [95, 66, 102, 57]},
{"name": "ABRA", "type": "1818", "exp": "117360", 'bst': [25, 20, 15, 90, 105], "Moveset": [94, 69, 115, 92]},
{"name": "KADABRA", "type": "1818", "exp": "117360", 'bst': [40, 35, 30, 105, 120], "Moveset": [60, 86, 105, 69]},
{"name": "ALAKAZAM", "type": "1818", "exp": "117360", 'bst': [55, 50, 45, 120, 135], "Moveset": [93, 115, 134, 91]},
{"name": "MACHOP", "type": "0101", "exp": "117360", 'bst': [70, 80, 50, 35, 35 ], "Moveset": [66, 157, 89, 34]},
{"name": "MACHOKE", "type": "0101", "exp": "117360", 'bst': [80, 100, 70, 45, 50 ], "Moveset": [89, 66, 70, 116]},
{"name": "MACHAMP", "type": "0101", "exp": "117360", 'bst': [90, 130, 80, 55, 65 ], "Moveset": [2, 67, 126, 91]},
{"name": "BELLSPROUT", "type": "1603", "exp": "117360", 'bst': [50, 75, 35, 40, 70 ], "Moveset": [51, 92, 74, 75]},
{"name": "WEEPINBELL", "type": "1603", "exp": "117360", 'bst': [65, 90, 50, 55, 85 ], "Moveset": [75, 51, 21, 92]},
{"name": "VICTREEBEL", "type": "1603", "exp": "117360", 'bst': [80, 105, 65, 70, 100], "Moveset": [72, 51, 35, 92]},
{"name": "TENTACOOL", "type": "1503", "exp": "156250", 'bst': [40, 40, 35, 70, 100], "Moveset": [57, 72, 51, 92]},
{"name": "TENTACRUEL", "type": "1503", "exp": "156250", 'bst': [80, 70, 65, 100, 120], "Moveset": [51, 103, 56, 15]},
{"name": "GEODUDE", "type": "0504", "exp": "117360", 'bst': [40, 80, 100, 20, 30 ], "Moveset": [89, 34, 157, 153]},
{"name": "GRAVELER", "type": "0504", "exp": "117360", 'bst': [55, 95, 115, 35, 45 ], "Moveset": [157, 89, 70, 120]},
{"name": "GOLEM", "type": "0504", "exp": "117360", 'bst': [80, 110, 130, 45, 55 ], "Moveset": [88, 5, 91, 120]},
{"name": "PONYTA", "type": "1414", "exp": "125000", 'bst': [50, 85, 55, 90, 65 ], "Moveset": [126, 115, 32, 34]},
{"name": "RAPIDASH", "type": "1414", "exp": "125000", 'bst': [65, 100, 70, 105, 80 ], "Moveset": [23, 97, 92, 83]},
{"name": "SLOWPOKE", "type": "1518", "exp": "125000", 'bst': [90, 65, 65, 15, 40 ], "Moveset": [57, 94, 86, 133]},
{"name": "SLOWBRO", "type": "1518", "exp": "125000", 'bst': [95, 75, 110, 30, 80 ], "Moveset": [57, 29, 91, 50]},
{"name": "MAGNEMITE", "type": "1717", "exp": "125000", 'bst': [25, 35, 70, 45, 95 ], "Moveset": [85, 86, 48, 38]},
{"name": "MAGNETON", "type": "1717", "exp": "125000", 'bst': [50, 60, 95, 70, 120], "Moveset": [86, 48, 87, 103]},
{"name": "FARFETCH'D", "type": "0002", "exp": "125000", 'bst': [52, 65, 55, 60, 58 ], "Moveset": [163, 28, 92, 19]},
{"name": "DODUO", "type": "0002", "exp": "125000", 'bst': [35, 85, 45, 75, 35 ], "Moveset": [65, 161, 104, 115]},
{"name": "DODRIO", "type": "0002", "exp": "125000", 'bst': [60, 110, 70, 100, 60 ], "Moveset": [19, 161, 115, 164]},
{"name": "SEEL", "type": "1515", "exp": "125000", 'bst': [65, 45, 55, 45, 70 ], "Moveset": [58, 70, 104, 57]},
{"name": "DEWGONG", "type": "1519", "exp": "125000", 'bst': [90, 70, 80, 70, 95 ], "Moveset": [36, 62, 156, 57]},
{"name": "GRIMER", "type": "0303", "exp": "125000", 'bst': [80, 80, 50, 25, 40 ], "Moveset": [124, 34, 153, 72]},
{"name": "MUK", "type": "0303", "exp": "125000", 'bst': [105, 105, 75, 50, 65 ], "Moveset": [124, 87, 72, 103]},
{"name": "SHELLDER", "type": "1515", "exp": "156250", 'bst': [30, 65, 100, 40, 45 ], "Moveset": [58, 153, 57, 161]},
{"name": "CLOYSTER", "type": "1519", "exp": "156250", 'bst': [50, 95, 180, 70, 85 ], "Moveset": [62, 120, 128, 131]},
{"name": "GASTLY", "type": "0803", "exp": "117360", 'bst': [30, 35, 30, 80, 100], "Moveset": [94, 101, 153, 109]},
{"name": "HAUNTER", "type": "0803", "exp": "117360", 'bst': [45, 50, 45, 95, 115], "Moveset": [94, 85, 120, 109]},
{"name": "GENGAR", "type": "0803", "exp": "117360", 'bst': [60, 65, 60, 110, 130], "Moveset": [95, 138, 85, 109]},
{"name": "ONIX", "type": "0504", "exp": "125000", 'bst': [35, 45, 160, 70, 30 ], "Moveset": [89, 157, 153, 103]},
{"name": "DROWZEE", "type": "1818", "exp": "125000", 'bst': [60, 48, 45, 42, 90 ], "Moveset": [95, 69, 94, 115]},
{"name": "HYPNO", "type": "1818", "exp": "125000", 'bst': [85, 73, 70, 67, 115], "Moveset": [95, 138, 68, 29]},
{"name": "KRABBY", "type": "1515", "exp": "125000", 'bst': [30, 105, 90, 50, 25 ], "Moveset": [152, 92, 34, 59]},
{"name": "KINGLER", "type": "1515", "exp": "125000", 'bst': [55, 130, 115, 75, 50 ], "Moveset": [152, 70, 117, 43]},
{"name": "VOLTORB", "type": "1717", "exp": "125000", 'bst': [40, 30, 50, 100, 55 ], "Moveset": [85, 86, 115, 153]},
{"name": "ELECTRODE", "type": "1717", "exp": "125000", 'bst': [60, 50, 70, 140, 80 ], "Moveset": [87, 92, 129, 120]},
{"name": "EXEGGCUTE", "type": "1618", "exp": "156250", 'bst': [60, 40, 80, 40, 60 ], "Moveset": [73, 76, 121, 94]},
{"name": "EXEGGUTOR", "type": "1618", "exp": "156250", 'bst': [95, 95, 85, 55, 125], "Moveset": [73, 95, 72, 121]},
{"name": "CUBONE", "type": "0404", "exp": "125000", 'bst': [50, 50, 95, 35, 40 ], "Moveset": [155, 34, 58, 69]},
{"name": "MAROWAK", "type": "0404", "exp": "125000", 'bst': [60, 80, 110, 45, 50 ], "Moveset": [155, 37, 126, 116]},
{"name": "HITMONLEE", "type": "0101", "exp": "125000", 'bst': [50, 120, 53, 87, 35 ], "Moveset": [136, 70, 68, 116]},
{"name": "HITMONCHAN", "type": "0101", "exp": "125000", 'bst': [50, 105, 79, 76, 35 ], "Moveset": [66, 70, 8, 9]},
{"name": "LICKITUNG", "type": "0000", "exp": "125000", 'bst': [90, 55, 75, 30, 60 ], "Moveset": [89, 34, 103, 48]},
{"name": "KOFFING", "type": "0303", "exp": "125000", 'bst': [40, 65, 95, 35, 60 ], "Moveset": [124, 92, 85, 126]},
{"name": "WEEZING", "type": "0303", "exp": "125000", 'bst': [65, 90, 120, 60, 85 ], "Moveset": [124, 63, 114, 108]},
{"name": "RHYHORN", "type": "0405", "exp": "156250", 'bst': [80, 85, 95, 25, 30 ], "Moveset": [34, 89, 87, 157]},
{"name": "RHYDON", "type": "0405", "exp": "156250", 'bst': [105, 130, 120, 40, 45 ], "Moveset": [70, 91, 57, 164]},
{"name": "CHANSEY", "type": "0000", "exp": "100000", 'bst': [250, 5, 5, 50, 105], "Moveset": [58, 87, 38, 115]},
{"name": "TANGELA", "type": "1616", "exp": "125000", 'bst': [65, 55, 115, 60, 100], "Moveset": [77, 36, 72, 74]},
{"name": "KANGASKHAN", "type": "0000", "exp": "125000", 'bst': [105, 95, 80, 90, 40 ], "Moveset": [146, 157, 43, 85]},
{"name": "HORSEA", "type": "1515", "exp": "125000", 'bst': [30, 40, 70, 60, 70 ], "Moveset": [56, 92, 108, 58]},
{"name": "SEADRA", "type": "1515", "exp": "125000", 'bst': [55, 65, 95, 85, 95 ], "Moveset": [108, 56, 129, 97]},
{"name": "GOLDEEN", "type": "1515", "exp": "125000", 'bst': [45, 67, 60, 63, 50 ], "Moveset": [57, 92, 38, 58]},
{"name": "SEAKING", "type": "1515", "exp": "125000", 'bst': [80, 92, 65, 68, 80 ], "Moveset": [127, 59, 48, 30]},
{"name": "STARYU", "type": "1515", "exp": "156250", 'bst': [30, 45, 55, 85, 70 ], "Moveset": [85, 105, 57, 94]},
{"name": "STARMIE", "type": "1518", "exp": "156250", 'bst': [60, 75, 85, 115, 100], "Moveset": [61, 87, 107, 161]},
{"name": "MR. MIME", "type": "1818", "exp": "125000", 'bst': [40, 45, 65, 90, 100], "Moveset": [112, 94, 69, 68]},
{"name": "SCYTHER", "type": "0702", "exp": "125000", 'bst': [70, 110, 80, 105, 55 ], "Moveset": [104, 17, 163, 92]},
{"name": "JYNX", "type": "1918", "exp": "125000", 'bst': [65, 50, 35, 95, 95 ], "Moveset": [142, 8, 37, 94]},
{"name": "ELECTABUZZ", "type": "1717", "exp": "125000", 'bst': [65, 83, 57, 105, 85 ], "Moveset": [9, 148, 86, 69]},
{"name": "MAGMAR", "type": "1414", "exp": "125000", 'bst': [65, 95, 57, 93, 85 ], "Moveset": [109, 7, 108, 70]},
{"name": "PINSIR", "type": "0707", "exp": "156250", 'bst': [65, 125, 100, 85, 55 ], "Moveset": [163, 102, 106, 12]},
{"name": "TAUROS", "type": "0000", "exp": "156250", 'bst': [75, 100, 95, 110, 70 ], "Moveset": [70, 117, 126, 39]},
{"name": "MAGIKARP", "type": "1515", "exp": "156250", 'bst': [20, 10, 55, 80, 20 ], "Moveset": [150, 33, 0, 0]},
{"name": "GYARADOS", "type": "1502", "exp": "156250", 'bst': [95, 125, 79, 81, 100], "Moveset": [82, 56, 36, 43]},
{"name": "LAPRAS", "type": "1519", "exp": "156250", 'bst': [130, 85, 80, 60, 95 ], "Moveset": [109, 47, 58, 61]},
{"name": "DITTO", "type": "0000", "exp": "125000", 'bst': [48, 48, 48, 48, 48 ], "Moveset": [144, 0, 0, 0]},
{"name": "EEVEE", "type": "0000", "exp": "125000", 'bst': [55, 55, 50, 55, 65 ], "Moveset": [92, 34, 28, 116]},
{"name": "VAPOREON", "type": "1515", "exp": "125000", 'bst': [130, 65, 60, 65, 110], "Moveset": [151, 62, 57, 98]},
{"name": "JOLTEON", "type": "1717", "exp": "125000", 'bst': [65, 65, 60, 130, 110], "Moveset": [87, 92, 42, 24]},
{"name": "FLAREON", "type": "1414", "exp": "125000", 'bst': [65, 130, 60, 65, 110], "Moveset": [126, 28, 92, 38]},
{"name": "PORYGON", "type": "0000", "exp": "125000", 'bst': [65, 60, 70, 40, 75 ], "Moveset": [160, 94, 105, 161]},
{"name": "OMANYTE", "type": "0515", "exp": "125000", 'bst': [35, 40, 100, 35, 90 ], "Moveset": [59, 57, 38, 104]},
{"name": "OMASTAR", "type": "0515", "exp": "125000", 'bst': [70, 60, 125, 55, 115], "Moveset": [56, 131, 43, 110]},
{"name": "KABUTO", "type": "0515", "exp": "125000", 'bst': [30, 80, 90, 55, 45 ], "Moveset": [163, 56, 58, 92]},
{"name": "KABUTOPS", "type": "0515", "exp": "125000", 'bst': [60, 115, 105, 80, 70 ], "Moveset": [56, 14, 66, 36]},
{"name": "AERODACTYL", "type": "0502", "exp": "156250", 'bst': [80, 105, 65, 130, 60 ], "Moveset": [48, 36, 19, 115]},
{"name": "SNORLAX", "type": "0000", "exp": "156250", 'bst': [160, 110, 65, 30, 65 ], "Moveset": [87, 29, 156, 117]},
{"name": "ARTICUNO", "type": "1902", "exp": "156250", 'bst': [90, 85, 100, 85, 125], "Moveset": [58, 143, 99, 102]},
{"name": "ZAPDOS", "type": "1702", "exp": "156250", 'bst': [90, 90, 85, 100, 125], "Moveset": [87, 143, 164, 148]},
{"name": "MOLTRES", "type": "1402", "exp": "156250", 'bst': [90, 100, 90, 90, 125], "Moveset": [126, 143, 36, 117]},
{"name": "DRATINI", "type": "1A1A", "exp": "156250", 'bst': [41, 64, 45, 50, 50 ], "Moveset": [34, 82, 59, 86]},
{"name": "DRAGONAIR", "type": "1A1A", "exp": "156250", 'bst': [61, 84, 65, 70, 70 ], "Moveset": [63, 85, 126, 86]},
{"name": "DRAGONITE", "type": "1A02", "exp": "156250", 'bst': [91, 134, 95, 80, 100], "Moveset": [21, 102, 57, 164]},
]
poke_cup_list = [
{"name": "BULBASAUR", "type": "1603", "exp": "117360", 'bst': [45, 49, 49, 45, 65 ], "Moveset": [73, 92, 34, 75]},
{"name": "IVYSAUR", "type": "1603", "exp": "117360", 'bst': [60, 62, 63, 60, 80 ], "Moveset": [75, 79, 74, 38]},
{"name": "VENUSAUR", "type": "1603", "exp": "117360", 'bst': [80, 82, 83, 80, 100], "Moveset": [73, 77, 76, 36]},
{"name": "CHARMANDER", "type": "1414", "exp": "117360", 'bst': [39, 52, 43, 65, 50 ], "Moveset": [53, 163, 91, 83]},
{"name": "CHARMELEON", "type": "1414", "exp": "117360", 'bst': [58, 64, 58, 80, 65 ], "Moveset": [53, 68, 69, 70]},
{"name": "CHARIZARD", "type": "1402", "exp": "117360", 'bst': [78, 84, 78, 100, 85 ], "Moveset": [19, 14, 83, 126]},
{"name": "SQUIRTLE", "type": "1515", "exp": "117360", 'bst': [44, 48, 65, 43, 50 ], "Moveset": [57, 59, 34, 91]},
{"name": "WARTORTLE", "type": "1515", "exp": "117360", 'bst': [59, 63, 80, 58, 65 ], "Moveset": [57, 70, 156, 58]},
{"name": "BLASTOISE", "type": "1515", "exp": "117360", 'bst': [79, 83, 100, 78, 85 ], "Moveset": [56, 130, 110, 69]},
{"name": "CATERPIE", "type": "0707", "exp": "125000", 'bst': [45, 30, 35, 45, 20 ], "Moveset": [81, 33, 0, 0]},
{"name": "METAPOD", "type": "0707", "exp": "125000", 'bst': [50, 20, 55, 30, 25 ], "Moveset": [81, 33, 0, 0]},
{"name": "BUTTERFREE", "type": "0702", "exp": "125000", 'bst': [60, 45, 50, 70, 80 ], "Moveset": [94, 48, 72, 78]},
{"name": "WEEDLE", "type": "0703", "exp": "125000", 'bst': [40, 35, 30, 50, 20 ], "Moveset": [81, 40, 0, 0]},
{"name": "KAKUNA", "type": "0703", "exp": "125000", 'bst': [45, 25, 50, 35, 25 ], "Moveset": [81, 40, 0, 0]},
{"name": "BEEDRILL", "type": "0703", "exp": "125000", 'bst': [65, 80, 40, 75, 45 ], "Moveset": [41, 63, 92, 116]},
{"name": "PIDGEY", "type": "0002", "exp": "117360", 'bst': [40, 45, 40, 56, 35 ], "Moveset": [19, 92, 38, 104]},
{"name": "PIDGEOTTO", "type": "0002", "exp": "117360", 'bst': [63, 60, 55, 71, 50 ], "Moveset": [19, 98, 28, 36]},
{"name": "PIDGEOT", "type": "0002", "exp": "117360", 'bst': [83, 80, 75, 91, 70 ], "Moveset": [119, 19, 98, 28]},
{"name": "RATTATA", "type": "0000", "exp": "125000", 'bst': [30, 56, 35, 72, 25 ], "Moveset": [162, 59, 98, 158]},
{"name": "RATICATE", "type": "0000", "exp": "125000", 'bst': [55, 81, 60, 97, 50 ], "Moveset": [158, 63, 116, 87]},
{"name": "SPEAROW", "type": "0002", "exp": "125000", 'bst': [40, 60, 30, 70, 31 ], "Moveset": [65, 119, 104, 38]},
{"name": "FEAROW", "type": "0002", "exp": "125000", 'bst': [65, 90, 65, 100, 61 ], "Moveset": [65, 119, 31, 129]},
{"name": "EKANS", "type": "0303", "exp": "125000", 'bst': [35, 60, 44, 55, 40 ], "Moveset": [89, 51, 103, 34]},
{"name": "ARBOK", "type": "0303", "exp": "125000", 'bst': [60, 85, 69, 80, 65 ], "Moveset": [137, 35, 91, 70]},
{"name": "PIKACHU", "type": "1717", "exp": "125000", 'bst': [35, 55, 30, 90, 50 ], "Moveset": [85, 21, 86, 69]},
{"name": "RAICHU", "type": "1717", "exp": "125000", 'bst': [60, 90, 55, 100, 90 ], "Moveset": [87, 86, 148, 25]},
{"name": "SANDSHREW", "type": "0404", "exp": "125000", 'bst': [50, 75, 85, 40, 30 ], "Moveset": [89, 163, 69, 28]},
{"name": "SANDSLASH", "type": "0404", "exp": "125000", 'bst': [75, 100, 110, 65, 55 ], "Moveset": [91, 129, 69, 28]},
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [55, 47, 52, 41, 40 ], "Moveset": [92, 85, 34, 59]},
{"name": "NIDORINA", "type": "0303", "exp": "117360", 'bst': [70, 62, 67, 56, 55 ], "Moveset": [92, 87, 38, 58]},
{"name": "NIDOQUEEN", "type": "0304", "exp": "117360", 'bst': [90, 82, 87, 76, 75 ], "Moveset": [92, 24, 44, 89]},
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [46, 57, 40, 50, 40 ], "Moveset": [59, 34, 116, 85]},
{"name": "NIDORINO", "type": "0303", "exp": "117360", 'bst': [61, 72, 57, 65, 55 ], "Moveset": [38, 32, 116, 87]},
{"name": "NIDOKING", "type": "0304", "exp": "117360", 'bst': [81, 92, 77, 85, 75 ], "Moveset": [89, 32, 99, 164]},
{"name": "CLEFAIRY", "type": "0000", "exp": "100000", 'bst': [70, 45, 48, 35, 60 ], "Moveset": [85, 94, 34, 59]},
{"name": "CLEFABLE", "type": "0000", "exp": "100000", 'bst': [95, 70, 73, 60, 85 ], "Moveset": [47, 161, 107, 58]},
{"name": "VULPIX", "type": "1414", "exp": "125000", 'bst': [38, 41, 40, 65, 65 ], "Moveset": [53, 91, 109, 38]},
{"name": "NINETALES", "type": "1414", "exp": "125000", 'bst': [73, 76, 75, 100, 100], "Moveset": [126, 130, 109, 39]},
{"name": "JIGGLYPUFF", "type": "0000", "exp": "100000", 'bst': [115, 45, 20, 20, 25 ], "Moveset": [47, 34, 69, 94]},
{"name": "WIGGLYTUFF", "type": "0000", "exp": "100000", 'bst': [140, 70, 45, 45, 50 ], "Moveset": [47, 38, 66, 85]},
{"name": "ZUBAT", "type": "0302", "exp": "125000", 'bst': [40, 45, 35, 55, 40 ], "Moveset": [109, 72, 92, 38]},
{"name": "GOLBAT", "type": "0302", "exp": "125000", 'bst': [75, 80, 70, 90, 75 ], "Moveset": [109, 72, 44, 114]},
{"name": "ODDISH", "type": "1603", "exp": "117360", 'bst': [45, 50, 55, 30, 75 ], "Moveset": [80, 92, 72, 38]},
{"name": "GLOOM", "type": "1603", "exp": "117360", 'bst': [60, 65, 70, 40, 85 ], "Moveset": [80, 36, 72, 78]},
{"name": "VILEPLUME", "type": "1603", "exp": "117360", 'bst': [75, 80, 85, 50, 100], "Moveset": [80, 79, 51, 15]},
{"name": "PARAS", "type": "0716", "exp": "125000", 'bst': [35, 70, 55, 25, 55 ], "Moveset": [147, 163, 91, 72]},
{"name": "PARASECT", "type": "0716", "exp": "125000", 'bst': [60, 95, 80, 30, 80 ], "Moveset": [147, 36, 91, 76]},
{"name": "VENONAT", "type": "0703", "exp": "125000", 'bst': [60, 55, 50, 45, 40 ], "Moveset": [94, 72, 38, 78]},
{"name": "VENOMOTH", "type": "0703", "exp": "125000", 'bst': [70, 65, 60, 90, 90 ], "Moveset": [94, 48, 76, 129]},
{"name": "DIGLETT", "type": "0404", "exp": "125000", 'bst': [10, 55, 25, 95, 45 ], "Moveset": [89, 163, 28, 157]},
{"name": "DUGTRIO", "type": "0404", "exp": "125000", 'bst': [35, 80, 50, 120, 70 ], "Moveset": [91, 28, 92, 63]},
{"name": "MEOWTH", "type": "0000", "exp": "125000", 'bst': [40, 45, 35, 90, 40 ], "Moveset": [163, 85, 129, 104]},
{"name": "PERSIAN", "type": "0000", "exp": "125000", 'bst': [65, 70, 60, 115, 65 ], "Moveset": [163, 61, 102, 45]},
{"name": "PSYDUCK", "type": "1515", "exp": "125000", 'bst': [50, 52, 48, 55, 50 ], "Moveset": [57, 93, 91, 59]},
{"name": "GOLDUCK", "type": "1515", "exp": "125000", 'bst': [80, 82, 78, 85, 80 ], "Moveset": [58, 57, 92, 50]},
{"name": "MANKEY", "type": "0101", "exp": "125000", 'bst': [40, 80, 35, 70, 35 ], "Moveset": [66, 157, 69, 103]},
{"name": "PRIMEAPE", "type": "0101", "exp": "125000", 'bst': [65, 105, 60, 95, 60 ], "Moveset": [154, 157, 67, 103]},
{"name": "GROWLITHE", "type": "1414", "exp": "156250", 'bst': [55, 70, 45, 60, 50 ], "Moveset": [53, 34, 115, 91]},
{"name": "ARCANINE", "type": "1414", "exp": "156250", 'bst': [90, 110, 80, 95, 80 ], "Moveset": [126, 36, 82, 164]},
{"name": "POLIWAG", "type": "1515", "exp": "117360", 'bst': [40, 50, 40, 90, 40 ], "Moveset": [34, 59, 57, 133]},
{"name": "POLIWHIRL", "type": "1515", "exp": "117360", 'bst': [65, 65, 65, 90, 50 ], "Moveset": [95, 57, 58, 89]},
{"name": "POLIWRATH", "type": "1501", "exp": "117360", 'bst': [90, 85, 95, 70, 70 ], "Moveset": [95, 66, 68, 56]},
{"name": "ABRA", "type": "1818", "exp": "117360", 'bst': [25, 20, 15, 90, 105], "Moveset": [94, 69, 115, 86]},
{"name": "KADABRA", "type": "1818", "exp": "117360", 'bst': [40, 35, 30, 105, 120], "Moveset": [94, 68, 105, 91]},
{"name": "ALAKAZAM", "type": "1818", "exp": "117360", 'bst': [55, 50, 45, 120, 135], "Moveset": [60, 118, 50, 161]},
{"name": "MACHOP", "type": "0101", "exp": "117360", 'bst': [70, 80, 50, 35, 35 ], "Moveset": [66, 157, 89, 116]},
{"name": "MACHOKE", "type": "0101", "exp": "117360", 'bst': [80, 100, 70, 45, 50 ], "Moveset": [66, 70, 157, 116]},
{"name": "MACHAMP", "type": "0101", "exp": "117360", 'bst': [90, 130, 80, 55, 65 ], "Moveset": [67, 70, 68, 116]},
{"name": "BELLSPROUT", "type": "1603", "exp": "117360", 'bst': [50, 75, 35, 40, 70 ], "Moveset": [75, 74, 72, 78]},
{"name": "WEEPINBELL", "type": "1603", "exp": "117360", 'bst': [65, 90, 50, 55, 85 ], "Moveset": [75, 51, 35, 92]},
{"name": "VICTREEBEL", "type": "1603", "exp": "117360", 'bst': [80, 105, 65, 70, 100], "Moveset": [76, 51, 115, 21]},
{"name": "TENTACOOL", "type": "1503", "exp": "156250", 'bst': [40, 40, 35, 70, 100], "Moveset": [57, 48, 72, 59]},
{"name": "TENTACRUEL", "type": "1503", "exp": "156250", 'bst': [80, 70, 65, 100, 120], "Moveset": [51, 48, 56, 15]},
{"name": "GEODUDE", "type": "0504", "exp": "117360", 'bst': [40, 80, 100, 20, 30 ], "Moveset": [89, 69, 157, 153]},
{"name": "GRAVELER", "type": "0504", "exp": "117360", 'bst': [55, 95, 115, 35, 45 ], "Moveset": [89, 69, 70, 120]},
{"name": "GOLEM", "type": "0504", "exp": "117360", 'bst': [80, 110, 130, 45, 55 ], "Moveset": [91, 69, 126, 118]},
{"name": "PONYTA", "type": "1414", "exp": "125000", 'bst': [50, 85, 55, 90, 65 ], "Moveset": [126, 97, 32, 34]},
{"name": "RAPIDASH", "type": "1414", "exp": "125000", 'bst': [65, 100, 70, 105, 80 ], "Moveset": [126, 23, 92, 83]},
{"name": "SLOWPOKE", "type": "1518", "exp": "125000", 'bst': [90, 65, 65, 15, 40 ], "Moveset": [57, 94, 86, 133]},
{"name": "SLOWBRO", "type": "1518", "exp": "125000", 'bst': [95, 75, 110, 30, 80 ], "Moveset": [57, 94, 50, 110]},
{"name": "MAGNEMITE", "type": "1717", "exp": "125000", 'bst': [25, 35, 70, 45, 95 ], "Moveset": [85, 86, 48, 38]},
{"name": "MAGNETON", "type": "1717", "exp": "125000", 'bst': [50, 60, 95, 70, 120], "Moveset": [87, 103, 48, 129]},
{"name": "FARFETCH'D", "type": "0002", "exp": "125000", 'bst': [52, 65, 55, 60, 58 ], "Moveset": [163, 28, 92, 19]},
{"name": "DODUO", "type": "0002", "exp": "125000", 'bst': [35, 85, 45, 75, 35 ], "Moveset": [65, 161, 104, 115]},
{"name": "DODRIO", "type": "0002", "exp": "125000", 'bst': [60, 110, 70, 100, 60 ], "Moveset": [19, 161, 97, 115]},
{"name": "SEEL", "type": "1515", "exp": "125000", 'bst': [65, 45, 55, 45, 70 ], "Moveset": [58, 34, 32, 57]},
{"name": "DEWGONG", "type": "1519", "exp": "125000", 'bst': [90, 70, 80, 70, 95 ], "Moveset": [62, 29, 156, 57]},
{"name": "GRIMER", "type": "0303", "exp": "125000", 'bst': [80, 80, 50, 25, 40 ], "Moveset": [124, 34, 103, 153]},
{"name": "MUK", "type": "0303", "exp": "125000", 'bst': [105, 105, 75, 50, 65 ], "Moveset": [124, 85, 63, 120]},
{"name": "SHELLDER", "type": "1515", "exp": "156250", 'bst': [30, 65, 100, 40, 45 ], "Moveset": [57, 153, 59, 161]},
{"name": "CLOYSTER", "type": "1519", "exp": "156250", 'bst': [50, 95, 180, 70, 85 ], "Moveset": [128, 131, 58, 48]},
{"name": "GASTLY", "type": "0803", "exp": "117360", 'bst': [30, 35, 30, 80, 100], "Moveset": [95, 138, 94, 109]},
{"name": "HAUNTER", "type": "0803", "exp": "117360", 'bst': [45, 50, 45, 95, 115], "Moveset": [72, 94, 153, 109]},
{"name": "GENGAR", "type": "0803", "exp": "117360", 'bst': [60, 65, 60, 110, 130], "Moveset": [85, 101, 95, 109]},
{"name": "ONIX", "type": "0504", "exp": "125000", 'bst': [35, 45, 160, 70, 30 ], "Moveset": [89, 157, 70, 153]},
{"name": "DROWZEE", "type": "1818", "exp": "125000", 'bst': [60, 48, 45, 42, 90 ], "Moveset": [95, 138, 94, 161]},
{"name": "HYPNO", "type": "1818", "exp": "125000", 'bst': [85, 73, 70, 67, 115], "Moveset": [95, 29, 138, 96]},
{"name": "KRABBY", "type": "1515", "exp": "125000", 'bst': [30, 105, 90, 50, 25 ], "Moveset": [152, 12, 38, 59]},
{"name": "KINGLER", "type": "1515", "exp": "125000", 'bst': [55, 130, 115, 75, 50 ], "Moveset": [152, 12, 23, 164]},
{"name": "VOLTORB", "type": "1717", "exp": "125000", 'bst': [40, 30, 50, 100, 55 ], "Moveset": [85, 86, 129, 153]},
{"name": "ELECTRODE", "type": "1717", "exp": "125000", 'bst': [60, 50, 70, 140, 80 ], "Moveset": [87, 86, 129, 120]},
{"name": "EXEGGCUTE", "type": "1618", "exp": "156250", 'bst': [60, 40, 80, 40, 60 ], "Moveset": [94, 153, 73, 92]},
{"name": "EXEGGUTOR", "type": "1618", "exp": "156250", 'bst': [95, 95, 85, 55, 125], "Moveset": [72, 78, 73, 121]},
{"name": "CUBONE", "type": "0404", "exp": "125000", 'bst': [50, 50, 95, 35, 40 ], "Moveset": [89, 66, 59, 70]},
{"name": "MAROWAK", "type": "0404", "exp": "125000", 'bst': [60, 80, 110, 45, 50 ], "Moveset": [155, 37, 126, 116]},
{"name": "HITMONLEE", "type": "0101", "exp": "125000", 'bst': [50, 120, 53, 87, 35 ], "Moveset": [136, 25, 118, 69]},
{"name": "HITMONCHAN", "type": "0101", "exp": "125000", 'bst': [50, 105, 79, 76, 35 ], "Moveset": [66, 9, 8, 70]},
{"name": "LICKITUNG", "type": "0000", "exp": "125000", 'bst': [90, 55, 75, 30, 60 ], "Moveset": [70, 59, 87, 126]},
{"name": "KOFFING", "type": "0303", "exp": "125000", 'bst': [40, 65, 95, 35, 60 ], "Moveset": [124, 92, 85, 153]},
{"name": "WEEZING", "type": "0303", "exp": "125000", 'bst': [65, 90, 120, 60, 85 ], "Moveset": [124, 63, 126, 120]},
{"name": "RHYHORN", "type": "0405", "exp": "156250", 'bst': [80, 85, 95, 25, 30 ], "Moveset": [89, 34, 157, 126]},
{"name": "RHYDON", "type": "0405", "exp": "156250", 'bst': [105, 130, 120, 40, 45 ], "Moveset": [91, 70, 87, 57]},
{"name": "CHANSEY", "type": "0000", "exp": "100000", 'bst': [250, 5, 5, 50, 105], "Moveset": [87, 126, 107, 156]},
{"name": "TANGELA", "type": "1616", "exp": "125000", 'bst': [65, 55, 115, 60, 100], "Moveset": [72, 74, 92, 38]},
{"name": "KANGASKHAN", "type": "0000", "exp": "125000", 'bst': [105, 95, 80, 90, 40 ], "Moveset": [146, 157, 57, 85]},
{"name": "HORSEA", "type": "1515", "exp": "125000", 'bst': [30, 40, 70, 60, 70 ], "Moveset": [56, 92, 108, 58]},
{"name": "SEADRA", "type": "1515", "exp": "125000", 'bst': [55, 65, 95, 85, 95 ], "Moveset": [57, 92, 108, 129]},
{"name": "GOLDEEN", "type": "1515", "exp": "125000", 'bst': [45, 67, 60, 63, 50 ], "Moveset": [57, 48, 32, 59]},
{"name": "SEAKING", "type": "1515", "exp": "125000", 'bst': [80, 92, 65, 68, 80 ], "Moveset": [127, 48, 30, 58]},
{"name": "STARYU", "type": "1515", "exp": "156250", 'bst': [30, 45, 55, 85, 70 ], "Moveset": [56, 105, 85, 94]},
{"name": "STARMIE", "type": "1518", "exp": "156250", 'bst': [60, 75, 85, 115, 100], "Moveset": [57, 87, 129, 106]},
{"name": "MR. MIME", "type": "1818", "exp": "125000", 'bst': [40, 45, 65, 90, 100], "Moveset": [112, 94, 118, 69]},
{"name": "SCYTHER", "type": "0702", "exp": "125000", 'bst': [70, 110, 80, 105, 55 ], "Moveset": [163, 17, 43, 104]},
{"name": "JYNX", "type": "1918", "exp": "125000", 'bst': [65, 50, 35, 95, 95 ], "Moveset": [8, 5, 94, 142]},
{"name": "ELECTABUZZ", "type": "1717", "exp": "125000", 'bst': [65, 83, 57, 105, 85 ], "Moveset": [9, 5, 94, 86]},
{"name": "MAGMAR", "type": "1414", "exp": "125000", 'bst': [65, 95, 57, 93, 85 ], "Moveset": [7, 5, 94, 108]},
{"name": "PINSIR", "type": "0707", "exp": "156250", 'bst': [65, 125, 100, 85, 55 ], "Moveset": [70, 106, 69, 12]},
{"name": "TAUROS", "type": "0000", "exp": "156250", 'bst': [75, 100, 95, 110, 70 ], "Moveset": [38, 126, 39, 117]},
{"name": "MAGIKARP", "type": "1515", "exp": "156250", 'bst': [20, 10, 55, 80, 20 ], "Moveset": [150, 33, 0, 0]},
{"name": "GYARADOS", "type": "1502", "exp": "156250", 'bst': [95, 125, 79, 81, 100], "Moveset": [57, 82, 44, 126]},
{"name": "LAPRAS", "type": "1519", "exp": "156250", 'bst': [130, 85, 80, 60, 95 ], "Moveset": [58, 76, 34, 47]},
{"name": "DITTO", "type": "0000", "exp": "125000", 'bst': [48, 48, 48, 48, 48 ], "Moveset": [144, 0, 0, 0]},
{"name": "EEVEE", "type": "0000", "exp": "125000", 'bst': [55, 55, 50, 55, 65 ], "Moveset": [34, 129, 28, 92]},
{"name": "VAPOREON", "type": "1515", "exp": "125000", 'bst': [130, 65, 60, 65, 110], "Moveset": [57, 98, 28, 151]},
{"name": "JOLTEON", "type": "1717", "exp": "125000", 'bst': [65, 65, 60, 130, 110], "Moveset": [85, 42, 92, 28]},
{"name": "FLAREON", "type": "1414", "exp": "125000", 'bst': [65, 130, 60, 65, 110], "Moveset": [126, 36, 123, 28]},
{"name": "PORYGON", "type": "0000", "exp": "125000", 'bst': [65, 60, 70, 40, 75 ], "Moveset": [161, 94, 159, 160]},
{"name": "OMANYTE", "type": "0515", "exp": "125000", 'bst': [35, 40, 100, 35, 90 ], "Moveset": [57, 58, 38, 104]},
{"name": "OMASTAR", "type": "0515", "exp": "125000", 'bst': [70, 60, 125, 55, 115], "Moveset": [56, 66, 131, 110]},
{"name": "KABUTO", "type": "0515", "exp": "125000", 'bst': [30, 80, 90, 55, 45 ], "Moveset": [56, 59, 163, 104]},
{"name": "KABUTOPS", "type": "0515", "exp": "125000", 'bst': [60, 115, 105, 80, 70 ], "Moveset": [57, 14, 25, 66]},
{"name": "AERODACTYL", "type": "0502", "exp": "156250", 'bst': [80, 105, 65, 130, 60 ], "Moveset": [19, 63, 48, 82]},
{"name": "SNORLAX", "type": "0000", "exp": "156250", 'bst': [160, 110, 65, 30, 65 ], "Moveset": [25, 157, 118, 156]},
{"name": "ARTICUNO", "type": "1902", "exp": "156250", 'bst': [90, 85, 100, 85, 125], "Moveset": [58, 143, 13, 164]},
{"name": "ZAPDOS", "type": "1702", "exp": "156250", 'bst': [90, 90, 85, 100, 125], "Moveset": [85, 143, 86, 148]},
{"name": "MOLTRES", "type": "1402", "exp": "156250", 'bst': [90, 100, 90, 90, 125], "Moveset": [126, 19, 129, 164]},
{"name": "DRATINI", "type": "1A1A", "exp": "156250", 'bst': [41, 64, 45, 50, 50 ], "Moveset": [63, 34, 85, 86]},
{"name": "DRAGONAIR", "type": "1A1A", "exp": "156250", 'bst': [61, 84, 65, 70, 70 ], "Moveset": [63, 129, 58, 86]},
{"name": "DRAGONITE", "type": "1A02", "exp": "156250", 'bst': [91, 134, 95, 80, 100], "Moveset": [21, 82, 87, 97]},
]
prime_cup_list = [
{"name": "BULBASAUR", "type": "1603", "exp": "1059860", 'bst': [45, 49, 49, 45, 65 ], "Moveset": [73, 75, 74, 34]},
{"name": "IVYSAUR", "type": "1603", "exp": "1059860", 'bst': [60, 62, 63, 60, 80 ], "Moveset": [73, 75, 74, 72]},
{"name": "VENUSAUR", "type": "1603", "exp": "1059860", 'bst': [80, 82, 83, 80, 100], "Moveset": [73, 76, 74, 79]},
{"name": "CHARMANDER", "type": "1414", "exp": "1059860", 'bst': [39, 52, 43, 65, 50 ], "Moveset": [53, 34, 69, 91]},
{"name": "CHARMELEON", "type": "1414", "exp": "1059860", 'bst': [58, 64, 58, 80, 65 ], "Moveset": [53, 163, 91, 66]},
{"name": "CHARIZARD", "type": "1402", "exp": "1059860", 'bst': [78, 84, 78, 100, 85 ], "Moveset": [126, 19, 83, 14]},
{"name": "SQUIRTLE", "type": "1515", "exp": "1059860", 'bst': [44, 48, 65, 43, 50 ], "Moveset": [56, 59, 34, 91]},
{"name": "WARTORTLE", "type": "1515", "exp": "1059860", 'bst': [59, 63, 80, 58, 65 ], "Moveset": [57, 69, 91, 92]},
{"name": "BLASTOISE", "type": "1515", "exp": "1059860", 'bst': [79, 83, 100, 78, 85 ], "Moveset": [56, 130, 110, 39]},
{"name": "CATERPIE", "type": "0707", "exp": "1000000", 'bst': [45, 30, 35, 45, 20 ], "Moveset": [33, 81, 0, 0]},
{"name": "METAPOD", "type": "0707", "exp": "1000000", 'bst': [50, 20, 55, 30, 25 ], "Moveset": [33, 81, 0, 0]},
{"name": "BUTTERFREE", "type": "0702", "exp": "1000000", 'bst': [60, 45, 50, 70, 80 ], "Moveset": [94, 72, 129, 78]},
{"name": "WEEDLE", "type": "0703", "exp": "1000000", 'bst': [40, 35, 30, 50, 20 ], "Moveset": [40, 81, 0, 0]},
{"name": "KAKUNA", "type": "0703", "exp": "1000000", 'bst': [45, 25, 50, 35, 25 ], "Moveset": [40, 81, 0, 0]},
{"name": "BEEDRILL", "type": "0703", "exp": "1000000", 'bst': [65, 80, 40, 75, 45 ], "Moveset": [41, 63, 72, 116]},
{"name": "PIDGEY", "type": "0002", "exp": "1059860", 'bst': [40, 45, 40, 56, 35 ], "Moveset": [19, 28, 119, 18]},
{"name": "PIDGEOTTO", "type": "0002", "exp": "1059860", 'bst': [63, 60, 55, 71, 50 ], "Moveset": [19, 28, 129, 92]},
{"name": "PIDGEOT", "type": "0002", "exp": "1059860", 'bst': [83, 80, 75, 91, 70 ], "Moveset": [98, 119, 28, 19]},
{"name": "RATTATA", "type": "0000", "exp": "1000000", 'bst': [30, 56, 35, 72, 25 ], "Moveset": [162, 34, 91, 92]},
{"name": "RATICATE", "type": "0000", "exp": "1000000", 'bst': [55, 81, 60, 97, 50 ], "Moveset": [162, 158, 98, 92]},
{"name": "SPEAROW", "type": "0002", "exp": "1000000", 'bst': [40, 60, 30, 70, 31 ], "Moveset": [65, 129, 104, 19]},
{"name": "FEAROW", "type": "0002", "exp": "1000000", 'bst': [65, 90, 65, 100, 61 ], "Moveset": [65, 119, 63, 45]},
{"name": "EKANS", "type": "0303", "exp": "1000000", 'bst': [35, 60, 44, 55, 40 ], "Moveset": [38, 137, 89, 72]},
{"name": "ARBOK", "type": "0303", "exp": "1000000", 'bst': [60, 85, 69, 80, 65 ], "Moveset": [91, 137, 70, 51]},
{"name": "PIKACHU", "type": "1717", "exp": "1000000", 'bst': [35, 55, 30, 90, 50 ], "Moveset": [85, 86, 129, 115]},
{"name": "RAICHU", "type": "1717", "exp": "1000000", 'bst': [60, 90, 55, 100, 90 ], "Moveset": [87, 86, 98, 25]},
{"name": "SANDSHREW", "type": "0404", "exp": "1000000", 'bst': [50, 75, 85, 40, 30 ], "Moveset": [28, 89, 163, 157]},
{"name": "SANDSLASH", "type": "0404", "exp": "1000000", 'bst': [75, 100, 110, 65, 55 ], "Moveset": [28, 91, 70, 157]},
{"name": "NIDORAN", "type": "0303", "exp": "1059860", 'bst': [55, 47, 52, 41, 40 ], "Moveset": [34, 59, 85, 92]},
{"name": "NIDORINA", "type": "0303", "exp": "1059860", 'bst': [70, 62, 67, 56, 55 ], "Moveset": [34, 61, 87, 92]},
{"name": "NIDOQUEEN", "type": "0304", "exp": "1059860", 'bst': [90, 82, 87, 76, 75 ], "Moveset": [89, 24, 157, 92]},
{"name": "NIDORAN", "type": "0303", "exp": "1059860", 'bst': [46, 57, 40, 50, 40 ], "Moveset": [34, 59, 87, 32]},
{"name": "NIDORINO", "type": "0303", "exp": "1059860", 'bst': [61, 72, 57, 65, 55 ], "Moveset": [34, 85, 58, 32]},
{"name": "NIDOKING", "type": "0304", "exp": "1059860", 'bst': [81, 92, 77, 85, 75 ], "Moveset": [30, 89, 117, 32]},
{"name": "CLEFAIRY", "type": "0000", "exp": "800000", 'bst': [70, 45, 48, 35, 60 ], "Moveset": [118, 34, 86, 59]},
{"name": "CLEFABLE", "type": "0000", "exp": "800000", 'bst': [95, 70, 73, 60, 85 ], "Moveset": [118, 70, 86, 87]},
{"name": "VULPIX", "type": "1414", "exp": "1000000", 'bst': [38, 41, 40, 65, 65 ], "Moveset": [53, 91, 109, 92]},
{"name": "NINETALES", "type": "1414", "exp": "1000000", 'bst': [73, 76, 75, 100, 100], "Moveset": [126, 98, 109, 39]},
{"name": "JIGGLYPUFF", "type": "0000", "exp": "800000", 'bst': [115, 45, 20, 20, 25 ], "Moveset": [47, 148, 34, 69]},
{"name": "WIGGLYTUFF", "type": "0000", "exp": "800000", 'bst': [140, 70, 45, 45, 50 ], "Moveset": [47, 50, 70, 63]},
{"name": "ZUBAT", "type": "0302", "exp": "1000000", 'bst': [40, 45, 35, 55, 40 ], "Moveset": [109, 129, 72, 114]},
{"name": "GOLBAT", "type": "0302", "exp": "1000000", 'bst': [75, 80, 70, 90, 75 ], "Moveset": [48, 63, 72, 114]},
{"name": "ODDISH", "type": "1603", "exp": "1059860", 'bst': [45, 50, 55, 30, 75 ], "Moveset": [80, 72, 78, 38]},
{"name": "GLOOM", "type": "1603", "exp": "1059860", 'bst': [60, 65, 70, 40, 85 ], "Moveset": [80, 72, 78, 51]},
{"name": "VILEPLUME", "type": "1603", "exp": "1059860", 'bst': [75, 80, 85, 50, 100], "Moveset": [76, 72, 78, 51]},
{"name": "PARAS", "type": "0716", "exp": "1000000", 'bst': [35, 70, 55, 25, 55 ], "Moveset": [163, 147, 91, 72]},
{"name": "PARASECT", "type": "0716", "exp": "1000000", 'bst': [60, 95, 80, 30, 80 ], "Moveset": [163, 147, 74, 72]},
{"name": "VENONAT", "type": "0703", "exp": "1000000", 'bst': [60, 55, 50, 45, 40 ], "Moveset": [94, 72, 38, 92]},
{"name": "VENOMOTH", "type": "0703", "exp": "1000000", 'bst': [70, 65, 60, 90, 90 ], "Moveset": [94, 72, 79, 148]},
{"name": "DIGLETT", "type": "0404", "exp": "1000000", 'bst': [10, 55, 25, 95, 45 ], "Moveset": [89, 90, 163, 28]},
{"name": "DUGTRIO", "type": "0404", "exp": "1000000", 'bst': [35, 80, 50, 120, 70 ], "Moveset": [91, 157, 45, 28]},
{"name": "MEOWTH", "type": "0000", "exp": "1000000", 'bst': [40, 45, 35, 90, 40 ], "Moveset": [61, 103, 163, 85]},
{"name": "PERSIAN", "type": "0000", "exp": "1000000", 'bst': [65, 70, 60, 115, 65 ], "Moveset": [63, 103, 44, 87]},
{"name": "PSYDUCK", "type": "1515", "exp": "1000000", 'bst': [50, 52, 48, 55, 50 ], "Moveset": [56, 59, 91, 50]},
{"name": "GOLDUCK", "type": "1515", "exp": "1000000", 'bst': [80, 82, 78, 85, 80 ], "Moveset": [61, 58, 93, 50]},
{"name": "MANKEY", "type": "0101", "exp": "1000000", 'bst': [40, 80, 35, 70, 35 ], "Moveset": [66, 37, 91, 68]},
{"name": "PRIMEAPE", "type": "0101", "exp": "1000000", 'bst': [65, 105, 60, 95, 60 ], "Moveset": [67, 37, 69, 68]},
{"name": "GROWLITHE", "type": "1414", "exp": "1250000", 'bst': [55, 70, 45, 60, 50 ], "Moveset": [53, 91, 34, 104]},
{"name": "ARCANINE", "type": "1414", "exp": "1250000", 'bst': [90, 110, 80, 95, 80 ], "Moveset": [126, 91, 43, 97]},
{"name": "POLIWAG", "type": "1515", "exp": "1059860", 'bst': [40, 50, 40, 90, 40 ], "Moveset": [56, 59, 94, 133]},
{"name": "POLIWHIRL", "type": "1515", "exp": "1059860", 'bst': [65, 65, 65, 90, 50 ], "Moveset": [57, 58, 94, 133]},
{"name": "POLIWRATH", "type": "1501", "exp": "1059860", 'bst': [90, 85, 95, 70, 70 ], "Moveset": [61, 66, 95, 133]},
{"name": "ABRA", "type": "1818", "exp": "1059860", 'bst': [25, 20, 15, 90, 105], "Moveset": [94, 86, 104, 34]},
{"name": "KADABRA", "type": "1818", "exp": "1059860", 'bst': [40, 35, 30, 105, 120], "Moveset": [94, 105, 115, 91]},
{"name": "ALAKAZAM", "type": "1818", "exp": "1059860", 'bst': [55, 50, 45, 120, 135], "Moveset": [60, 134, 115, 63]},
{"name": "MACHOP", "type": "0101", "exp": "1059860", 'bst': [70, 80, 50, 35, 35 ], "Moveset": [66, 34, 69, 116]},
{"name": "MACHOKE", "type": "0101", "exp": "1059860", 'bst': [80, 100, 70, 45, 50 ], "Moveset": [66, 91, 69, 116]},
{"name": "MACHAMP", "type": "0101", "exp": "1059860", 'bst': [90, 130, 80, 55, 65 ], "Moveset": [67, 5, 43, 116]},
{"name": "BELLSPROUT", "type": "1603", "exp": "1059860", 'bst': [50, 75, 35, 40, 70 ], "Moveset": [75, 92, 35, 38]},
{"name": "WEEPINBELL", "type": "1603", "exp": "1059860", 'bst': [65, 90, 50, 55, 85 ], "Moveset": [75, 72, 74, 78]},
{"name": "VICTREEBEL", "type": "1603", "exp": "1059860", 'bst': [80, 105, 65, 70, 100], "Moveset": [75, 51, 35, 79]},
{"name": "TENTACOOL", "type": "1503", "exp": "1250000", 'bst': [40, 40, 35, 70, 100], "Moveset": [57, 59, 72, 92]},
{"name": "TENTACRUEL", "type": "1503", "exp": "1250000", 'bst': [80, 70, 65, 100, 120], "Moveset": [61, 35, 103, 92]},
{"name": "GEODUDE", "type": "0504", "exp": "1059860", 'bst': [40, 80, 100, 20, 30 ], "Moveset": [157, 89, 69, 126]},
{"name": "GRAVELER", "type": "0504", "exp": "1059860", 'bst': [55, 95, 115, 35, 45 ], "Moveset": [157, 89, 126, 118]},
{"name": "GOLEM", "type": "0504", "exp": "1059860", 'bst': [80, 110, 130, 45, 55 ], "Moveset": [88, 91, 111, 126]},
{"name": "PONYTA", "type": "1414", "exp": "1000000", 'bst': [50, 85, 55, 90, 65 ], "Moveset": [83, 97, 32, 92]},
{"name": "RAPIDASH", "type": "1414", "exp": "1000000", 'bst': [65, 100, 70, 105, 80 ], "Moveset": [126, 23, 115, 39]},
{"name": "SLOWPOKE", "type": "1518", "exp": "1000000", 'bst': [90, 65, 65, 15, 40 ], "Moveset": [57, 94, 133, 86]},
{"name": "SLOWBRO", "type": "1518", "exp": "1000000", 'bst': [95, 75, 110, 30, 80 ], "Moveset": [57, 94, 50, 5]},
{"name": "MAGNEMITE", "type": "1717", "exp": "1000000", 'bst': [25, 35, 70, 45, 95 ], "Moveset": [85, 86, 129, 148]},
{"name": "MAGNETON", "type": "1717", "exp": "1000000", 'bst': [50, 60, 95, 70, 120], "Moveset": [87, 86, 48, 148]},
{"name": "FARFETCH'D", "type": "0002", "exp": "1000000", 'bst': [52, 65, 55, 60, 58 ], "Moveset": [163, 28, 19, 92]},
{"name": "DODUO", "type": "0002", "exp": "1000000", 'bst': [35, 85, 45, 75, 35 ], "Moveset": [65, 34, 115, 104]},
{"name": "DODRIO", "type": "0002", "exp": "1000000", 'bst': [60, 110, 70, 100, 60 ], "Moveset": [161, 19, 45, 97]},
{"name": "SEEL", "type": "1515", "exp": "1000000", 'bst': [65, 45, 55, 45, 70 ], "Moveset": [57, 59, 34, 104]},
{"name": "DEWGONG", "type": "1519", "exp": "1000000", 'bst': [90, 70, 80, 70, 95 ], "Moveset": [62, 57, 29, 32]},
{"name": "GRIMER", "type": "0303", "exp": "1000000", 'bst': [80, 80, 50, 25, 40 ], "Moveset": [124, 34, 85, 151]},
{"name": "MUK", "type": "0303", "exp": "1000000", 'bst': [105, 105, 75, 50, 65 ], "Moveset": [124, 126, 103, 151]},
{"name": "SHELLDER", "type": "1515", "exp": "1250000", 'bst': [30, 65, 100, 40, 45 ], "Moveset": [59, 57, 129, 48]},
{"name": "CLOYSTER", "type": "1519", "exp": "1250000", 'bst': [50, 95, 180, 70, 85 ], "Moveset": [58, 61, 128, 48]},
{"name": "GASTLY", "type": "0803", "exp": "1059860", 'bst': [30, 35, 30, 80, 100], "Moveset": [95, 94, 109, 101]},
{"name": "HAUNTER", "type": "0803", "exp": "1059860", 'bst': [45, 50, 45, 95, 115], "Moveset": [95, 138, 109, 94]},
{"name": "GENGAR", "type": "0803", "exp": "1059860", 'bst': [60, 65, 60, 110, 130], "Moveset": [95, 138, 118, 101]},
{"name": "ONIX", "type": "0504", "exp": "1000000", 'bst': [35, 45, 160, 70, 30 ], "Moveset": [157, 89, 90, 120]},
{"name": "DROWZEE", "type": "1818", "exp": "1000000", 'bst': [60, 48, 45, 42, 90 ], "Moveset": [95, 138, 69, 94]},
{"name": "HYPNO", "type": "1818", "exp": "1000000", 'bst': [85, 73, 70, 67, 115], "Moveset": [95, 139, 29, 94]},
{"name": "KRABBY", "type": "1515", "exp": "1000000", 'bst': [30, 105, 90, 50, 25 ], "Moveset": [57, 34, 12, 59]},
{"name": "KINGLER", "type": "1515", "exp": "1000000", 'bst': [55, 130, 115, 75, 50 ], "Moveset": [152, 70, 12, 92]},
{"name": "VOLTORB", "type": "1717", "exp": "1000000", 'bst': [40, 30, 50, 100, 55 ], "Moveset": [85, 86, 36, 115]},
{"name": "ELECTRODE", "type": "1717", "exp": "1000000", 'bst': [60, 50, 70, 140, 80 ], "Moveset": [87, 86, 129, 148]},
{"name": "EXEGGCUTE", "type": "1618", "exp": "1250000", 'bst': [60, 40, 80, 40, 60 ], "Moveset": [73, 92, 94, 120]},
{"name": "EXEGGUTOR", "type": "1618", "exp": "1250000", 'bst': [95, 95, 85, 55, 125], "Moveset": [23, 79, 94, 76]},
{"name": "CUBONE", "type": "0404", "exp": "1000000", 'bst': [50, 50, 95, 35, 40 ], "Moveset": [155, 59, 37, 116]},
{"name": "MAROWAK", "type": "0404", "exp": "1000000", 'bst': [60, 80, 110, 45, 50 ], "Moveset": [125, 29, 37, 116]},
{"name": "HITMONLEE", "type": "0101", "exp": "1000000", 'bst': [50, 120, 53, 87, 35 ], "Moveset": [27, 26, 136, 116]},
{"name": "HITMONCHAN", "type": "0101", "exp": "1000000", 'bst': [50, 105, 79, 76, 35 ], "Moveset": [5, 7, 8, 9]},
{"name": "LICKITUNG", "type": "0000", "exp": "1000000", 'bst': [90, 55, 75, 30, 60 ], "Moveset": [34, 87, 89, 59]},
{"name": "KOFFING", "type": "0303", "exp": "1000000", 'bst': [40, 65, 95, 35, 60 ], "Moveset": [124, 87, 114, 92]},
{"name": "WEEZING", "type": "0303", "exp": "1000000", 'bst': [65, 90, 120, 60, 85 ], "Moveset": [124, 87, 114, 102]},
{"name": "RHYHORN", "type": "0405", "exp": "1250000", 'bst': [80, 85, 95, 25, 30 ], "Moveset": [34, 89, 157, 90]},
{"name": "RHYDON", "type": "0405", "exp": "1250000", 'bst': [105, 130, 120, 40, 45 ], "Moveset": [30, 89, 87, 90]},
{"name": "CHANSEY", "type": "0000", "exp": "800000", 'bst': [250, 5, 5, 50, 105], "Moveset": [121, 156, 118, 69]},
{"name": "TANGELA", "type": "1616", "exp": "1000000", 'bst': [65, 55, 115, 60, 100], "Moveset": [72, 76, 74, 78]},
{"name": "KANGASKHAN", "type": "0000", "exp": "1000000", 'bst': [105, 95, 80, 90, 40 ], "Moveset": [146, 157, 57, 164]},
{"name": "HORSEA", "type": "1515", "exp": "1000000", 'bst': [30, 40, 70, 60, 70 ], "Moveset": [56, 58, 92, 108]},
{"name": "SEADRA", "type": "1515", "exp": "1000000", 'bst': [55, 65, 95, 85, 95 ], "Moveset": [57, 38, 92, 108]},
{"name": "GOLDEEN", "type": "1515", "exp": "1000000", 'bst': [45, 67, 60, 63, 50 ], "Moveset": [57, 32, 104, 97]},
{"name": "SEAKING", "type": "1515", "exp": "1000000", 'bst': [80, 92, 65, 68, 80 ], "Moveset": [127, 32, 48, 31]},
{"name": "STARYU", "type": "1515", "exp": "1250000", 'bst': [30, 45, 55, 85, 70 ], "Moveset": [57, 94, 107, 105]},
{"name": "STARMIE", "type": "1518", "exp": "1250000", 'bst': [60, 75, 85, 115, 100], "Moveset": [61, 87, 107, 129]},
{"name": "MR. MIME", "type": "1818", "exp": "1000000", 'bst': [40, 45, 65, 90, 100], "Moveset": [112, 113, 94, 63]},
{"name": "SCYTHER", "type": "0702", "exp": "1000000", 'bst': [70, 110, 80, 105, 55 ], "Moveset": [116, 63, 129, 104]},
{"name": "JYNX", "type": "1918", "exp": "1000000", 'bst': [65, 50, 35, 95, 95 ], "Moveset": [142, 34, 8, 94]},
{"name": "ELECTABUZZ", "type": "1717", "exp": "1000000", 'bst': [65, 83, 57, 105, 85 ], "Moveset": [9, 86, 118, 115]},
{"name": "MAGMAR", "type": "1414", "exp": "1000000", 'bst': [65, 95, 57, 93, 85 ], "Moveset": [7, 5, 109, 94]},
{"name": "PINSIR", "type": "0707", "exp": "1250000", 'bst': [65, 125, 100, 85, 55 ], "Moveset": [163, 12, 69, 92]},
{"name": "TAUROS", "type": "0000", "exp": "1250000", 'bst': [75, 100, 95, 110, 70 ], "Moveset": [23, 130, 117, 126]},
{"name": "MAGIKARP", "type": "1515", "exp": "1250000", 'bst': [20, 10, 55, 80, 20 ], "Moveset": [150, 33, 0, 0]},
{"name": "GYARADOS", "type": "1502", "exp": "1250000", 'bst': [95, 125, 79, 81, 100], "Moveset": [61, 44, 126, 43]},
{"name": "LAPRAS", "type": "1519", "exp": "1250000", 'bst': [130, 85, 80, 60, 95 ], "Moveset": [61, 54, 47, 58]},
{"name": "DITTO", "type": "0000", "exp": "1000000", 'bst': [48, 48, 48, 48, 48 ], "Moveset": [144, 0, 0, 0]},
{"name": "EEVEE", "type": "0000", "exp": "1000000", 'bst': [55, 55, 50, 55, 65 ], "Moveset": [38, 116, 28, 98]},
{"name": "VAPOREON", "type": "1515", "exp": "1000000", 'bst': [130, 65, 60, 65, 110], "Moveset": [56, 151, 114, 98]},
{"name": "JOLTEON", "type": "1717", "exp": "1000000", 'bst': [65, 65, 60, 130, 110], "Moveset": [87, 42, 28, 98]},
{"name": "FLAREON", "type": "1414", "exp": "1000000", 'bst': [65, 130, 60, 65, 110], "Moveset": [126, 123, 28, 98]},
{"name": "PORYGON", "type": "0000", "exp": "1000000", 'bst': [65, 60, 70, 40, 75 ], "Moveset": [60, 161, 160, 105]},
{"name": "OMANYTE", "type": "0515", "exp": "1000000", 'bst': [35, 40, 100, 35, 90 ], "Moveset": [56, 34, 58, 92]},
{"name": "OMASTAR", "type": "0515", "exp": "1000000", 'bst': [70, 60, 125, 55, 115], "Moveset": [57, 131, 32, 92]},
{"name": "KABUTO", "type": "0515", "exp": "1000000", 'bst': [30, 80, 90, 55, 45 ], "Moveset": [57, 59, 163, 104]},
{"name": "KABUTOPS", "type": "0515", "exp": "1000000", 'bst': [60, 115, 105, 80, 70 ], "Moveset": [56, 25, 58, 14]},
{"name": "AERODACTYL", "type": "0502", "exp": "1250000", 'bst': [80, 105, 65, 130, 60 ], "Moveset": [44, 48, 19, 126]},
{"name": "SNORLAX", "type": "0000", "exp": "1250000", 'bst': [160, 110, 65, 30, 65 ], "Moveset": [36, 118, 156, 117]},
{"name": "ARTICUNO", "type": "1902", "exp": "1250000", 'bst': [90, 85, 100, 85, 125], "Moveset": [58, 143, 54, 97]},
{"name": "ZAPDOS", "type": "1702", "exp": "1250000", 'bst': [90, 90, 85, 100, 125], "Moveset": [87, 143, 117, 148]},
{"name": "MOLTRES", "type": "1402", "exp": "1250000", 'bst': [90, 100, 90, 90, 125], "Moveset": [126, 143, 97, 115]},
{"name": "DRATINI", "type": "1A1A", "exp": "1250000", 'bst': [41, 64, 45, 50, 50 ], "Moveset": [59, 85, 34, 126]},
{"name": "DRAGONAIR", "type": "1A1A", "exp": "1250000", 'bst': [61, 84, 65, 70, 70 ], "Moveset": [85, 34, 58, 126]},
{"name": "DRAGONITE", "type": "1A02", "exp": "1250000", 'bst': [91, 134, 95, 80, 100], "Moveset": [87, 35, 21, 126]},
]
petit_cup_list = [
{"name": "BULBASAUR", "type": "1603", "exp": "11735", 'bst': [45, 49, 49, 45, 65 ], "DexNum": 1, "Moveset": [73, 72, 76, 15]},
{"name": "CHARMANDER", "type": "1414", "exp": "11735", 'bst': [39, 52, 43, 65, 50 ], "DexNum": 4, "Moveset": [126, 99, 45, 5]},
{"name": "SQUIRTLE", "type": "1515", "exp": "11735", 'bst': [44, 48, 65, 43, 50 ], "DexNum": 7, "Moveset": [44, 61, 92, 66]},
{"name": "CATERPIE", "type": "0707", "exp": "15625", 'bst': [45, 30, 35, 45, 20 ], "DexNum": 10, "Moveset": [33, 81, 0, 0]},
{"name": "WEEDLE", "type": "0703", "exp": "15625", 'bst': [40, 35, 30, 50, 20 ], "DexNum": 13, "Moveset": [40, 81, 0, 0]},
{"name": "PIDGEY", "type": "0002", "exp": "11735", 'bst': [40, 45, 40, 56, 35 ], "DexNum": 16, "Moveset": [28, 98, 19, 38]},
{"name": "RATTATA", "type": "0000", "exp": "15625", 'bst': [30, 56, 35, 72, 25 ], "DexNum": 19, "Moveset": [98, 158, 61, 91]},
{"name": "SPEAROW", "type": "0002", "exp": "15625", 'bst': [40, 60, 30, 70, 31 ], "DexNum": 21, "Moveset": [38, 119, 19, 92]},
{"name": "EKANS", "type": "0303", "exp": "15625", 'bst': [35, 60, 44, 55, 40 ], "DexNum": 23, "Moveset": [44, 137, 91, 72]},
{"name": "PIKACHU", "type": "1717", "exp": "15625", 'bst': [35, 55, 30, 90, 50 ], "DexNum": 25, "Moveset": [86, 21, 87, 148]},
{"name": "SANDSHREW", "type": "0404", "exp": "15625", 'bst': [50, 75, 85, 40, 30 ], "DexNum": 27, "Moveset": [163, 40, 91, 157]},
{"name": "NIDORAN", "type": "0303", "exp": "11735", 'bst': [55, 47, 52, 41, 40 ], "DexNum": 29, "Moveset": [24, 59, 36, 92]},
{"name": "NIDORAN", "type": "0303", "exp": "11735", 'bst': [46, 57, 40, 50, 40 ], "DexNum": 32, "Moveset": [24, 32, 34, 92]},
{"name": "CLEFAIRY", "type": "0000", "exp": "12500", 'bst': [70, 45, 48, 35, 60 ], "DexNum": 35, "Moveset": [47, 126, 161, 118]},
{"name": "VULPIX", "type": "1414", "exp": "15625", 'bst': [38, 41, 40, 65, 65 ], "DexNum": 37, "Moveset": [92, 38, 91, 52]},
{"name": "JIGGLYPUFF", "type": "0000", "exp": "12500", 'bst': [115, 45, 20, 20, 25 ], "DexNum": 39,"Moveset": [47, 94, 36, 66] },
{"name": "ZUBAT", "type": "0302", "exp": "15625", 'bst': [40, 45, 35, 55, 40 ], "DexNum": 41, "Moveset": [109, 38, 92, 72]},
{"name": "ODDISH", "type": "1603", "exp": "11735", 'bst': [45, 50, 55, 30, 75 ], "DexNum": 43, "Moveset": [51, 79, 76, 15]},
{"name": "PARAS", "type": "0716", "exp": "15625", 'bst': [35, 70, 55, 25, 55 ], "DexNum": 46, "Moveset": [78, 72, 141, 91]},
{"name": "DIGLETT", "type": "0404", "exp": "15625", 'bst': [10, 55, 25, 95, 45 ], "DexNum": 50, "Moveset": [91, 28, 15, 157]},
{"name": "MEOWTH", "type": "0000", "exp": "15625", 'bst': [40, 45, 35, 90, 40 ], "DexNum": 52, "Moveset": [44, 103, 61, 85]},
{"name": "PSYDUCK", "type": "1515", "exp": "15625", 'bst': [50, 52, 48, 55, 50 ], "DexNum": 54, "Moveset": [61, 102, 5, 66]},
{"name": "GROWLITHE", "type": "1414", "exp": "19531", 'bst': [55, 70, 45, 60, 50 ], "DexNum": 58, "Moveset": [126, 44, 102, 43]},
{"name": "POLIWAG", "type": "1515", "exp": "11735", 'bst': [40, 50, 40, 90, 40 ], "DexNum": 60, "Moveset": [95, 130, 149, 57]},
{"name": "ABRA", "type": "1818", "exp": "11735", 'bst': [25, 20, 15, 90, 105], "DexNum": 63, "Moveset": [118, 149, 34, 86]},
{"name": "MACHOP", "type": "0101", "exp": "11735", 'bst': [70, 80, 50, 35, 35 ], "DexNum": 66, "Moveset": [2, 67, 69, 126]},
{"name": "BELLSPROUT", "type": "1603", "exp": "11735", 'bst': [50, 75, 35, 40, 70 ], "DexNum": 69, "Moveset": [35, 72, 74, 77]},
{"name": "GEODUDE", "type": "0504", "exp": "11735", 'bst': [40, 80, 100, 20, 30 ], "DexNum": 74, "Moveset": [88, 120, 91, 70]},
{"name": "MAGNEMITE", "type": "1717", "exp": "15625", 'bst': [25, 35, 70, 45, 95 ], "DexNum": 81, "Moveset": [148, 129, 86, 87]},
{"name": "FARFETCH'D", "type": "0002", "exp": "15625", 'bst': [52, 65, 55, 60, 58 ], "DexNum": 83, "Moveset": [31, 14, 28, 19]},
{"name": "SHELLDER", "type": "1515", "exp": "19531", 'bst': [30, 65, 100, 40, 45 ], "DexNum": 90, "Moveset": [48, 128, 58, 120]},
{"name": "GASTLY", "type": "0803", "exp": "11735", 'bst': [30, 35, 30, 80, 100], "DexNum": 92, "Moveset": [109, 101, 87, 72]},
{"name": "KRABBY", "type": "1515", "exp": "15625", 'bst': [30, 105, 90, 50, 25 ], "DexNum": 98, "Moveset": [12, 57, 14, 70]},
{"name": "VOLTORB", "type": "1717", "exp": "15625", 'bst': [40, 30, 50, 100, 55 ], "DexNum": 100, "Moveset": [103, 86, 87, 36]},
{"name": "EXEGGCUTE", "type": "1618", "exp": "19531", 'bst': [60, 40, 80, 40, 60 ], "DexNum": 102, "Moveset": [95, 149, 121, 115]},
{"name": "CUBONE", "type": "0404", "exp": "15625", 'bst': [50, 50, 95, 35, 40 ], "DexNum": 104, "Moveset": [125, 39, 126, 29]},
{"name": "KOFFING", "type": "0303", "exp": "15625", 'bst': [40, 65, 95, 35, 60 ], "DexNum": 109, "Moveset": [123, 92, 126, 85]},
{"name": "HORSEA", "type": "1515", "exp": "15625", 'bst': [30, 40, 70, 60, 70 ], "DexNum": 116, "Moveset": [108, 61, 129, 58]},
{"name": "GOLDEEN", "type": "1515", "exp": "15625", 'bst': [45, 67, 60, 63, 50 ], "DexNum": 118, "Moveset": [48, 30, 57, 32]},
{"name": "MAGIKARP", "type": "1515", "exp": "19531", 'bst': [20, 10, 55, 80, 20 ], "DexNum": 129, "Moveset": [150, 33, 0, 0]},
{"name": "DITTO", "type": "0000", "exp": "15625", 'bst': [48, 48, 48, 48, 48 ], "DexNum": 132, "Moveset": [144, 0, 0, 0]},
{"name": "EEVEE", "type": "0000", "exp": "15625", 'bst': [55, 55, 50, 55, 65 ], "DexNum": 133, "Moveset": [28, 98, 38, 164]},
{"name": "OMANYTE", "type": "0515", "exp": "15625", 'bst': [35, 40, 100, 35, 90 ], "DexNum": 138, "Moveset": [110, 61, 38, 92]},
{"name": "KABUTO", "type": "0515", "exp": "15625", 'bst': [30, 80, 90, 55, 45 ], "DexNum": 140, "Moveset": [58, 36, 57, 117]},
{"name": "DRATINI", "type": "1A1A", "exp": "19531", 'bst': [41, 64, 45, 50, 50 ], "DexNum": 147, "Moveset": [86, 35, 87, 126]},
]
pika_cup_list = [
{"name": "BULBASAUR", "type": "1603", "exp": "2035", 'bst': [45, 49, 49, 45, 65 ], "DexNum": 1, "Moveset": [73, 92, 72, 38]},
{"name": "IVYSAUR", "type": "1603", "exp": "2035", 'bst': [60, 62, 63, 60, 80 ], "DexNum": 2, "Moveset": [14, 34, 76, 73]},
{"name": "CHARMANDER", "type": "1414", "exp": "2035", 'bst': [39, 52, 43, 65, 50 ], "DexNum": 4, "Moveset": [126, 69, 70, 45]},
{"name": "CHARMELEON", "type": "1414", "exp": "2035", 'bst': [58, 64, 58, 80, 65 ], "DexNum": 5, "Moveset": [14, 25, 92, 52]},
{"name": "SQUIRTLE", "type": "1515", "exp": "2035", 'bst': [44, 48, 65, 43, 50 ], "DexNum": 7, "Moveset": [33, 91, 57, 59]},
{"name": "WARTORTLE", "type": "1515", "exp": "2035", 'bst': [59, 63, 80, 58, 65 ], "DexNum": 8, "Moveset": [34, 117, 57, 115]},
{"name": "CATERPIE", "type": "0707", "exp": "3375", 'bst': [45, 30, 35, 45, 20 ], "DexNum": 10, "Moveset": [81, 33, 0, 0]},
{"name": "METAPOD", "type": "0707", "exp": "3375", 'bst': [50, 20, 55, 30, 25 ], "DexNum": 11, "Moveset": [33, 81, 0, 0]},
{"name": "BUTTERFREE", "type": "0702", "exp": "3375", 'bst': [60, 45, 50, 70, 80 ], "DexNum": 12, "Moveset": [77, 63, 149, 36]},
{"name": "WEEDLE", "type": "0703", "exp": "3375", 'bst': [40, 35, 30, 50, 20 ], "DexNum": 13, "Moveset": [81, 40, 0, 0]},
{"name": "KAKUNA", "type": "0703", "exp": "3375", 'bst': [45, 25, 50, 35, 25 ], "DexNum": 14, "Moveset": [81, 40, 0, 0]},
{"name": "BEEDRILL", "type": "0703", "exp": "3375", 'bst': [65, 80, 40, 75, 45 ], "DexNum": 15, "Moveset": [31, 104, 14, 63]},
{"name": "PIDGEY", "type": "0002", "exp": "2035", 'bst': [40, 45, 40, 56, 35 ], "DexNum": 16, "Moveset": [115, 19, 92, 38]},
{"name": "PIDGEOTTO", "type": "0002", "exp": "2035", 'bst': [63, 60, 55, 71, 50 ], "DexNum": 17, "Moveset": [143, 36, 98, 28]},
{"name": "RATTATA", "type": "0000", "exp": "3375", 'bst': [30, 56, 35, 72, 25 ], "DexNum": 19, "Moveset": [87, 98, 59, 91]},
{"name": "RATICATE", "type": "0000", "exp": "3375", 'bst': [55, 81, 60, 97, 50 ], "DexNum": 20, "Moveset": [158, 92, 58, 129]},
{"name": "SPEAROW", "type": "0002", "exp": "3375", 'bst': [40, 60, 30, 70, 31 ], "DexNum": 21, "Moveset": [38, 104, 19, 102]},
{"name": "FEAROW", "type": "0002", "exp": "3375", 'bst': [65, 90, 65, 100, 61 ], "DexNum": 22, "Moveset": [19, 104, 64, 102]},
{"name": "EKANS", "type": "0303", "exp": "3375", 'bst': [35, 60, 44, 55, 40 ], "DexNum": 23, "Moveset": [35, 40, 89, 43]},
{"name": "PIKACHU", "type": "1717", "exp": "3375", 'bst': [35, 55, 30, 90, 50 ], "DexNum": 25, "Moveset": [98, 66, 85, 86]},
{"name": "RAICHU", "type": "1717", "exp": "3375", 'bst': [60, 90, 55, 100, 90 ], "DexNum": 26, "Moveset": [87, 86, 69, 45]},
{"name": "SANDSHREW", "type": "0404", "exp": "3375", 'bst': [50, 75, 85, 40, 30 ], "DexNum": 27, "Moveset": [28, 89, 66, 14]},
{"name": "NIDORAN", "type": "0303", "exp": "2035", 'bst': [55, 47, 52, 41, 40 ], "DexNum": 29, "Moveset": [92, 87, 59, 34]},
{"name": "NIDORINA", "type": "0303", "exp": "2035", 'bst': [70, 62, 67, 56, 55 ], "DexNum": 30, "Moveset": [92, 58, 36, 32]},
{"name": "NIDOQUEEN", "type": "0304", "exp": "2035", 'bst': [90, 82, 87, 76, 75 ], "DexNum": 31, "Moveset": [90, 24, 57, 115]},
{"name": "NIDORAN", "type": "0303", "exp": "2035", 'bst': [46, 57, 40, 50, 40 ], "DexNum": 32, "Moveset": [59, 85, 34, 92]},
{"name": "NIDORINO", "type": "0303", "exp": "2035", 'bst': [61, 72, 57, 65, 55 ], "DexNum": 33, "Moveset": [32, 58, 24, 30]},
{"name": "NIDOKING", "type": "0304", "exp": "2035", 'bst': [81, 92, 77, 85, 75 ], "DexNum": 34, "Moveset": [40, 89, 61, 24]},
{"name": "CLEFAIRY", "type": "0000", "exp": "2700", 'bst': [70, 45, 48, 35, 60 ], "DexNum": 35, "Moveset": [86, 161, 94, 118]},
{"name": "CLEFABLE", "type": "0000", "exp": "2700", 'bst': [95, 70, 73, 60, 85 ], "DexNum": 36, "Moveset": [118, 161, 47, 104]},
{"name": "VULPIX", "type": "1414", "exp": "3375", 'bst': [38, 41, 40, 65, 65 ], "DexNum": 37, "Moveset": [38, 126, 91, 104]},
{"name": "NINETALES", "type": "1414", "exp": "3375", 'bst': [73, 76, 75, 100, 100], "DexNum": 38, "Moveset": [91, 52, 63, 115]},
{"name": "JIGGLYPUFF", "type": "0000", "exp": "2700", 'bst': [115, 45, 20, 20, 25 ], "DexNum": 39, "Moveset": [47, 34, 86, 58]},
{"name": "WIGGLYTUFF", "type": "0000", "exp": "2700", 'bst': [140, 70, 45, 45, 50 ], "DexNum": 40, "Moveset": [87, 5, 47, 104]},
{"name": "ZUBAT", "type": "0302", "exp": "3375", 'bst': [40, 45, 35, 55, 40 ], "DexNum": 41, "Moveset": [48, 129, 72, 92]},
{"name": "ODDISH", "type": "1603", "exp": "2035", 'bst': [45, 50, 55, 30, 75 ], "DexNum": 43, "Moveset": [92, 14, 72, 36]},
{"name": "PARAS", "type": "0716", "exp": "3375", 'bst': [35, 70, 55, 25, 55 ], "DexNum": 46, "Moveset": [78, 91, 72, 36]},
{"name": "VENONAT", "type": "0703", "exp": "3375", 'bst': [60, 55, 50, 45, 40 ], "DexNum": 48, "Moveset": [48, 94, 148, 38]},
{"name": "DIGLETT", "type": "0404", "exp": "3375", 'bst': [10, 55, 25, 95, 45 ], "DexNum": 50, "Moveset": [89, 104, 36, 90]},
{"name": "MEOWTH", "type": "0000", "exp": "3375", 'bst': [40, 45, 35, 90, 40 ], "DexNum": 52, "Moveset": [104, 85, 34, 156]},
{"name": "PSYDUCK", "type": "1515", "exp": "3375", 'bst': [50, 52, 48, 55, 50 ], "DexNum": 54, "Moveset": [61, 59, 91, 102]},
{"name": "MANKEY", "type": "0101", "exp": "3375", 'bst': [40, 80, 35, 70, 35 ], "DexNum": 56, "Moveset": [67, 2, 91, 68]},
{"name": "GROWLITHE", "type": "1414", "exp": "4218", 'bst': [55, 70, 45, 60, 50 ], "DexNum": 58, "Moveset": [91, 126, 38, 115]},
{"name": "ARCANINE", "type": "1414", "exp": "4218", 'bst': [90, 110, 80, 95, 80 ], "DexNum": 59, "Moveset": [91, 44, 52, 104]},
{"name": "POLIWAG", "type": "1515", "exp": "2035", 'bst': [40, 50, 40, 90, 40 ], "DexNum": 60, "Moveset": [57, 34, 59, 92]},
{"name": "POLIWHIRL", "type": "1515", "exp": "2035", 'bst': [65, 65, 65, 90, 50 ], "DexNum": 61, "Moveset": [57, 38, 118, 89]},
{"name": "POLIWRATH", "type": "1501", "exp": "2035", 'bst': [90, 85, 95, 70, 70 ], "DexNum": 62, "Moveset": [57, 3, 118, 95]},
{"name": "ABRA", "type": "1818", "exp": "2035", 'bst': [25, 20, 15, 90, 105], "DexNum": 63, "Moveset": [94, 86, 69, 115]},
{"name": "KADABRA", "type": "1818", "exp": "2035", 'bst': [40, 35, 30, 105, 120], "DexNum": 64, "Moveset": [94, 118, 104, 69]},
{"name": "ALAKAZAM", "type": "1818", "exp": "2035", 'bst': [55, 50, 45, 120, 135], "DexNum": 65, "Moveset": [149, 118, 86, 5]},
{"name": "MACHOP", "type": "0101", "exp": "2035", 'bst': [70, 80, 50, 35, 35 ], "DexNum": 66, "Moveset": [2, 66, 126, 117]},
{"name": "BELLSPROUT", "type": "1603", "exp": "2035", 'bst': [50, 75, 35, 40, 70 ], "DexNum": 69, "Moveset": [74, 36, 72, 115]},
{"name": "TENTACOOL", "type": "1503", "exp": "4218", 'bst': [40, 40, 35, 70, 100], "DexNum": 72, "Moveset": [57, 51, 48, 92]},
{"name": "TENTACRUEL", "type": "1503", "exp": "4218", 'bst': [80, 70, 65, 100, 120], "DexNum": 73, "Moveset": [48, 35, 92, 72]},
{"name": "GEODUDE", "type": "0504", "exp": "2035", 'bst': [40, 80, 100, 20, 30 ], "DexNum": 74, "Moveset": [5, 89, 157, 111]},
{"name": "PONYTA", "type": "1414", "exp": "3375", 'bst': [50, 85, 55, 90, 65 ], "DexNum": 77, "Moveset": [126, 32, 115, 129]},
{"name": "SLOWPOKE", "type": "1518", "exp": "3375", 'bst': [90, 65, 65, 15, 40 ], "DexNum": 79, "Moveset": [94, 57, 148, 91]},
{"name": "MAGNEMITE", "type": "1717", "exp": "3375", 'bst': [25, 35, 70, 45, 95 ], "DexNum": 81, "Moveset": [86, 85, 129, 164]},
{"name": "FARFETCH'D", "type": "0002", "exp": "3375", 'bst': [52, 65, 55, 60, 58 ], "DexNum": 83, "Moveset": [28, 31, 19, 115]},
{"name": "SEEL", "type": "1515", "exp": "3375", 'bst': [65, 45, 55, 45, 70 ], "DexNum": 86, "Moveset": [57, 29, 32, 59]},
{"name": "SHELLDER", "type": "1515", "exp": "4218", 'bst': [30, 65, 100, 40, 45 ], "DexNum": 90, "Moveset": [59, 161, 153, 57]},
{"name": "CLOYSTER", "type": "1519", "exp": "4218", 'bst': [50, 95, 180, 70, 85 ], "DexNum": 91, "Moveset": [48, 128, 63, 62]},
{"name": "GASTLY", "type": "0803", "exp": "2035", 'bst': [30, 35, 30, 80, 100], "DexNum": 92, "Moveset": [109, 94, 101, 153]},
{"name": "HAUNTER", "type": "0803", "exp": "2035", 'bst': [45, 50, 45, 95, 115], "DexNum": 93, "Moveset": [109, 85, 101, 120]},
{"name": "GENGAR", "type": "0803", "exp": "2035", 'bst': [60, 65, 60, 110, 130], "DexNum": 94, "Moveset": [109, 101, 72, 118]},
{"name": "ONIX", "type": "0504", "exp": "3375", 'bst': [35, 45, 160, 70, 30 ], "DexNum": 95, "Moveset": [157, 70, 89, 120]},
{"name": "DROWZEE", "type": "1818", "exp": "3375", 'bst': [60, 48, 45, 42, 90 ], "DexNum": 96, "Moveset": [95, 94, 138, 161]},
{"name": "KRABBY", "type": "1515", "exp": "3375", 'bst': [30, 105, 90, 50, 25 ], "DexNum": 98, "Moveset": [58, 34, 57, 92]},
{"name": "KINGLER", "type": "1515", "exp": "3375", 'bst': [55, 130, 115, 75, 50 ], "DexNum": 99, "Moveset": [57, 70, 104, 102]},
{"name": "VOLTORB", "type": "1717", "exp": "3375", 'bst': [40, 30, 50, 100, 55 ], "DexNum": 100, "Moveset": [153, 36, 85, 86]},
{"name": "EXEGGCUTE", "type": "1618", "exp": "4218", 'bst': [60, 40, 80, 40, 60 ], "DexNum": 102, "Moveset": [94, 104, 121, 92]},
{"name": "EXEGGUTOR", "type": "1618", "exp": "4218", 'bst': [95, 95, 85, 55, 125], "DexNum": 103, "Moveset": [92, 140, 72, 149]},
{"name": "CUBONE", "type": "0404", "exp": "3375", 'bst': [50, 50, 95, 35, 40 ], "DexNum": 104, "Moveset": [70, 89, 39, 59]},
{"name": "LICKITUNG", "type": "0000", "exp": "3375", 'bst': [90, 55, 75, 30, 60 ], "DexNum": 108, "Moveset": [38, 48, 126, 87]},
{"name": "KOFFING", "type": "0303", "exp": "3375", 'bst': [40, 65, 95, 35, 60 ], "DexNum": 109, "Moveset": [126, 92, 85, 120]},
{"name": "RHYHORN", "type": "0405", "exp": "4218", 'bst': [80, 85, 95, 25, 30 ], "DexNum": 111, "Moveset": [157, 89, 30, 164]},
{"name": "CHANSEY", "type": "0000", "exp": "2700", 'bst': [250, 5, 5, 50, 105], "DexNum": 113, "Moveset": [161, 68, 61, 85]},
{"name": "HORSEA", "type": "1515", "exp": "3375", 'bst': [30, 40, 70, 60, 70 ], "DexNum": 116, "Moveset": [57, 59, 92, 129]},
{"name": "SEADRA", "type": "1515", "exp": "3375", 'bst': [55, 65, 95, 85, 95 ], "DexNum": 117, "Moveset": [108, 61, 58, 102]},
{"name": "GOLDEEN", "type": "1515", "exp": "3375", 'bst': [45, 67, 60, 63, 50 ], "DexNum": 118, "Moveset": [57, 38, 58, 32]},
{"name": "STARYU", "type": "1515", "exp": "4218", 'bst': [30, 45, 55, 85, 70 ], "DexNum": 120, "Moveset": [57, 94, 161, 86]},
{"name": "STARMIE", "type": "1518", "exp": "4218", 'bst': [60, 75, 85, 115, 100], "DexNum": 121, "Moveset": [149, 61, 87, 164]},
{"name": "MR. MIME", "type": "1818", "exp": "3375", 'bst': [40, 45, 65, 90, 100], "DexNum": 122, "Moveset": [25, 94, 112, 118]},
{"name": "SCYTHER", "type": "0702", "exp": "3375", 'bst': [70, 110, 80, 105, 55 ], "DexNum": 123, "Moveset": [98, 14, 63, 104]},
{"name": "PINSIR", "type": "0707", "exp": "4218", 'bst': [65, 125, 100, 85, 55 ], "DexNum": 127, "Moveset": [36, 66, 117, 102]},
{"name": "MAGIKARP", "type": "1515", "exp": "4218", 'bst': [20, 10, 55, 80, 20 ], "DexNum": 129, "Moveset": [150, 33, 0, 0]},
{"name": "GYARADOS", "type": "1502", "exp": "4218", 'bst': [95, 125, 79, 81, 100], "DexNum": 130, "Moveset": [56, 44, 156, 43]},
{"name": "LAPRAS", "type": "1519", "exp": "4218", 'bst': [130, 85, 80, 60, 95 ], "DexNum": 131, "Moveset": [61, 58, 45, 130]},
{"name": "DITTO", "type": "0000", "exp": "3375", 'bst': [48, 48, 48, 48, 48 ], "DexNum": 132, "Moveset": [144, 0, 0, 0]},
{"name": "PORYGON", "type": "0000", "exp": "3375", 'bst': [65, 60, 70, 40, 75 ], "DexNum": 137, "Moveset": [160, 159, 161, 94]},
{"name": "DRATINI", "type": "1A1A", "exp": "4218", 'bst': [41, 64, 45, 50, 50 ], "DexNum": 147, "Moveset": [126, 59, 34, 86]},
]
kanto_attack_dict = {
"PHY1": [34, 89, 163],
"PHY2": [38, 63, 65, 70, 136, 155, 161],
"PHY3": [23, 24, 25, 26, 29, 30, 36, 37, 44, 66, 120, 124, 146, 153, 157, 158],
"PHY4": [2, 4, 5, 11, 15, 21, 27, 41, 67, 69, 91, 101, 121, 125, 129, 131, 143, 154, 162],
"PHY5": [1, 3, 6, 10, 16, 17, 20, 31, 33, 35, 42, 51, 64, 88, 98, 117, 130, 140],
"PHY6": [13, 19, 40, 49, 99, 122, 123, 132, 141, 68],
"PHY7": [68],
"SPE1": [53, 57, 58, 85, 94, 59],
"SPE2": [87, 126, 7, 8, 9],
"SPE3": [56, 127, 128, 152],
"SPE4": [60, 61, 62, 75, 76, 80, 93],
"SPE5": [138, 149, 55, 72, 83, 84],
"SPE6": [52, 145],
"SPE7": [22, 71, 82],
"STA1": [86, 79, 142],
"STA2": [95, 78, 109, 137],
"STA3": [47, 97, 133, 156],
"STA4": [14, 28, 48, 74, 77, 92, 103, 104, 105, 107, 108, 112, 113, 114, 115, 116, 134, 135, 139, 151, 164],
"STA5": [12, 32, 39, 43, 45, 50, 54, 81, 90, 96, 106, 110, 111, 148, 159],
"STA6": [73, 102, 118, 119, 144, 160],
"STA7": [18, 46, 100, 150],
"NORMAL": [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 15, 16, 20, 21, 23, 25, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 49, 63, 70, 98, 99, 117, 120, 121, 129, 130, 131, 132, 140, 146, 153, 154, 158, 161, 162, 163],
"FIGHTING": [24, 26, 27, 66, 67, 68, 69, 136],
"FLYING": [17, 19, 64, 65, 143],
"POISON": [40, 51, 123, 124],
"GROUND": [89, 90, 91, 125, 155],
"ROCK": [88, 157],
"BUG": [41, 42, 141],
"GHOST": [101, 122],
"FIRE": [7, 52, 53, 83, 126],
"WATER": [55, 56, 57, 61, 127, 128, 145, 152],
"GRASS": [22, 71, 72, 75, 76, 80],
"ELECTRIC": [9, 84, 85, 87],
"PSYCHIC": [60, 93, 94, 138, 149],
"ICE": [8, 58, 59, 62],
"DRAGON": [82]
}
# random number rolling boundaries for picking a move bucket
# lower BSTs are weighted towards the left, which should give weaker mons better moves on average
# as you go up in BST teir, the weights shift to the right towards a tendancy for weaker moves
stat_distribution_list = [
[18.0, 41.4, 59.3, 74.0, 86.6, 98.0, 100.0],
[14.9, 31.9, 52.9, 69.9, 84.8, 98.0, 100.0],
[13.2, 27.6, 44.6, 65.6, 82.7, 97.0, 100.0],
[12.3, 25.8, 40.9, 58.3, 79.6, 97.0, 100.0],
[11.5, 23.6, 36.8, 52.3, 71.3, 96.0, 100.0],
]
bst_weights = [
[100, 100, 100, 100, 100], # uniform distribution
[200, 150, 80, 50, 20], # weight one side
[180, 80, 80, 80, 80], # heavily weight one stat
[160, 50, 150, 90, 50] # random bursts
]
#We don't need lists for pokeball and greatball cup round 1 since they are all level 50 and level 51 respectively
pokecupr1_ultra_levels = [
[53, 51, 51, 53, 51, 51],
[51, 50, 54, 51, 50, 50],
[50, 51, 50, 54, 54, 51],
[53, 50, 50, 52, 50, 55],
[50, 51, 50, 54, 51, 54],
[51, 51, 51, 51, 51, 53],
[52, 51, 50, 54, 50, 52],
[55, 50, 50, 50, 50, 50]
]
pokecupr1_master_levels = [
[51, 52, 51, 51, 52, 52],
[50, 50, 53, 51, 54, 51],
[51, 54, 51, 50, 50, 53],
[53, 52, 50, 51, 51, 50],
[50, 51, 52, 53, 54, 50],
[51, 53, 50, 52, 52, 50],
[50, 52, 53, 53, 50, 50],
[55, 50, 50, 50, 50, 55]
]
petitcupr1_levels = [
[25, 25, 25, 25, 25, 25],
[25, 26, 26, 26, 25, 25],
[25, 25, 25, 30, 25, 30],
[26, 26, 27, 26, 26, 27],
[26, 26, 27, 26, 27, 27],
[26, 27, 26, 27, 27, 27],
[30, 25, 25, 25, 25, 30],
[25, 25, 25, 25, 30, 30]
]
pikacupr1_levels = [
[16, 15, 15, 15, 15, 15],
[15, 16, 15, 15, 15, 15],
[16, 15, 16, 15, 15, 16],
[16, 17, 16, 16, 16, 15],
[16, 15, 15, 16, 18, 18],
[20, 16, 15, 15, 16, 18],
[20, 20, 15, 15, 15, 15],
[18, 16, 16, 18, 16, 16]
]
@@ -0,0 +1,22 @@
import math
class levelExpCalculator:
@classmethod
def getExpValue(self, lvl, growthRate: str):
expValue = 0
if(growthRate == "slow"):
expValue = 5 * math.pow(lvl, 3) / 4
return expValue
if(growthRate == "mediumslow"):
expValue = ((6/5) * math.pow(lvl, 3)) - (15*(math.pow(lvl, 2))) + (100*lvl) - 140
return expValue
if(growthRate == "mediumfast"):
expValue = math.pow(lvl, 3)
return expValue
if(growthRate == "fast"):
expValue = 4 * math.pow(lvl, 3) / 5
return expValue
else:
print("Invalid growth rate.")
return expValue
@@ -0,0 +1,144 @@
import random
from . import constants
def get_random_move(attack_type, distribution):
key_str = ""
if attack_type not in ['PHY', 'SPE', 'STA']:
key_str = attack_type
else:
roll = random.randrange(1, 100)
if roll <= distribution[0]:
key_str = attack_type + "1"
elif roll <= distribution[1]:
key_str = attack_type + "2"
elif roll <= distribution[2]:
key_str = attack_type + "3"
elif roll <= distribution[3]:
key_str = attack_type + "4"
elif roll > distribution[4]:
key_str = attack_type + "5"
elif roll <= distribution[5]:
key_str = attack_type + "6"
elif roll <= distribution[6]:
key_str = attack_type + "7"
# Spore clause
if attack_type == 'STA' and random.randint(1, 200) == 1:
return 147
return random.choice(constants.kanto_attack_dict[key_str])
def get_type_name(type_num):
if random.random() < 0.5:
type_str = type_num.hex()[0:2].upper()
else:
type_str = type_num.hex()[2:].upper()
if type_str == '01':
return 'FIGHTING'
elif type_str == '02':
return 'FLYING'
elif type_str == '03':
return 'POISON'
elif type_str == '04':
return 'GROUND'
elif type_str == '05':
return 'ROCK'
elif type_str == '07':
return 'BUG'
elif type_str == '08':
return 'GHOST'
elif type_str == '14':
return 'FIRE'
elif type_str == '15':
return 'WATER'
elif type_str == '16':
return 'GRASS'
elif type_str == '17':
return 'ELECTRIC'
elif type_str == '18':
return 'PSYCHIC'
elif type_str == '19':
return 'ICE'
elif type_str == '1A':
return 'DRAGON'
else: # type == '00' or a bad value got in here
return 'NORMAL'
class MovesetGenerator:
@staticmethod
def get_random_moveset(bst_list, rando_factor, pkm_type):
bst = sum(bst_list)
# first type of move is always a damaging move that lines up with higher attacking stat
first_type = "PHY" if bst_list[1] > bst_list[4] else "SPE"
# second move is a STAB damaging move if factor is at least 2
if (rando_factor < 4):
second_type = get_type_name(pkm_type)
else:
one_in_three = random.randrange(1, 99)
if one_in_three <= 33:
second_type = "PHY"
elif one_in_three <= 66:
second_type = "SPE"
else:
second_type = "STA"
# third move afflicts a status or affects stats if factor is at least 3
if (rando_factor < 3):
third_type = "STA"
else:
one_in_three = random.randrange(1, 99)
if one_in_three <= 33:
third_type = "PHY"
elif one_in_three <= 66:
third_type = "SPE"
else:
third_type = "STA"
# fourth move is random
one_in_three = random.randrange(1, 99)
if one_in_three <= 33:
fourth_type = "PHY"
elif one_in_three <= 66:
fourth_type = "SPE"
else:
fourth_type = "STA"
attack_types = [first_type, second_type, third_type, fourth_type]
moveset = []
if (rando_factor == 2):
if bst <= 225:
distribution = constants.stat_distribution_list[0]
elif bst <= 300:
distribution = constants.stat_distribution_list[1]
elif bst <= 375:
distribution = constants.stat_distribution_list[2]
elif bst <= 450:
distribution = constants.stat_distribution_list[3]
else:
distribution = constants.stat_distribution_list[4]
elif (rando_factor == 3):
if bst <= 300:
distribution = constants.stat_distribution_list[random.randrange(0, 1)]
elif bst <= 450:
distribution = constants.stat_distribution_list[random.randrange(2, 3)]
else:
distribution = constants.stat_distribution_list[4]
else:
distribution = constants.stat_distribution_list[random.randrange(0, 4)]
for atk_type in attack_types:
random_move = get_random_move(atk_type, distribution)
while True:
if random_move in moveset:
random_move = get_random_move(atk_type, distribution)
else:
break
moveset.append(random_move)
return moveset
@@ -0,0 +1,57 @@
import random
from . import constants
class BaseValuesRandomizer:
@classmethod
def randomize_stats(cls, vanilla_stats, random_factor):
min_val = 20
max_val = 235
bst_list = []
for stat in vanilla_stats:
bst_list.append(stat)
bst = sum(bst_list)
new_stats_bytes = bytearray()
# Start with an array of 5 numbers, all at the minimum value
new_stats = [min_val] * 5
current_sum = sum(new_stats)
# Increment numbers until we reach BST
while current_sum < bst:
# Randomly select an index to increase
idx = cls.select_index(random_factor)
# Only increase if it won't exceed max_val
if new_stats[idx] < max_val:
new_stats[idx] += 1
current_sum += 1
else:
# Check if all numbers are maxed out (should never happen with correct BST input)
if all(n == max_val for n in new_stats):
raise RuntimeError("All stats reached max_val but BST is not yet met. Something went wrong!")
random.shuffle(new_stats)
for stat in new_stats:
try:
new_stats_bytes.extend(stat.to_bytes(1, "big"))
except OverflowError:
print("ERROR: BST is too high.")
print("BST_STR: " + str(vanilla_stats))
print("BST: " + str(bst))
print("STATS: " + str(new_stats))
exit(1)
return new_stats_bytes
@classmethod
def select_index(cls, random_factor):
random_factor = random_factor - 1
weight_map = {
1: constants.bst_weights[0] if random.random() < 0.5 else constants.bst_weights[1],
2: constants.bst_weights[2],
3: constants.bst_weights[3]
}
return random.choices([0, 1, 2, 3, 4], weights=weight_map.get(random_factor, constants.bst_weights[0]))[0]
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
import math
import random
class Util:
@staticmethod
def calculate_stat(stat, ev, iv, level):
return int((((stat + iv) * 2 + math.floor(math.ceil(math.sqrt(ev)) / 4)) * level)/100) + 5
@staticmethod
def calculate_hp_stat(stat, ev, iv, level):
return int((((stat + iv) * 2 + math.floor(math.ceil(math.sqrt(ev)) / 4)) * level)/100) + level + 10
@staticmethod
def random_int_set(min_val, max_val, count):
return random.sample(range(min_val, max_val), count)
@staticmethod
def random_string_hex(length):
int_set = random.sample(range(0, 15), length)
return_hex = ""
for integer in int_set:
return_hex = return_hex + hex(integer)[2:]
return return_hex
@@ -0,0 +1,23 @@
import math
from . import util
class DisplayDataWriter:
@staticmethod
def write_gym_tower_display(new_display_stats_set, evs, iv_str, lvl):
ivs = [0, 0, 0, 0, 0]
iv_binary = "{0:016b}".format(int(iv_str, 16))
for i in range(0, 4):
int_val = int(iv_binary[i * 4:(i * 4 + 4)], 2)
ivs[i + 1] = int_val
if int_val % 2 != 0:
ivs[0] = int(ivs[0] + math.pow(2, 3 - i))
display_stats = bytearray()
new_displays_int = [int(x) for x in new_display_stats_set]
display = util.Util.calculate_hp_stat(new_displays_int[0], evs[0], ivs[0], lvl)
display_stats.extend(display.to_bytes(2, "big"))
for j in range(1, 5):
display = util.Util.calculate_stat(new_displays_int[j], evs[j], ivs[j], lvl)
display_stats.extend(display.to_bytes(2, "big"))
return display_stats
+11
View File
@@ -0,0 +1,11 @@
# The first thing you should make for your world is an archipelago.json manifest file.
# You can reference APQuest's, but you should change the "game" field (obviously),
# and you should also change the "minimum_ap_version" - probably to the current value of Utils.__version__.
# Apart from the regular apworld code that allows generating multiworld seeds with your game,
# your apworld might have other "components" that should be launchable from the Archipelago Launcher.
# You can ignore this for now. If you are specifically interested in components, you can read components.py.
# The main thing we do in our __init__.py is importing our world class from our world.py to initialize it.
# Obviously, this world class needs to exist first. For this, read world.py.
from .world import Schedule1World as Schedule1World
+1
View File
@@ -0,0 +1 @@
{"game": "Schedule I", "minimum_ap_version": "0.6.6", "world_version": "3.5.4", "authors": ["MacH8s"], "compatible_version": 7, "version": 7}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+250
View File
@@ -0,0 +1,250 @@
{
"Overworld": {
"connections": {
"Welcome to Hyland Point": true,
"Northtown" : true
}
},
"Northtown": {
"connections": {"Westville": {"randomize_level_unlocks&!randomize_customers" : {"has": "Westville Region Unlock"}},
"Weed Recipe Checks": {"randomize_level_unlocks" : {"has": "Mixing Station Unlock"}}}
},
"Westville": {
"connections": {"Downtown": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Westville": 2}}},
"Meth Recipe Checks": {"randomize_level_unlocks" : {"has_any": [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]],
"has_all" : ["Acid Unlock",
"Phosphorus Unlock",
"Chemistry Station Unlock",
"Lab Oven Unlock",
"Warehouse Access"]},
"randomize_suppliers" : {"has": "Shirley Watts Unlocked"},
"randomize_customers&!randomize_suppliers" : {"has_any" : [["Meg Cooley Unlocked", "Jerry Montero Unlocked"]]}}}
},
"Downtown": {
"connections": {"Docks": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Downtown": 7}},
"randomize_level_unlocks" : {"has": "Fertilizer Unlock"}},
"Vibin' on the 'Cybin": {"randomize_level_unlocks" : {"has" : "Warehouse Access"},
"randomize_suppliers" : {"has": "Fungal Phil Unlocked"},
"randomize_customers&!randomize_suppliers" : {"has_any" : [["Elizabeth Homley Unlocked", "Kevin Oakley Unlocked"]]}}}
},
"Docks": {
"connections": {"Suburbia": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Docks": 7}}},
"Cocaine Recipe Checks": {"randomize_level_unlocks" : {"has_any": [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]],
"has_all" : ["Cauldron Unlock", "Lab Oven Unlock", "Gasoline Unlock", "Warehouse Access"]},
"randomize_suppliers" : {"has": "Salvador Moreno Unlocked"},
"randomize_customers&!randomize_suppliers" : {"has_any" : [["Mac Cooper Unlocked", "Javier Pérez Unlocked"]]}}}
},
"Suburbia": {
"connections": {"Uptown": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Suburbia": 7}},
"randomize_level_unlocks" : {"has": "Drying Rack Unlock"}}}
},
"Uptown": {
"connections": {}
},
"Weed Recipe Checks": {
"connections": {}
},
"Meth Recipe Checks": {
"connections": {}
},
"Shrooms Recipe Checks": {
"connections": {}
},
"Cocaine Recipe Checks": {
"connections": {}
},
"Welcome to Hyland Point": {
"connections": {
"Getting Started": true
}
},
"Getting Started": {
"connections": {
"Money Management": true
}
},
"Money Management": {
"connections": {
"Gearing Up|1": true,
"Clean Cash": true
}
},
"Clean Cash": {
"connections": {
}
},
"Gearing Up|1": {
"connections": {
"Gearing Up|2": true,
"Packin'": true,
"Keeping it Fresh": true
}
},
"Keeping it Fresh": {
"connections": {
}
},
"Packin'": {
"connections": {
}
},
"Gearing Up|2": {
"connections": {
"On the Grind|1": true
}
},
"On the Grind|1": {
"connections": {
"Moving Up": true,
"On the Grind|2": true
}
},
"On the Grind|2": {
"connections": {
}
},
"Moving Up": {
"connections": {
"Dodgy Dealing": {"randomize_customers": {"has_from_list":
{"Austin Steiner Unlocked": 10,
"Beth Penn Unlocked": 10,
"Chloe Bowers Unlocked": 10,
"Donna Martin Unlocked": 10,
"Geraldine Poon Unlocked": 10,
"Jessi Waters Unlocked": 10,
"Kathy Henderson Unlocked": 10,
"Kyle Cooley Unlocked": 10,
"Ludwig Meyer Unlocked": 10,
"Mick Lubbin Unlocked": 10,
"Mrs. Ming Unlocked": 10,
"Peggy Myers Unlocked": 10,
"Peter File Unlocked": 10,
"Sam Thompson Unlocked": 10,
"Charles Rowland Unlocked": 10,
"Dean Webster Unlocked": 10,
"Doris Lubbin Unlocked": 10,
"George Greene Unlocked": 10,
"Jerry Montero Unlocked": 10,
"Joyce Ball Unlocked": 10,
"Keith Wagner Unlocked": 10,
"Kim Delaney Unlocked": 10,
"Meg Cooley Unlocked": 10,
"Trent Sherman Unlocked": 10,
"Bruce Norton Unlocked": 10,
"Elizabeth Homley Unlocked": 10,
"Eugene Buckley Unlocked": 10,
"Greg Figgle Unlocked": 10,
"Jeff Gilmore Unlocked": 10,
"Jennifer Rivera Unlocked": 10,
"Kevin Oakley Unlocked": 10,
"Louis Fourier Unlocked": 10,
"Philip Wentworth Unlocked": 10,
"Randy Caulfield Unlocked": 10,
"Lucy Pennington Unlocked": 10,
"Anna Chesterfield Unlocked": 10,
"Billy Kramer Unlocked": 10,
"Cranky Frank Unlocked": 10,
"Genghis Barn Unlocked": 10,
"Javier Pérez Unlocked": 10,
"Kelly Reynolds Unlocked": 10,
"Lisa Gardener Unlocked": 10,
"Mac Cooper Unlocked": 10,
"Marco Barone Unlocked": 10,
"Melissa Wood Unlocked": 10,
"Sherman Giles Unlocked": 10,
"Alison Knight Unlocked": 10,
"Carl Bundy Unlocked": 10,
"Chris Sullivan Unlocked": 10,
"Dennis Kennedy Unlocked": 10,
"Hank Stevenson Unlocked": 10,
"Harold Colt Unlocked": 10,
"Jack Knight Unlocked": 10,
"Jackie Stevenson Unlocked": 10,
"Jeremy Wilkinson Unlocked": 10,
"Karen Kennedy Unlocked": 10,
"Fiona Hancock Unlocked": 10,
"Herbert Bleuball Unlocked": 10,
"Irene Meadows Unlocked": 10,
"Jen Heard Unlocked": 10,
"Lily Turner Unlocked": 10,
"Michael Boog Unlocked": 10,
"Pearl Moore Unlocked": 10,
"Ray Hoffman Unlocked": 10,
"Tobias Wentworth Unlocked": 10,
"Walter Cussler Unlocked": 10}}}
}
},
"Dodgy Dealing": {
"connections": {
"Mixing Mania": {"randomize_customers": {"has_any" : [["Chloe Bowers Unlocked", "Ludwig Meyer Unlocked", "Beth Penn Unlocked"]]}}
}
},
"Mixing Mania": {
"connections": {
"Making the Rounds": {"randomize_level_unlocks" : {"has": "Mixing Station Unlock"}}
}
},
"Making the Rounds": {
"connections": {
"Needin' the Green": true
}
},
"Needin' the Green": {
"connections": {
"Wretched Hive of Scum and Villainy": true
}
},
"Vibin' on the 'Cybin": {
"connections": {
"Shrooms Recipe Checks": {"randomize_level_unlocks": {"has_any": [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]]}}
}
},
"Wretched Hive of Scum and Villainy": {
"connections": {
"We Need To Cook|1": {"randomize_level_unlocks": {"has": "Warehouse Access"}}
}
},
"We Need To Cook|1": {
"connections": {
"We Need To Cook|2": {"randomize_customers": {"has_any": [["Meg Cooley Unlocked", "Jerry Montero Unlocked"]]},
"randomize_suppliers": {"has": "Shirley Watts Unlocked"}}
}
},
"We Need To Cook|2": {
"connections": {
"Unfavourable Agreements": {"randomize_level_unlocks": {"has_all" :["Chemistry Station Unlock",
"Lab Oven Unlock",
"Acid Unlock",
"Phosphorus Unlock"]},
"randomize_customers": {"has_from_list": {
"Charles Rowland Unlocked": 5,
"Dean Webster Unlocked": 5,
"Doris Lubbin Unlocked": 5,
"George Greene Unlocked": 5,
"Jerry Montero Unlocked": 5,
"Joyce Ball Unlocked": 5,
"Kim Delaney Unlocked": 5,
"Meg Cooley Unlocked": 5,
"Trent Sherman Unlocked": 5,
"Keith Wagner Unlocked": 5}}}
}
},
"Unfavourable Agreements": {
"connections": {
"Finishing the Job": true,
"Cartel Influence": true
}
},
"Cartel Influence": {
"connections": {}
},
"Finishing the Job": {
"connections": {
}
}
}
+93
View File
@@ -0,0 +1,93 @@
{
"randomize_customers" : {"has_from_list" : [{"Charles Rowland Unlocked": 5,
"Dean Webster Unlocked": 5,
"Doris Lubbin Unlocked": 5,
"George Greene Unlocked": 5,
"Jerry Montero Unlocked": 5,
"Joyce Ball Unlocked": 5,
"Kim Delaney Unlocked": 5,
"Meg Cooley Unlocked": 5,
"Trent Sherman Unlocked": 5,
"Keith Wagner Unlocked": 5},
{"Austin Steiner Unlocked": 10,
"Beth Penn Unlocked": 10,
"Chloe Bowers Unlocked": 10,
"Donna Martin Unlocked": 10,
"Geraldine Poon Unlocked": 10,
"Jessi Waters Unlocked": 10,
"Kathy Henderson Unlocked": 10,
"Kyle Cooley Unlocked": 10,
"Ludwig Meyer Unlocked": 10,
"Mick Lubbin Unlocked": 10,
"Mrs. Ming Unlocked": 10,
"Peggy Myers Unlocked": 10,
"Peter File Unlocked": 10,
"Sam Thompson Unlocked": 10,
"Charles Rowland Unlocked": 10,
"Dean Webster Unlocked": 10,
"Doris Lubbin Unlocked": 10,
"George Greene Unlocked": 10,
"Jerry Montero Unlocked": 10,
"Joyce Ball Unlocked": 10,
"Keith Wagner Unlocked": 10,
"Kim Delaney Unlocked": 10,
"Meg Cooley Unlocked": 10,
"Trent Sherman Unlocked": 10,
"Bruce Norton Unlocked": 10,
"Elizabeth Homley Unlocked": 10,
"Eugene Buckley Unlocked": 10,
"Greg Figgle Unlocked": 10,
"Jeff Gilmore Unlocked": 10,
"Jennifer Rivera Unlocked": 10,
"Kevin Oakley Unlocked": 10,
"Louis Fourier Unlocked": 10,
"Philip Wentworth Unlocked": 10,
"Randy Caulfield Unlocked": 10,
"Lucy Pennington Unlocked": 10,
"Anna Chesterfield Unlocked": 10,
"Billy Kramer Unlocked": 10,
"Cranky Frank Unlocked": 10,
"Genghis Barn Unlocked": 10,
"Javier Pérez Unlocked": 10,
"Kelly Reynolds Unlocked": 10,
"Lisa Gardener Unlocked": 10,
"Mac Cooper Unlocked": 10,
"Marco Barone Unlocked": 10,
"Melissa Wood Unlocked": 10,
"Sherman Giles Unlocked": 10,
"Alison Knight Unlocked": 10,
"Carl Bundy Unlocked": 10,
"Chris Sullivan Unlocked": 10,
"Dennis Kennedy Unlocked": 10,
"Hank Stevenson Unlocked": 10,
"Harold Colt Unlocked": 10,
"Jack Knight Unlocked": 10,
"Jackie Stevenson Unlocked": 10,
"Jeremy Wilkinson Unlocked": 10,
"Karen Kennedy Unlocked": 10,
"Fiona Hancock Unlocked": 10,
"Herbert Bleuball Unlocked": 10,
"Irene Meadows Unlocked": 10,
"Jen Heard Unlocked": 10,
"Lily Turner Unlocked": 10,
"Michael Boog Unlocked": 10,
"Pearl Moore Unlocked": 10,
"Ray Hoffman Unlocked": 10,
"Tobias Wentworth Unlocked": 10,
"Walter Cussler Unlocked": 10}],
"has_any" : [["Chloe Bowers Unlocked", "Ludwig Meyer Unlocked", "Beth Penn Unlocked"],
["Meg Cooley Unlocked", "Jerry Montero Unlocked"],
["Mac Cooper Unlocked", "Javier Pérez Unlocked"]],
"has_all" : ["Billy Kramer Unlocked", "Sam Thompson Unlocked"]},
"randomize_level_unlocks" : {"has_any" : [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]],
"has_all" : [
"Cauldron Unlock",
"Gasoline Unlock",
"Warehouse Access",
"Chemistry Station Unlock",
"Lab Oven Unlock",
"Acid Unlock",
"Phosphorus Unlock"]},
"randomize_suppliers" : {"has_all": ["Salvador Moreno Unlocked", "Shirley Watts Unlocked"]},
"randomize_cartel_influence" : {"has_all_counts": {"Cartel Influence, Suburbia" : 7}}
}
+38
View File
@@ -0,0 +1,38 @@
# Schedule I
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
- Sends checks from all missions.
- Sends and receives all customer checks and items.
- Customers can be unlocked by receiving them through archipelago when randomize_customers is true in the YAML.
- checks for samples will be sent no matter the settings and are functional.
- Dealers will send checks when recruiting regardless of settings.
- Dealer AP unlock will allow user to then recruit them in game. Check is still possible when having them as a possible contact.
- Suppliers will not be unlockable if suppliers are randomized and only unlocked through ap items
- Suppliers give checks for unlocking them when suppliers are not randomized
- Every Action that would cause cartel influence in a region to drop is a check (x7 per region)
- Unable to reduce cartel influence naturally and cartel influence items added to pool when randomize_cartel_influence is true
- Level up rewards are suppressed when randomize_level_up_rewards is true
- Level up rewards are added to the item pool when randomize_level_up_rewards is true
- Whenever you'd nomrally get unlocks for leveling up, you get a check regardless of the option
- Deathlink is sent when a player dies or when they arrested. Recieved deathlink causes player to get arrested
- Property and busniesses give checks when purchased
- Randomized properties or businesses will not be unlocked when purchased if randomization on, properties and/or businesses will be added to the item pool
- Recipe checks are sent when recipes are learned
- Cash for trash are sent every 10 trash burned
- Filler items will be sent as deaddrop quests
## Once I'm inside Schedule1, how do I play Schedule1AP
Use In-Game UI to connect to server. Once connected, Create a new world and skip the prologue.
Make sure to save as often as you can, and you are able to rejoin. Restart your game if you need to rejoin the world!
If you want to play with friends (Untested): Invite them to your lobby. All of you connect as same archipelago Info, Load into world.
## A statement on the ownership over Schedule1AP
Schedule I apworld is MIT license. Created by MacH8s
+33
View File
@@ -0,0 +1,33 @@
# Schedule 1 Archipelago Randomizer Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- [The Schedule I apworld](https://github.com/MacH8s/Narcopelago/releases/latest),
- [Thunderstore Mod Manager](https://www.overwolf.com/app/thunderstore-thunderstore_mod_manager)
- [Narcopelago Mod](https://thunderstore.io/c/schedule-i/p/Narcopelago/Narcopelago/)
## How to play
First, you need a room to connect to. For this, you or someone you know has to generate a game.
This will not be explained here,
but you can check the [Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game).
You also need to have [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installed
and the [The Schedule I apworld](https://github.com/MacH8s/Narcopelago/releases/latest) installed into Archipelago.
### Install Mod
Install Thunderstore Mod Manager and open it.
Choose Schedule I and make a profile for Archipelago, name it whatever you like.
Search for 'Narcopelago' in the mod search and install it.
From there you can launch the game as Modded on the top right and your install has been complete! You must launch the game this way every time you want to play Archipelago.
### Joining Game
Use In-Game UI to connect to server. Once connected, Create a new world and skip the prologue.
Make sure to save as often as you can, and you are able to rejoin. Restart your game if you need to rejoin the world!
If you want to play with friends (Untested): Invite them to your lobby. All of you connect as same archipelago Info, Load into world.
## Switching Rooms
Restart your game to switch rooms. There may be some issues if you don't do so even if it shows things are working.
+265
View File
@@ -0,0 +1,265 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING:
from .world import Schedule1World
ITEM_NAME_TO_ID = {}
RAW_ITEM_CLASSIFICATIONS = {}
fillers = []
traps = []
# Mapping from JSON classification strings to ItemClassification flags
CLASSIFICATION_MAP = {
"USEFUL": ItemClassification.useful,
"PROGRESSION": ItemClassification.progression,
"FILLER": ItemClassification.filler,
"PROGRESSION_SKIP_BALANCING": ItemClassification.progression_skip_balancing,
"TRAP": ItemClassification.trap
}
def load_items_data(data):
"""Load item data from JSON and populate ITEM_NAME_TO_ID and RAW_ITEM_CLASSIFICATIONS."""
global ITEM_NAME_TO_ID, RAW_ITEM_CLASSIFICATIONS
ITEM_NAME_TO_ID = {item.name: item.modern_id for item in data.items.values()}
RAW_ITEM_CLASSIFICATIONS = {item.name: item.classification for item in data.items.values()}
# Each Item instance must correctly report the "game" it belongs to.
# To make this simple, it is common practice to subclass the basic Item class and override the "game" field.
class Schedule1Item(Item):
game = "Schedule I"
# To do this, it must define a function called world.get_filler_item_name(), which we will define in world.py later.
# For now, let's make a function that returns the name of a random filler item here in items.py.
def get_random_filler_item_name(world: Schedule1World) -> str:
# For this purpose, we need to use a random generator.
# IMPORTANT: Whenever you need to use a random generator, you must use world.random.
# This ensures that generating with the same generator seed twice yields the same output.
# DO NOT use a bare random object from Python's built-in random module.
# Check if we should generate a trap item based on the trap_chance option.
if world.random.randint(0, 99) < world.options.trap_chance:
return world.random.choice(traps)
# Otherwise, return a random filler item.
return world.random.choice(fillers)
def check_option_enabled(world: Schedule1World, option_name: str) -> bool:
"""Check if an option is enabled based on option name string."""
option_map = {
"randomize_customers": world.options.randomize_customers,
"randomize_dealers": world.options.randomize_dealers,
"randomize_suppliers": world.options.randomize_suppliers,
"randomize_level_unlocks": world.options.randomize_level_unlocks,
"randomize_cartel_influence": world.options.randomize_cartel_influence,
"randomize_business_properties": world.options.randomize_business_properties,
"randomize_drug_making_properties": world.options.randomize_drug_making_properties,
}
return bool(option_map.get(option_name, False))
def check_option_condition(world: Schedule1World, condition_key: str) -> bool:
"""
Parse and evaluate a compound option condition string.
Supports:
- Simple: "randomize_level_unlocks" (option must be true)
- Negation: "!randomize_level_unlocks" (option must be false)
- Compound AND: "randomize_level_unlocks&!randomize_customers"
(first must be true AND second must be false)
Returns True if the condition is satisfied, False otherwise.
"""
parts = condition_key.split('&')
for part in parts:
part = part.strip()
if not part:
continue
if part.startswith('!'):
option_name = part[1:]
expected_value = False
else:
option_name = part
expected_value = True
actual_value = check_option_enabled(world, option_name)
if actual_value != expected_value:
return False
return True
def resolve_classification(world: Schedule1World, classification_data) -> ItemClassification:
"""
Resolve the classification from raw JSON data based on world options.
classification_data can be:
- A string: "PROGRESSION"
- A list: ["PROGRESSION", "USEFUL"]
- A dict with conditions: {"!randomize_customers": ["PROGRESSION", "USEFUL"], "default": ["USEFUL"]}
"""
# Determine which classification strings to use
if isinstance(classification_data, dict):
# Conditional classification - find matching condition
classification_strings = None
for condition_key, value in classification_data.items():
if condition_key == "default":
continue # Handle default last
if check_option_condition(world, condition_key):
classification_strings = value
break
# Fall back to default if no condition matched
if classification_strings is None:
classification_strings = classification_data.get("default", "FILLER")
else:
classification_strings = classification_data
# Convert to ItemClassification
if isinstance(classification_strings, list):
classification = CLASSIFICATION_MAP[classification_strings[0]]
for class_name in classification_strings[1:]:
classification |= CLASSIFICATION_MAP[class_name]
else:
classification = CLASSIFICATION_MAP[classification_strings]
return classification
def create_item_with_correct_classification(world: Schedule1World, name: str) -> Schedule1Item:
# Our world class must have a create_item() function that can create any of our items by name at any time.
# So, we make this helper function that creates the item by name with the correct classification.
# Note: This function's content could just be the contents of world.create_item in world.py directly,
# but it seemed nicer to have it in its own function over here in items.py.
classification = resolve_classification(world, RAW_ITEM_CLASSIFICATIONS[name])
return Schedule1Item(name, classification, ITEM_NAME_TO_ID[name], world.player)
# With those two helper functions defined, let's now get to actually creating and submitting our itempool.
def create_all_items(world: Schedule1World, data) -> None:
# Creating items should generally be done via the world's create_item method.
# First, we create a list containing all the items that always exist.
itempool: list[Item] = []
# Create bundles bundles
# Hard coding the bundles here based on options is more efficient than adding them through the json data
for _ in range(world.options.number_of_cash_bundles):
itempool += [world.create_item("Cash Bundle")]
for _ in range(world.options.number_of_xp_bundles):
itempool += [world.create_item("XP Bundle")]
if world.options.randomize_level_unlocks:
# If the randomize_level_unlocks option is enabled, create all items tagged as "Level Up Reward".
itempool += [world.create_item(item.name) for item in data.items.values()
if "Level Up Reward" in item.tags]
# Add cartel influence items based on options
if world.options.randomize_cartel_influence:
if not world.options.randomize_customers:
for _ in range(world.options.cartel_influence_items_per_region):
itempool += [world.create_item(item.name) for item in data.items.values()
if "Cartel Influence" in item.tags and "Westville" not in item.tags
and "Suburbia" not in item.tags]
# Suburbia is required for Finishing the Job
for _ in range(world.options.cartel_influence_items_per_region):
itempool += [world.create_item(item.name) for item in data.items.values()
if "Cartel Influence" in item.tags and "Suburbia" in item.tags]
# Westville starts at 500 less cartel influence. Will have 5 less cartel items as well to declutter
# Westville is required for Vibin the Cybin
for _ in range(world.options.cartel_influence_items_per_region - 5):
itempool += [world.create_item(item.name) for item in data.items.values()
if "Cartel Influence" in item.tags and "Westville" in item.tags]
if world.options.randomize_business_properties:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Business Property" in item.tags]
if world.options.randomize_drug_making_properties:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Drug Making Property" in item.tags]
if world.options.randomize_dealers:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Dealer" in item.tags and "Default" not in item.tags]
if world.options.randomize_customers:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Customer" in item.tags and "Default" not in item.tags]
if world.options.randomize_suppliers:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Supplier" in item.tags and "Default" not in item.tags]
if world.options.randomize_sewer_key:
itempool += [world.create_item(item.name) for item in data.items.values()
if "Sewer" in item.tags]
# Removed these from checks
if world.options.randomize_customers:
starting_kyle_cooley = world.create_item("Kyle Cooley Unlocked")
world.push_precollected(starting_kyle_cooley)
starting_austin_steiner = world.create_item("Austin Steiner Unlocked")
world.push_precollected(starting_austin_steiner)
starting_kathy_henderson = world.create_item("Kathy Henderson Unlocked")
world.push_precollected(starting_kathy_henderson)
starting_jessi_waters = world.create_item("Jessi Waters Unlocked")
world.push_precollected(starting_jessi_waters)
starting_sam_thompson = world.create_item("Sam Thompson Unlocked")
world.push_precollected(starting_sam_thompson)
starting_mick_lubbin = world.create_item("Mick Lubbin Unlocked")
world.push_precollected(starting_mick_lubbin)
# Set up traps
for item in data.items.values():
resolved_classification = resolve_classification(world, item.classification)
if resolved_classification == ItemClassification.trap:
# Create list of traps
traps.append(item.name)
filler_conditions = {
"Bad Filler" : world.options.ban_bad_filler_items,
"Ban Progression Skip" : world.options.ban_progression_skip_items}
# set up fillers
for item in data.items.values():
resolved_classification = resolve_classification(world, item.classification)
if resolved_classification == ItemClassification.filler:
is_valid = True
for tag, should_ban in filler_conditions.items():
if should_ban and tag in item.tags:
is_valid = False
break
if is_valid:
fillers.append(item.name)
# The length of our itempool is easy to determine, since we have it as a list.
number_of_items = len(itempool)
# What we actually want is the number of *unfilled* locations. Luckily, there is a helper method for this:
number_of_unfilled_locations = len(world.multiworld.get_unfilled_locations(world.player))
# Now, we just subtract the number of items from the number of locations to get the number of empty item slots.
needed_number_of_filler_items = number_of_unfilled_locations - number_of_items
# Finally, we create that many filler items and add them to the itempool.
itempool += [world.create_filler() for _ in range(needed_number_of_filler_items)]
# This is how the generator actually knows about the existence of our items.
world.multiworld.itempool += itempool
+104
View File
@@ -0,0 +1,104 @@
"""
Centralized data loader for Schedule1 world.
Loads and parses items.json, locations.json, and regions.json into structured objects for easy access.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import pkgutil
from typing import Any, Dict, List, Union
import orjson
def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]:
return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name).decode("utf-8-sig"))
@dataclass
class ItemData:
"""Represents item data from items.json"""
name: str
modern_id: int
classification: Union[str, List[str], Dict[str, Union[str, List[str]]]]
tags: List[str]
@dataclass
class LocationData:
"""Represents location data from locations.json"""
name: str
region: str
requirements: Union[bool, Dict[str, Any]]
tags: List[str]
modern_id: int
@dataclass
class RegionData:
"""Represents region data from regions.json"""
name: str
connections: Dict[str, Union[bool, Dict[str, Any]]]
class Schedule1ItemData:
"""Container for all Schedule1 game data loaded from JSON files"""
def __init__(self):
items_raw = load_json_data("items.json")
# Parse items into ItemData objects
# Classification is stored raw - resolution happens in items.py based on world options
self.items: Dict[str, ItemData] = {}
for item_name, item_info in items_raw.items():
self.items[item_name] = ItemData(
name=item_name,
modern_id=item_info["modern_id"],
classification=item_info["classification"],
tags=item_info["tags"]
)
class Schedule1LocationData:
"""Container for all Schedule1 location data loaded from JSON files"""
def __init__(self):
locations_raw = load_json_data("locations.json")
# Parse locations into LocationData objects
self.locations: Dict[str, LocationData] = {}
for location_name, location_info in locations_raw.items():
self.locations[location_name] = LocationData(
name=location_name,
region=location_info["region"],
requirements=location_info["requirements"],
tags=location_info["tags"],
modern_id=location_info["modern_id"]
)
class Schedule1RegionData:
"""Container for all Schedule1 region data loaded from JSON files"""
def __init__(self):
regions_raw = load_json_data("regions.json")
# Parse regions into RegionData objects
self.regions: Dict[str, RegionData] = {}
for region_name, region_info in regions_raw.items():
self.regions[region_name] = RegionData(
name=region_name,
connections=region_info["connections"]
)
class Schedule1VictoryData:
"""Container for victory conditions loaded from victory.json"""
def __init__(self):
# victory.json is structured as {option_name: {method: value, ...}, ...}
# This is the same structure as requirements in locations/regions
self.requirements: Dict[str, Any] = load_json_data("victory.json")
# Create singleton instances for easy import
schedule1_item_data = Schedule1ItemData()
schedule1_location_data = Schedule1LocationData()
schedule1_region_data = Schedule1RegionData()
schedule1_victory_data = Schedule1VictoryData()
+118
View File
@@ -0,0 +1,118 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Dict
from BaseClasses import ItemClassification, Location
from . import items
from math import ceil
if TYPE_CHECKING:
from .world import Schedule1World
# Every location must have a unique integer ID associated with it.
LOCATION_NAME_TO_ID = {}
def load_locations_data(data):
"""Load location data from JSON and populate LOCATION_NAME_TO_ID."""
global LOCATION_NAME_TO_ID
LOCATION_NAME_TO_ID = {name: loc.modern_id for name, loc in data.locations.items()}
# must include all locations, even those not created based on options
drug_types = [
("Weed", 195),
("Meth", 210),
("Shrooms", 225),
("Cocaine", 240),
]
for drug_name, start_id in drug_types:
for i in range(1, 16):
LOCATION_NAME_TO_ID[f"{drug_name} Recipe Check, {i}"] = start_id + (i - 1)
start_id = 600 # Starting ID for Cash for Trash locations
for i in range(1, 51):
LOCATION_NAME_TO_ID[f"Cash for Trash {i}, Collect {i * 10} pieces of trash"] = start_id + (i - 1)
# Each Location instance must correctly report the "game" it belongs to.
# To make this simple, it is common practice to subclass the basic Location class and override the "game" field.
class Schedule1Location(Location):
game = "Schedule I"
# Let's make one more helper method before we begin actually creating locations.
# Later on in the code, we'll want specific subsections of LOCATION_NAME_TO_ID.
# To reduce the chance of copy-paste errors writing something like {"Chest": LOCATION_NAME_TO_ID["Chest"]},
# let's make a helper method that takes a list of location names and returns them as a dict with their IDs.
# Note: There is a minor typing quirk here. Some functions want location addresses to be an "int | None",
# so while our function here only ever returns dict[str, int], we annotate it as dict[str, int | None].
def get_location_names_with_ids(location_names: list[str]) -> dict[str, int | None]:
return {location_name: LOCATION_NAME_TO_ID[location_name] for location_name in location_names}
def create_all_locations(world: Schedule1World, locationData) -> None:
create_regular_locations(world, locationData)
def create_regular_locations(world: Schedule1World, data) -> None:
# Get all unique region names from location data
region_names = set(loc.region for loc in data.locations.values())
# Load all regions into a dictionary once for efficient access
regions_dict: Dict[str, any] = {}
for region_name in sorted(region_names):
regions_dict[region_name] = world.get_region(region_name)
# Group locations by region, excluding suppliers if randomized
locations_by_region: Dict[str, list[str]] = {region: [] for region in sorted(region_names)}
for loc_name, loc_data in data.locations.items():
# Skip supplier locations if randomize_suppliers is enabled
if world.options.randomize_suppliers and "Supplier" in loc_data.tags:
continue
locations_by_region[loc_data.region].append(loc_name)
# Add all locations to their respective regions
for region_name, location_names in locations_by_region.items():
if location_names: # Only add if there are locations
region = regions_dict[region_name]
region.add_locations(get_location_names_with_ids(location_names), Schedule1Location)
# Recipe checks - Only include the number specified by the RecipeChecks option
if world.options.recipe_checks > 0:
# Get each recipe region
weed_recipe_region = world.get_region("Weed Recipe Checks")
meth_recipe_region = world.get_region("Meth Recipe Checks")
shrooms_recipe_region = world.get_region("Shrooms Recipe Checks")
cocaine_recipe_region = world.get_region("Cocaine Recipe Checks")
# Drug types with their regions and starting IDs in LOCATION_NAME_TO_ID
# These magic numbers correspond to reserved IDs for recipe checks
drug_types = [
("Weed", weed_recipe_region),
("Meth", meth_recipe_region),
("Shrooms", shrooms_recipe_region),
("Cocaine", cocaine_recipe_region),
]
# Add needed recipe locations to location name to id
for drug_name, region in drug_types:
recipe_locations = []
for i in range(1, world.options.recipe_checks + 1):
recipe_locations.append(f"{drug_name} Recipe Check, {i}")
recipe_locations_dict = get_location_names_with_ids(recipe_locations)
region.add_locations(recipe_locations_dict, Schedule1Location)
# Cash for Trash checks - Only include the number specified by the CashForTrash option
# Add to Overworld region
cash_for_trash_count = world.options.cash_for_trash
if cash_for_trash_count > 0:
regions = {
100 : regions_dict["Overworld"],
200 : regions_dict["Dodgy Dealing"],
300 : regions_dict["Mixing Mania"],
400 : regions_dict["We Need To Cook|2"],
500 : regions_dict["Finishing the Job"]
}
cash_for_trash_locations = []
for i in range(1, cash_for_trash_count + 1):
cash_for_trash_locations.append(f"Cash for Trash {i}, Collect {i * 10} pieces of trash")
cash_for_trash_locations_dict = get_location_names_with_ids(cash_for_trash_locations)
regions[ceil(201 / 100) * 100].add_locations(cash_for_trash_locations_dict, Schedule1Location)
+322
View File
@@ -0,0 +1,322 @@
from dataclasses import dataclass
from Options import (Choice, DefaultOnToggle, OptionGroup, PerGameCommonOptions,
Range, DeathLink)
# In this file, we define the options the player can pick.
# The most common types of options are Toggle, Range and Choice.
# Options will be in the game's template yaml.
# They will be represented by checkboxes, sliders etc. on the game's options page on the website.
# (Note: Options can also be made invisible from either of these places by overriding Option.visibility.
# APQuest doesn't have an example of this, but this can be used for secret / hidden / advanced options.)
# For further reading on options, you can also read the Options API Document:
# https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/options%20api.md
# The first type of Option we'll discuss is the Toggle.
# A toggle is an option that can either be on or off. This will be represented by a checkbox on the website.
# The default for a toggle is "off".
# If you want a toggle to be on by default, you can use the "DefaultOnToggle" class instead of the "Toggle" class.
class Goal(Choice):
"""
The goal to win the game.
missions_networth: Reach a specificed net worth and complete the main story.
networth_only: Reach a specificed net worth.
missions_only: Complete the main story.
"""
display_name = "Goal"
option_networth_only = 0
option_missions_networth = 1
option_missions_only = 2
# Choice options must define an explicit default value.
default = option_missions_networth
class RandomizeLevelUnlocks(DefaultOnToggle):
"""
Leveling up is always a check regardless of this option.
Block Boss I, 30,350xp, will be the highest checks will go to.
Things you unlock by leveling up will be shuffled into the item pool if this option is enabled.
When this is enabled, you will no longer get level up rewards naturally.
"""
display_name = "Randomize Level Unlocks"
class NumberOfXpBundles(Range):
"""
Number of XP bundles to include in the item pool.
min to max xp bundles will be interpolated based on number of bundles.
Meaning there will be 1 min bundle and 1 max bundle and the rest will be evenly distributed in between.
If default min/max are used, 25 bundles give max level xp. 12 gives out about a quarter of that.
16,425 xp is needed to complete the game, but this can be done naturally, bundles speed up process.
If 1 bundle is chosen, only the minimum xp bundle will be in the item pool.
"""
range_start = 0
range_end = 20
default = 12
class AmountOfXpPerBundleMin(Range):
"""
Min amount of XP per bundle included in the item pool.
Each bundle is worth the specified amount of XP.
This option is only relevant if NumberOfXpBundles > 0.
"""
range_start = 1
range_end = 1000
default = 100
class AmountOfXpPerBundleMax(Range):
"""
Max amount of XP per bundle included in the item pool.
Each bundle is worth the specified amount of XP.
This option is only relevant if NumberOfXpBundles > 0.
"""
range_start = 1000
range_end = 10000
default = 5000
class NumberOfCashBundles(Range):
"""
Number of cash bundles to include in the item pool.
min to max cash bundles will be interpolated based on number of bundles.
Meaning there will be 1 min bundle and 1 max bundle and the rest will be evenly distributed in between.
If 1 bundle is chosen, only the minimum cash bundle will be in the item pool.
Defaults will give out ~87k cash with 20 bundles. 46 bundles gives out ~200k cash.
"""
range_start = 0
range_end = 20
default = 12
class AmountOfCashPerBundleMin(Range):
"""
Min amount of cash per bundle included in the item pool.
Each bundle is worth the specified amount of cash.
This option is only relevant if NumberOfCashBundles > 0.
"""
range_start = 1
range_end = 1500
default = 1500
class AmountOfCashPerBundleMax(Range):
"""
Max amount of cash per bundle included in the item pool.
Each bundle is worth the specified amount of cash.
This option is only relevant if NumberOfCashBundles > 0.
"""
range_start = 1500
range_end = 100000
default = 10000
class NetworthAmountRequired(Range):
"""
The net worth amount required to win the game.
This option is only relevant if the goal includes net worth.
"""
range_start = 10000
range_end = 10000000
default = 100000
class BanBadFillerItems(DefaultOnToggle):
"""
If enabled, bad filler items will not be included in the item pool.
"""
display_name = "Ban Bad Filler Items"
class BanProgressionSkipItems(DefaultOnToggle):
"""
If enabled, filler items that allow for progression skips will not be included in the item pool.
This can add variety to the seed to allow for out of logic skips.
"""
display_name = "Ban Progression Skip Filler Items"
class TrapChance(Range):
"""
Percentage chance that a filler item will be a trap.
"""
display_name = "Trap Chance"
range_start = 0
range_end = 100
default = 0
class RandomizeCartelInfluence(DefaultOnToggle):
"""
Determines if cartel influence will be randomized into the item pool.
7 Bundles of 100 cartel influence per in-game region that applies.
Every 100 cartel influcence by the player counts as a check regardless of this option.
This option removes the player's ability to earn cartel influence naturally
"""
display_name = "Randomize Cartel Influence"
class CartelInfluenceItemsPerRegion(Range):
"""
Number of cartel influence Items to include in the item pool per region.
Each item is worth 100 cartel influence.
7 needed to unlock region, recommend to add extra of each region.
Westville has 5 less checks and 5 less cartel influence items
This option is only relevant if Randomize Cartel Influence is enabled.
"""
range_start = 7
range_end = 12
default = 10
class RandomizeDrugMakingProperties(DefaultOnToggle):
"""
Determines if drug making properties will be added into the item pool.
Purchasing drug making properties become checks, but you do not purchase them.
Realtor will have AP items instead of drug making properties if this is enabled.
This does not include ones you must purchase through missions.
"""
display_name = "Randomize drug making Properties"
class RandomizeBusinessProperties(DefaultOnToggle):
"""
Determines if business properties will be added into the item pool.
The Realtor will have AP items instead of business properties if this is enabled.
"""
display_name = "Randomize business making Properties"
class RandomizeDealers(DefaultOnToggle):
"""
Determines if dealers will be added into the item pool.
Recruiting dealers become checks, but you do not recruit them.
This does not include Benji, who is required for story progression.
"""
display_name = "Randomize Dealers"
class RandomizeCustomers(DefaultOnToggle):
"""
Determines if customers will be added into the item pool.
Customers are checks regardless if this is toggled on or off
Player can still get successful samples as checks
"""
display_name = "Randomize Customers"
class RandomizeSuppliers(DefaultOnToggle):
"""
Determines if suppliers will be added into the item pool.
If enabled, befriending suppliers no longer become checks.
Albert Hoover is unlocked by default
"""
display_name = "Randomize Suppliers"
class RandomizeSewerKey(DefaultOnToggle):
"""
Determines if the Sewer Key will be added into the item pool.
If enabled, Jen Herd will no longer sell sewer key.
Buying the sewer key from Jen Herd is a check no matter if this option is toggled on or off.
"""
display_name = "Randomize Sewer Key"
class RecipeChecks(Range):
"""
Number of recipe checks to include in the item pool.
These are recipes per drug type.
10 means 10 weed recipes, 10 meth recipes, etc.
"""
range_start = 0
range_end = 15
default = 5
class CashForTrash(Range):
"""
Number of checks for each 10 pieces of trash collected
50 = 500 total pieces of trash which is equal to the achiemvement.
"""
range_start = 0
range_end = 50
default = 5
# We must now define a dataclass inheriting from PerGameCommonOptions that we put all our options in.
# This is in the format "option_name_in_snake_case: OptionClassName".
@dataclass
class Schedule1Options(PerGameCommonOptions):
goal: Goal
networth_amount_required: NetworthAmountRequired
ban_bad_filler_items: BanBadFillerItems
ban_progression_skip_items: BanProgressionSkipItems
trap_chance: TrapChance
number_of_xp_bundles: NumberOfXpBundles
amount_of_xp_per_bundle_min: AmountOfXpPerBundleMin
amount_of_xp_per_bundle_max: AmountOfXpPerBundleMax
number_of_cash_bundles: NumberOfCashBundles
amount_of_cash_per_bundle_min: AmountOfCashPerBundleMin
amount_of_cash_per_bundle_max: AmountOfCashPerBundleMax
randomize_level_unlocks: RandomizeLevelUnlocks
randomize_cartel_influence: RandomizeCartelInfluence
cartel_influence_items_per_region: CartelInfluenceItemsPerRegion
randomize_drug_making_properties: RandomizeDrugMakingProperties
randomize_business_properties: RandomizeBusinessProperties
randomize_dealers: RandomizeDealers
randomize_customers: RandomizeCustomers
randomize_suppliers: RandomizeSuppliers
randomize_sewer_key: RandomizeSewerKey
recipe_checks: RecipeChecks
cash_for_trash: CashForTrash
death_link: DeathLink
# If we want to group our options by similar type, we can do so as well. This looks nice on the website.
option_groups = [
OptionGroup(
"Gameplay Options",
[Goal, NetworthAmountRequired, NumberOfXpBundles, AmountOfXpPerBundleMin, AmountOfXpPerBundleMax,
NumberOfCashBundles, AmountOfCashPerBundleMin, AmountOfCashPerBundleMax,
BanBadFillerItems, BanProgressionSkipItems, TrapChance,
RandomizeLevelUnlocks, RandomizeCartelInfluence, CartelInfluenceItemsPerRegion,
RandomizeCustomers, RandomizeDealers, RandomizeSuppliers, RandomizeSewerKey,
RandomizeDrugMakingProperties, RandomizeBusinessProperties,
RecipeChecks, CashForTrash,
DeathLink],
)
]
# Finally, we can define some option presets if we want the player to be able to quickly choose a specific "mode".
option_presets = {
"Default": {
"goal": Goal.default,
"number_of_xp_bundles": NumberOfXpBundles.default,
"amount_of_xp_per_bundle_min": AmountOfXpPerBundleMin.default,
"amount_of_xp_per_bundle_max": AmountOfXpPerBundleMax.default,
"number_of_cash_bundles": NumberOfCashBundles.default,
"amount_of_cash_per_bundle_min": AmountOfCashPerBundleMin.default,
"amount_of_cash_per_bundle_max": AmountOfCashPerBundleMax.default,
"networth_amount_required": NetworthAmountRequired.default,
"ban_bad_filler_items": BanBadFillerItems.default,
"ban_progression_skip_items": BanProgressionSkipItems.default,
"trap_chance": TrapChance.default,
"randomize_cartel_influence": True,
"randomize_drug_making_properties": True,
"randomize_business_properties": True,
"randomize_dealers": True,
"randomize_customers": True,
"cartel_influence_items_per_region": CartelInfluenceItemsPerRegion.default,
"recipe_checks": RecipeChecks.default,
"cash_for_trash": CashForTrash.default,
"randomize_level_unlocks": True,
"randomize_suppliers": True,
"randomize_sewer_key": True,
"death_link": DeathLink.default,
}
}
+52
View File
@@ -0,0 +1,52 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Dict
from BaseClasses import Region
if TYPE_CHECKING:
from .world import Schedule1World as Schedule1World
# A region is a container for locations ("checks"), which connects to other regions via "Entrance" objects.
# Many games will model their Regions after physical in-game places, but you can also have more abstract regions.
# For a location to be in logic, its containing region must be reachable.
# The Entrances connecting regions can have rules - more on that in rules.py.
# This makes regions especially useful for traversal logic ("Can the player reach this part of the map?")
# Every location must be inside a region, and you must have at least one region.
# This is why we create regions first, and then later we create the locations (in locations.py).
def create_and_connect_regions(world: Schedule1World, region_data) -> None:
create_all_regions(world, region_data)
connect_regions(world, region_data)
def create_all_regions(world: Schedule1World, region_data) -> None:
# Create all regions from regions.json
regions = []
for region_name in region_data.regions.keys():
region = Region(region_name, world.player, world.multiworld)
regions.append(region)
# Add all regions to multiworld.regions so that AP knows about their existence
world.multiworld.regions += regions
def connect_regions(world: Schedule1World, region_data) -> None:
# Load all regions into a dictionary once to avoid repeated get_region calls
regions_dict: Dict[str, Region] = {
region_name: world.get_region(region_name)
for region_name in region_data.regions.keys()
}
# Connect all regions based on the connections defined in regions.json
for region_name, region_info in region_data.regions.items():
source_region = regions_dict[region_name]
# Iterate through all connections for this region
for connected_region_name in region_info.connections.keys():
target_region = regions_dict[connected_region_name]
entrance_name = f"{region_name} to {connected_region_name}"
source_region.connect(target_region, entrance_name)
+251
View File
@@ -0,0 +1,251 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Dict, Any, Union
from BaseClasses import CollectionState
from worlds.generic.Rules import add_rule, set_rule
if TYPE_CHECKING:
from .world import Schedule1World
def set_all_rules(world: Schedule1World, locationData, regionData, victoryData) -> None:
# In order for AP to generate an item layout that is actually possible for the player to complete,
# we need to define rules for our Entrances and Locations.
# Note: Regions do not have rules, the Entrances connecting them do!
# We'll do entrances first, then locations, and then finally we set our victory condition.
set_all_entrance_rules(world, regionData)
set_all_location_rules(world, locationData)
set_completion_condition(world, victoryData)
def check_option_enabled(world: Schedule1World, option_name: str) -> bool:
"""Check if an option is enabled based on option name string from JSON."""
option_map = {
"randomize_customers": world.options.randomize_customers,
"randomize_dealers": world.options.randomize_dealers,
"randomize_suppliers": world.options.randomize_suppliers,
"randomize_level_unlocks": world.options.randomize_level_unlocks,
"randomize_cartel_influence": world.options.randomize_cartel_influence,
"randomize_business_properties": world.options.randomize_business_properties,
"randomize_drug_making_properties": world.options.randomize_drug_making_properties,
}
return bool(option_map.get(option_name, False))
def check_option_condition(world: Schedule1World, condition_key: str) -> bool:
"""
Parse and evaluate a compound option condition string.
Supports:
- Simple: "randomize_level_unlocks" (option must be true)
- Negation: "!randomize_level_unlocks" (option must be false)
- Compound AND: "randomize_level_unlocks&!randomize_customers"
(first must be true AND second must be false)
Returns True if the condition is satisfied, False otherwise.
"""
# Split by '&' to get individual conditions
parts = condition_key.split('&')
for part in parts:
part = part.strip()
if not part:
continue
# Check for negation prefix
if part.startswith('!'):
option_name = part[1:]
expected_value = False
else:
option_name = part
expected_value = True
# Get the actual option value
actual_value = check_option_enabled(world, option_name)
# If this part doesn't match expected, the whole condition fails
if actual_value != expected_value:
return False
return True
def build_requirement_check(world: Schedule1World, method_name: str, value: Any) -> Callable[[CollectionState], bool]:
"""Build a requirement check function based on the method name and value from JSON."""
if method_name == "has":
# value is a single item name string
return lambda state, v=value: state.has(v, world.player)
elif method_name == "has_any":
# value is a list of lists, e.g. [["Item1", "Item2"]]
# We take the first list as the items to check
items = value[0] if isinstance(value[0], list) else value
return lambda state, v=items: state.has_any(v, world.player)
elif method_name == "has_all":
# value is a list of item names
return lambda state, v=value: state.has_all(v, world.player)
elif method_name == "has_all_counts":
# value is a dict of {item_name: count}
return lambda state, v=value: state.has_all_counts(v, world.player)
elif method_name == "has_from_list":
# value can be:
# - A single dict: {item_name: count, ...} where all counts are the same
# - A list of dicts: [{item_name: count, ...}, ...] for multiple tiers
# For a list, we build checks for each dict and require all to pass
if isinstance(value, list):
# List of dicts - build a check for each dict
checks = []
for tier_dict in value:
keys = list(tier_dict.keys())
count = list(tier_dict.values())[0] # All values in a tier should be the same
checks.append((keys, count))
return lambda state, c=checks: all(
state.has_from_list(keys, world.player, count) for keys, count in c
)
else:
# Single dict
keys = list(value.keys())
count = list(value.values())[0] # All values should be the same count
return lambda state, k=keys, c=count: state.has_from_list(k, world.player, c)
# Default: always true
return lambda state: True
def build_rule_from_requirements(world: Schedule1World, requirements: Union[bool, Dict[str, Any]], use_or_logic: bool = False) -> Callable[[CollectionState], bool]:
"""
Build a rule function from the requirements structure.
requirements can be:
- True (always accessible)
- A dict with option conditions as keys
use_or_logic: If True, only ONE condition needs to be satisfied (for Customer/Dealer/Supplier tags)
If False, ALL applicable conditions must be satisfied
"""
if requirements is True:
return lambda state: True
if not isinstance(requirements, dict):
return lambda state: True
# Build list of (option_name, checks) pairs
condition_checks: list[tuple[str, list[Callable[[CollectionState], bool]]]] = []
for option_name, checks in requirements.items():
if not isinstance(checks, dict):
continue
check_functions = []
for method_name, value in checks.items():
check_func = build_requirement_check(world, method_name, value)
check_functions.append(check_func)
if check_functions:
condition_checks.append((option_name, check_functions))
if not condition_checks:
return lambda state: True
def rule_function(state: CollectionState) -> bool:
results = []
for condition_key, check_functions in condition_checks:
if check_option_condition(world, condition_key):
# This option condition is satisfied, so its checks matter
# All checks within this condition must pass
option_result = all(check(state) for check in check_functions)
results.append(option_result)
if not results:
# No applicable options enabled - rule passes
return True
if use_or_logic:
# For Customer/Dealer/Supplier: only one needs to pass
return any(results)
else:
# For all others: all must pass
return all(results)
return rule_function
def set_all_entrance_rules(world: Schedule1World, regionData) -> None:
"""Set entrance rules based on region connection requirements from regions.json."""
# Load all entrances into a dictionary once
entrances_dict: Dict[str, Any] = {}
for region_name, region_info in regionData.regions.items():
for connected_region_name, requirements in region_info.connections.items():
entrance_name = f"{region_name} to {connected_region_name}"
try:
entrances_dict[entrance_name] = world.get_entrance(entrance_name)
except KeyError:
# Entrance might not exist if region wasn't created
continue
# Set rules for each entrance
for region_name, region_info in regionData.regions.items():
for connected_region_name, requirements in region_info.connections.items():
entrance_name = f"{region_name} to {connected_region_name}"
if entrance_name not in entrances_dict:
continue
entrance = entrances_dict[entrance_name]
rule = build_rule_from_requirements(world, requirements, use_or_logic=False)
set_rule(entrance, rule)
def set_all_location_rules(world: Schedule1World, locationData) -> None:
"""Set location rules based on requirements from locations.json."""
# Build a dict of location name -> location object for locations that exist
locations_dict: Dict[str, Any] = {}
for loc_name, loc_data in locationData.locations.items():
# Skip supplier locations if randomize_suppliers is enabled (they don't exist)
if world.options.randomize_suppliers and "Supplier" in loc_data.tags:
continue
try:
locations_dict[loc_name] = world.get_location(loc_name)
except KeyError:
# Location might not exist
continue
# Set rules for each location
for loc_name, loc_data in locationData.locations.items():
if loc_name not in locations_dict:
continue
location = locations_dict[loc_name]
requirements = loc_data.requirements
# Determine if this location uses OR logic (Customer, Dealer, or Supplier tags)
tags = loc_data.tags
use_or_logic = any(tag in tags for tag in ["Customer", "Dealer", "Supplier"])
rule = build_rule_from_requirements(world, requirements, use_or_logic=use_or_logic)
set_rule(location, rule)
def set_completion_condition(world: Schedule1World, victoryData) -> None:
# Victory conditions are loaded from victory.json
# > 0 means cartel is necessary, and we need to check all applicable conditions
if world.options.goal > 0:
# Build the victory rule from the requirements in victory.json
# All applicable option conditions must pass (AND logic)
rule = build_rule_from_requirements(world, victoryData.requirements, use_or_logic=False)
world.multiworld.completion_condition[world.player] = rule
else:
# Otherwise, money is farmable no matter what.
world.multiworld.completion_condition[world.player] = lambda state: True
+37
View File
@@ -0,0 +1,37 @@
from BaseClasses import Tutorial
from worlds.AutoWorld import WebWorld
from .options import option_groups, option_presets
# For our game to display correctly on the website, we need to define a WebWorld subclass.
class APSchedule1(WebWorld):
# We need to override the "game" field of the WebWorld superclass.
# This must be the same string as the regular World class.
game = "Schedule I"
# Your game pages will have a visual theme (affecting e.g. the background image).
# You can choose between dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, and stone.
theme = "partyTime"
# A WebWorld can have any number of tutorials, but should always have at least an English setup guide.
# Many WebWorlds just have one setup guide, but some have multiple, e.g. for different languages.
# We need to create a Tutorial object for every setup guide.
# In order, we need to provide a title, a description, a language, a filepath, a link, and authors.
# The filepath is relative to a "/docs/" directory in the root folder of your apworld.
# The "link" parameter is unused, but we still need to provide it.
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up Schedule 1 for MultiWorld.",
"English",
"setup_en.md",
"setup/en",
["NewSoupVi"],
)
# We add these tutorials to our WebWorld by overriding the "tutorials" field.
tutorials = [setup_en]
# If we have option groups and/or option presets, we need to specify these here as well.
option_groups = option_groups
options_presets = option_presets
+110
View File
@@ -0,0 +1,110 @@
from collections.abc import Mapping
from typing import Any
# Imports of base Archipelago modules must be absolute.
from worlds.AutoWorld import World
# Imports of your world's files must be relative.
from . import items, locations, options, regions, rules, web_world, json_data
# APQuest will go through all the parts of the world api one step at a time,
# with many examples and comments across multiple files.
# If you'd rather read one continuous document, or just like reading multiple sources,
# we also have this document specifying the entire world api:
# https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md
# The world class is the heart and soul of an apworld implementation.
# It holds all the data and functions required to build the world and submit it to the multiworld generator.
# You could have all your world code in just this one class, but for readability and better structure,
# it is common to split up world functionality into multiple files.
# This implementation in particular has the following additional files, each covering one topic:
# regions.py, locations.py, rules.py, items.py, options.py and web_world.py.
# It is recommended that you read these in that specific order, then come back to the world class.
class Schedule1World(World):
"""
Scheudle 1 is a game about manufacturing. Produce a range of drugs. Purchase properties and equipment.
Distribute your products through a network of dealers. Avoid the law and rival manufacturers.
Expand your empire and become the ultimate drug lord!
"""
# The docstring should contain a description of the game, to be displayed on the WebHost.
# You must override the "game" field to say the name of the game.
game = "Schedule I"
# The WebWorld is a definition class that governs how this world will be displayed on the website.
web = web_world.APSchedule1()
# This is how we associate the options defined in our options.py with our world.
options_dataclass = options.Schedule1Options
options: options.Schedule1Options # Common mistake: This has to be a colon (:), not an equals sign (=).
# Our world class must have a static location_name_to_id and item_name_to_id defined.
# We define these in regions.py and items.py respectively, so we just set them here.
# Load items from json into needed dicts.
items.load_items_data(json_data.schedule1_item_data)
locations.load_locations_data(json_data.schedule1_location_data)
location_name_to_id = locations.LOCATION_NAME_TO_ID
item_name_to_id = items.ITEM_NAME_TO_ID
# There is always one region that the generator starts from & assumes you can always go back to.
# This defaults to "Menu", but you can change it by overriding origin_region_name.
origin_region_name = "Overworld"
# Our world class must have certain functions ("steps") that get called during generation.
# The main ones are: create_regions, set_rules, create_items.
# For better structure and readability, we put each of these in their own file.
def create_regions(self) -> None:
regions.create_and_connect_regions(self, json_data.schedule1_region_data)
locations.create_all_locations(self, json_data.schedule1_location_data)
def set_rules(self) -> None:
rules.set_all_rules(self, json_data.schedule1_location_data, json_data.schedule1_region_data, json_data.schedule1_victory_data)
def create_items(self) -> None:
items.create_all_items(self, json_data.schedule1_item_data)
# Our world class must also have a create_item function that can create any one of our items by name at any time.
# We also put this in a different file, the same one that create_items is in.
def create_item(self, name: str) -> items.Schedule1Item:
return items.create_item_with_correct_classification(self, name)
# For features such as item links and panic-method start inventory, AP may ask your world to create extra filler.
# The way it does this is by calling get_filler_item_name.
# For this purpose, your world *must* have at least one infinitely repeatable item (usually filler).
# You must override this function and return this infinitely repeatable item's name.
# In our case, we defined a function called get_random_filler_item_name for this purpose in our items.py.
def get_filler_item_name(self) -> str:
return items.get_random_filler_item_name(self)
# There may be data that the game client will need to modify the behavior of the game.
# This is what slot_data exists for. Upon every client connection, the slot's slot_data is sent to the client.
# slot_data is just a dictionary using basic types, that will be converted to json when sent to the client.
def fill_slot_data(self) -> Mapping[str, Any]:
# If you need access to the player's chosen options on the client side, there is a helper for that.
return self.options.as_dict(
"goal",
"number_of_xp_bundles",
"amount_of_xp_per_bundle_min",
"amount_of_xp_per_bundle_max",
"number_of_cash_bundles",
"amount_of_cash_per_bundle_min",
"amount_of_cash_per_bundle_max",
"networth_amount_required",
"ban_bad_filler_items",
"ban_progression_skip_items",
"trap_chance",
"randomize_cartel_influence",
"randomize_drug_making_properties",
"randomize_business_properties",
"randomize_dealers",
"randomize_customers",
"randomize_suppliers",
"cartel_influence_items_per_region",
"recipe_checks",
"cash_for_trash",
"randomize_level_unlocks",
"randomize_sewer_key",
"death_link"
)
+265
View File
@@ -0,0 +1,265 @@
import settings
from typing import Dict, Any
from BaseClasses import MultiWorld, Region, Item, Tutorial
from worlds.AutoWorld import World, WebWorld
from Utils import visualize_regions
from worlds.generic.Rules import set_rule
from .items import (SonicFrontiersItem, SonicFrontiersItemData, item_list, kronos_amount, ares_amount, chaos_amount, ouranos_amount, fillers)
from .locations import (kronosRegion, SonicFrontiersAdvancement, aresRegion, chaosRegion, ouranosRegion, kronosMemoryTokenSet,
aresMemoryTokenSet, chaosMemoryTokenSet, ouranosMemoryTokenSet, kronosPurpleSet, kronosKocoSet, aresKocoSet,
aresPurpleSet, chaosKocoSet, chaosPurpleSet, ouranosKocoSet, ouranosPurpleSet, kronosNewKocoSet,
kronosMusicSet, aresMusicSet, aresNewKocoSet, chaosMusicSet, chaosNewKocoSet, ouranosMusicSet, ouranosNewKocoSet, all_items)
from .options import SonicFrontiersOptions
class SonicFrontiersWebWorld(WebWorld):
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to playing Sonic Frontiers with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["custom"]
)
tutorials = [setup_en]
class SonicFrontiersWorld(World):
game = "Sonic Frontiers"
topology_present = False
web = SonicFrontiersWebWorld()
item_name_to_id = {name: data.id for name, data in item_list.items()}
location_name_to_id = {name: data.id for name, data in all_items.items()}
options_dataclass = SonicFrontiersOptions
options: SonicFrontiersOptions
def create_kronos_island(self) -> Region:
return
def create_ares_island(self) -> Region:
return
def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld)
kronos_region = Region("Kronos", self.player, self.multiworld)
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosRegion.items()]
if(self.options.memory_token_sanity):
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosMemoryTokenSet.items()]
if(self.options.challenge_kocos):
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosNewKocoSet.items()]
if(self.options.music_notes):
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosMusicSet.items()]
if(self.options.purple_coin_sanity):
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosPurpleSet.items()]
if(self.options.koco_sanity):
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosKocoSet.items()]
menu_region.connect(kronos_region)
self.multiworld.regions += [menu_region, kronos_region]
if(self.options.goal > 0):
ares_region = Region("Ares", self.player, self.multiworld)
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresRegion.items()]
if(self.options.memory_token_sanity):
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresMemoryTokenSet.items()]
if(self.options.challenge_kocos):
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresNewKocoSet.items()]
if(self.options.music_notes):
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresMusicSet.items()]
if(self.options.purple_coin_sanity):
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresPurpleSet.items()]
if(self.options.koco_sanity):
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresKocoSet.items()]
self.multiworld.regions += [ares_region]
kronos_region.add_exits({"Ares": "Ares Entrance"})
if(self.options.goal > 1):
chaos_region = Region("Chaos", self.player, self.multiworld)
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosRegion.items()]
if(self.options.memory_token_sanity):
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosMemoryTokenSet.items()]
if(self.options.challenge_kocos):
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosNewKocoSet.items()]
if(self.options.music_notes):
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosMusicSet.items()]
if(self.options.purple_coin_sanity):
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosPurpleSet.items()]
if(self.options.koco_sanity):
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosKocoSet.items()]
self.multiworld.regions += [chaos_region]
ares_region.add_exits({"Chaos": "Chaos Entrance"})
if(self.options.goal > 2):
ouranos_region = Region("Ouranos", self.player, self.multiworld)
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosRegion.items()]
if(self.options.memory_token_sanity):
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosMemoryTokenSet.items()]
if(self.options.challenge_kocos):
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosNewKocoSet.items()]
if(self.options.music_notes):
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosMusicSet.items()]
if(self.options.purple_coin_sanity):
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosPurpleSet.items()]
if(self.options.koco_sanity):
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosKocoSet.items()]
self.multiworld.regions += [ouranos_region]
chaos_region.add_exits({"Ouranos": "Ouranos Entrance"})
if(self.options.goal == 0):
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Giganto", self.player)
if(self.options.goal == 1):
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Wyvern", self.player)
if(self.options.goal == 2):
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Knight", self.player)
if(self.options.goal == 3):
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Ouranos", self.player)
def set_rules(self) -> None:
for i in range(7):
set_rule(self.multiworld.get_location((f"1-2 All Missions ({i+1})"), self.player), lambda state: state.has("1-2 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"1-3 All Missions ({i+1})"), self.player), lambda state: state.has("1-3 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"1-4 All Missions ({i+1})"), self.player), lambda state: state.has("1-4 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"1-5 All Missions ({i+1})"), self.player), lambda state: state.has("1-5 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"1-6 All Missions ({i+1})"), self.player), lambda state: state.has("1-6 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"1-7 All Missions ({i+1})"), self.player), lambda state: state.has("1-7 Unlocked", self.player, 1))
if(self.options.goal > 0):
set_rule(self.multiworld.get_location((f"2-1 All Missions ({i+1})"), self.player), lambda state: state.has("2-1 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"2-2 All Missions ({i+1})"), self.player), lambda state: state.has("2-2 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"2-3 All Missions ({i+1})"), self.player), lambda state: state.has("2-3 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"2-4 All Missions ({i+1})"), self.player), lambda state: state.has("2-4 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"2-5 All Missions ({i+1})"), self.player), lambda state: state.has("2-5 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"2-6 All Missions ({i+1})"), self.player), lambda state: state.has("2-6 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"2-7 All Missions ({i+1})"), self.player), lambda state: state.has("2-7 Unlocked", self.player, 1))
if(self.options.goal > 1):
set_rule(self.multiworld.get_location((f"3-1 All Missions ({i+1})"), self.player), lambda state: state.has("3-1 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"3-2 All Missions ({i+1})"), self.player), lambda state: state.has("3-2 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"3-3 All Missions ({i+1})"), self.player), lambda state: state.has("3-3 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"3-4 All Missions ({i+1})"), self.player), lambda state: state.has("3-4 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"3-5 All Missions ({i+1})"), self.player), lambda state: state.has("3-5 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"3-6 All Missions ({i+1})"), self.player), lambda state: state.has("3-6 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"3-7 All Missions ({i+1})"), self.player), lambda state: state.has("3-7 Unlocked", self.player, 1)
and state.can_reach_location("Chaos Cyan Emerald", self.player))
if(self.options.goal > 2):
set_rule(self.multiworld.get_location((f"4-1 All Missions ({i+1})"), self.player), lambda state: state.has("4-1 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-2 All Missions ({i+1})"), self.player), lambda state: state.has("4-2 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-3 All Missions ({i+1})"), self.player), lambda state: state.has("4-3 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-4 All Missions ({i+1})"), self.player), lambda state: state.has("4-4 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-5 All Missions ({i+1})"), self.player), lambda state: state.has("4-5 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-6 All Missions ({i+1})"), self.player), lambda state: state.has("4-6 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-7 All Missions ({i+1})"), self.player), lambda state: state.has("4-7 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-8 All Missions ({i+1})"), self.player), lambda state: state.has("4-8 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location((f"4-9 All Missions ({i+1})"), self.player), lambda state: state.has("4-9 Unlocked", self.player, 1))
set_rule(self.multiworld.get_location(("Kronos Blue Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 2))
set_rule(self.multiworld.get_location(("Kronos Red Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 5))
set_rule(self.multiworld.get_location(("Kronos Green Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 5)
and state.has("Kronos Memory Treasure", self.player, 3) and state.has("Progressive Chaos Emerald", self.player, 2))
set_rule(self.multiworld.get_location(("Kronos Yellow Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 13))
set_rule(self.multiworld.get_location(("Kronos Cyan Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 13)
and state.has("Kronos Memory Treasure", self.player, 6) and state.has("Progressive Chaos Emerald", self.player, 4))
set_rule(self.multiworld.get_location(("Kronos White Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 20))
if(self.options.goal > 0):
set_rule(self.multiworld.get_location(("Ares Blue Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 7))
set_rule(self.multiworld.get_location(("Ares Red Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 14))
set_rule(self.multiworld.get_location(("Ares Green Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 14) and state.has("Progressive Chaos Emerald", self.player, 8))
set_rule(self.multiworld.get_location(("Ares Yellow Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 20))
set_rule(self.multiworld.get_location(("Ares Cyan Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 20) and state.has("Progressive Chaos Emerald", self.player, 10))
set_rule(self.multiworld.get_location(("Ares White Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 24))
if(self.options.goal > 1):
set_rule(self.multiworld.get_location(("Chaos Blue Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 7))
set_rule(self.multiworld.get_location(("Chaos Red Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 14))
set_rule(self.multiworld.get_location(("Chaos Green Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 14) and state.has("Progressive Chaos Emerald", self.player, 14))
set_rule(self.multiworld.get_location(("Chaos Yellow Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 20))
set_rule(self.multiworld.get_location(("Chaos Cyan Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 20) and state.has("Progressive Chaos Emerald", self.player, 16))
set_rule(self.multiworld.get_location(("Chaos White Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 25))
if(self.options.goal > 2):
set_rule(self.multiworld.get_location(("Ouranos Blue Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 3))
set_rule(self.multiworld.get_location(("Ouranos Red Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 9))
set_rule(self.multiworld.get_location(("Ouranos Green Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 16))
set_rule(self.multiworld.get_location(("Ouranos Yellow Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 23))
set_rule(self.multiworld.get_location(("Ouranos Cyan Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 30))
set_rule(self.multiworld.get_location(("Ouranos White Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 33))
if(self.options.goal > 0):
set_rule(self.multiworld.get_entrance("Ares Entrance", self.player), lambda state: state.has("Progressive Chaos Emerald", self.player, 6)
and state.has("Kronos Memory Treasure", self.player, 9) and state.has("Kronos Vault Key", self.player, 20)
and state.has("Stomp Attack", self.player) and state.has("Parry", self.player))
if(self.options.goal > 1):
set_rule(self.multiworld.get_entrance("Chaos Entrance", self.player),
lambda state: state.has("Progressive Chaos Emerald", self.player, 12) and state.has("Ares Memory Treasure", self.player, 32) and state.has("Ares Vault Key", self.player, 24))
if(self.options.goal > 2):
set_rule(self.multiworld.get_entrance("Ouranos Entrance", self.player),
lambda state: state.has("Progressive Chaos Emerald", self.player, 18) and state.has("Chaos Memory Treasure", self.player, 20) and state.has("Chaos Vault Key", self.player, 25))
if(self.options.goal == 0):
set_rule(self.multiworld.get_location("Defeat Giganto", self.player), lambda state: state.has("Progressive Chaos Emerald", self.player, 6)
and state.has("Kronos Memory Treasure", self.player, 9) and state.has("Kronos Vault Key", self.player, 20)
and state.has("Stomp Attack", self.player) and state.has("Parry", self.player))
if(self.options.goal == 1):
set_rule(self.multiworld.get_location("Defeat Wyvern", self.player), lambda state: state.has("Progressive Chaos Emerald", self.player, 12)
and state.has("Ares Memory Treasure", self.player, 32) and state.has("Ares Vault Key", self.player, 24))
if(self.options.goal == 2):
set_rule(self.multiworld.get_location("Defeat Knight", self.player),
lambda state: state.has("Progressive Chaos Emerald", self.player, 18) and state.has("Chaos Memory Treasure", self.player, 20) and state.has("Chaos Vault Key", self.player, 25))
if(self.options.goal == 3):
set_rule(self.multiworld.get_location("Defeat Supreme", self.player),
lambda state: state.has("Progressive Chaos Emerald", self.player, 24) and state.has("Ouranos Memory Treasure", self.player, 24) and state.has("Ouranos Vault Key", self.player, 33))
def create_items(self) -> None:
numItems = 0
for name, quantity in kronos_amount.items():
for i in range(quantity):
item = self.create_item(name)
self.multiworld.itempool.append(item)
numItems += 1
if self.options.goal > 0:
for name, quantity in ares_amount.items():
for i in range(quantity):
item = self.create_item(name)
self.multiworld.itempool.append(item)
numItems += 1
if self.options.goal > 1:
for name, quantity in chaos_amount.items():
for i in range(quantity):
item = self.create_item(name)
self.multiworld.itempool.append(item)
numItems += 1
if self.options.goal > 2:
for name, quantity in ouranos_amount.items():
for i in range(quantity):
item = self.create_item(name)
self.multiworld.itempool.append(item)
numItems += 1
filler = len(self.multiworld.get_locations(self.player)) - numItems
for _ in range(filler):
name = self.random.choices(list(fillers.keys()), weights = list(fillers.values()))[0]
item = self.create_item(name)
self.multiworld.itempool.append(item)
def create_item(self, name: str) -> Item:
item_data = item_list[name]
item = SonicFrontiersItem(name, item_data.item_class, item_data.id, self.player)
return item
def fill_slot_data(self) -> Dict[str, Any]:
return {
"death_link": self.options.death_link.value,
"goal": self.options.goal.value,
"memory_token_sanity": self.options.memory_token_sanity.value,
"cyberspace_times": self.options.cyberspace_times.value,
"music_notes": self.options.music_notes.value,
"challenge_kocos": self.options.challenge_kocos.value,
"purple_coin_sanity": self.options.purple_coin_sanity.value,
"koco_sanity": self.options.koco_sanity.value,
}
+1
View File
@@ -0,0 +1 @@
stop making me do this
+157
View File
@@ -0,0 +1,157 @@
from typing import NamedTuple
from BaseClasses import Item, ItemClassification
class SonicFrontiersItem(Item):
game: str = "Sonic Frontiers"
class SonicFrontiersItemData(NamedTuple):
id: int
item_class: ItemClassification = ItemClassification.progression
offset: int = 101000
item_list = {
"Kronos Memory Token": SonicFrontiersItemData(offset, ItemClassification.filler),
"Kronos Portal Gear": SonicFrontiersItemData(offset + 1, ItemClassification.filler),
"Kronos Vault Key": SonicFrontiersItemData(offset + 2, ItemClassification.progression),
"Ares Memory Token": SonicFrontiersItemData(offset + 3, ItemClassification.filler),
"Ares Portal Gear": SonicFrontiersItemData(offset + 4, ItemClassification.filler),
"Ares Vault Key": SonicFrontiersItemData(offset + 5, ItemClassification.progression),
"Chaos Memory Token": SonicFrontiersItemData(offset + 6, ItemClassification.filler),
"Chaos Portal Gear": SonicFrontiersItemData(offset + 7, ItemClassification.filler),
"Chaos Vault Key": SonicFrontiersItemData(offset + 8, ItemClassification.progression),
"Ouranos Memory Token": SonicFrontiersItemData(offset + 9, ItemClassification.filler),
"Ouranos Portal Gear": SonicFrontiersItemData(offset + 10, ItemClassification.filler),
"Ouranos Vault Key": SonicFrontiersItemData(offset + 11, ItemClassification.progression),
"Progressive Chaos Emerald": SonicFrontiersItemData(offset + 12, ItemClassification.progression),
"Kronos Memory Treasure": SonicFrontiersItemData(offset + 36, ItemClassification.progression),
"Ares Memory Treasure": SonicFrontiersItemData(offset + 37, ItemClassification.progression),
"Chaos Memory Treasure": SonicFrontiersItemData(offset + 38, ItemClassification.progression),
"Ouranos Memory Treasure": SonicFrontiersItemData(offset + 39, ItemClassification.progression),
"Red Power Seed": SonicFrontiersItemData(offset + 40, ItemClassification.filler),
"Blue Power Seed": SonicFrontiersItemData(offset + 41, ItemClassification.filler),
"Nothing!": SonicFrontiersItemData(offset + 42, ItemClassification.filler),
"Phantom Rush": SonicFrontiersItemData(offset + 43, ItemClassification.useful),
"Air Trick":SonicFrontiersItemData(offset + 44, ItemClassification.useful),
"Stomp Attack": SonicFrontiersItemData(offset + 45, ItemClassification.progression),
"Quick Cyloop": SonicFrontiersItemData(offset + 46, ItemClassification.useful),
"Spin Dash": SonicFrontiersItemData(offset + 47, ItemClassification.useful),
"Sonic Boom": SonicFrontiersItemData(offset + 48, ItemClassification.useful),
"Parry": SonicFrontiersItemData(offset + 49, ItemClassification.progression),
"Homing Shot": SonicFrontiersItemData(offset + 50, ItemClassification.useful),
"Spin Slash": SonicFrontiersItemData(offset + 51, ItemClassification.useful),
"Recovery Smash": SonicFrontiersItemData(offset +52, ItemClassification.useful),
"Cyclone Kick": SonicFrontiersItemData(offset + 53, ItemClassification.useful),
"Cross Slash": SonicFrontiersItemData(offset + 54, ItemClassification.useful),
"Grand Slam": SonicFrontiersItemData(offset + 55, ItemClassification.useful),
"1-2 Unlocked": SonicFrontiersItemData(offset + 56,ItemClassification.progression),
"1-3 Unlocked": SonicFrontiersItemData(offset + 57,ItemClassification.progression),
"1-4 Unlocked": SonicFrontiersItemData(offset + 58,ItemClassification.progression),
"1-5 Unlocked": SonicFrontiersItemData(offset + 59,ItemClassification.progression),
"1-6 Unlocked": SonicFrontiersItemData(offset + 60,ItemClassification.progression),
"1-7 Unlocked": SonicFrontiersItemData(offset + 61,ItemClassification.progression),
"2-1 Unlocked": SonicFrontiersItemData(offset + 62,ItemClassification.progression),
"2-2 Unlocked": SonicFrontiersItemData(offset + 63,ItemClassification.progression),
"2-3 Unlocked": SonicFrontiersItemData(offset + 64,ItemClassification.progression),
"2-4 Unlocked": SonicFrontiersItemData(offset + 65,ItemClassification.progression),
"2-5 Unlocked": SonicFrontiersItemData(offset + 66,ItemClassification.progression),
"2-6 Unlocked": SonicFrontiersItemData(offset + 67,ItemClassification.progression),
"2-7 Unlocked": SonicFrontiersItemData(offset + 68,ItemClassification.progression),
"3-1 Unlocked": SonicFrontiersItemData(offset + 69,ItemClassification.progression),
"3-2 Unlocked": SonicFrontiersItemData(offset + 70,ItemClassification.progression),
"3-3 Unlocked": SonicFrontiersItemData(offset + 71,ItemClassification.progression),
"3-4 Unlocked": SonicFrontiersItemData(offset + 72,ItemClassification.progression),
"3-5 Unlocked": SonicFrontiersItemData(offset + 73,ItemClassification.progression),
"3-6 Unlocked": SonicFrontiersItemData(offset + 74,ItemClassification.progression),
"3-7 Unlocked": SonicFrontiersItemData(offset + 75,ItemClassification.progression),
"4-1 Unlocked": SonicFrontiersItemData(offset + 76,ItemClassification.progression),
"4-2 Unlocked": SonicFrontiersItemData(offset + 78,ItemClassification.progression),
"4-3 Unlocked": SonicFrontiersItemData(offset + 79,ItemClassification.progression),
"4-4 Unlocked": SonicFrontiersItemData(offset + 80,ItemClassification.progression),
"4-5 Unlocked": SonicFrontiersItemData(offset + 81,ItemClassification.progression),
"4-6 Unlocked": SonicFrontiersItemData(offset + 82,ItemClassification.progression),
"4-7 Unlocked": SonicFrontiersItemData(offset + 83,ItemClassification.progression),
"4-8 Unlocked": SonicFrontiersItemData(offset + 84,ItemClassification.progression),
"4-9 Unlocked": SonicFrontiersItemData(offset + 85,ItemClassification.progression),
"Victory": SonicFrontiersItemData(offset + 86,ItemClassification.progression),
}
kronos_amount = {
"Kronos Memory Treasure": 9,
"Kronos Vault Key": 20,
"Progressive Chaos Emerald": 6,
"1-2 Unlocked" : 1,
"1-3 Unlocked" : 1,
"1-4 Unlocked" : 1,
"1-5 Unlocked" : 1,
"1-6 Unlocked" : 1,
"1-7 Unlocked" : 1,
"Phantom Rush": 1,
"Air Trick": 1,
"Stomp Attack": 1,
"Quick Cyloop": 1,
"Spin Dash": 1,
"Sonic Boom": 1,
"Parry": 1,
"Homing Shot": 1,
"Spin Slash": 1,
"Recovery Smash": 1,
"Cyclone Kick": 1,
"Cross Slash": 1,
"Grand Slam": 1,
}
ares_amount = {
"Ares Memory Treasure": 32,
"Ares Vault Key": 24,
"Progressive Chaos Emerald": 6,
"2-1 Unlocked" : 1,
"2-2 Unlocked" : 1,
"2-3 Unlocked" : 1,
"2-4 Unlocked" : 1,
"2-5 Unlocked" : 1,
"2-6 Unlocked" : 1,
"2-7 Unlocked" : 1,
}
chaos_amount = {
"Chaos Memory Treasure": 20,
"Chaos Vault Key": 25,
"Progressive Chaos Emerald": 6,
"3-1 Unlocked" : 1,
"3-2 Unlocked" : 1,
"3-3 Unlocked" : 1,
"3-4 Unlocked" : 1,
"3-5 Unlocked" : 1,
"3-6 Unlocked" : 1,
"3-7 Unlocked" : 1,
}
ouranos_amount = {
"4-1 Unlocked" : 1,
"4-2 Unlocked" : 1,
"4-3 Unlocked" : 1,
"4-4 Unlocked" : 1,
"4-5 Unlocked" : 1,
"4-6 Unlocked" : 1,
"4-7 Unlocked" : 1,
"4-8 Unlocked" : 1,
"4-9 Unlocked" : 1,
"Ouranos Memory Treasure": 26,
"Ouranos Vault Key": 33,
"Progressive Chaos Emerald": 6,
}
fillers = {
"Red Power Seed": 15,
"Blue Power Seed": 15,
"Nothing!": 10,
"Kronos Portal Gear": 5,
"Kronos Vault Key": 5,
"Ares Portal Gear": 4,
"Ares Vault Key": 4,
"Chaos Vault Key": 3,
"Chaos Portal Gear": 3,
"Ouranos Portal Gear": 2,
"Ouranos Vault Key": 2,
}
#
#traps = {
# "Water",
# "Timescale";
+344
View File
@@ -0,0 +1,344 @@
from BaseClasses import Location
import typing
class AdvData(typing.NamedTuple):
id: typing.Optional[int]
class SonicFrontiersAdvancement(Location):
game: str = "Sonic Frontiers"
def create_locations():
counter = 0
kronosItems = [
"Kronos Memory Token", "Kronos Memory Token Treasure", "Kronos Portal Gear", "Kronos Vault Key" "Kronos Map Challenge"
]
aresItems = [
"Ares Memory Token", "Ares Memory Token Treasure", "Ares Portal Gear", "Ares Vault Key", "Ares Map Challenge"
]
chaosItems = [
"Chaos Memory Token", "Chaos Memory Token Treasure", "Chaos Portal Gear", "Chaos Vault Key", "Chaos Map Challenge"
]
ouranosItems = [
"Ouranos Memory Token", "Ouranos Portal Gear", "Ouranos Vault Key", "Ouranos Map Challenge"
]
kronosStages = [
"1-1",
"1-2",
"1-3",
"1-4",
"1-5",
"1-6",
"1-7",
]
aresStages = [
"2-1",
"2-2",
"2-3",
"2-4",
"2-5",
"2-6",
"2-7",
]
chaosStages = [
"3-1",
"3-2",
"3-3",
"3-4",
"3-5",
"3-6",
"3-7",
]
ouranosStages = [
"4-1",
"4-2",
"4-3",
"4-4",
"4-5",
"4-6",
"4-7",
"4-8",
"4-9",
]
skills = [
"Phantom Rush", "Air Trick", "Stomp Attack",
"Quick Cyloop", "Loop Kick", "Sonic Boom",
"Wild Rush", "Homing Shot", "Spin Slash",
"Recovery Smash", "Cyclone Kick","Cross Slash",
"Grand Slam"
]
kronosEmeralds = [
"Kronos Blue Emerald",
"Kronos Red Emerald",
"Kronos Green Emerald",
"Kronos Yellow Emerald",
"Kronos Cyan Emerald",
"Kronos White Emerald"
]
aresEmeralds = [
"Ares Blue Emerald",
"Ares Red Emerald",
"Ares Green Emerald",
"Ares Yellow Emerald",
"Ares Cyan Emerald",
"Ares White Emerald"
]
chaosEmeralds = [
"Chaos Blue Emerald",
"Chaos Red Emerald",
"Chaos Green Emerald",
"Chaos Yellow Emerald",
"Chaos Cyan Emerald",
"Chaos White Emerald"
]
ouranosEmeralds = [
"Ouranos Blue Emerald",
"Ouranos Red Emerald",
"Ouranos Green Emerald",
"Ouranos Yellow Emerald",
"Ouranos Cyan Emerald",
"Ouranos White Emerald"
]
sonicItems = [
"Skill Points (200)",
"Red Power Seed",
"Blue Power Seed",
"Kocos (20)"
]
##
## Kronos
##
#10000
counter = 0
for i in range(91):
kronosMemoryTokenSet[f"Kronos Memory Token {counter+1}"] = AdvData(kronosOff + counter)
counter += 1
#10091
counter = 0
for i in range(9):
kronosRegion[f"Kronos Memory Treasure {counter+1}"] = AdvData(kronosOff + tokenDigOffset + counter)
counter += 1
counter = 0
for i in range(17):
kronosRegion[f"Kronos Portal Gear {i+1}"] = AdvData(kronosOff + gearOffset + counter)
counter += 1
counter = 0
for i in range(5):
kronosRegion[f"Kronos Vault Key {1+i}"] = AdvData(kronosOff+keyOffset+counter)
counter += 1
counter = 0
for emerald in kronosEmeralds:
kronosRegion[f"{emerald}"] = AdvData(kronosOff+counter+emeraldOffset)
counter += 1
counter = 0
mapCounter = 1
for i in range(25):
if(mapCounter< 10):
kronosRegion[f"Map Challenge M-00{mapCounter}"] = AdvData(kronosOff+counter+mapChallengeOffset)
counter += 1
mapCounter += 1
else:
kronosRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(kronosOff+counter+mapChallengeOffset)
counter += 1
mapCounter += 1
for i in range(8):
kronosNewKocoSet[f"Kronos Challenge Koco {i+1}"] = AdvData(kronosOff+i+newKocoOffset)
for i in range(13):
kronosMusicSet[f"Kronos Music Note {i+1}"] = AdvData(kronosOff+i+musicOffset)
for i in range(65):
kronosPurpleSet[f"Kronos Purple Coin {i+1}"] = AdvData(kronosOff + i + purpleCoinOffset)
for i in range(274):
kronosKocoSet[f"Kronos Koco {i+1}"] = AdvData(kronosOff + i + kocoOffset)
kronosRegion["Defeat Giganto"] = AdvData(kronosOff+bossOffset)
##
## Ares
##
counter = 0
for i in range(285):
aresMemoryTokenSet[f"Ares Memory Token {i+1}"] = AdvData(aresOff + counter)
counter += 1
for i in range(26):
aresRegion[f"Ares Memory Treasure {i+1}"] = AdvData(aresOff + tokenDigOffset + i)
counter = 0
for i in range(11):
aresRegion[f"Ares Portal Gear {i+1}"] = AdvData(aresOff + gearOffset+counter)
counter += 1
counter = 0
for i in range(7):
aresRegion[f"Ares Vault Key {1+i}"] = AdvData(aresOff+keyOffset+counter)
counter += 1
counter = 0
for emerald in aresEmeralds:
aresRegion[f"{emerald}"] = AdvData(aresOff+emeraldOffset+counter)
counter += 1
counter = 0
for i in range(29):
aresRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(aresOff+mapChallengeOffset+counter)
counter += 1
mapCounter += 1
for i in range(8):
aresNewKocoSet[f"Ares Challenge Koco {i+1}"] = AdvData(aresOff+i+newKocoOffset)
for i in range(13):
aresMusicSet[f"Ares Music Note {i+1}"] = AdvData(aresOff+i+musicOffset)
for i in range(148):
aresPurpleSet[f"Ares Purple Coin {i+1}"] = AdvData(aresOff + i + purpleCoinOffset)
for i in range(360):
aresKocoSet[f"Ares Koco {i+1}"] = AdvData(aresOff + i + kocoOffset)
aresRegion["Defeat Wyvern"] = AdvData(aresOff+bossOffset)
##
## Chaos
##
for i in range(252):
chaosMemoryTokenSet[f"Chaos Memory Token {counter+1}"] = AdvData(chaosOff + i)
for i in range(20):
chaosRegion[f"Chaos Memory Treasure {i+1}"] = AdvData(chaosOff + tokenDigOffset+i)
counter = 0
for emerald in chaosEmeralds:
chaosRegion[f"{emerald}"] = AdvData(chaosOff+emeraldOffset+counter)
counter += 1
for i in range(8):
chaosRegion[f"Chaos Vault Key {1+i}"] = AdvData(chaosOff+keyOffset+i)
for i in range(13):
chaosRegion[f"Chaos Portal Gear {i+1}"] = AdvData(chaosOff +gearOffset+ i)
for i in range(24):
chaosRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(chaosOff+mapChallengeOffset+i)
mapCounter += 1
counter = 0
for i in range(8):
chaosNewKocoSet[f"Chaos Challenge Koco {i+1}"] = AdvData(chaosOff+i+newKocoOffset)
for i in range(13):
chaosMusicSet[f"Chaos Music Note {i+1}"] = AdvData(chaosOff+i+musicOffset)
for i in range(188):
chaosPurpleSet[f"Chaos Purple Coin {i+1}"] = AdvData(chaosOff+purpleCoinOffset+i)
for i in range(356):
chaosKocoSet[f"Chaos Koco {i+1}"] = AdvData(chaosOff+kocoOffset+i)
chaosRegion["Defeat Knight"] = AdvData(chaosOff+bossOffset)
##
## Ouranos
##
counter = 0
for i in range(200):
ouranosMemoryTokenSet[f"Ouranos Memory Token {counter+1}"] = AdvData(ouranosOff + counter)
counter += 1
for i in range(16):
ouranosRegion[f"Ouranos Memory Treasure {i+1}"] = AdvData(ouranosOff + tokenDigOffset+i)
counter = 0
for i in range(21):
ouranosRegion[f"Ouranos Portal Gear {i+1}"] = AdvData(ouranosOff + gearOffset+counter)
counter += 1
counter = 0
for i in range(8):
ouranosRegion[f"Ouranos Vault Key {1+i}"] = AdvData(ouranosOff+keyOffset+counter)
counter += 1
counter = 0
for emerald in ouranosEmeralds:
ouranosRegion[f"{emerald}"] = AdvData(ouranosOff+emeraldOffset+counter)
counter += 1
counter = 0
for i in range(27):
if(mapCounter > 100):
ouranosRegion[f"Map Challenge M-{mapCounter}"] = AdvData(ouranosOff+mapChallengeOffset+counter)
counter += 1
mapCounter += 1
else:
ouranosRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(ouranosOff+mapChallengeOffset+counter)
counter += 1
mapCounter += 1
counter = 0
for stage in kronosStages:
for i in range(7):
kronosRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset + counter)
counter += 1
for stage in aresStages:
for i in range(7):
aresRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset + counter)
counter += 1
for stage in chaosStages:
for i in range(7):
chaosRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset+counter)
counter += 1
for stage in ouranosStages:
for i in range(7):
ouranosRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset+ counter)
counter += 1
for i in range(8):
ouranosNewKocoSet[f"Ouranos Challenge Koco {i+1}"] = AdvData(ouranosOff+i+newKocoOffset)
for i in range(13):
ouranosMusicSet[f"Ouranos Music Note {i+1}"] = AdvData(ouranosOff+i+musicOffset)
for i in range(317):
ouranosPurpleSet[f"Ouranos Purple Coin {i+1}"] = AdvData(ouranosOff+purpleCoinOffset+i)
for i in range(358):
ouranosKocoSet[f"Ouranos Koco {i+1}"] = AdvData(ouranosOff+kocoOffset+i)
ouranosRegion["Defeat Supreme"] = AdvData(ouranosOff+bossOffset)
##Essentially add all of these into a region. How do I add them? idfk
kronosRegion = {}
aresRegion = {}
chaosRegion = {}
ouranosRegion = {}
victoryRegion = {}
kronosMemoryTokenSet = {}
aresMemoryTokenSet = {}
chaosMemoryTokenSet = {}
ouranosMemoryTokenSet = {}
kronosPurpleSet = {}
aresPurpleSet = {}
chaosPurpleSet = {}
ouranosPurpleSet = {}
kronosKocoSet = {}
aresKocoSet = {}
chaosKocoSet = {}
ouranosKocoSet = {}
kronosNewKocoSet = {}
aresNewKocoSet = {}
chaosNewKocoSet = {}
ouranosNewKocoSet = {}
kronosMusicSet = {}
aresMusicSet = {}
chaosMusicSet = {}
ouranosMusicSet = {}
kronosOff: int = 10000
aresOff: int = 20000
chaosOff: int = 30000
ouranosOff: int = 40000
cyberspaceOffset: int = 50000
tokenDigOffset: int = 500
emeraldOffset: int = 1000
musicOffset: int = 1500
gearOffset: int = 2000
newKocoOffset: int = 2500
keyOffset: int = 3000
purpleCoinOffset: int = 4000 #10000 + i + 4000
mapChallengeOffset: int = 5000
kocoOffset: int = 6000
bossOffset: int = 7000
ringOffset: int = 100000
create_locations()
kronosItems = kronosRegion | kronosMusicSet | kronosKocoSet | kronosMemoryTokenSet | kronosNewKocoSet | kronosPurpleSet
aresItems = aresRegion | aresMusicSet | aresKocoSet | aresMemoryTokenSet | aresNewKocoSet | aresPurpleSet
chaosItems = chaosRegion | chaosMusicSet | chaosKocoSet | chaosMemoryTokenSet | chaosNewKocoSet | chaosPurpleSet
ouranosItems = ouranosRegion | ouranosMusicSet | ouranosKocoSet | ouranosMemoryTokenSet | ouranosNewKocoSet | ouranosPurpleSet
all_items = kronosItems | aresItems | chaosItems | ouranosItems
+71
View File
@@ -0,0 +1,71 @@
from dataclasses import dataclass
from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCommonOptions, ExcludeLocations # , OptionGroup
class Goal(Choice):
"""
Which Titan to defeat in order to complete the randomizer
Note: Only Giganto/Kronos Island for this release
"""
display_name = "Goal"
option_defeat_giganto = 0
#option_defeat_wyvern = 1
#option_defeat_knight = 2
#option_defeat_supreme = 3
default = 0
class MemoryTokenSanity(Toggle):
"""
Set whether All Memory Tokens should be locations
"""
display_name = "Memory Token Sanity"
default = 0
class MapChallenges(DefaultOnToggle):
display_name = "Map Challenge Sanity"
class HarderCyberspaceTimes(Toggle):
"""
This makes all Cyberspace stages have a harder S-Rank requirement.
Note: This is meant for speedrunners, do not enable this unless you're up for a challenge.
"""
display_name = "Harder Cyberspace Challenge Times"
default = 0
class MusicNotes(Toggle):
"""
Set whether Music Notes should be locations
"""
display_name = "Music Notes"
default = 0
class ChallengeKocos(Toggle):
"""
Set whether Challenge Kocos should be locations
"""
display_name = "Challenge Kocos"
default = 0
class CyberspaceStages(Toggle):
display_name = "Cyberspace Stages Missions"
default = 0
class PurpleCoinSanity(Toggle):
"""
Set whether All Purple Coins should be locations
"""
display_name = "Purple Coin Sanity"
default = 0
class KocoSanity(Toggle):
"""
Set whether All Kocos should be locations
"""
display_name = "Koco Sanity"
default = 0
@dataclass
class SonicFrontiersOptions(PerGameCommonOptions):
goal: Goal
death_link: DeathLink
memory_token_sanity: MemoryTokenSanity
cyberspace_times: HarderCyberspaceTimes
music_notes: MusicNotes
challenge_kocos: ChallengeKocos
purple_coin_sanity: PurpleCoinSanity
koco_sanity: KocoSanity
+4 -4
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -1,2 +1,2 @@
maseya-z3pr>=1.0.0rc1
xxtea>=3.0.0
maseya-z3pr==1.0.0rc1
xxtea==3.7.0
File diff suppressed because it is too large Load Diff
+666
View File
@@ -0,0 +1,666 @@
import random
import time
import worlds._bizhawk as bizhawk
from .RAMAddress import RAM
from typing import TYPE_CHECKING, Optional, Dict, Set, ClassVar, Any, Tuple, Union
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
class ApeEscapeMemoryInput:
def __init__(self, bizhawk_client_context: "BizHawkClientContext"):
self.bizhawk_client_context = bizhawk_client_context
self.all_digital_buttons = list(RAM.BUTTON_BIT_MAP.keys())
# Combined list of all possible inputs for the handler to choose from (includes Pseudo-Right Joystick)
self.all_inputs = self.all_digital_buttons + [RAM.RIGHT_JOYSTICK_PSEUDO_INPUT]
async def set_inputs(self, desired_inputs: dict):
"""
Constructs and sends memory write requests to BizHawk to set controller inputs.
This method sends writes ONLY for the inputs explicitly present in desired_inputs.
If an input (digital or analog) is NOT in desired_inputs, it's assumed to be released,
and its memory address will NOT be explicitly written by this client for that frame.
This relies on the game's natural input polling to reset non-written inputs.
Args:
desired_inputs (dict): A dictionary where keys are input names (e.g., "P1 X", "P1 R_X")
and values are True/False for digital buttons, or 0-255 for analog.
"""
writes_list = []
# --- 1. Construct Digital Button Bytes ---
# Start with all bits set to 1 (all digital buttons unpressed in inverse logic)
# This will be the base for the digital button memory write.
new_digital_word = 0xFFFF
for input_name, state in desired_inputs.items():
if input_name in RAM.BUTTON_BIT_MAP:
bit_pos = RAM.BUTTON_BIT_MAP[input_name]
if state is True: # If button is desired to be pressed
# Clear its corresponding bit (set to 0) in the 16-bit word
new_digital_word &= ~(1 << bit_pos)
# If state is False, leave the bit as 1 (unpressed), which is default in new_digital_word.
# Split the 16-bit word into two 8-bit integers
byte_low_value = new_digital_word & 0xFF
byte_high_value = (new_digital_word >> 8) & 0xFF
# Add digital button writes to the list
writes_list.append((RAM.BUTTON_BYTE_ADDR_LOW, [byte_low_value], "MainRAM"))
writes_list.append((RAM.BUTTON_BYTE_ADDR_HIGH, [byte_high_value], "MainRAM"))
# --- 2. Construct Analog Stick Bytes ---
analog_axis_addresses = {
"P1 R_Y": RAM.ANALOG_START_ADDR,
"P1 R_X": RAM.ANALOG_START_ADDR + 1,
"P1 L_Y": RAM.ANALOG_START_ADDR + 2,
"P1 L_X": RAM.ANALOG_START_ADDR + 3,
}
# Iterate through desired_inputs and add writes only for present analog axes.
for stick_axis, value_to_set in desired_inputs.items():
if stick_axis in analog_axis_addresses:
clamped_value = max(0, min(255, value_to_set))
writes_list.append((analog_axis_addresses[stick_axis], [clamped_value], "MainRAM"))
# --- 3. Send all constructed writes to BizHawk ---
try:
await bizhawk.write(self.bizhawk_client_context.bizhawk_ctx, writes_list)
except Exception as e:
print(f"ERROR: Failed to send input memory writes: {e}")
raise
# --- MonkeyMashHandler class ---
class MonkeyMashHandler:
MAX_TRAP_DURATION = 20 # Maximum duration for the trap in seconds
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
self.bizhawk_client_context = bizhawk_client_context
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
self.is_active = False
self.duration = 0
self.remaining_time = 0
self.last_update = 0
self.pause = False
self.input_controller = ApeEscapeMemoryInput(
self.bizhawk_client_context) if self.bizhawk_client_context else None
self.input_frequency = 1 # Time between NEW random inputs (e.g., generate new input every 1s)
self.last_input_time = 0
self.input_hold_time = 0.5 # How long the inputs will be pressed
self.current_held_inputs = {} # Stores inputs that are currently being pressed
self.press_start_time = None # Timestamp when the current brief press started
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
def activate_monkey(self, duration_seconds: int):
if not self.is_active:
self.is_active = True
self.duration = duration_seconds
self.remaining_time = duration_seconds
self.last_update = time.time()
self.last_input_time = 0
self.current_held_inputs = {}
self.press_start_time = None
print(f"Monkey Button Mash activated for {duration_seconds} seconds.")
else:
new_remaining_time = self.remaining_time + duration_seconds
self.remaining_time = min(new_remaining_time, self.MAX_TRAP_DURATION)
self.duration = self.remaining_time
print(f"Monkey Button Mash extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_TRAP_DURATION}s)")
self.sentMessage = False
async def send_monkey_inputs(self):
if self.input_controller is None or self.bizhawk_client_context.bizhawk_ctx.connection_status != bizhawk.ConnectionStatus.CONNECTED:
print("Error: BizHawk connection not ready for inputs. Cannot send inputs.")
self.is_active = False
self.current_held_inputs = {}
self.press_start_time = None
return
current_time = time.time()
if self.pause:
self.current_held_inputs = {}
self.press_start_time = None
self.last_update = current_time
return
if self.is_active and self.remaining_time > 0:
elapsed_time_since_last_update = current_time - self.last_update
self.remaining_time -= elapsed_time_since_last_update
self.last_update = current_time
if self.remaining_time <= 0:
self.remaining_time = 0
# State 1: It's time to generate a NEW input sequence (press for hold_time)
if current_time - self.last_input_time >= self.input_frequency:
newly_generated_inputs = {}
# Randomly select MULTIPLE inputs (digital buttons or the "Right Joystick" pseudo-input)
num_inputs_to_change = random.randint(1, 3)
inputs_to_change = random.sample(self.input_controller.all_inputs, num_inputs_to_change)
for input_name in inputs_to_change:
if input_name == RAM.RIGHT_JOYSTICK_PSEUDO_INPUT: # Handle the Right Joystick as one unit
# Randomly pick one axis to be 0xFF, the other to be random
axis_to_be_max = random.choice(["P1 R_Y", "P1 R_X"])
axis_to_be_random = "P1 R_Y" if axis_to_be_max == "P1 R_X" else "P1 R_X"
newly_generated_inputs[axis_to_be_max] = 0xFF
newly_generated_inputs[axis_to_be_random] = random.randint(0x00, 0xFF)
# Left Joystick values are not touched here.
else:
# For digital buttons, set to pressed (True)
newly_generated_inputs[input_name] = True
self.current_held_inputs = newly_generated_inputs
self.press_start_time = current_time
self.last_input_time = current_time
#print(f"[{self.remaining_time:.2f}s remaining] Initiating brief press of: {self.current_held_inputs}")
# State 2: Check if a brief press is active and within its hold window
if self.press_start_time is not None and (current_time - self.press_start_time < self.input_hold_time):
try:
await self.input_controller.set_inputs(self.current_held_inputs)
except bizhawk.NotConnectedError:
print("BizHawk connection lost during input hold. Deactivating monkey.")
self.is_active = False
self.current_held_inputs = {}
self.press_start_time = None
return
except Exception as e:
print(f"Failed to hold inputs via memory write: {e}")
self.is_active = False
print("Monkey Button Mash deactivated due to input hold error.")
return
else:
# State 3: If no press is active, or hold time has expired, stop sending *active* inputs.
if self.current_held_inputs or self.press_start_time is not None:
print(f"[{self.remaining_time:.2f}s remaining] Releasing inputs. Held: {self.current_held_inputs}")
self.current_held_inputs = {} # Clear tracker for *briefly held* inputs
self.press_start_time = None # Reset press start time
if self.is_active and self.remaining_time <= 0:
self.is_active = False
self.remaining_time = 0
self.current_held_inputs = {}
self.press_start_time = None
print("Monkey Button Mash finished.")
class RainbowCookieHandler:
"""
Manages the state and effects of the Rainbow Cookie power-up.
When active, makes Spike invincible and activates his golden form.
"""
MAX_DURATION = 20 # Maximum duration for the Rainbow Cookie in seconds
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
self.bizhawk_client_context = bizhawk_client_context
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
self.is_active = False # True if Rainbow Cookie effects are currently active
self.duration = 0 # The initial or current duration set for the cookie
self.remaining_time = 0 # How much time is left for the effects
self.last_update = 0 # Timestamp of the last update, for calculating elapsed time
self.pause = False # Flag to pause the cookie's timer/effects
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
async def activate_rainbow_cookie(self, duration_seconds: int):
"""
Activates the Rainbow Cookie effects (invincibility and golden form).
If already active, extends the duration up to MAX_DURATION.
Args:
duration_seconds (int): The number of seconds to activate/extend the cookie's effects.
"""
if not self.is_active:
# First activation
self.is_active = True
self.duration = duration_seconds
self.remaining_time = duration_seconds
self.last_update = time.time()
print(f"Rainbow Cookie activated for {duration_seconds} seconds.")
await self._apply_effects(True) # Apply effects immediately
else:
# Extend existing duration
new_remaining_time = self.remaining_time + duration_seconds
self.remaining_time = min(new_remaining_time, self.MAX_DURATION)
self.duration = self.remaining_time # Update current duration if extended
print(
f"Rainbow Cookie extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_DURATION}s)")
self.sentMessage = False
async def _apply_effects(self, enable: bool):
"""
Internal method to apply or remove the Rainbow Cookie's effects
by writing to BizHawk memory addresses.
Args:
enable (bool): If True, enables effects; if False, disables them.
"""
if self.bizhawk_context is None or self.bizhawk_context.connection_status != bizhawk.ConnectionStatus.CONNECTED:
print("Warning: BizHawk not connected. Cannot apply/remove Rainbow Cookie effects.")
return
writes_list = []
invincible_value = RAM.INVINCIBLE_ON_VALUE if enable else RAM.INVINCIBLE_OFF_VALUE
golden_value = RAM.GOLDEN_ON_VALUE if enable else RAM.GOLDEN_OFF_VALUE
invincible_bytes = list(invincible_value.to_bytes(4, "little"))
writes_list.append((RAM.SPIKE_INVINCIBILITY_ADDR, invincible_bytes, "MainRAM"))
writes_list.append((RAM.SPIKE_GOLDEN_FORM_ADDR, [golden_value], "MainRAM"))
try:
await bizhawk.write(self.bizhawk_context, writes_list)
print(f"Rainbow Cookie effects {'applied' if enable else 'removed'}.")
except Exception as e:
print(f"ERROR: Failed to {'apply' if enable else 'remove'} Rainbow Cookie effects: {e}")
raise
async def update_state_and_deactivate(self):
"""
Updates the remaining time for the Rainbow Cookie.
If the duration runs out, deactivates the effects.
This method should be called periodically in the main loop of the client.
It also re-applies the golden visual effect if it's lost and the cookie is active.
"""
if not self.is_active:
return
if self.pause:
# If paused, don't decrement remaining_time, but update last_update
# to prevent a large time jump when unpaused.
self.last_update = time.time()
return
current_time = time.time()
elapsed_time_since_last_update = current_time - self.last_update
self.remaining_time -= elapsed_time_since_last_update
self.last_update = current_time
# Check and re-apply golden visual effect if it's not active but the cookie is
if self.bizhawk_context and self.bizhawk_context.connection_status == bizhawk.ConnectionStatus.CONNECTED:
try:
# Read the current value of the golden form address
current_golden_value_bytes = await bizhawk.read(self.bizhawk_context, [(RAM.SPIKE_GOLDEN_FORM_ADDR, 1, "MainRAM")])
current_golden_value = int.from_bytes(current_golden_value_bytes[0], byteorder="little")
if current_golden_value is not None and current_golden_value != RAM.GOLDEN_ON_VALUE:
print("Rainbow Cookie active, but golden visual effect lost. Reapplying...")
await self._apply_effects(True)
except Exception as e:
print(f"ERROR: Failed to read golden form address for reapplication: {e}")
# Log error but don't stop the loop for this non-critical re-application check
if self.remaining_time <= 0:
self.remaining_time = 0
self.is_active = False
print("Rainbow Cookie duration finished. Deactivating effects.")
await self._apply_effects(False) # Remove effects
class StunTrapHandler:
"""
Manages the state and effects of the Stun Trap.
When active, makes Spike invincible and activates his golden form.
"""
MAX_DURATION = 2
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
self.bizhawk_client_context = bizhawk_client_context
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
self.is_active = False # True if Rainbow Cookie effects are currently active
self.RoomType = "Special"
self.duration = 0 # The initial or current duration set for the cookie
self.remaining_time = 0 # How much time is left for the effects
self.last_update = 0 # Timestamp of the last update, for calculating elapsed time
self.pause = False # Flag to pause the cookie's timer/effects
self.lastspikestate = 0x00 # To store last SpikeState on activation
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
async def activate_StunTrap(self, duration_seconds: int,lastspikestate,currentRoom):
"""
Activates the Stun Trap effects.
If already active, extends the duration up to MAX_DURATION.
Args:
duration_seconds (int): The number of seconds to activate/extend the cookie's effects.
"""
# Only store a new lastspikestate if the trap is not activated
# (Since if it's already active we already have the last spike state)
if not self.is_active:
self.lastspikestate = lastspikestate
# First activation
self.is_active = True
read_list = []
read_list += [(RAM.SpecialRoom_CameraMode, 1, "MainRAM"),(RAM.Boss_CameraMode, 1, "MainRAM"),(RAM.Inside_CameraMode, 1, "MainRAM")]
CameraMode_reads = await bizhawk.read(self.bizhawk_context, read_list)
isSpecialRoom = int.from_bytes(CameraMode_reads[0], byteorder="little") == 0x01
isBossRoom = int.from_bytes(CameraMode_reads[1], byteorder="little") == 0x01
isInside = int.from_bytes(CameraMode_reads[2], byteorder="little") == 0x01
if isSpecialRoom:
self.RoomType = "Special"
elif isBossRoom:
self.RoomType = "Boss"
elif isInside:
self.RoomType = "Inside"
else:
self.RoomType = "Outside"
self.duration = duration_seconds
self.remaining_time = duration_seconds
self.last_update = time.time()
print(f"Stun Trap activated for {duration_seconds} seconds.")
await self._apply_effects(True,currentRoom) # Apply effects immediately
#else:
# Activate it each time, do not extend it
# Extend existing duration
#new_remaining_time = self.remaining_time + duration_seconds
#self.remaining_time = min(new_remaining_time, self.MAX_DURATION)
#self.duration = self.remaining_time # Update current duration if extended
#print(f"Stun Trap extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_DURATION}s)")
#self.sentMessage = False
async def _apply_effects(self, enable: bool,currentRoom):
"""
Internal method to apply or remove the Rainbow Cookie's effects
by writing to BizHawk memory addresses.
Args:
enable (bool): If True, enables effects; if False, disables them.
"""
if self.bizhawk_context is None or self.bizhawk_context.connection_status != bizhawk.ConnectionStatus.CONNECTED:
print("Warning: BizHawk not connected. Cannot apply/remove Stun Trap effects.")
return
writes_list = []
Spike_PosUpdatesAddress = RAM.Spike_PosUpdates
Spike_PosUpdates_keys = list(Spike_PosUpdatesAddress.keys())
Spike_PosUpdates_values = list(Spike_PosUpdatesAddress.values())
#for x in range(len(Spike_PosUpdates_keys)):
# PosUpdates_values = list(Spike_PosUpdates_values[x])
# PosUpdates_bytes = PosUpdates_values[0]
# PosUpdates_onvalue = PosUpdates_values[1].to_bytes(PosUpdates_bytes, "little")
# PosUpdates_offvalue = PosUpdates_values[2].to_bytes(PosUpdates_bytes, "little")
# PosUpdates_address = (Spike_PosUpdates_keys[x])
if enable:
writes_list += [(RAM.Spike_CanMove, 0x01.to_bytes(1,"little"), "MainRAM")]
else:
writes_list += [(RAM.Spike_CanMove, 0x00.to_bytes(1,"little"), "MainRAM")]
Spike_VelocityUpdatesAddress = RAM.Spike_VelocityUpdates
Spike_VelocityUpdates_keys = list(Spike_VelocityUpdatesAddress.keys())
Spike_VelocityUpdates_values = list(Spike_VelocityUpdatesAddress.values())
#for x in range(len(Spike_VelocityUpdates_keys)):
# VelocityUpdates_values = list(Spike_VelocityUpdates_values[x])
# VelocityUpdates_bytes = VelocityUpdates_values[0]
# VelocityUpdates_onvalue = VelocityUpdates_values[1].to_bytes(VelocityUpdates_bytes, "little")
# VelocityUpdates_offvalue = VelocityUpdates_values[2].to_bytes(VelocityUpdates_bytes, "little")
# VelocityUpdates_address = (Spike_VelocityUpdates_keys[x])
# if enable:
# writes_list += [(VelocityUpdates_address, VelocityUpdates_offvalue, "MainRAM")]
# else:
# writes_list += [(VelocityUpdates_address, VelocityUpdates_onvalue, "MainRAM")]
#LastState = self.lastspikestate
#InvalidLastStates = [0x80, 0x81, 0x82, 0x83, 0x84,0x2F,0x30,0x58]
# If enabling the Trap set it to 0x58, else set it to the last saved state
#if enable:
# Spikestate2_value = 0x58
# CameraMode = 0x00
#else:
# #If LastState is invalid,
# Spikestate2_value = 0x00 if LastState in InvalidLastStates else LastState
# CameraMode = 0x01
# self.lastspikestate = 0x00
#SpecialRooms = [30,83, 84, 87, 88, 90, 91]
#BossRooms = [item for item in RAM.bossListLocal.keys() if item not in SpecialRooms]
#if self.RoomType == "Special":
# CameraModeAddress = RAM.SpecialRoom_CameraMode
#elif self.RoomType == "Boss":
# CameraModeAddress = RAM.Boss_CameraMode
#else:
# if self.RoomType == "Inside":
# CameraModeAddress = RAM.Inside_CameraMode
# else:
# CameraModeAddress = RAM.Outside_CameraMode
#Spikestate2_bytes = list(Spikestate2_value.to_bytes(1, "little"))
#CameraMode_bytes = list(CameraMode.to_bytes(1, "little"))
#writes_list.append((RAM.spikeState2Address, Spikestate2_bytes, "MainRAM"))
#writes_list.append((CameraModeAddress, CameraMode_bytes, "MainRAM"))
try:
await bizhawk.write(self.bizhawk_context, writes_list)
print(f"Stun Trap effects {'applied' if enable else 'removed'}.")
except Exception as e:
print(f"ERROR: Failed to {'apply' if enable else 'remove'} Stun Trap effects: {e}")
raise
async def update_state_and_deactivate(self,currentRoom):
"""
Updates the remaining time for the Rainbow Cookie.
If the duration runs out, deactivates the effects.
This method should be called periodically in the main loop of the client.
It also re-applies the golden visual effect if it's lost and the cookie is active.
"""
if not self.is_active:
return
if self.pause:
# If paused, don't decrement remaining_time, but update last_update
# to prevent a large time jump when unpaused.
self.last_update = time.time()
return
current_time = time.time()
elapsed_time_since_last_update = current_time - self.last_update
self.remaining_time -= elapsed_time_since_last_update
self.last_update = current_time
if self.remaining_time <= 0:
self.remaining_time = 0
self.is_active = False
print("Stun Trap duration finished. Deactivating effects.")
await self._apply_effects(False,currentRoom) # Remove effects
class CameraRotateHandler:
"""
Manages the state and effects of the Rainbow Cookie power-up.
When active, makes Spike invincible and activates his golden form.
"""
MAX_DURATION = 40 # Maximum duration for the Rainbow Cookie in seconds
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
self.bizhawk_client_context = bizhawk_client_context
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
self.chosen_side = "Left" # Store which side the Camera Rotate is currently on
self.RoomType = "Special"
self.is_active = False # True if effects are currently active
self.duration = 0 # The initial or current duration set
self.remaining_time = 0 # How much time is left for the effects
self.last_update = 0 # Timestamp of the last update, for calculating elapsed time
self.pause = False # Flag to pause the timer/effects
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
async def activate_camera_rotate(self, duration_seconds: int, currentRoom):
"""
Activates the Rainbow Cookie effects (invincibility and golden form).
If already active, extends the duration up to MAX_DURATION.
Args:
duration_seconds (int): The number of seconds to activate/extend the cookie's effects.
"""
if not self.is_active:
# First activation
self.is_active = True
self.duration = duration_seconds
self.remaining_time = duration_seconds
self.last_update = time.time()
possibleSides = ["Left","Right"]
self.chosen_side = possibleSides[random.randint(0,1)]
print(f"Camera Rotate activated for {duration_seconds} seconds.")
else:
## Extend existing duration
new_remaining_time = self.remaining_time + duration_seconds
self.remaining_time = min(new_remaining_time, self.MAX_DURATION)
self.duration = self.remaining_time # Update current duration if extended
print(f"Camera Rotate extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_DURATION}s)")
await self._apply_effects(True, currentRoom) # Apply effects immediately
self.sentMessage = False
async def _apply_effects(self, enable: bool,currentRoom):
"""
Internal method to apply or remove the Rainbow Cookie's effects
by writing to BizHawk memory addresses.
Args:
enable (bool): If True, enables effects; if False, disables them.
"""
if self.bizhawk_context is None or self.bizhawk_context.connection_status != bizhawk.ConnectionStatus.CONNECTED:
print("Warning: BizHawk not connected. Cannot apply/remove Rainbow Cookie effects.")
return
read_list = []
read_list += [(RAM.SpecialRoom_CameraMode, 1, "MainRAM"),(RAM.Boss_CameraMode, 1, "MainRAM"),(RAM.Inside_CameraMode, 1, "MainRAM")]
CameraMode_reads = await bizhawk.read(self.bizhawk_context, read_list)
isSpecialRoom = int.from_bytes(CameraMode_reads[0], byteorder="little") == 0x01
isBossRoom = int.from_bytes(CameraMode_reads[1], byteorder="little") == 0x01
isInside = int.from_bytes(CameraMode_reads[2], byteorder="little") == 0x01
if isSpecialRoom:
self.RoomType = "Special"
elif isBossRoom:
self.RoomType = "Boss"
elif isInside:
self.RoomType = "Inside"
else:
self.RoomType = "Outside"
writes_list = []
#SpecialRooms = [83, 84, 87, 88, 90, 91]
#BossRooms = [item for item in RAM.bossListLocal.keys() if item not in SpecialRooms]
#InABossRoom = False
CameraRotate_value = 0xFF if enable else 0x00
CameraRotate_bytes = list(CameraRotate_value.to_bytes(1, "little"))
LeftRotateAddress2 = ""
LeftRotateAddress = ""
RightRotateAddress = ""
RightRotateAddress2 = ""
if self.RoomType == "Special":
LeftRotateAddress = RAM.SpecialRoom_CameraRotateLeft
RightRotateAddress = RAM.SpecialRoom_CameraRotateRight
elif self.RoomType == "Boss":
LeftRotateAddress = RAM.Boss_CameraRotateLeft
RightRotateAddress = RAM.Boss_CameraRotateRight
else:
if self.RoomType == "Inside":
LeftRotateAddress = RAM.Inside_CameraRotateLeft
RightRotateAddress = RAM.Inside_CameraRotateLeft
else:
LeftRotateAddress = RAM.Outside_CameraRotateLeft
RightRotateAddress = RAM.Outside_CameraRotateRight
if self.chosen_side == "Left":
writes_list.append((LeftRotateAddress, CameraRotate_bytes, "MainRAM"))
else:
writes_list.append((RightRotateAddress, CameraRotate_bytes, "MainRAM"))
try:
await bizhawk.write(self.bizhawk_context, writes_list)
print(f"Camera Rotate effects {'applied' if enable else 'removed'}.")
except Exception as e:
print(f"ERROR: Failed to {'apply' if enable else 'remove'} Camera Rotate effects: {e}")
raise
async def update_state_and_deactivate(self,currentRoom):
"""
Updates the remaining time for the Camera Rotate.
If the duration runs out, deactivates the effects.
This method should be called periodically in the main loop of the client.
It also re-applies the camera Rotate effect if it's lost and the cookie is active.
"""
if not self.is_active:
return
if self.pause:
# If paused, don't decrement remaining_time, but update last_update
# to prevent a large time jump when unpaused.
self.last_update = time.time()
return
current_time = time.time()
elapsed_time_since_last_update = current_time - self.last_update
self.remaining_time -= elapsed_time_since_last_update
self.last_update = current_time
# Check and re-apply Rotate effect if it's not active but the effect is
if self.bizhawk_context and self.bizhawk_context.connection_status == bizhawk.ConnectionStatus.CONNECTED:
try:
#SpecialRooms = [83, 84, 87, 88, 90, 91]
#BossRooms = [item for item in RAM.bossListLocal.keys() if item not in SpecialRooms]
#InABossRoom = False
read_list = []
read_list += [(RAM.SpecialRoom_CameraMode, 1, "MainRAM"), (RAM.Boss_CameraMode, 1, "MainRAM"),(RAM.Inside_CameraMode, 1, "MainRAM")]
CameraMode_reads = await bizhawk.read(self.bizhawk_context, read_list)
isSpecialRoom = int.from_bytes(CameraMode_reads[0], byteorder="little") == 0x01
isBossRoom = int.from_bytes(CameraMode_reads[1], byteorder="little") == 0x01
isInside = int.from_bytes(CameraMode_reads[2], byteorder="little") == 0x01
if isSpecialRoom:
self.RoomType = "Special"
elif isBossRoom:
self.RoomType = "Boss"
elif isInside:
self.RoomType = "Inside"
else:
self.RoomType = "Outside"
CameraRotate_value = 0xFF
if self.RoomType == "Special":
LeftRotateAddress = RAM.SpecialRoom_CameraRotateLeft
RightRotateAddress = RAM.SpecialRoom_CameraRotateRight
elif self.RoomType == "Boss":
LeftRotateAddress = RAM.Boss_CameraRotateLeft
RightRotateAddress = RAM.Boss_CameraRotateRight
else:
if self.RoomType == "Inside":
LeftRotateAddress = RAM.Inside_CameraRotateLeft
RightRotateAddress = RAM.Inside_CameraRotateLeft
else:
LeftRotateAddress = RAM.Outside_CameraRotateLeft
RightRotateAddress = RAM.Outside_CameraRotateRight
if self.chosen_side == "Left":
CameraRotateAddress = LeftRotateAddress
else:
CameraRotateAddress = RightRotateAddress
# Read the current value of the chosen CameraRotate address
current_camera_rotate_bytes = await bizhawk.read(self.bizhawk_context, [(CameraRotateAddress, 1, "MainRAM")])
current_camera_rotate = int.from_bytes(current_camera_rotate_bytes[0], byteorder="little")
if current_camera_rotate is not None and current_camera_rotate != 0xFF:
print("Camera Rotate active, but rotate effect lost. Reapplying...")
await self._apply_effects(True,currentRoom)
except Exception as e:
print(f"ERROR: Failed to read rotate form address for reapplication: {e}")
# Log error but don't stop the loop for this non-critical re-application check
if self.remaining_time <= 0:
self.remaining_time = 0
self.is_active = False
print("Camera Rotate duration finished. Deactivating effects.")
await self._apply_effects(False,currentRoom) # Remove effects
+178
View File
@@ -0,0 +1,178 @@
import typing
from typing import Optional, Dict, Set
from BaseClasses import ItemClassification, Item
from .Strings import AEItem
from .RAMAddress import RAM
base_apeescape_item_id = 128000000
class ApeEscapeItem(Item):
game: str = "Ape Escape"
GROUPED_ITEMS: Dict[str, Set[str]] = {}
# base IDs are the index in the static item data table, which is
# not the same order as the items in RAM (but offset 0 is a 16-bit address of
# location of room and position data)
item_table = {
# Gadgets
AEItem.Club.value: RAM.items["Club"],
AEItem.Net.value: RAM.items["Net"],
AEItem.Radar.value: RAM.items["Radar"],
AEItem.Sling.value: RAM.items["Sling"],
AEItem.Hoop.value: RAM.items["Hoop"],
AEItem.Punch.value: RAM.items["Punch"],
AEItem.Flyer.value: RAM.items["Flyer"],
AEItem.Car.value: RAM.items["Car"],
AEItem.WaterNet.value: RAM.items["WaterNet"],
AEItem.ProgWaterNet.value: RAM.items["ProgWaterNet"],
AEItem.WaterCatch.value: RAM.items["WaterCatch"],
# Keys
AEItem.Key.value: RAM.items["Key"],
# No longer needed since we made it an event item
#AEItem.Victory.value: RAM.items["Victory"],
# Monkey Lamps
AEItem.CB_Lamp.value: RAM.items["CB_Lamp"],
AEItem.DI_Lamp.value: RAM.items["DI_Lamp"],
AEItem.CrC_Lamp.value: RAM.items["CrC_Lamp"],
AEItem.CP_Lamp.value: RAM.items["CP_Lamp"],
AEItem.SF_Lamp.value: RAM.items["SF_Lamp"],
AEItem.TVT_Lobby_Lamp.value: RAM.items["TVT_Lobby_Lamp"],
AEItem.TVT_Tank_Lamp.value: RAM.items["TVT_Tank_Lamp"],
AEItem.MM_Lamp.value: RAM.items["MM_Lamp"],
AEItem.MM_DoubleDoorKey.value: RAM.items["MM_DoubleDoorKey"],
# Other
AEItem.Token.value: RAM.items["Token"],
# Junk
AEItem.Nothing.value: RAM.items["Nothing"],
AEItem.Shirt.value: RAM.items["Shirt"],
AEItem.Triangle.value: RAM.items["Triangle"],
AEItem.BigTriangle.value: RAM.items["BigTriangle"],
AEItem.BiggerTriangle.value: RAM.items["BiggerTriangle"],
AEItem.Cookie.value: RAM.items["Cookie"],
AEItem.FiveCookies.value: RAM.items["FiveCookies"],
AEItem.Flash.value: RAM.items["Flash"],
AEItem.ThreeFlash.value: RAM.items["ThreeFlash"],
AEItem.Rocket.value: RAM.items["Rocket"],
AEItem.ThreeRocket.value: RAM.items["ThreeRocket"],
# Traps
AEItem.BananaPeelTrap.value: RAM.items["BananaPeelTrap"],
AEItem.GadgetShuffleTrap.value: RAM.items["GadgetShuffleTrap"],
AEItem.MonkeyMashTrap.value: RAM.items["MonkeyMashTrap"],
AEItem.IcyHotPantsTrap.value: RAM.items["IcyHotPantsTrap"],
AEItem.StunTrap.value: RAM.items["StunTrap"],
AEItem.CameraRotateTrap.value: RAM.items["CameraRotateTrap"],
# SpecialItems
AEItem.RainbowCookie.value: RAM.items["RainbowCookie"],
AEItem.FAKE_OOL_ITEM.value: RAM.items["FAKE_OOL_ITEM"],
}
gadgetsValues = {
AEItem.Club.value: 0x00,
AEItem.Net.value: 0x01,
AEItem.Radar.value: 0x02,
AEItem.Sling.value: 0x03,
AEItem.Hoop.value: 0x04,
AEItem.Punch.value: 0x05,
AEItem.Flyer.value: 0x06,
AEItem.Car.value: 0x07,
}
event_table = {
}
trap_to_local_traps: typing.Dict[str, str] = {
# Converts received traps to local trap names
# Our native Traps
AEItem.BananaPeelTrap.value: AEItem.BananaPeelTrap.value,
AEItem.GadgetShuffleTrap.value: AEItem.GadgetShuffleTrap.value,
AEItem.MonkeyMashTrap.value: AEItem.MonkeyMashTrap.value,
AEItem.IcyHotPantsTrap.value: AEItem.IcyHotPantsTrap.value,
AEItem.StunTrap.value: AEItem.StunTrap.value,
AEItem.CameraRotateTrap.value: AEItem.CameraRotateTrap.value,
# Common other trap names
"Banana Trap": AEItem.BananaPeelTrap.value,
"Chaos Control Trap": AEItem.StunTrap.value,
"Confuse Trap": AEItem.MonkeyMashTrap.value,
"Confusion Trap": AEItem.MonkeyMashTrap.value,
"Freeze Trap": AEItem.StunTrap.value,
"Frozen Trap": AEItem.StunTrap.value,
"Hiccup Trap": AEItem.IcyHotPantsTrap.value,
"Ice Floor Trap": AEItem.BananaPeelTrap.value,
"Jump Trap": AEItem.IcyHotPantsTrap.value,
"Jumping Jacks Trap": AEItem.IcyHotPantsTrap.value,
"Paralyze Trap": AEItem.StunTrap.value,
"Push Trap": AEItem.BananaPeelTrap.value,
"Screen Flip Trap": AEItem.CameraRotateTrap.value,
"Slip Trap": AEItem.BananaPeelTrap.value,
"Spring Trap": AEItem.IcyHotPantsTrap.value,
"SvC Effect": AEItem.CameraRotateTrap.value,
"Swap Trap" : AEItem.GadgetShuffleTrap.value,
# Traps idea :
# Fast Trap (Depending on direction always set to max velocity?)
# Home Trap (Time Hub Trap? or maybe only warp to level entry?)
# Ice Trap (Slipery Floor?)
# Zoom Trap
# Mailbox Trap (Tells a message to the player in a mailbox
}
trap_name_to_value: typing.Dict[str, int] = {
AEItem.BananaPeelTrap.value: RAM.items["BananaPeelTrap"],
AEItem.GadgetShuffleTrap.value: RAM.items["GadgetShuffleTrap"],
AEItem.MonkeyMashTrap.value: RAM.items["MonkeyMashTrap"],
AEItem.IcyHotPantsTrap.value: RAM.items["IcyHotPantsTrap"],
AEItem.StunTrap.value: RAM.items["StunTrap"],
AEItem.CameraRotateTrap.value: RAM.items["CameraRotateTrap"],
}
def createItemGroups():
# Alliases for items
GROUPED_ITEMS.setdefault("Club", []).append("Stun Club")
GROUPED_ITEMS.setdefault("Net", []).append("Time Net")
GROUPED_ITEMS.setdefault("Radar", []).append("Monkey Radar")
GROUPED_ITEMS.setdefault("Slingshot", []).append("Slingback Shooter")
GROUPED_ITEMS.setdefault("Sling", []).append("Slingback Shooter")
GROUPED_ITEMS.setdefault("Hoop", []).append("Super Hoop")
GROUPED_ITEMS.setdefault("Punch", []).append("Magic Punch")
GROUPED_ITEMS.setdefault("Flyer", []).append("Sky Flyer")
GROUPED_ITEMS.setdefault("Car", []).append("R.C. Car")
# Removed because unit tests said having a group and item named the same is bad
# GROUPED_ITEMS.setdefault("Water Net", []).append("Progressive Water Net")
# Item Groups
GROUPED_ITEMS.setdefault("Gadgets", []).append("Stun Club")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Time Net")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Monkey Radar")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Slingback Shooter")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Super Hoop")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Magic Punch")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Sky Flyer")
GROUPED_ITEMS.setdefault("Gadgets", []).append("R.C. Car")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Water Net")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Progressive Water Net")
GROUPED_ITEMS.setdefault("Gadgets", []).append("Water Catch")
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.CB_Lamp.value)
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.DI_Lamp.value)
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.CrC_Lamp.value)
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.CP_Lamp.value)
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.SF_Lamp.value)
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.TVT_Lobby_Lamp.value)
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.TVT_Tank_Lamp.value)
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.MM_Lamp.value)
createItemGroups()

Some files were not shown because too many files have changed in this diff Show More