forked from mirror/Archipelago
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7971961166 | |||
| 9246bd9541 | |||
|
|
fb45a2f87e | ||
|
|
2e5356ad05 | ||
|
|
8457ff3e4b | ||
|
|
70fc3e05fb | ||
|
|
d01c9577ab | ||
|
|
260bae359d | ||
|
|
3016379b85 | ||
|
|
03b638d027 | ||
|
|
3c802d03a1 | ||
|
|
a8e926a1a9 | ||
|
|
56c2272bfd | ||
|
|
47e581bc30 | ||
|
|
3235863f2e | ||
|
|
f00d29e072 | ||
|
|
d000c0f265 | ||
|
|
94136ac223 | ||
|
|
72ff9b1a7d | ||
|
|
4b37283d22 | ||
|
|
c3659fb3ef | ||
|
|
1a8a71f593 | ||
|
|
c255ea8fc6 | ||
|
|
fd81553420 | ||
|
|
2c279cef09 | ||
|
|
07a1ec0a1d | ||
|
|
0b6ba103c5 | ||
|
|
123e1f5d95 | ||
|
|
44e424362e | ||
|
|
371db53371 | ||
|
|
5b99118dda | ||
|
|
4bb6cac7c4 | ||
|
|
99601ccebc | ||
|
|
53956b7d4d | ||
|
|
b38548f89b | ||
|
|
a8ac828241 | ||
|
|
fc2cb3c961 | ||
|
|
9efcba5323 | ||
|
|
9f29859810 | ||
|
|
366fd3712a | ||
|
|
b53f9d3773 | ||
|
|
3ecd856e29 | ||
|
|
b372b02273 | ||
|
|
f26313367e | ||
|
|
a3e8f69909 | ||
|
|
922c7fe86a | ||
|
|
e49ba2ff6f | ||
|
|
61d5120f66 | ||
|
|
ff5402c410 | ||
|
|
fcccbfca65 | ||
|
|
2db5435474 | ||
|
|
eeb022fa0c | ||
|
|
b30b2ecb07 | ||
|
|
699ca8adf6 | ||
|
|
fefd790de6 | ||
| 30fa0658b0 | |||
| 44a0c44036 | |||
|
|
d83da1b818 | ||
|
|
0de09cd794 | ||
|
|
48c201af19 | ||
|
|
b0300d3063 | ||
|
|
e0e34894a3 | ||
|
|
18e3a8911f | ||
|
|
c505b1c32c | ||
|
|
e22e434258 | ||
|
|
8b91f9ff72 | ||
|
|
fadcfbdfea | ||
|
|
3c4c294f9c | ||
|
|
27a7e538df | ||
|
|
cb0cadcc5f | ||
|
|
2e1035a29f | ||
|
|
21c7f3cd92 | ||
|
|
13b6a5f4b2 | ||
|
|
78e8082a6f | ||
|
|
1de91fab67 | ||
|
|
4ef5436559 | ||
|
|
f2a6a769b0 | ||
|
|
8a767bd2ad | ||
|
|
7df243b860 | ||
|
|
f35d91933b | ||
|
|
286769a0f3 | ||
|
|
1dd91ec85b | ||
|
|
6adeb8b95e | ||
|
|
3e0d42bf9e | ||
|
|
41e22dabda | ||
|
|
39e7ee315e | ||
|
|
3e032e6cd6 | ||
|
|
609f4af600 | ||
|
|
4c27e35445 | ||
|
|
b0c967c039 | ||
|
|
c51da00bfb | ||
|
|
f3389f5d8b | ||
|
|
3b1971be66 | ||
|
|
4cb518930c | ||
|
|
c835bff570 | ||
|
|
6ee02fc62d | ||
|
|
8095f922bc | ||
|
|
77e5f3733e | ||
|
|
c47687dd21 | ||
|
|
8662433142 | ||
|
|
5f073c2a76 | ||
|
|
c5d67dd97a | ||
|
|
9b421450b1 | ||
|
|
a6740e7be3 | ||
|
|
65ef35f1b4 | ||
|
|
520253e762 | ||
|
|
aa3614a32b | ||
|
|
94492c45cb | ||
|
|
8f261bb27c | ||
|
|
ddd08342c8 | ||
|
|
c7db213ee9 | ||
|
|
220248dd3d | ||
|
|
5932160f15 | ||
|
|
76e0619b79 | ||
|
|
646a52a2e7 | ||
|
|
e1322df8b0 | ||
|
|
092a9dc6bd | ||
|
|
9f71fe707f | ||
|
|
b8311a62e7 | ||
|
|
13830ff4cb | ||
|
|
c1b858b2cf | ||
|
|
a035ac579c | ||
|
|
20c10e33c4 | ||
|
|
a4e4ce1c72 | ||
|
|
983936af8c | ||
|
|
62dfeac441 | ||
|
|
b81e1a228a | ||
|
|
5899920e48 | ||
|
|
8dee460397 | ||
|
|
cda54e0bea | ||
|
|
0554bf4e2d | ||
|
|
b92803e77f | ||
|
|
69e83071ff | ||
|
|
875765e6dc | ||
|
|
db56e26df9 | ||
|
|
5a88641228 | ||
|
|
16559e7595 | ||
|
|
d594d5d4a7 | ||
|
|
e950a2fa58 | ||
|
|
1df38cb782 | ||
|
|
c6400b6673 | ||
|
|
dbf2325c01 | ||
|
|
dd5b25399a | ||
|
|
8178ee4e58 | ||
|
|
ad1b41ea81 | ||
|
|
efd8528db0 | ||
|
|
e54a15978f | ||
|
|
d78b9ded2d | ||
|
|
53e8130c9c | ||
|
|
55c70a5ba8 | ||
|
|
ebbdd7bfda | ||
|
|
863f161466 | ||
|
|
9305ecb3bc | ||
|
|
0002bb8e5a | ||
|
|
b42fb77451 | ||
|
|
5a8e166289 | ||
|
|
5fa719143c | ||
|
|
a906f139c3 | ||
|
|
56363ea7e7 | ||
|
|
01e1e1fe11 | ||
|
|
4477dc7a66 | ||
|
|
45994e344e | ||
|
|
51d5e1afae | ||
|
|
577b958c4d | ||
|
|
ce38d8ced6 | ||
|
|
d65fcf286d | ||
|
|
5a6a0b37d6 |
@@ -51,7 +51,6 @@ EnemizerCLI/
|
|||||||
/SNI/
|
/SNI/
|
||||||
/sni-*/
|
/sni-*/
|
||||||
/appimagetool*
|
/appimagetool*
|
||||||
/host.yaml
|
|
||||||
/options.yaml
|
/options.yaml
|
||||||
/config.yaml
|
/config.yaml
|
||||||
/logs/
|
/logs/
|
||||||
|
|||||||
4
.github/pyright-config.json
vendored
4
.github/pyright-config.json
vendored
@@ -2,11 +2,15 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"../BizHawkClient.py",
|
"../BizHawkClient.py",
|
||||||
"../Patch.py",
|
"../Patch.py",
|
||||||
|
"../rule_builder/cached_world.py",
|
||||||
|
"../rule_builder/options.py",
|
||||||
|
"../rule_builder/rules.py",
|
||||||
"../test/param.py",
|
"../test/param.py",
|
||||||
"../test/general/test_groups.py",
|
"../test/general/test_groups.py",
|
||||||
"../test/general/test_helpers.py",
|
"../test/general/test_helpers.py",
|
||||||
"../test/general/test_memory.py",
|
"../test/general/test_memory.py",
|
||||||
"../test/general/test_names.py",
|
"../test/general/test_names.py",
|
||||||
|
"../test/general/test_rule_builder.py",
|
||||||
"../test/multiworld/__init__.py",
|
"../test/multiworld/__init__.py",
|
||||||
"../test/multiworld/test_multiworlds.py",
|
"../test/multiworld/test_multiworlds.py",
|
||||||
"../test/netutils/__init__.py",
|
"../test/netutils/__init__.py",
|
||||||
|
|||||||
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
# This workflow will build a release-like distribution when manually dispatched
|
# This workflow will build a release-like distribution when manually dispatched:
|
||||||
|
# a Windows x64 7zip, a Windows x64 Installer, a Linux AppImage and a Linux binary .tar.gz.
|
||||||
|
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||||
choco install innosetup --version=6.2.2 --allow-downgrade
|
choco install innosetup --version=6.7.0 --allow-downgrade
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|||||||
143
.github/workflows/docker.yml
vendored
143
.github/workflows/docker.yml
vendored
@@ -11,144 +11,43 @@ on:
|
|||||||
- "!.github/workflows/**"
|
- "!.github/workflows/**"
|
||||||
- ".github/workflows/docker.yml"
|
- ".github/workflows/docker.yml"
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- "dock-dev"
|
||||||
tags:
|
tags:
|
||||||
- "v?[0-9]+.[0-9]+.[0-9]*"
|
- "v?[0-9]+.[0-9]+.[0-9]*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
push_to_registry:
|
||||||
|
name: Push Docker image to Docker Hub
|
||||||
runs-on: ubuntu-latest
|
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:
|
permissions:
|
||||||
contents: read
|
|
||||||
packages: write
|
packages: write
|
||||||
strategy:
|
contents: read
|
||||||
matrix:
|
attestations: write
|
||||||
include:
|
id-token: write
|
||||||
- platform: amd64
|
|
||||||
runner: ubuntu-latest
|
|
||||||
suffix: amd64
|
|
||||||
cache-scope: amd64
|
|
||||||
- platform: arm64
|
|
||||||
runner: ubuntu-24.04-arm
|
|
||||||
suffix: arm64
|
|
||||||
cache-scope: arm64
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Log in to Docker Hub
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
username: ${{ secrets.DOCKERHUB_USER }}
|
||||||
username: ${{ github.actor }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Compute suffixed tags
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: tags
|
id: meta
|
||||||
run: |
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
|
with:
|
||||||
suffixed=()
|
images: ubufugu/dockipelago
|
||||||
for t in "${tags[@]}"; do
|
|
||||||
suffixed+=("$t-${{ matrix.suffix }}")
|
|
||||||
done
|
|
||||||
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
id: push
|
||||||
|
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/${{ matrix.platform }}
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.tags.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ needs.prepare.outputs.labels }}
|
labels: ${{ steps.meta.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
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -45,7 +45,11 @@ EnemizerCLI/
|
|||||||
/SNI/
|
/SNI/
|
||||||
/sni-*/
|
/sni-*/
|
||||||
/appimagetool*
|
/appimagetool*
|
||||||
|
<<<<<<< Updated upstream
|
||||||
|
/VC_redist.x64.exe
|
||||||
/host.yaml
|
/host.yaml
|
||||||
|
=======
|
||||||
|
>>>>>>> Stashed changes
|
||||||
/options.yaml
|
/options.yaml
|
||||||
/config.yaml
|
/config.yaml
|
||||||
/logs/
|
/logs/
|
||||||
@@ -63,7 +67,10 @@ Output Logs/
|
|||||||
/installdelete.iss
|
/installdelete.iss
|
||||||
/data/user.kv
|
/data/user.kv
|
||||||
/datapackage
|
/datapackage
|
||||||
|
/datapackage_export.json
|
||||||
/custom_worlds
|
/custom_worlds
|
||||||
|
# stubgen output
|
||||||
|
/out/
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
|
<configuration default="false" name="Build APWorlds" type="PythonConfigurationType" factoryName="Python">
|
||||||
<module name="Archipelago" />
|
<module name="Archipelago" />
|
||||||
<option name="ENV_FILES" value="" />
|
<option name="ENV_FILES" value="" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
@@ -8,10 +8,10 @@ import secrets
|
|||||||
import warnings
|
import warnings
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from collections import Counter, deque, defaultdict
|
from collections import Counter, deque, defaultdict
|
||||||
from collections.abc import Collection, MutableSequence
|
from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, MutableSequence, Set
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Literal, Mapping, NamedTuple,
|
from typing import (AbstractSet, Any, ClassVar, Dict, List, Literal, NamedTuple,
|
||||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING, Literal, overload)
|
Optional, Protocol, Tuple, Union, TYPE_CHECKING, overload)
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
|
||||||
from typing_extensions import NotRequired, TypedDict
|
from typing_extensions import NotRequired, TypedDict
|
||||||
@@ -22,6 +22,7 @@ import Utils
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from entrance_rando import ERPlacementState
|
from entrance_rando import ERPlacementState
|
||||||
|
from rule_builder.rules import Rule
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ class MultiWorld():
|
|||||||
local_items: Dict[int, Options.LocalItems]
|
local_items: Dict[int, Options.LocalItems]
|
||||||
non_local_items: Dict[int, Options.NonLocalItems]
|
non_local_items: Dict[int, Options.NonLocalItems]
|
||||||
progression_balancing: Dict[int, Options.ProgressionBalancing]
|
progression_balancing: Dict[int, Options.ProgressionBalancing]
|
||||||
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
completion_condition: Dict[int, CollectionRule]
|
||||||
indirect_connections: Dict[Region, Set[Entrance]]
|
indirect_connections: Dict[Region, Set[Entrance]]
|
||||||
exclude_locations: Dict[int, Options.ExcludeLocations]
|
exclude_locations: Dict[int, Options.ExcludeLocations]
|
||||||
priority_locations: Dict[int, Options.PriorityLocations]
|
priority_locations: Dict[int, Options.PriorityLocations]
|
||||||
@@ -726,6 +727,7 @@ class CollectionState():
|
|||||||
advancements: Set[Location]
|
advancements: Set[Location]
|
||||||
path: Dict[Union[Region, Entrance], PathValue]
|
path: Dict[Union[Region, Entrance], PathValue]
|
||||||
locations_checked: Set[Location]
|
locations_checked: Set[Location]
|
||||||
|
"""Internal cache for Advancement Locations already checked by this CollectionState. Not for use in logic."""
|
||||||
stale: Dict[int, bool]
|
stale: Dict[int, bool]
|
||||||
allow_partial_entrances: bool
|
allow_partial_entrances: bool
|
||||||
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
|
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
|
||||||
@@ -766,7 +768,7 @@ class CollectionState():
|
|||||||
else:
|
else:
|
||||||
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
||||||
|
|
||||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque[Entrance]):
|
||||||
reachable_regions = self.reachable_regions[player]
|
reachable_regions = self.reachable_regions[player]
|
||||||
blocked_connections = self.blocked_connections[player]
|
blocked_connections = self.blocked_connections[player]
|
||||||
# run BFS on all connections, and keep track of those blocked by missing items
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
@@ -784,13 +786,16 @@ class CollectionState():
|
|||||||
blocked_connections.update(new_region.exits)
|
blocked_connections.update(new_region.exits)
|
||||||
queue.extend(new_region.exits)
|
queue.extend(new_region.exits)
|
||||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||||
|
self.multiworld.worlds[player].reached_region(self, new_region)
|
||||||
|
|
||||||
# Retry connections if the new region can unblock them
|
# Retry connections if the new region can unblock them
|
||||||
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
|
entrances = self.multiworld.indirect_connections.get(new_region)
|
||||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
if entrances is not None:
|
||||||
queue.append(new_entrance)
|
relevant_entrances = entrances.intersection(blocked_connections)
|
||||||
|
relevant_entrances.difference_update(queue)
|
||||||
|
queue.extend(relevant_entrances)
|
||||||
|
|
||||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque[Entrance]):
|
||||||
reachable_regions = self.reachable_regions[player]
|
reachable_regions = self.reachable_regions[player]
|
||||||
blocked_connections = self.blocked_connections[player]
|
blocked_connections = self.blocked_connections[player]
|
||||||
new_connection: bool = True
|
new_connection: bool = True
|
||||||
@@ -812,6 +817,7 @@ class CollectionState():
|
|||||||
queue.extend(new_region.exits)
|
queue.extend(new_region.exits)
|
||||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||||
new_connection = True
|
new_connection = True
|
||||||
|
self.multiworld.worlds[player].reached_region(self, new_region)
|
||||||
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
||||||
queue.extend(blocked_connections)
|
queue.extend(blocked_connections)
|
||||||
|
|
||||||
@@ -1169,13 +1175,17 @@ class CollectionState():
|
|||||||
self.prog_items[player][item] = count
|
self.prog_items[player][item] = count
|
||||||
|
|
||||||
|
|
||||||
|
CollectionRule = Callable[[CollectionState], bool]
|
||||||
|
DEFAULT_COLLECTION_RULE: CollectionRule = staticmethod(lambda state: True)
|
||||||
|
|
||||||
|
|
||||||
class EntranceType(IntEnum):
|
class EntranceType(IntEnum):
|
||||||
ONE_WAY = 1
|
ONE_WAY = 1
|
||||||
TWO_WAY = 2
|
TWO_WAY = 2
|
||||||
|
|
||||||
|
|
||||||
class Entrance:
|
class Entrance:
|
||||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
access_rule: CollectionRule = DEFAULT_COLLECTION_RULE
|
||||||
hide_path: bool = False
|
hide_path: bool = False
|
||||||
player: int
|
player: int
|
||||||
name: str
|
name: str
|
||||||
@@ -1362,7 +1372,7 @@ class Region:
|
|||||||
self,
|
self,
|
||||||
location_name: str,
|
location_name: str,
|
||||||
item_name: str | None = None,
|
item_name: str | None = None,
|
||||||
rule: Callable[[CollectionState], bool] | None = None,
|
rule: CollectionRule | Rule[Any] | None = None,
|
||||||
location_type: type[Location] | None = None,
|
location_type: type[Location] | None = None,
|
||||||
item_type: type[Item] | None = None,
|
item_type: type[Item] | None = None,
|
||||||
show_in_spoiler: bool = True,
|
show_in_spoiler: bool = True,
|
||||||
@@ -1390,7 +1400,7 @@ class Region:
|
|||||||
event_location = location_type(self.player, location_name, None, self)
|
event_location = location_type(self.player, location_name, None, self)
|
||||||
event_location.show_in_spoiler = show_in_spoiler
|
event_location.show_in_spoiler = show_in_spoiler
|
||||||
if rule is not None:
|
if rule is not None:
|
||||||
event_location.access_rule = rule
|
self.multiworld.worlds[self.player].set_rule(event_location, rule)
|
||||||
|
|
||||||
event_item = item_type(item_name, ItemClassification.progression, None, self.player)
|
event_item = item_type(item_name, ItemClassification.progression, None, self.player)
|
||||||
|
|
||||||
@@ -1401,7 +1411,7 @@ class Region:
|
|||||||
return event_item
|
return event_item
|
||||||
|
|
||||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
rule: Optional[CollectionRule | Rule[Any]] = None) -> Entrance:
|
||||||
"""
|
"""
|
||||||
Connects this Region to another Region, placing the provided rule on the connection.
|
Connects this Region to another Region, placing the provided rule on the connection.
|
||||||
|
|
||||||
@@ -1409,8 +1419,8 @@ class Region:
|
|||||||
:param name: name of the connection being created
|
:param name: name of the connection being created
|
||||||
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
|
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
|
||||||
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
|
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
|
||||||
if rule:
|
if rule is not None:
|
||||||
exit_.access_rule = rule
|
self.multiworld.worlds[self.player].set_rule(exit_, rule)
|
||||||
exit_.connect(connecting_region)
|
exit_.connect(connecting_region)
|
||||||
return exit_
|
return exit_
|
||||||
|
|
||||||
@@ -1435,7 +1445,7 @@ class Region:
|
|||||||
return entrance
|
return entrance
|
||||||
|
|
||||||
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
|
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
|
||||||
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]:
|
rules: Mapping[str, CollectionRule | Rule[Any]] | None = None) -> List[Entrance]:
|
||||||
"""
|
"""
|
||||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||||
|
|
||||||
@@ -1474,7 +1484,7 @@ class Location:
|
|||||||
show_in_spoiler: bool = True
|
show_in_spoiler: bool = True
|
||||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||||
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
||||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
access_rule: CollectionRule = DEFAULT_COLLECTION_RULE
|
||||||
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
||||||
item: Optional[Item] = None
|
item: Optional[Item] = None
|
||||||
|
|
||||||
@@ -1551,7 +1561,7 @@ class ItemClassification(IntFlag):
|
|||||||
skip_balancing = 0b01000
|
skip_balancing = 0b01000
|
||||||
""" should technically never occur on its own
|
""" should technically never occur on its own
|
||||||
Item that is logically relevant, but progression balancing should not touch.
|
Item that is logically relevant, but progression balancing should not touch.
|
||||||
|
|
||||||
Possible reasons for why an item should not be pulled ahead by progression balancing:
|
Possible reasons for why an item should not be pulled ahead by progression balancing:
|
||||||
1. This item is quite insignificant, so pulling it earlier doesn't help (currency/etc.)
|
1. This item is quite insignificant, so pulling it earlier doesn't help (currency/etc.)
|
||||||
2. It is important for the player experience that this item is evenly distributed in the seed (e.g. goal items) """
|
2. It is important for the player experience that this item is evenly distributed in the seed (e.g. goal items) """
|
||||||
@@ -1559,13 +1569,13 @@ class ItemClassification(IntFlag):
|
|||||||
deprioritized = 0b10000
|
deprioritized = 0b10000
|
||||||
""" Should technically never occur on its own.
|
""" Should technically never occur on its own.
|
||||||
Will not be considered for priority locations,
|
Will not be considered for priority locations,
|
||||||
unless Priority Locations Fill runs out of regular progression items before filling all priority locations.
|
unless Priority Locations Fill runs out of regular progression items before filling all priority locations.
|
||||||
|
|
||||||
Should be used for items that would feel bad for the player to find on a priority location.
|
Should be used for items that would feel bad for the player to find on a priority location.
|
||||||
Usually, these are items that are plentiful or insignificant. """
|
Usually, these are items that are plentiful or insignificant. """
|
||||||
|
|
||||||
progression_deprioritized_skip_balancing = 0b11001
|
progression_deprioritized_skip_balancing = 0b11001
|
||||||
""" Since a common case of both skip_balancing and deprioritized is "insignificant progression",
|
""" Since a common case of both skip_balancing and deprioritized is "insignificant progression",
|
||||||
these items often want both flags. """
|
these items often want both flags. """
|
||||||
|
|
||||||
progression_skip_balancing = 0b01001 # only progression gets balanced
|
progression_skip_balancing = 0b01001 # only progression gets balanced
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ if __name__ == "__main__":
|
|||||||
from MultiServer import CommandProcessor, mark_raw
|
from MultiServer import CommandProcessor, mark_raw
|
||||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
|
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
|
||||||
from Utils import Version, stream_input, async_start
|
from Utils import gui_enabled, Version, stream_input, async_start
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import network_data_package, AutoWorldRegister
|
||||||
import os
|
import os
|
||||||
import ssl
|
import ssl
|
||||||
@@ -35,9 +35,6 @@ if typing.TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logging.getLogger("Client")
|
logger = logging.getLogger("Client")
|
||||||
|
|
||||||
# without terminal, we have to use gui mode
|
|
||||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
|
||||||
|
|
||||||
|
|
||||||
@Utils.cache_argsless
|
@Utils.cache_argsless
|
||||||
def get_ssl_context():
|
def get_ssl_context():
|
||||||
@@ -65,6 +62,8 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
|
|
||||||
def _cmd_exit(self) -> bool:
|
def _cmd_exit(self) -> bool:
|
||||||
"""Close connections and client"""
|
"""Close connections and client"""
|
||||||
|
if self.ctx.ui:
|
||||||
|
self.ctx.ui.stop()
|
||||||
self.ctx.exit_event.set()
|
self.ctx.exit_event.set()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -774,7 +773,7 @@ class CommonContext:
|
|||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
parts = title.split(', ', 1)
|
parts = title.split(', ', 1)
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
text = parts[1] + '\n\n' + text
|
text = f"{parts[1]}\n\n{text}" if text else parts[1]
|
||||||
title = parts[0]
|
title = parts[0]
|
||||||
# display error
|
# display error
|
||||||
self._messagebox = MessageBox(title, text, error=True)
|
self._messagebox = MessageBox(title, text, error=True)
|
||||||
@@ -897,6 +896,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
|||||||
"May not be running Archipelago on that address or port.")
|
"May not be running Archipelago on that address or port.")
|
||||||
except websockets.InvalidURI:
|
except websockets.InvalidURI:
|
||||||
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
|
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
ctx.handle_connection_loss("Failed to connect to the multiworld server. Connection timed out.")
|
||||||
except OSError:
|
except OSError:
|
||||||
ctx.handle_connection_loss("Failed to connect to the multiworld server")
|
ctx.handle_connection_loss("Failed to connect to the multiworld server")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -97,4 +97,10 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|||||||
# Ensure no runtime ModuleUpdate.
|
# Ensure no runtime ModuleUpdate.
|
||||||
ENV SKIP_REQUIREMENTS_UPDATE=true
|
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" ]
|
ENTRYPOINT [ "python", "WebHost.py" ]
|
||||||
|
|||||||
1
Fill.py
1
Fill.py
@@ -280,6 +280,7 @@ def remaining_fill(multiworld: MultiWorld,
|
|||||||
item_to_place = itempool.pop()
|
item_to_place = itempool.pop()
|
||||||
spot_to_fill: typing.Optional[Location] = None
|
spot_to_fill: typing.Optional[Location] = None
|
||||||
|
|
||||||
|
# going through locations in the same order as the provided `locations` argument
|
||||||
for i, location in enumerate(locations):
|
for i, location in enumerate(locations):
|
||||||
if location_can_fill_item(location, item_to_place):
|
if location_can_fill_item(location, item_to_place):
|
||||||
# popping by index is faster than removing by content,
|
# popping by index is faster than removing by content,
|
||||||
|
|||||||
128
Generate.py
128
Generate.py
@@ -23,7 +23,7 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
|
|||||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||||
|
|
||||||
|
|
||||||
def mystery_argparse(argv: list[str] | None = None):
|
def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
from settings import get_settings
|
from settings import get_settings
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
defaults = settings.generator
|
defaults = settings.generator
|
||||||
@@ -68,7 +68,7 @@ def mystery_argparse(argv: list[str] | None = None):
|
|||||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||||
if not os.path.isabs(args.meta_file_path):
|
if not os.path.isabs(args.meta_file_path):
|
||||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
args.plando = PlandoOptions.from_option_string(args.plando)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
@@ -119,9 +119,9 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
|||||||
else:
|
else:
|
||||||
meta_weights = None
|
meta_weights = None
|
||||||
|
|
||||||
|
player_id: int = 1
|
||||||
player_id = 1
|
player_files: dict[int, str] = {}
|
||||||
player_files = {}
|
player_errors: list[str] = []
|
||||||
for file in os.scandir(args.player_files_path):
|
for file in os.scandir(args.player_files_path):
|
||||||
fname = file.name
|
fname = file.name
|
||||||
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
|
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
|
||||||
@@ -135,9 +135,13 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
|||||||
else:
|
else:
|
||||||
weights_for_file.append(yaml)
|
weights_for_file.append(yaml)
|
||||||
weights_cache[fname] = tuple(weights_for_file)
|
weights_cache[fname] = tuple(weights_for_file)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
|
logging.exception(f"Exception reading weights in file {fname}")
|
||||||
|
player_errors.append(
|
||||||
|
f"{len(player_errors) + 1}. "
|
||||||
|
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
# sort dict for consistent results across platforms:
|
# sort dict for consistent results across platforms:
|
||||||
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
|
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
|
||||||
@@ -152,6 +156,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
|||||||
args.multi = max(player_id - 1, args.multi)
|
args.multi = max(player_id - 1, args.multi)
|
||||||
|
|
||||||
if args.multi == 0:
|
if args.multi == 0:
|
||||||
|
if player_errors:
|
||||||
|
errors = "\n\n".join(player_errors)
|
||||||
|
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||||
|
f"See logs for full tracebacks.\n\n{errors}")
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"No individual player files found and number of players is 0. "
|
"No individual player files found and number of players is 0. "
|
||||||
"Provide individual player files or specify the number of players via host.yaml or --multi."
|
"Provide individual player files or specify the number of players via host.yaml or --multi."
|
||||||
@@ -161,6 +169,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
|||||||
f"{seed_name} Seed {seed} with plando: {args.plando}")
|
f"{seed_name} Seed {seed} with plando: {args.plando}")
|
||||||
|
|
||||||
if not weights_cache:
|
if not weights_cache:
|
||||||
|
if player_errors:
|
||||||
|
errors = "\n\n".join(player_errors)
|
||||||
|
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||||
|
f"See logs for full tracebacks.\n\n{errors}")
|
||||||
raise Exception(f"No weights found. "
|
raise Exception(f"No weights found. "
|
||||||
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
||||||
f"A mix is also permitted.")
|
f"A mix is also permitted.")
|
||||||
@@ -171,10 +183,6 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
|||||||
args.sprite_pool = dict.fromkeys(range(1, args.multi+1), None)
|
args.sprite_pool = dict.fromkeys(range(1, args.multi+1), None)
|
||||||
args.name = {}
|
args.name = {}
|
||||||
|
|
||||||
settings_cache: dict[str, tuple[argparse.Namespace, ...]] = \
|
|
||||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
|
||||||
for fname, yamls in weights_cache.items()}
|
|
||||||
|
|
||||||
if meta_weights:
|
if meta_weights:
|
||||||
for category_name, category_dict in meta_weights.items():
|
for category_name, category_dict in meta_weights.items():
|
||||||
for key in category_dict:
|
for key in category_dict:
|
||||||
@@ -197,47 +205,85 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
|||||||
else:
|
else:
|
||||||
yaml[category_name][key] = option
|
yaml[category_name][key] = option
|
||||||
|
|
||||||
player_path_cache = {}
|
settings_cache: dict[str, tuple[argparse.Namespace, ...] | None] = {fname: None for fname in weights_cache}
|
||||||
|
if args.sameoptions:
|
||||||
|
for fname, yamls in weights_cache.items():
|
||||||
|
try:
|
||||||
|
settings_cache[fname] = tuple(roll_settings(yaml, args.plando) for yaml in yamls)
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"Exception reading settings in file {fname}")
|
||||||
|
player_errors.append(
|
||||||
|
f"{len(player_errors) + 1}. "
|
||||||
|
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
|
||||||
|
)
|
||||||
|
# Exit early here to avoid throwing the same errors again later
|
||||||
|
if player_errors:
|
||||||
|
errors = "\n\n".join(player_errors)
|
||||||
|
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||||
|
f"See logs for full tracebacks.\n\n{errors}")
|
||||||
|
|
||||||
|
player_path_cache: dict[int, str] = {}
|
||||||
for player in range(1, args.multi + 1):
|
for player in range(1, args.multi + 1):
|
||||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||||
name_counter = Counter()
|
name_counter: Counter[str] = Counter()
|
||||||
args.player_options = {}
|
args.player_options = {}
|
||||||
|
|
||||||
player = 1
|
player = 1
|
||||||
while player <= args.multi:
|
while player <= args.multi:
|
||||||
path = player_path_cache[player]
|
path = player_path_cache[player]
|
||||||
if path:
|
if not path:
|
||||||
|
player_errors.append(f'No weights specified for player {player}')
|
||||||
|
player += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
for doc_index, yaml in enumerate(weights_cache[path]):
|
||||||
|
name = yaml.get("name")
|
||||||
try:
|
try:
|
||||||
settings: tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
|
# Use the cached settings object if it exists, otherwise roll settings within the try-catch
|
||||||
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
|
# Invariant: settings_cache[path] and weights_cache[path] have the same length
|
||||||
for settingsObject in settings:
|
cached = settings_cache[path]
|
||||||
for k, v in vars(settingsObject).items():
|
settings_object: argparse.Namespace = (cached[doc_index] if cached else roll_settings(yaml, args.plando))
|
||||||
if v is not None:
|
|
||||||
try:
|
|
||||||
getattr(args, k)[player] = v
|
|
||||||
except AttributeError:
|
|
||||||
setattr(args, k, {player: v})
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
|
||||||
|
|
||||||
# name was not specified
|
for k, v in vars(settings_object).items():
|
||||||
if player not in args.name:
|
if v is not None:
|
||||||
if path == args.weights_file_path:
|
try:
|
||||||
# weights file, so we need to make the name unique
|
getattr(args, k)[player] = v
|
||||||
args.name[player] = f"Player{player}"
|
except AttributeError:
|
||||||
else:
|
setattr(args, k, {player: v})
|
||||||
# use the filename
|
except Exception as e:
|
||||||
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||||
args.name[player] = handle_name(args.name[player], player, name_counter)
|
|
||||||
|
# name was not specified
|
||||||
|
if player not in args.name:
|
||||||
|
if path == args.weights_file_path:
|
||||||
|
# weights file, so we need to make the name unique
|
||||||
|
args.name[player] = f"Player{player}"
|
||||||
|
else:
|
||||||
|
# use the filename
|
||||||
|
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||||
|
args.name[player] = handle_name(args.name[player], player, name_counter)
|
||||||
|
|
||||||
player += 1
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {path} is invalid. Please fix your yaml.") from e
|
logging.exception(f"Exception reading settings in file {path} document #{doc_index + 1} "
|
||||||
else:
|
f"(name: {args.name.get(player, name)})")
|
||||||
raise RuntimeError(f'No weights specified for player {player}')
|
player_errors.append(
|
||||||
|
f"{len(player_errors) + 1}. "
|
||||||
|
f"File {path} document #{doc_index + 1} (name: {args.name.get(player, name)}) is invalid. "
|
||||||
|
f"Please fix your yaml.\n{Utils.get_all_causes(e)}")
|
||||||
|
|
||||||
|
# increment for each yaml document in the file
|
||||||
|
player += 1
|
||||||
|
|
||||||
if len(set(name.lower() for name in args.name.values())) != len(args.name):
|
if len(set(name.lower() for name in args.name.values())) != len(args.name):
|
||||||
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}")
|
player_errors.append(
|
||||||
|
f"{len(player_errors) + 1}. "
|
||||||
|
f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if player_errors:
|
||||||
|
errors = "\n\n".join(player_errors)
|
||||||
|
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||||
|
f"See logs for full tracebacks.\n\n{errors}")
|
||||||
|
|
||||||
return args, seed
|
return args, seed
|
||||||
|
|
||||||
@@ -316,7 +362,7 @@ class SafeFormatter(string.Formatter):
|
|||||||
return kwargs.get(key, "{" + key + "}")
|
return kwargs.get(key, "{" + key + "}")
|
||||||
|
|
||||||
|
|
||||||
def handle_name(name: str, player: int, name_counter: Counter):
|
def handle_name(name: str, player: int, name_counter: Counter[str]):
|
||||||
name_counter[name.lower()] += 1
|
name_counter[name.lower()] += 1
|
||||||
number = name_counter[name.lower()]
|
number = name_counter[name.lower()]
|
||||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||||
@@ -454,7 +500,7 @@ def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
|||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
|
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type[Options.Option], plando_options: PlandoOptions):
|
||||||
try:
|
try:
|
||||||
if option_key in game_weights:
|
if option_key in game_weights:
|
||||||
if not option.supports_weighting:
|
if not option.supports_weighting:
|
||||||
|
|||||||
16
Launcher.py
16
Launcher.py
@@ -31,6 +31,10 @@ import settings
|
|||||||
import Utils
|
import Utils
|
||||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
||||||
user_path)
|
user_path)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_logging('Launcher')
|
||||||
|
|
||||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||||
|
|
||||||
|
|
||||||
@@ -218,12 +222,17 @@ def launch(exe, in_terminal=False):
|
|||||||
|
|
||||||
def create_shortcut(button: Any, component: Component) -> None:
|
def create_shortcut(button: Any, component: Component) -> None:
|
||||||
from pyshortcuts import make_shortcut
|
from pyshortcuts import make_shortcut
|
||||||
script = sys.argv[0]
|
env = os.environ
|
||||||
wkdir = Utils.local_path()
|
if "APPIMAGE" in env:
|
||||||
|
script = env["ARGV0"]
|
||||||
|
wkdir = None # defaults to ~ on Linux
|
||||||
|
else:
|
||||||
|
script = sys.argv[0]
|
||||||
|
wkdir = Utils.local_path()
|
||||||
|
|
||||||
script = f"{script} \"{component.display_name}\""
|
script = f"{script} \"{component.display_name}\""
|
||||||
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
||||||
startmenu=False, terminal=False, working_dir=wkdir)
|
startmenu=False, terminal=False, working_dir=wkdir, noexe=Utils.is_frozen())
|
||||||
button.menu.dismiss()
|
button.menu.dismiss()
|
||||||
|
|
||||||
|
|
||||||
@@ -488,7 +497,6 @@ def main(args: argparse.Namespace | dict | None = None):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_logging('Launcher')
|
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
|||||||
3
Main.py
3
Main.py
@@ -207,6 +207,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
|||||||
else:
|
else:
|
||||||
logger.info("Progression balancing skipped.")
|
logger.info("Progression balancing skipped.")
|
||||||
|
|
||||||
|
AutoWorld.call_all(multiworld, "finalize_multiworld")
|
||||||
|
AutoWorld.call_all(multiworld, "pre_output")
|
||||||
|
|
||||||
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
||||||
multiworld.random.passthrough = False
|
multiworld.random.passthrough = False
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ import multiprocessing
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 9):
|
if sys.platform in ("win32", "darwin") and not (3, 11, 9) <= sys.version_info < (3, 14, 0):
|
||||||
# Official micro version updates. This should match the number in docs/running from source.md.
|
# Official micro version updates. This should match the number in docs/running from source.md.
|
||||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.11.9+ is supported.")
|
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. "
|
||||||
|
"Official 3.11.9 through 3.13.x is supported.")
|
||||||
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 13):
|
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 13):
|
||||||
# There are known security issues, but no easy way to install fixed versions on Windows for testing.
|
# There are known security issues, but no easy way to install fixed versions on Windows for testing.
|
||||||
warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.")
|
warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.")
|
||||||
elif sys.version_info < (3, 11, 0):
|
elif not (3, 11, 0) <= sys.version_info < (3, 14, 0):
|
||||||
# Other platforms may get security backports instead of micro updates, so the number is unreliable.
|
# Other platforms may get security backports instead of micro updates, so the number is unreliable.
|
||||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.11.0+ is supported.")
|
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.11.0 through 3.13.x is supported.")
|
||||||
|
|
||||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||||
_skip_update = bool(
|
_skip_update = bool(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import time
|
|||||||
import typing
|
import typing
|
||||||
import weakref
|
import weakref
|
||||||
import zlib
|
import zlib
|
||||||
|
from signal import SIGINT, SIGTERM, signal
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
@@ -69,6 +70,12 @@ def remove_from_list(container, value):
|
|||||||
|
|
||||||
|
|
||||||
def pop_from_container(container, value):
|
def pop_from_container(container, value):
|
||||||
|
if isinstance(container, list) and isinstance(value, int) and len(container) <= value:
|
||||||
|
return container
|
||||||
|
|
||||||
|
if isinstance(container, dict) and value not in container:
|
||||||
|
return container
|
||||||
|
|
||||||
try:
|
try:
|
||||||
container.pop(value)
|
container.pop(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -490,7 +497,8 @@ class Context:
|
|||||||
|
|
||||||
self.read_data = {}
|
self.read_data = {}
|
||||||
# there might be a better place to put this.
|
# there might be a better place to put this.
|
||||||
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
|
race_mode = decoded_obj.get("race_mode", 0)
|
||||||
|
self.read_data["race_mode"] = lambda: race_mode
|
||||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||||
if mdata_ver > version_tuple:
|
if mdata_ver > version_tuple:
|
||||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
|
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
|
||||||
@@ -911,12 +919,6 @@ async def server(websocket: "ServerConnection", path: str = "/", ctx: Context =
|
|||||||
|
|
||||||
|
|
||||||
async def on_client_connected(ctx: Context, client: Client):
|
async def on_client_connected(ctx: Context, client: Client):
|
||||||
players = []
|
|
||||||
for team, clients in ctx.clients.items():
|
|
||||||
for slot, connected_clients in clients.items():
|
|
||||||
if connected_clients:
|
|
||||||
name = ctx.player_names[team, slot]
|
|
||||||
players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name))
|
|
||||||
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
||||||
games.add("Archipelago")
|
games.add("Archipelago")
|
||||||
await ctx.send_msgs(client, [{
|
await ctx.send_msgs(client, [{
|
||||||
@@ -1301,6 +1303,13 @@ class CommandMeta(type):
|
|||||||
commands.update(base.commands)
|
commands.update(base.commands)
|
||||||
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
|
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
|
||||||
command_name.startswith("_cmd_")})
|
command_name.startswith("_cmd_")})
|
||||||
|
for command_name, method in commands.items():
|
||||||
|
# wrap async def functions so they run on default asyncio loop
|
||||||
|
if inspect.iscoroutinefunction(method):
|
||||||
|
def _wrapper(self, *args, _method=method, **kwargs):
|
||||||
|
return async_start(_method(self, *args, **kwargs))
|
||||||
|
functools.update_wrapper(_wrapper, method)
|
||||||
|
commands[command_name] = _wrapper
|
||||||
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)
|
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)
|
||||||
|
|
||||||
|
|
||||||
@@ -1364,7 +1373,10 @@ class CommandProcessor(metaclass=CommandMeta):
|
|||||||
argname += "=" + parameter.default
|
argname += "=" + parameter.default
|
||||||
argtext += argname
|
argtext += argname
|
||||||
argtext += " "
|
argtext += " "
|
||||||
doctext = '\n '.join(inspect.getdoc(method).split('\n'))
|
method_doc = inspect.getdoc(method)
|
||||||
|
if method_doc is None:
|
||||||
|
method_doc = "(missing help text)"
|
||||||
|
doctext = "\n ".join(method_doc.split("\n"))
|
||||||
s += f"{self.marker}{command} {argtext}\n {doctext}\n"
|
s += f"{self.marker}{command} {argtext}\n {doctext}\n"
|
||||||
return s
|
return s
|
||||||
|
|
||||||
@@ -2560,6 +2572,8 @@ async def console(ctx: Context):
|
|||||||
input_text = await queue.get()
|
input_text = await queue.get()
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
ctx.commandprocessor(input_text)
|
ctx.commandprocessor(input_text)
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
ctx.logger.info("ConsoleTask cancelled")
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -2726,6 +2740,26 @@ async def main(args: argparse.Namespace):
|
|||||||
console_task = asyncio.create_task(console(ctx))
|
console_task = asyncio.create_task(console(ctx))
|
||||||
if ctx.auto_shutdown:
|
if ctx.auto_shutdown:
|
||||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
|
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
try:
|
||||||
|
for remove_signal in [SIGINT, SIGTERM]:
|
||||||
|
asyncio.get_event_loop().remove_signal_handler(remove_signal)
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
ctx.commandprocessor._cmd_exit()
|
||||||
|
|
||||||
|
def shutdown(signum, frame):
|
||||||
|
stop()
|
||||||
|
|
||||||
|
try:
|
||||||
|
for sig in [SIGINT, SIGTERM]:
|
||||||
|
asyncio.get_event_loop().add_signal_handler(sig, stop)
|
||||||
|
except NotImplementedError:
|
||||||
|
# add_signal_handler is only implemented for UNIX platforms
|
||||||
|
for sig in [SIGINT, SIGTERM]:
|
||||||
|
signal(sig, shutdown)
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
console_task.cancel()
|
console_task.cancel()
|
||||||
if ctx.shutdown_task:
|
if ctx.shutdown_task:
|
||||||
|
|||||||
187
Options.py
187
Options.py
@@ -24,6 +24,39 @@ if typing.TYPE_CHECKING:
|
|||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
_RANDOM_OPTS = [
|
||||||
|
"random", "random-low", "random-middle", "random-high",
|
||||||
|
"random-range-low-<min>-<max>", "random-range-middle-<min>-<max>",
|
||||||
|
"random-range-high-<min>-<max>", "random-range-<min>-<max>",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
|
||||||
|
"""
|
||||||
|
Integer triangular distribution for `lower` inclusive to `end` inclusive.
|
||||||
|
|
||||||
|
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
|
||||||
|
"""
|
||||||
|
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
|
||||||
|
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
|
||||||
|
# when a != b, so ensure the result is never more than `end`.
|
||||||
|
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
|
||||||
|
|
||||||
|
|
||||||
|
def random_weighted_range(text: str, range_start: int, range_end: int):
|
||||||
|
if text == "random-low":
|
||||||
|
return triangular(range_start, range_end, 0.0)
|
||||||
|
elif text == "random-high":
|
||||||
|
return triangular(range_start, range_end, 1.0)
|
||||||
|
elif text == "random-middle":
|
||||||
|
return triangular(range_start, range_end)
|
||||||
|
elif text == "random":
|
||||||
|
return random.randint(range_start, range_end)
|
||||||
|
else:
|
||||||
|
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||||
|
f"Acceptable values are: {', '.join(_RANDOM_OPTS)}.")
|
||||||
|
|
||||||
|
|
||||||
def roll_percentage(percentage: int | float) -> bool:
|
def roll_percentage(percentage: int | float) -> bool:
|
||||||
"""Roll a percentage chance.
|
"""Roll a percentage chance.
|
||||||
percentage is expected to be in range [0, 100]"""
|
percentage is expected to be in range [0, 100]"""
|
||||||
@@ -417,10 +450,12 @@ class Toggle(NumericOption):
|
|||||||
def from_text(cls, text: str) -> Toggle:
|
def from_text(cls, text: str) -> Toggle:
|
||||||
if text == "random":
|
if text == "random":
|
||||||
return cls(random.choice(list(cls.name_lookup)))
|
return cls(random.choice(list(cls.name_lookup)))
|
||||||
elif text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
elif text.lower() in {"off", "0", "false", "none", "null", "no", "disabled"}:
|
||||||
return cls(0)
|
return cls(0)
|
||||||
else:
|
elif text.lower() in {"on", "1", "true", "yes", "enabled"}:
|
||||||
return cls(1)
|
return cls(1)
|
||||||
|
else:
|
||||||
|
raise OptionError(f"Option {cls.__name__} does not support a value of {text}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any):
|
def from_any(cls, data: typing.Any):
|
||||||
@@ -523,9 +558,9 @@ class Choice(NumericOption):
|
|||||||
|
|
||||||
class TextChoice(Choice):
|
class TextChoice(Choice):
|
||||||
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
||||||
value: typing.Union[str, int]
|
value: str | int
|
||||||
|
|
||||||
def __init__(self, value: typing.Union[str, int]):
|
def __init__(self, value: str | int):
|
||||||
assert isinstance(value, str) or isinstance(value, int), \
|
assert isinstance(value, str) or isinstance(value, int), \
|
||||||
f"'{value}' is not a valid option for '{self.__class__.__name__}'"
|
f"'{value}' is not a valid option for '{self.__class__.__name__}'"
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -546,7 +581,7 @@ class TextChoice(Choice):
|
|||||||
return cls(text)
|
return cls(text)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_option_name(cls, value: T) -> str:
|
def get_option_name(cls, value: str | int) -> str:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
return super().get_option_name(value)
|
return super().get_option_name(value)
|
||||||
@@ -688,12 +723,6 @@ class Range(NumericOption):
|
|||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 1
|
range_end = 1
|
||||||
|
|
||||||
_RANDOM_OPTS = [
|
|
||||||
"random", "random-low", "random-middle", "random-high",
|
|
||||||
"random-range-low-<min>-<max>", "random-range-middle-<min>-<max>",
|
|
||||||
"random-range-high-<min>-<max>", "random-range-<min>-<max>",
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, value: int):
|
def __init__(self, value: int):
|
||||||
if value < self.range_start:
|
if value < self.range_start:
|
||||||
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
||||||
@@ -742,25 +771,16 @@ class Range(NumericOption):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def weighted_range(cls, text) -> Range:
|
def weighted_range(cls, text) -> Range:
|
||||||
if text == "random-low":
|
if text.startswith("random-range-"):
|
||||||
return cls(cls.triangular(cls.range_start, cls.range_end, 0.0))
|
|
||||||
elif text == "random-high":
|
|
||||||
return cls(cls.triangular(cls.range_start, cls.range_end, 1.0))
|
|
||||||
elif text == "random-middle":
|
|
||||||
return cls(cls.triangular(cls.range_start, cls.range_end))
|
|
||||||
elif text.startswith("random-range-"):
|
|
||||||
return cls.custom_range(text)
|
return cls.custom_range(text)
|
||||||
elif text == "random":
|
|
||||||
return cls(random.randint(cls.range_start, cls.range_end))
|
|
||||||
else:
|
else:
|
||||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
return cls(random_weighted_range(text, cls.range_start, cls.range_end))
|
||||||
f"Acceptable values are: {', '.join(cls._RANDOM_OPTS)}.")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_range(cls, text) -> Range:
|
def custom_range(cls, text) -> Range:
|
||||||
textsplit = text.split("-")
|
textsplit = text.split("-")
|
||||||
try:
|
try:
|
||||||
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
|
random_range = [int(textsplit[-2]), int(textsplit[-1])]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||||
random_range.sort()
|
random_range.sort()
|
||||||
@@ -768,14 +788,9 @@ class Range(NumericOption):
|
|||||||
raise Exception(
|
raise Exception(
|
||||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||||
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||||
if text.startswith("random-range-low"):
|
if textsplit[2] in ("low", "middle", "high"):
|
||||||
return cls(cls.triangular(random_range[0], random_range[1], 0.0))
|
return cls(random_weighted_range(f"{textsplit[0]}-{textsplit[2]}", *random_range))
|
||||||
elif text.startswith("random-range-middle"):
|
return cls(random_weighted_range("random", *random_range))
|
||||||
return cls(cls.triangular(random_range[0], random_range[1]))
|
|
||||||
elif text.startswith("random-range-high"):
|
|
||||||
return cls(cls.triangular(random_range[0], random_range[1], 1.0))
|
|
||||||
else:
|
|
||||||
return cls(random.randint(random_range[0], random_range[1]))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any) -> Range:
|
def from_any(cls, data: typing.Any) -> Range:
|
||||||
@@ -790,18 +805,6 @@ class Range(NumericOption):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
|
|
||||||
"""
|
|
||||||
Integer triangular distribution for `lower` inclusive to `end` inclusive.
|
|
||||||
|
|
||||||
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
|
|
||||||
"""
|
|
||||||
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
|
|
||||||
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
|
|
||||||
# when a != b, so ensure the result is never more than `end`.
|
|
||||||
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
|
|
||||||
|
|
||||||
|
|
||||||
class NamedRange(Range):
|
class NamedRange(Range):
|
||||||
special_range_names: typing.Dict[str, int] = {}
|
special_range_names: typing.Dict[str, int] = {}
|
||||||
@@ -891,7 +894,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
|||||||
def __iter__(self) -> typing.Iterator[typing.Any]:
|
def __iter__(self) -> typing.Iterator[typing.Any]:
|
||||||
return self.value.__iter__()
|
return self.value.__iter__()
|
||||||
|
|
||||||
|
|
||||||
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
||||||
default = {}
|
default = {}
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
@@ -906,7 +909,8 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||||
|
|
||||||
def get_option_name(self, value):
|
@classmethod
|
||||||
|
def get_option_name(cls, value):
|
||||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||||
|
|
||||||
def __getitem__(self, item: str) -> typing.Any:
|
def __getitem__(self, item: str) -> typing.Any:
|
||||||
@@ -986,7 +990,8 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
|||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
def get_option_name(self, value):
|
@classmethod
|
||||||
|
def get_option_name(cls, value):
|
||||||
return ", ".join(map(str, value))
|
return ", ".join(map(str, value))
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
@@ -996,13 +1001,19 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
|||||||
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||||
default = frozenset()
|
default = frozenset()
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
|
random_str: str | None
|
||||||
|
|
||||||
def __init__(self, value: typing.Iterable[str]):
|
def __init__(self, value: typing.Iterable[str], random_str: str | None = None):
|
||||||
self.value = set(deepcopy(value))
|
self.value = set(deepcopy(value))
|
||||||
|
self.random_str = random_str
|
||||||
super(OptionSet, self).__init__()
|
super(OptionSet, self).__init__()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_text(cls, text: str):
|
def from_text(cls, text: str):
|
||||||
|
check_text = text.lower().split(",")
|
||||||
|
if ((cls.valid_keys or cls.verify_item_name or cls.verify_location_name)
|
||||||
|
and len(check_text) == 1 and check_text[0].startswith("random")):
|
||||||
|
return cls((), check_text[0])
|
||||||
return cls([option.strip() for option in text.split(",")])
|
return cls([option.strip() for option in text.split(",")])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1011,7 +1022,37 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
|||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
def get_option_name(self, value):
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||||
|
if self.random_str and not self.value:
|
||||||
|
choice_list = sorted(self.valid_keys)
|
||||||
|
if self.verify_item_name:
|
||||||
|
choice_list.extend(sorted(world.item_names))
|
||||||
|
if self.verify_location_name:
|
||||||
|
choice_list.extend(sorted(world.location_names))
|
||||||
|
if self.random_str.startswith("random-range-"):
|
||||||
|
textsplit = self.random_str.split("-")
|
||||||
|
try:
|
||||||
|
random_range = [int(textsplit[-2]), int(textsplit[-1])]
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid random range {self.random_str} for option {self.__class__.__name__} "
|
||||||
|
f"for player {player_name}")
|
||||||
|
random_range.sort()
|
||||||
|
if random_range[0] < 0 or random_range[1] > len(choice_list):
|
||||||
|
raise Exception(
|
||||||
|
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||||
|
f"0-{len(choice_list)} for option {self.__class__.__name__} for player {player_name}")
|
||||||
|
if textsplit[2] in ("low", "middle", "high"):
|
||||||
|
choice_count = random_weighted_range(f"{textsplit[0]}-{textsplit[2]}",
|
||||||
|
random_range[0], random_range[1])
|
||||||
|
else:
|
||||||
|
choice_count = random_weighted_range("random", random_range[0], random_range[1])
|
||||||
|
else:
|
||||||
|
choice_count = random_weighted_range(self.random_str, 0, len(choice_list))
|
||||||
|
self.value = set(random.sample(choice_list, k=choice_count))
|
||||||
|
super(Option, self).verify(world, player_name, plando_options)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_option_name(cls, value):
|
||||||
return ", ".join(sorted(value))
|
return ", ".join(sorted(value))
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
@@ -1656,7 +1697,7 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self.value)
|
return len(self.value)
|
||||||
|
|
||||||
|
|
||||||
class Removed(FreeText):
|
class Removed(FreeText):
|
||||||
"""This Option has been Removed."""
|
"""This Option has been Removed."""
|
||||||
rich_text_doc = True
|
rich_text_doc = True
|
||||||
@@ -1742,8 +1783,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
from Utils import local_path, __version__
|
from Utils import local_path, __version__
|
||||||
|
|
||||||
full_path: str
|
full_path: str
|
||||||
|
preset_folder = os.path.join(target_folder, "Presets")
|
||||||
|
|
||||||
os.makedirs(target_folder, exist_ok=True)
|
os.makedirs(target_folder, exist_ok=True)
|
||||||
|
os.makedirs(preset_folder, exist_ok=True)
|
||||||
|
|
||||||
# clean out old
|
# clean out old
|
||||||
for file in os.listdir(target_folder):
|
for file in os.listdir(target_folder):
|
||||||
@@ -1751,11 +1794,16 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||||
os.unlink(full_path)
|
os.unlink(full_path)
|
||||||
|
|
||||||
def dictify_range(option: Range):
|
for file in os.listdir(preset_folder):
|
||||||
data = {option.default: 50}
|
full_path = os.path.join(preset_folder, file)
|
||||||
|
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||||
|
os.unlink(full_path)
|
||||||
|
|
||||||
|
def dictify_range(option: Range, option_val: int | str):
|
||||||
|
data = {option_val: 50}
|
||||||
for sub_option in ["random", "random-low", "random-high",
|
for sub_option in ["random", "random-low", "random-high",
|
||||||
f"random-range-{option.range_start}-{option.range_end}"]:
|
f"random-range-{option.range_start}-{option.range_end}"]:
|
||||||
if sub_option != option.default:
|
if sub_option != option_val:
|
||||||
data[sub_option] = 0
|
data[sub_option] = 0
|
||||||
notes = {
|
notes = {
|
||||||
"random-low": "random value weighted towards lower values",
|
"random-low": "random value weighted towards lower values",
|
||||||
@@ -1768,6 +1816,8 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
if number in data:
|
if number in data:
|
||||||
data[name] = data[number]
|
data[name] = data[number]
|
||||||
del data[number]
|
del data[number]
|
||||||
|
elif name in data:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
data[name] = 0
|
data[name] = 0
|
||||||
|
|
||||||
@@ -1783,20 +1833,27 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
|
|
||||||
for game_name, world in AutoWorldRegister.world_types.items():
|
for game_name, world in AutoWorldRegister.world_types.items():
|
||||||
if not world.hidden or generate_hidden:
|
if not world.hidden or generate_hidden:
|
||||||
|
presets = world.web.options_presets.copy()
|
||||||
|
presets.update({"": {}})
|
||||||
|
|
||||||
option_groups = get_option_groups(world)
|
option_groups = get_option_groups(world)
|
||||||
|
for name, preset in presets.items():
|
||||||
res = template.render(
|
res = template.render(
|
||||||
option_groups=option_groups,
|
option_groups=option_groups,
|
||||||
__version__=__version__,
|
__version__=__version__,
|
||||||
game=game_name,
|
game=game_name,
|
||||||
world_version=world.world_version.as_simple_string(),
|
world_version=world.world_version.as_simple_string(),
|
||||||
yaml_dump=yaml_dump_scalar,
|
yaml_dump=yaml_dump_scalar,
|
||||||
dictify_range=dictify_range,
|
dictify_range=dictify_range,
|
||||||
cleandoc=cleandoc,
|
cleandoc=cleandoc,
|
||||||
)
|
preset_name=name,
|
||||||
|
preset=preset,
|
||||||
with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f:
|
)
|
||||||
f.write(res)
|
preset_name = f" - {name}" if name else ""
|
||||||
|
with open(os.path.join(preset_folder if name else target_folder,
|
||||||
|
get_file_safe_name(game_name + preset_name) + ".yaml"),
|
||||||
|
"w", encoding="utf-8-sig") as f:
|
||||||
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
def dump_player_options(multiworld: MultiWorld) -> None:
|
def dump_player_options(multiworld: MultiWorld) -> None:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
||||||
ToggleButton, MarkupDropdown, ResizableTextField)
|
ToggleButton, MarkupDropdown, ResizableTextField)
|
||||||
|
from kivy.clock import Clock
|
||||||
from kivy.uix.behaviors.button import ButtonBehavior
|
from kivy.uix.behaviors.button import ButtonBehavior
|
||||||
from kivymd.uix.behaviors import RotateBehavior
|
from kivymd.uix.behaviors import RotateBehavior
|
||||||
from kivymd.uix.anchorlayout import MDAnchorLayout
|
from kivymd.uix.anchorlayout import MDAnchorLayout
|
||||||
@@ -28,7 +29,7 @@ import webbrowser
|
|||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from worlds.AutoWorld import AutoWorldRegister, World
|
from worlds.AutoWorld import AutoWorldRegister, World
|
||||||
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed,
|
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList,
|
||||||
OptionCounter, Visibility)
|
OptionCounter, Visibility)
|
||||||
|
|
||||||
|
|
||||||
@@ -269,55 +270,76 @@ class OptionsCreator(ThemedApp):
|
|||||||
self.options = {}
|
self.options = {}
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def export_options(self, button: Widget):
|
@staticmethod
|
||||||
if 0 < len(self.name_input.text) < 17 and self.current_game:
|
def show_result_snack(text: str) -> None:
|
||||||
file_name = Utils.save_filename("Export Options File As...", [("YAML", ["*.yaml"])],
|
MDSnackbar(MDSnackbarText(text=text), y=dp(24), pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
|
||||||
|
|
||||||
|
def on_export_result(self, text: str | None) -> None:
|
||||||
|
self.container.disabled = False
|
||||||
|
if text is not None:
|
||||||
|
Clock.schedule_once(lambda _: self.show_result_snack(text), 0)
|
||||||
|
|
||||||
|
def export_options_background(self, options: dict[str, typing.Any]) -> None:
|
||||||
|
try:
|
||||||
|
file_name = Utils.save_filename("Export Options File As...", [("YAML", [".yaml"])],
|
||||||
Utils.get_file_safe_name(f"{self.name_input.text}.yaml"))
|
Utils.get_file_safe_name(f"{self.name_input.text}.yaml"))
|
||||||
|
except Exception:
|
||||||
|
self.on_export_result("Could not open dialog. Already open?")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not file_name:
|
||||||
|
self.on_export_result(None) # No file selected. No need to show a message for this.
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_name, 'w') as f:
|
||||||
|
f.write(Utils.dump(options, sort_keys=False))
|
||||||
|
f.close()
|
||||||
|
self.on_export_result("File saved successfully.")
|
||||||
|
except Exception:
|
||||||
|
self.on_export_result("Could not save file.")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def export_options(self, button: Widget) -> None:
|
||||||
|
if 0 < len(self.name_input.text) < 17 and self.current_game:
|
||||||
|
import threading
|
||||||
options = {
|
options = {
|
||||||
"name": self.name_input.text,
|
"name": self.name_input.text,
|
||||||
"description": f"YAML generated by Archipelago {Utils.__version__}.",
|
"description": f"YAML generated by Archipelago {Utils.__version__}.",
|
||||||
"game": self.current_game,
|
"game": self.current_game,
|
||||||
self.current_game: {k: check_random(v) for k, v in self.options.items()}
|
self.current_game: {k: check_random(v) for k, v in self.options.items()}
|
||||||
}
|
}
|
||||||
try:
|
threading.Thread(target=self.export_options_background, args=(options,), daemon=True).start()
|
||||||
with open(file_name, 'w') as f:
|
self.container.disabled = True
|
||||||
f.write(Utils.dump(options, sort_keys=False))
|
|
||||||
f.close()
|
|
||||||
MDSnackbar(MDSnackbarText(text="File saved successfully."), y=dp(24), pos_hint={"center_x": 0.5},
|
|
||||||
size_hint_x=0.5).open()
|
|
||||||
except FileNotFoundError:
|
|
||||||
MDSnackbar(MDSnackbarText(text="Saving cancelled."), y=dp(24), pos_hint={"center_x": 0.5},
|
|
||||||
size_hint_x=0.5).open()
|
|
||||||
elif not self.name_input.text:
|
elif not self.name_input.text:
|
||||||
MDSnackbar(MDSnackbarText(text="Name must not be empty."), y=dp(24), pos_hint={"center_x": 0.5},
|
self.show_result_snack("Name must not be empty.")
|
||||||
size_hint_x=0.5).open()
|
|
||||||
elif not self.current_game:
|
elif not self.current_game:
|
||||||
MDSnackbar(MDSnackbarText(text="You must select a game to play."), y=dp(24), pos_hint={"center_x": 0.5},
|
self.show_result_snack("You must select a game to play.")
|
||||||
size_hint_x=0.5).open()
|
|
||||||
else:
|
else:
|
||||||
MDSnackbar(MDSnackbarText(text="Name cannot be longer than 16 characters."), y=dp(24),
|
self.show_result_snack("Name cannot be longer than 16 characters.")
|
||||||
pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
|
|
||||||
|
|
||||||
def create_range(self, option: typing.Type[Range], name: str):
|
def create_range(self, option: typing.Type[Range], name: str, bind=True):
|
||||||
def update_text(range_box: VisualRange):
|
def update_text(range_box: VisualRange):
|
||||||
self.options[name] = int(range_box.slider.value)
|
self.options[name] = int(range_box.slider.value)
|
||||||
range_box.tag.text = str(int(range_box.slider.value))
|
range_box.tag.text = str(int(range_box.slider.value))
|
||||||
return
|
return
|
||||||
|
|
||||||
box = VisualRange(option=option, name=name)
|
box = VisualRange(option=option, name=name)
|
||||||
box.slider.bind(on_touch_move=lambda _, _1: update_text(box))
|
if bind:
|
||||||
|
box.slider.bind(value=lambda _, _1: update_text(box))
|
||||||
self.options[name] = option.default
|
self.options[name] = option.default
|
||||||
return box
|
return box
|
||||||
|
|
||||||
def create_named_range(self, option: typing.Type[NamedRange], name: str):
|
def create_named_range(self, option: typing.Type[NamedRange], name: str):
|
||||||
def set_to_custom(range_box: VisualNamedRange):
|
def set_to_custom(range_box: VisualNamedRange):
|
||||||
if (not self.options[name] == range_box.range.slider.value) \
|
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
||||||
and (not self.options[name] in option.special_range_names or
|
if range_box.range.slider.value in option.special_range_names.values():
|
||||||
range_box.range.slider.value != option.special_range_names[self.options[name]]):
|
value = next(key for key, val in option.special_range_names.items()
|
||||||
# we should validate the touch here,
|
if val == range_box.range.slider.value)
|
||||||
# but this is much cheaper
|
self.options[name] = value
|
||||||
|
set_button_text(box.choice, value.title())
|
||||||
|
else:
|
||||||
self.options[name] = int(range_box.range.slider.value)
|
self.options[name] = int(range_box.range.slider.value)
|
||||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
|
||||||
set_button_text(range_box.choice, "Custom")
|
set_button_text(range_box.choice, "Custom")
|
||||||
|
|
||||||
def set_button_text(button: MDButton, text: str):
|
def set_button_text(button: MDButton, text: str):
|
||||||
@@ -326,7 +348,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
def set_value(text: str, range_box: VisualNamedRange):
|
def set_value(text: str, range_box: VisualNamedRange):
|
||||||
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
|
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
|
||||||
option.range_end)
|
option.range_end)
|
||||||
range_box.range.tag.text = str(int(range_box.range.slider.value))
|
range_box.range.tag.text = str(option.special_range_names[text.lower()])
|
||||||
set_button_text(range_box.choice, text)
|
set_button_text(range_box.choice, text)
|
||||||
self.options[name] = text.lower()
|
self.options[name] = text.lower()
|
||||||
range_box.range.slider.dropdown.dismiss()
|
range_box.range.slider.dropdown.dismiss()
|
||||||
@@ -335,13 +357,18 @@ class OptionsCreator(ThemedApp):
|
|||||||
# for some reason this fixes an issue causing some to not open
|
# for some reason this fixes an issue causing some to not open
|
||||||
box.range.slider.dropdown.open()
|
box.range.slider.dropdown.open()
|
||||||
|
|
||||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name, bind=False))
|
||||||
if option.default in option.special_range_names:
|
default: int | str = option.default
|
||||||
|
if default in option.special_range_names:
|
||||||
# value can get mismatched in this case
|
# value can get mismatched in this case
|
||||||
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
|
box.range.slider.value = min(max(option.special_range_names[default], option.range_start),
|
||||||
option.range_end)
|
option.range_end)
|
||||||
box.range.tag.text = str(int(box.range.slider.value))
|
box.range.tag.text = str(int(box.range.slider.value))
|
||||||
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
elif default in option.special_range_names.values():
|
||||||
|
# better visual
|
||||||
|
default = next(key for key, val in option.special_range_names.items() if val == option.default)
|
||||||
|
set_button_text(box.choice, default.title())
|
||||||
|
box.range.slider.bind(value=lambda _, _2: set_to_custom(box))
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
"text": choice.title(),
|
"text": choice.title(),
|
||||||
@@ -351,7 +378,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
]
|
]
|
||||||
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
|
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
|
||||||
box.choice.bind(on_release=open_dropdown)
|
box.choice.bind(on_release=open_dropdown)
|
||||||
self.options[name] = option.default
|
self.options[name] = default
|
||||||
return box
|
return box
|
||||||
|
|
||||||
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
|
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
|
||||||
@@ -427,8 +454,12 @@ class OptionsCreator(ThemedApp):
|
|||||||
valid_keys = sorted(option.valid_keys)
|
valid_keys = sorted(option.valid_keys)
|
||||||
if option.verify_item_name:
|
if option.verify_item_name:
|
||||||
valid_keys += list(world.item_name_to_id.keys())
|
valid_keys += list(world.item_name_to_id.keys())
|
||||||
|
if option.convert_name_groups:
|
||||||
|
valid_keys += list(world.item_name_groups.keys())
|
||||||
if option.verify_location_name:
|
if option.verify_location_name:
|
||||||
valid_keys += list(world.location_name_to_id.keys())
|
valid_keys += list(world.location_name_to_id.keys())
|
||||||
|
if option.convert_name_groups:
|
||||||
|
valid_keys += list(world.location_name_groups.keys())
|
||||||
|
|
||||||
if not issubclass(option, OptionCounter):
|
if not issubclass(option, OptionCounter):
|
||||||
def apply_changes(button):
|
def apply_changes(button):
|
||||||
@@ -450,14 +481,6 @@ class OptionsCreator(ThemedApp):
|
|||||||
dialog.scrollbox.layout.spacing = dp(5)
|
dialog.scrollbox.layout.spacing = dp(5)
|
||||||
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]
|
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]
|
||||||
|
|
||||||
if name not in self.options:
|
|
||||||
# convert from non-mutable to mutable
|
|
||||||
# We use list syntax even for sets, set behavior is enforced through GUI
|
|
||||||
if issubclass(option, OptionCounter):
|
|
||||||
self.options[name] = deepcopy(option.default)
|
|
||||||
else:
|
|
||||||
self.options[name] = sorted(option.default)
|
|
||||||
|
|
||||||
if issubclass(option, OptionCounter):
|
if issubclass(option, OptionCounter):
|
||||||
for value in sorted(self.options[name]):
|
for value in sorted(self.options[name]):
|
||||||
dialog.add_set_item(value, self.options[name].get(value, None))
|
dialog.add_set_item(value, self.options[name].get(value, None))
|
||||||
@@ -471,6 +494,15 @@ class OptionsCreator(ThemedApp):
|
|||||||
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
|
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
|
||||||
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
|
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
|
||||||
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))
|
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))
|
||||||
|
|
||||||
|
if name not in self.options:
|
||||||
|
# convert from non-mutable to mutable
|
||||||
|
# We use list syntax even for sets, set behavior is enforced through GUI
|
||||||
|
if issubclass(option, OptionCounter):
|
||||||
|
self.options[name] = deepcopy(option.default)
|
||||||
|
else:
|
||||||
|
self.options[name] = sorted(option.default)
|
||||||
|
|
||||||
return main_button
|
return main_button
|
||||||
|
|
||||||
def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:
|
def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:
|
||||||
@@ -509,8 +541,10 @@ class OptionsCreator(ThemedApp):
|
|||||||
self.options[name] = "random-" + str(self.options[name])
|
self.options[name] = "random-" + str(self.options[name])
|
||||||
else:
|
else:
|
||||||
self.options[name] = self.options[name].replace("random-", "")
|
self.options[name] = self.options[name].replace("random-", "")
|
||||||
if self.options[name].isnumeric() or self.options[name] in ("True", "False"):
|
if self.options[name].isnumeric():
|
||||||
self.options[name] = eval(self.options[name])
|
self.options[name] = int(self.options[name])
|
||||||
|
elif self.options[name] in ("True", "False"):
|
||||||
|
self.options[name] = self.options[name] == "True"
|
||||||
|
|
||||||
base_object = instance.parent.parent
|
base_object = instance.parent.parent
|
||||||
label_object = instance.parent
|
label_object = instance.parent
|
||||||
@@ -632,7 +666,7 @@ class OptionsCreator(ThemedApp):
|
|||||||
self.create_options_panel(world_btn)
|
self.create_options_panel(world_btn)
|
||||||
|
|
||||||
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
|
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
|
||||||
if world == "Archipelago":
|
if cls.hidden:
|
||||||
continue
|
continue
|
||||||
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
|
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
|
||||||
pos_hint={"x": 0.03, "center_y": 0.5})
|
pos_hint={"x": 0.03, "center_y": 0.5})
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ Currently, the following games are supported:
|
|||||||
* Celeste (Open World)
|
* Celeste (Open World)
|
||||||
* Choo-Choo Charles
|
* Choo-Choo Charles
|
||||||
* APQuest
|
* APQuest
|
||||||
|
* Satisfactory
|
||||||
|
* EarthBound
|
||||||
|
* Mega Man 3
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
import typing
|
import typing
|
||||||
import bsdiff4
|
import bsdiff4
|
||||||
@@ -15,6 +16,9 @@ from CommonClient import CommonContext, server_loop, \
|
|||||||
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||||
from Utils import async_start
|
from Utils import async_start
|
||||||
|
|
||||||
|
# Heartbeat for position sharing via bounces, in seconds
|
||||||
|
UNDERTALE_STATUS_INTERVAL = 30.0
|
||||||
|
UNDERTALE_ONLINE_TIMEOUT = 60.0
|
||||||
|
|
||||||
class UndertaleCommandProcessor(ClientCommandProcessor):
|
class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||||
def __init__(self, ctx):
|
def __init__(self, ctx):
|
||||||
@@ -109,6 +113,11 @@ class UndertaleContext(CommonContext):
|
|||||||
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
||||||
# self.save_game_folder: files go in this path to pass data between us and the actual game
|
# self.save_game_folder: files go in this path to pass data between us and the actual game
|
||||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
self.last_sent_position: typing.Optional[tuple] = None
|
||||||
|
self.last_room: typing.Optional[str] = None
|
||||||
|
self.last_status_write: float = 0.0
|
||||||
|
self.other_undertale_status: dict[int, dict] = {}
|
||||||
|
|
||||||
|
|
||||||
def patch_game(self):
|
def patch_game(self):
|
||||||
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
||||||
@@ -219,6 +228,9 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||||
str(ctx.slot)+" RoutesDone pacifist",
|
str(ctx.slot)+" RoutesDone pacifist",
|
||||||
str(ctx.slot)+" RoutesDone genocide"]}])
|
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||||
|
if any(info.game == "Undertale" and slot != ctx.slot
|
||||||
|
for slot, info in ctx.slot_info.items()):
|
||||||
|
ctx.set_notify("undertale_room_status")
|
||||||
if args["slot_data"]["only_flakes"]:
|
if args["slot_data"]["only_flakes"]:
|
||||||
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
||||||
f.close()
|
f.close()
|
||||||
@@ -263,6 +275,12 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
||||||
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
||||||
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
||||||
|
if "undertale_room_status" in args["keys"] and args["keys"]["undertale_room_status"]:
|
||||||
|
status = args["keys"]["undertale_room_status"]
|
||||||
|
ctx.other_undertale_status = {
|
||||||
|
int(key): val for key, val in status.items()
|
||||||
|
if int(key) != ctx.slot
|
||||||
|
}
|
||||||
elif cmd == "SetReply":
|
elif cmd == "SetReply":
|
||||||
if args["value"] is not None:
|
if args["value"] is not None:
|
||||||
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
||||||
@@ -271,17 +289,19 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
ctx.completed_routes["genocide"] = args["value"]
|
ctx.completed_routes["genocide"] = args["value"]
|
||||||
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
||||||
ctx.completed_routes["neutral"] = args["value"]
|
ctx.completed_routes["neutral"] = args["value"]
|
||||||
|
if args.get("key") == "undertale_room_status" and args.get("value"):
|
||||||
|
ctx.other_undertale_status = {
|
||||||
|
int(key): val for key, val in args["value"].items()
|
||||||
|
if int(key) != ctx.slot
|
||||||
|
}
|
||||||
elif cmd == "ReceivedItems":
|
elif cmd == "ReceivedItems":
|
||||||
start_index = args["index"]
|
start_index = args["index"]
|
||||||
|
|
||||||
if start_index == 0:
|
if start_index == 0:
|
||||||
ctx.items_received = []
|
ctx.items_received = []
|
||||||
elif start_index != len(ctx.items_received):
|
elif start_index != len(ctx.items_received):
|
||||||
sync_msg = [{"cmd": "Sync"}]
|
await ctx.check_locations(ctx.locations_checked)
|
||||||
if ctx.locations_checked:
|
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||||
sync_msg.append({"cmd": "LocationChecks",
|
|
||||||
"locations": list(ctx.locations_checked)})
|
|
||||||
await ctx.send_msgs(sync_msg)
|
|
||||||
if start_index == len(ctx.items_received):
|
if start_index == len(ctx.items_received):
|
||||||
counter = -1
|
counter = -1
|
||||||
placedWeapon = 0
|
placedWeapon = 0
|
||||||
@@ -368,9 +388,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
elif cmd == "Bounced":
|
elif cmd == "Bounced":
|
||||||
tags = args.get("tags", [])
|
data = args.get("data", {})
|
||||||
if "Online" in tags:
|
if "x" in data and "room" in data:
|
||||||
data = args.get("data", {})
|
|
||||||
if data["player"] != ctx.slot and data["player"] is not None:
|
if data["player"] != ctx.slot and data["player"] is not None:
|
||||||
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
@@ -381,21 +400,63 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
|
|
||||||
async def multi_watcher(ctx: UndertaleContext):
|
async def multi_watcher(ctx: UndertaleContext):
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
path = ctx.save_game_folder
|
if "Online" in ctx.tags and any(
|
||||||
for root, dirs, files in os.walk(path):
|
info.game == "Undertale" and slot != ctx.slot
|
||||||
for file in files:
|
for slot, info in ctx.slot_info.items()):
|
||||||
if "spots.mine" in file and "Online" in ctx.tags:
|
now = time.time()
|
||||||
with open(os.path.join(root, file), "r") as mine:
|
path = ctx.save_game_folder
|
||||||
this_x = mine.readline()
|
for root, dirs, files in os.walk(path):
|
||||||
this_y = mine.readline()
|
for file in files:
|
||||||
this_room = mine.readline()
|
if "spots.mine" in file:
|
||||||
this_sprite = mine.readline()
|
with open(os.path.join(root, file), "r") as mine:
|
||||||
this_frame = mine.readline()
|
this_x = mine.readline()
|
||||||
mine.close()
|
this_y = mine.readline()
|
||||||
message = [{"cmd": "Bounce", "tags": ["Online"],
|
this_room = mine.readline()
|
||||||
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
|
this_sprite = mine.readline()
|
||||||
"spr": this_sprite, "frm": this_frame}}]
|
this_frame = mine.readline()
|
||||||
await ctx.send_msgs(message)
|
|
||||||
|
if this_room != ctx.last_room or \
|
||||||
|
now - ctx.last_status_write >= UNDERTALE_STATUS_INTERVAL:
|
||||||
|
ctx.last_room = this_room
|
||||||
|
ctx.last_status_write = now
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "Set",
|
||||||
|
"key": "undertale_room_status",
|
||||||
|
"default": {},
|
||||||
|
"want_reply": False,
|
||||||
|
"operations": [{"operation": "update",
|
||||||
|
"value": {str(ctx.slot): {"room": this_room,
|
||||||
|
"time": now}}}]
|
||||||
|
}])
|
||||||
|
|
||||||
|
# If player was visible but timed out (heartbeat) or left the room, remove them.
|
||||||
|
for slot, entry in ctx.other_undertale_status.items():
|
||||||
|
if entry.get("room") != this_room or \
|
||||||
|
now - entry.get("time", now) > UNDERTALE_ONLINE_TIMEOUT:
|
||||||
|
playerspot = os.path.join(ctx.save_game_folder,
|
||||||
|
f"FRISK{slot}.playerspot")
|
||||||
|
if os.path.exists(playerspot):
|
||||||
|
os.remove(playerspot)
|
||||||
|
|
||||||
|
current_position = (this_x, this_y, this_room, this_sprite, this_frame)
|
||||||
|
if current_position == ctx.last_sent_position:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Empty status dict = no data yet → send to bootstrap.
|
||||||
|
online_in_room = any(
|
||||||
|
entry.get("room") == this_room and
|
||||||
|
now - entry.get("time", now) <= UNDERTALE_ONLINE_TIMEOUT
|
||||||
|
for entry in ctx.other_undertale_status.values()
|
||||||
|
)
|
||||||
|
if ctx.other_undertale_status and not online_in_room:
|
||||||
|
continue
|
||||||
|
|
||||||
|
message = [{"cmd": "Bounce", "games": ["Undertale"],
|
||||||
|
"data": {"player": ctx.slot, "x": this_x, "y": this_y,
|
||||||
|
"room": this_room, "spr": this_sprite,
|
||||||
|
"frm": this_frame}}]
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
ctx.last_sent_position = current_position
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
@@ -409,10 +470,9 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
for file in files:
|
for file in files:
|
||||||
if ".item" in file:
|
if ".item" in file:
|
||||||
os.remove(os.path.join(root, file))
|
os.remove(os.path.join(root, file))
|
||||||
sync_msg = [{"cmd": "Sync"}]
|
await ctx.check_locations(ctx.locations_checked)
|
||||||
if ctx.locations_checked:
|
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
|
||||||
await ctx.send_msgs(sync_msg)
|
|
||||||
ctx.syncing = False
|
ctx.syncing = False
|
||||||
if ctx.got_deathlink:
|
if ctx.got_deathlink:
|
||||||
ctx.got_deathlink = False
|
ctx.got_deathlink = False
|
||||||
@@ -447,7 +507,7 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
for l in lines:
|
for l in lines:
|
||||||
sending = sending+[(int(l.rstrip('\n')))+12000]
|
sending = sending+[(int(l.rstrip('\n')))+12000]
|
||||||
finally:
|
finally:
|
||||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
|
await ctx.check_locations(sending)
|
||||||
if "victory" in file and str(ctx.route) in file:
|
if "victory" in file and str(ctx.route) in file:
|
||||||
victory = True
|
victory = True
|
||||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||||
|
|||||||
189
Utils.py
189
Utils.py
@@ -18,10 +18,14 @@ import logging
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from settings import Settings, get_settings
|
from settings import Settings, get_settings
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||||
from yaml import load, load_all, dump
|
from yaml import load, load_all, dump
|
||||||
|
from pathspec import PathSpec, GitIgnoreSpec
|
||||||
|
from typing_extensions import deprecated
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
||||||
@@ -48,7 +52,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.6.5"
|
__version__ = "0.6.7"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -314,6 +318,7 @@ def get_public_ipv6() -> str:
|
|||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||||
def get_options() -> Settings:
|
def get_options() -> Settings:
|
||||||
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
|
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||||
return get_settings()
|
return get_settings()
|
||||||
@@ -387,6 +392,14 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
|
|||||||
logging.debug(f"Could not store data package: {e}")
|
logging.debug(f"Could not store data package: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def read_apignore(filename: str | pathlib.Path) -> PathSpec | None:
|
||||||
|
try:
|
||||||
|
with open(filename) as ignore_file:
|
||||||
|
return GitIgnoreSpec.from_lines(ignore_file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
||||||
import LttPAdjuster
|
import LttPAdjuster
|
||||||
adjuster_settings = Namespace()
|
adjuster_settings = Namespace()
|
||||||
@@ -802,29 +815,32 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
|||||||
except tkinter.TclError:
|
except tkinter.TclError:
|
||||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
try:
|
||||||
initialfile=suggest or None)
|
return tkinter.filedialog.askopenfilename(
|
||||||
|
title=title,
|
||||||
|
filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||||
|
initialfile=suggest or None,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
|
||||||
def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
||||||
-> typing.Optional[str]:
|
-> typing.Optional[str]:
|
||||||
logging.info(f"Opening file save dialog for {title}.")
|
logging.info(f"Opening file save dialog for {title}.")
|
||||||
|
|
||||||
def run(*args: str):
|
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
|
||||||
|
|
||||||
if is_linux:
|
if is_linux:
|
||||||
# prefer native dialog
|
# prefer native dialog
|
||||||
from shutil import which
|
from shutil import which
|
||||||
kdialog = which("kdialog")
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||||
return run(kdialog, f"--title={title}", "--getsavefilename", suggest or ".", k_filters)
|
return _run_for_stdout(kdialog, f"--title={title}", "--getsavefilename", suggest or ".", k_filters)
|
||||||
zenity = which("zenity")
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||||
selection = (f"--filename={suggest}",) if suggest else ()
|
selection = (f"--filename={suggest}",) if suggest else ()
|
||||||
return run(zenity, f"--title={title}", "--file-selection", "--save", *z_filters, *selection)
|
return _run_for_stdout(zenity, f"--title={title}", "--file-selection", "--save", *z_filters, *selection)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
try:
|
try:
|
||||||
@@ -847,8 +863,14 @@ def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
|||||||
except tkinter.TclError:
|
except tkinter.TclError:
|
||||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
return tkinter.filedialog.asksaveasfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
try:
|
||||||
initialfile=suggest or None)
|
return tkinter.filedialog.asksaveasfilename(
|
||||||
|
title=title,
|
||||||
|
filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||||
|
initialfile=suggest or None,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
|
||||||
def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
|
def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
|
||||||
@@ -896,6 +918,13 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||||
|
if not gui_enabled:
|
||||||
|
if error:
|
||||||
|
logging.error(f"{title}: {text}")
|
||||||
|
else:
|
||||||
|
logging.info(f"{title}: {text}")
|
||||||
|
return
|
||||||
|
|
||||||
if is_kivy_running():
|
if is_kivy_running():
|
||||||
from kvui import MessageBox
|
from kvui import MessageBox
|
||||||
MessageBox(title, text, error).open()
|
MessageBox(title, text, error).open()
|
||||||
@@ -931,6 +960,9 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
|||||||
root.update()
|
root.update()
|
||||||
|
|
||||||
|
|
||||||
|
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||||
|
"""Checks if the user wanted no GUI mode and has a terminal to use it with."""
|
||||||
|
|
||||||
def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))):
|
def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))):
|
||||||
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||||
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
||||||
@@ -975,6 +1007,7 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
|
|||||||
|
|
||||||
|
|
||||||
def deprecate(message: str, add_stacklevels: int = 0):
|
def deprecate(message: str, add_stacklevels: int = 0):
|
||||||
|
"""also use typing_extensions.deprecated wherever you use this"""
|
||||||
if __debug__:
|
if __debug__:
|
||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
||||||
@@ -1039,6 +1072,7 @@ def _extend_freeze_support() -> None:
|
|||||||
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
|
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Use multiprocessing.freeze_support() instead")
|
||||||
def freeze_support() -> None:
|
def freeze_support() -> None:
|
||||||
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
|
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
@@ -1050,9 +1084,18 @@ def freeze_support() -> None:
|
|||||||
_extend_freeze_support()
|
_extend_freeze_support()
|
||||||
|
|
||||||
|
|
||||||
def visualize_regions(root_region: Region, file_name: str, *,
|
def visualize_regions(
|
||||||
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
|
root_region: Region,
|
||||||
linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None:
|
file_name: str,
|
||||||
|
*,
|
||||||
|
show_entrance_names: bool = False,
|
||||||
|
show_locations: bool = True,
|
||||||
|
show_other_regions: bool = True,
|
||||||
|
linetype_ortho: bool = True,
|
||||||
|
regions_to_highlight: set[Region] | None = None,
|
||||||
|
entrance_highlighting: dict[int, int] | None = None,
|
||||||
|
detail_other_regions: bool = False,
|
||||||
|
auto_assign_colors: bool = False) -> None:
|
||||||
"""Visualize the layout of a world as a PlantUML diagram.
|
"""Visualize the layout of a world as a PlantUML diagram.
|
||||||
|
|
||||||
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
||||||
@@ -1069,6 +1112,13 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
|||||||
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
||||||
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
||||||
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
|
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
|
||||||
|
:param entrance_highlighting: a mapping from your world's entrance randomization groups to RGB values, used to color
|
||||||
|
your entrances
|
||||||
|
:param detail_other_regions: (default False) If enabled, will fully visualize regions that aren't reachable
|
||||||
|
from root_region.
|
||||||
|
:param auto_assign_colors: (default False) If enabled, will automatically assign random colors to entrances of the
|
||||||
|
same randomization group. Uses entrance_highlighting first, and only picks random colors for entrance groups
|
||||||
|
not found in the passed-in map
|
||||||
|
|
||||||
Example usage in World code:
|
Example usage in World code:
|
||||||
from Utils import visualize_regions
|
from Utils import visualize_regions
|
||||||
@@ -1094,6 +1144,34 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
|||||||
regions: typing.Deque[Region] = deque((root_region,))
|
regions: typing.Deque[Region] = deque((root_region,))
|
||||||
multiworld: MultiWorld = root_region.multiworld
|
multiworld: MultiWorld = root_region.multiworld
|
||||||
|
|
||||||
|
colors_used: set[int] = set()
|
||||||
|
if entrance_highlighting:
|
||||||
|
for color in entrance_highlighting.values():
|
||||||
|
# filter the colors to their most-significant bits to avoid too similar colors
|
||||||
|
colors_used.add(color & 0xF0F0F0)
|
||||||
|
else:
|
||||||
|
# assign an empty dict to not crash later
|
||||||
|
# the parameter is optional for ease of use when you don't care about colors
|
||||||
|
entrance_highlighting = {}
|
||||||
|
|
||||||
|
def select_color(group: int) -> int:
|
||||||
|
# specifically spacing color indexes by three different prime numbers (3, 5, 7) for the RGB components to avoid
|
||||||
|
# obvious cyclical color patterns
|
||||||
|
COLOR_INDEX_SPACING: int = 0x357
|
||||||
|
new_color_index: int = (group * COLOR_INDEX_SPACING) % 0x1000
|
||||||
|
new_color = ((new_color_index & 0xF00) << 12) + \
|
||||||
|
((new_color_index & 0xF0) << 8) + \
|
||||||
|
((new_color_index & 0xF) << 4)
|
||||||
|
while new_color in colors_used:
|
||||||
|
# while this is technically unbounded, expected collisions are low. There are 4095 possible colors
|
||||||
|
# and worlds are unlikely to get to anywhere close to that many entrance groups
|
||||||
|
# intentionally not using multiworld.random to not affect output when debugging with this tool
|
||||||
|
new_color_index += COLOR_INDEX_SPACING
|
||||||
|
new_color = ((new_color_index & 0xF00) << 12) + \
|
||||||
|
((new_color_index & 0xF0) << 8) + \
|
||||||
|
((new_color_index & 0xF) << 4)
|
||||||
|
return new_color
|
||||||
|
|
||||||
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
|
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
|
||||||
name = obj.name
|
name = obj.name
|
||||||
if isinstance(obj, Item):
|
if isinstance(obj, Item):
|
||||||
@@ -1113,18 +1191,28 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
|||||||
|
|
||||||
def visualize_exits(region: Region) -> None:
|
def visualize_exits(region: Region) -> None:
|
||||||
for exit_ in region.exits:
|
for exit_ in region.exits:
|
||||||
|
color_code: str = ""
|
||||||
|
if exit_.randomization_group in entrance_highlighting:
|
||||||
|
color_code = f" #{entrance_highlighting[exit_.randomization_group]:0>6X}"
|
||||||
if exit_.connected_region:
|
if exit_.connected_region:
|
||||||
if show_entrance_names:
|
if show_entrance_names:
|
||||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"")
|
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"{color_code}")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"")
|
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"{color_code}")
|
||||||
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"")
|
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"{color_code}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"")
|
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"{color_code}")
|
||||||
else:
|
else:
|
||||||
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"")
|
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\" {color_code}")
|
||||||
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"")
|
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"{color_code}")
|
||||||
|
for entrance in region.entrances:
|
||||||
|
color_code: str = ""
|
||||||
|
if entrance.randomization_group in entrance_highlighting:
|
||||||
|
color_code = f" #{entrance_highlighting[entrance.randomization_group]:0>6X}"
|
||||||
|
if not entrance.parent_region:
|
||||||
|
uml.append(f"circle \"unconnected entrance:\\n{fmt(entrance)}\"{color_code}")
|
||||||
|
uml.append(f"\"unconnected entrance:\\n{fmt(entrance)}\" --> \"{fmt(region)}\"{color_code}")
|
||||||
|
|
||||||
def visualize_locations(region: Region) -> None:
|
def visualize_locations(region: Region) -> None:
|
||||||
any_lock = any(location.locked for location in region.locations)
|
any_lock = any(location.locked for location in region.locations)
|
||||||
@@ -1145,9 +1233,27 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
|||||||
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
|
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
|
||||||
uml.append("package \"other regions\" <<Cloud>> {")
|
uml.append("package \"other regions\" <<Cloud>> {")
|
||||||
for region in other_regions:
|
for region in other_regions:
|
||||||
uml.append(f"class \"{fmt(region)}\"")
|
if detail_other_regions:
|
||||||
|
visualize_region(region)
|
||||||
|
else:
|
||||||
|
uml.append(f"class \"{fmt(region)}\"")
|
||||||
uml.append("}")
|
uml.append("}")
|
||||||
|
|
||||||
|
if auto_assign_colors:
|
||||||
|
all_entrances: list[Entrance] = []
|
||||||
|
for region in multiworld.get_regions(root_region.player):
|
||||||
|
all_entrances.extend(region.entrances)
|
||||||
|
all_entrances.extend(region.exits)
|
||||||
|
all_groups: list[int] = sorted(set([entrance.randomization_group for entrance in all_entrances]))
|
||||||
|
for group in all_groups:
|
||||||
|
if group not in entrance_highlighting:
|
||||||
|
if len(colors_used) >= 0x1000:
|
||||||
|
# on the off chance someone makes 4096 different entrance groups, don't cycle forever
|
||||||
|
break
|
||||||
|
new_color: int = select_color(group)
|
||||||
|
entrance_highlighting[group] = new_color
|
||||||
|
colors_used.add(new_color)
|
||||||
|
|
||||||
uml.append("@startuml")
|
uml.append("@startuml")
|
||||||
uml.append("hide circle")
|
uml.append("hide circle")
|
||||||
uml.append("hide empty members")
|
uml.append("hide empty members")
|
||||||
@@ -1158,7 +1264,7 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
|||||||
seen.add(current_region)
|
seen.add(current_region)
|
||||||
visualize_region(current_region)
|
visualize_region(current_region)
|
||||||
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
|
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
|
||||||
if show_other_regions:
|
if show_other_regions or detail_other_regions:
|
||||||
visualize_other_regions()
|
visualize_other_regions()
|
||||||
uml.append("@enduml")
|
uml.append("@enduml")
|
||||||
|
|
||||||
@@ -1187,6 +1293,15 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
|
|||||||
return isinstance(obj, typing.Iterable)
|
return isinstance(obj, typing.Iterable)
|
||||||
|
|
||||||
|
|
||||||
|
def utcnow() -> datetime:
|
||||||
|
"""
|
||||||
|
Implementation of Python's datetime.utcnow() function for use after deprecation.
|
||||||
|
Needed for timezone-naive UTC datetimes stored in databases with PonyORM (upstream).
|
||||||
|
https://ponyorm.org/ponyorm-list/2014-August/000113.html
|
||||||
|
"""
|
||||||
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
|
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
|
||||||
"""
|
"""
|
||||||
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.
|
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.
|
||||||
@@ -1222,3 +1337,35 @@ class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
|
|||||||
t.start()
|
t.start()
|
||||||
self._threads.add(t)
|
self._threads.add(t)
|
||||||
# NOTE: don't add to _threads_queues so we don't block on shutdown
|
# NOTE: don't add to _threads_queues so we don't block on shutdown
|
||||||
|
|
||||||
|
|
||||||
|
def get_full_typename(t: type) -> str:
|
||||||
|
"""Returns the full qualified name of a type, including its module (if not builtins)."""
|
||||||
|
module = t.__module__
|
||||||
|
if module and module != "builtins":
|
||||||
|
return f"{module}.{t.__qualname__}"
|
||||||
|
return t.__qualname__
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_causes(ex: Exception) -> str:
|
||||||
|
"""Return a string describing the recursive causes of this exception.
|
||||||
|
|
||||||
|
:param ex: The exception to be described.
|
||||||
|
:return A multiline string starting with the initial exception on the first line and each resulting exception
|
||||||
|
on subsequent lines with progressive indentation.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Exception: Invalid value 'bad'.
|
||||||
|
Which caused: Options.OptionError: Error generating option
|
||||||
|
Which caused: ValueError: File bad.yaml is invalid.
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
cause = ex
|
||||||
|
causes = [f"{get_full_typename(type(ex))}: {ex}"]
|
||||||
|
while cause := cause.__cause__:
|
||||||
|
causes.append(f"{get_full_typename(type(cause))}: {cause}")
|
||||||
|
top = causes[-1]
|
||||||
|
others = "".join(f"\n{' ' * (i + 1)}Which caused: {c}" for i, c in enumerate(reversed(causes[:-1])))
|
||||||
|
return f"{top}{others}"
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ if typing.TYPE_CHECKING:
|
|||||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||||
settings.no_gui = True
|
settings.no_gui = True
|
||||||
configpath = os.path.abspath("config.yaml")
|
configpath = os.path.abspath("config.yaml")
|
||||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
if not os.path.exists(configpath):
|
||||||
|
# fall back to config.yaml in user_path if config does not exist in cwd to match settings.py
|
||||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,20 @@
|
|||||||
# WebHost
|
# WebHost
|
||||||
|
|
||||||
|
## Asset License
|
||||||
|
|
||||||
|
The image files used in the page design were specifically designed for archipelago.gg and are **not** covered by the top
|
||||||
|
level LICENSE.
|
||||||
|
See individual LICENSE files in `./static/static/**`.
|
||||||
|
|
||||||
|
You are only allowed to use them for personal use, testing and development.
|
||||||
|
If the site is reachable over the internet, have a robots.txt in place (see `ASSET_RIGHTS` in `config.yaml`)
|
||||||
|
and do not promote it publicly. Alternatively replace or remove the assets.
|
||||||
|
|
||||||
## Contribution Guidelines
|
## Contribution Guidelines
|
||||||
**Thank you for your interest in contributing to the Archipelago website!**
|
|
||||||
Much of the content on the website is generated automatically, but there are some things
|
|
||||||
that need a personal touch. For those things, we rely on contributions from both the core
|
|
||||||
team and the community. The current primary maintainer of the website is Farrak Kilhn.
|
|
||||||
He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`.
|
|
||||||
|
|
||||||
### Small Changes
|
Pages should preferably be rendered on the server side with Jinja. Features should work with noscript if feasible.
|
||||||
Little changes like adding a button or a couple new select elements are perfectly fine.
|
Design changes have to fit the overall design.
|
||||||
Tweaks to style specific to a PR's content are also probably not a problem. For example, if
|
|
||||||
you build a new page which needs two side by side tables, and you need to write a CSS file
|
|
||||||
specific to your page, that is perfectly reasonable.
|
|
||||||
|
|
||||||
### Content Additions
|
Introduction of JS dependencies should first be discussed on Discord or in a draft PR.
|
||||||
Once you develop a new feature or add new content the website, make a pull request. It will
|
|
||||||
be reviewed by the community and there will probably be some discussion around it. Depending
|
|
||||||
on the size of the feature, and if new styles are required, there may be an additional step
|
|
||||||
before the PR is accepted wherein Farrak works with the designer to implement styles.
|
|
||||||
|
|
||||||
### Restrictions on Style Changes
|
See also [docs/style.md](/docs/style.md) for the style guide.
|
||||||
A professional designer is paid to develop the styles and assets for the Archipelago website.
|
|
||||||
In an effort to maintain a consistent look and feel, pull requests which *exclusively*
|
|
||||||
change site styles are rejected. Please note this applies to code which changes the overall
|
|
||||||
look and feel of the site, not to small tweaks to CSS for your custom page. The intention
|
|
||||||
behind these restrictions is to maintain a curated feel for the design of the site. If
|
|
||||||
any PR affects the overall feel of the site but includes additive changes, there will
|
|
||||||
likely be a conversation about how to implement those changes without compromising the
|
|
||||||
curated site style. It is therefore worth noting there are a couple files which, if
|
|
||||||
changed in your pull request, will cause it to draw additional scrutiny.
|
|
||||||
|
|
||||||
These closely guarded files are:
|
|
||||||
- `globalStyles.css`
|
|
||||||
- `islandFooter.css`
|
|
||||||
- `landing.css`
|
|
||||||
- `markdown.css`
|
|
||||||
- `tooltip.css`
|
|
||||||
|
|
||||||
### Site Themes
|
|
||||||
There are several themes available for game pages. It is possible to request a new theme in
|
|
||||||
the `#art-and-design` channel on Discord. Because themes are created by the designer, they
|
|
||||||
are not free, and take some time to create. Farrak works closely with the designer to implement
|
|
||||||
these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year
|
|
||||||
are added. If a proposed theme seems like a cool idea and the community likes it, there is a
|
|
||||||
good chance it will become a reality.
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from pony.flask import Pony
|
|||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from Utils import title_sorted, get_file_safe_name
|
from Utils import title_sorted, get_file_safe_name
|
||||||
|
from .cli import CLI
|
||||||
|
|
||||||
UPLOAD_FOLDER = os.path.relpath('uploads')
|
UPLOAD_FOLDER = os.path.relpath('uploads')
|
||||||
LOGS_FOLDER = os.path.relpath('logs')
|
LOGS_FOLDER = os.path.relpath('logs')
|
||||||
@@ -23,6 +24,17 @@ app.jinja_env.filters['any'] = any
|
|||||||
app.jinja_env.filters['all'] = all
|
app.jinja_env.filters['all'] = all
|
||||||
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
|
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
|
||||||
|
|
||||||
|
# overwrites of flask default config
|
||||||
|
app.config["DEBUG"] = False
|
||||||
|
app.config["PORT"] = 80
|
||||||
|
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||||
|
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||||
|
# if you want to deploy, make sure you have a non-guessable secret key
|
||||||
|
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||||
|
app.config["SESSION_PERMANENT"] = True
|
||||||
|
app.config["MAX_FORM_MEMORY_SIZE"] = 2 * 1024 * 1024 # 2 MB, needed for large option pages such as SC2
|
||||||
|
|
||||||
|
# custom config
|
||||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||||
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
||||||
@@ -30,19 +42,14 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
|
|||||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||||
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
||||||
app.config["DEBUG"] = False
|
|
||||||
app.config["PORT"] = 80
|
|
||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
|
|
||||||
# if you want to deploy, make sure you have a non-guessable secret key
|
|
||||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
|
||||||
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
||||||
app.config["JOB_THRESHOLD"] = 1
|
app.config["JOB_THRESHOLD"] = 1
|
||||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||||
app.config["JOB_TIME"] = 600
|
app.config["JOB_TIME"] = 600
|
||||||
|
# maximum time in seconds since last activity for a room to be hosted
|
||||||
|
app.config["MAX_ROOM_TIMEOUT"] = 259200
|
||||||
# memory limit for generator processes in bytes
|
# memory limit for generator processes in bytes
|
||||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||||
app.config['SESSION_PERMANENT'] = True
|
|
||||||
|
|
||||||
# waitress uses one thread for I/O, these are for processing of views that then get sent
|
# waitress uses one thread for I/O, these are for processing of views that then get sent
|
||||||
# archipelago.gg uses gunicorn + nginx; ignoring this option
|
# archipelago.gg uses gunicorn + nginx; ignoring this option
|
||||||
@@ -60,6 +67,7 @@ app.config["ASSET_RIGHTS"] = False
|
|||||||
|
|
||||||
cache = Cache()
|
cache = Cache()
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
CLI(app)
|
||||||
|
|
||||||
|
|
||||||
def to_python(value: str) -> uuid.UUID:
|
def to_python(value: str) -> uuid.UUID:
|
||||||
|
|||||||
@@ -2,10 +2,20 @@
|
|||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
from ..models import Seed, Slot
|
from ..models import Seed, Slot
|
||||||
|
|
||||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||||
|
cors = CORS(api_endpoints, resources={
|
||||||
|
r"/api/datapackage/*": {"origins": "*"},
|
||||||
|
r"/api/datapackage": {"origins": "*"},
|
||||||
|
r"/api/datapackage_checksum/*": {"origins": "*"},
|
||||||
|
r"/api/room_status/*": {"origins": "*"},
|
||||||
|
r"/api/tracker/*": {"origins": "*"},
|
||||||
|
r"/api/static_tracker/*": {"origins": "*"},
|
||||||
|
r"/api/slot_data_tracker/*": {"origins": "*"}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
def get_players(seed: Seed) -> List[Tuple[str, str]]:
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import typing
|
import typing
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta
|
||||||
from threading import Event, Thread
|
from threading import Event, Thread
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pony.orm import db_session, select, commit, PrimaryKey
|
from pony.orm import db_session, select, commit, PrimaryKey, desc
|
||||||
|
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads, utcnow
|
||||||
from .locker import Locker, AlreadyRunningException
|
from .locker import Locker, AlreadyRunningException
|
||||||
|
|
||||||
_stop_event = Event()
|
_stop_event = Event()
|
||||||
@@ -129,10 +129,11 @@ def autohost(config: dict):
|
|||||||
with db_session:
|
with db_session:
|
||||||
rooms = select(
|
rooms = select(
|
||||||
room for room in Room if
|
room for room in Room if
|
||||||
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
room.last_activity >= utcnow() - timedelta(
|
||||||
|
seconds=config["MAX_ROOM_TIMEOUT"])).order_by(desc(Room.last_port))
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
|
||||||
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
|
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
|
||||||
hosters[room.id.int % len(hosters)].start_room(room.id)
|
hosters[room.id.int % len(hosters)].start_room(room.id)
|
||||||
|
|
||||||
except AlreadyRunningException:
|
except AlreadyRunningException:
|
||||||
|
|||||||
8
WebHostLib/cli/__init__.py
Normal file
8
WebHostLib/cli/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
|
||||||
|
class CLI:
|
||||||
|
def __init__(self, app: Flask) -> None:
|
||||||
|
from .stats import stats_cli
|
||||||
|
|
||||||
|
app.cli.add_command(stats_cli)
|
||||||
36
WebHostLib/cli/stats.py
Normal file
36
WebHostLib/cli/stats.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import click
|
||||||
|
from flask.cli import AppGroup
|
||||||
|
from pony.orm import raw_sql
|
||||||
|
|
||||||
|
from Utils import format_SI_prefix
|
||||||
|
|
||||||
|
stats_cli = AppGroup("stats")
|
||||||
|
|
||||||
|
|
||||||
|
@stats_cli.command("show")
|
||||||
|
def show() -> None:
|
||||||
|
from pony.orm import db_session, select
|
||||||
|
|
||||||
|
from WebHostLib.models import GameDataPackage
|
||||||
|
|
||||||
|
total_games_package_count: int = 0
|
||||||
|
total_games_package_size: int
|
||||||
|
top_10_package_sizes: list[tuple[int, str]] = []
|
||||||
|
|
||||||
|
with db_session:
|
||||||
|
data_length = raw_sql("LENGTH(data)")
|
||||||
|
data_length_desc = raw_sql("LENGTH(data) DESC")
|
||||||
|
data_length_sum = raw_sql("SUM(LENGTH(data))")
|
||||||
|
total_games_package_count = GameDataPackage.select().count()
|
||||||
|
total_games_package_size = select(data_length_sum for _ in GameDataPackage).first() # type: ignore
|
||||||
|
top_10_package_sizes = list(
|
||||||
|
select((data_length, dp.checksum) for dp in GameDataPackage) # type: ignore
|
||||||
|
.order_by(lambda _, _2: data_length_desc)
|
||||||
|
.limit(10)
|
||||||
|
)
|
||||||
|
|
||||||
|
click.echo(f"Total number of games packages: {total_games_package_count}")
|
||||||
|
click.echo(f"Total size of games packages: {format_SI_prefix(total_games_package_size, power=1024)}B")
|
||||||
|
click.echo(f"Top {len(top_10_package_sizes)} biggest games packages:")
|
||||||
|
for size, checksum in top_10_package_sizes:
|
||||||
|
click.echo(f" {checksum}: {size:>8d}")
|
||||||
@@ -89,19 +89,24 @@ class WebHostContext(Context):
|
|||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||||
|
|
||||||
def listen_to_db_commands(self):
|
async def listen_to_db_commands(self):
|
||||||
cmdprocessor = DBCommandProcessor(self)
|
cmdprocessor = DBCommandProcessor(self)
|
||||||
|
|
||||||
while not self.exit_event.is_set():
|
while not self.exit_event.is_set():
|
||||||
with db_session:
|
await self.main_loop.run_in_executor(None, self._process_db_commands, cmdprocessor)
|
||||||
commands = select(command for command in Command if command.room.id == self.room_id)
|
try:
|
||||||
if commands:
|
await asyncio.wait_for(self.exit_event.wait(), 5)
|
||||||
for command in commands:
|
except asyncio.TimeoutError:
|
||||||
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
pass
|
||||||
command.delete()
|
|
||||||
commit()
|
def _process_db_commands(self, cmdprocessor):
|
||||||
del commands
|
with db_session:
|
||||||
time.sleep(5)
|
commands = select(command for command in Command if command.room.id == self.room_id)
|
||||||
|
if commands:
|
||||||
|
for command in commands:
|
||||||
|
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
||||||
|
command.delete()
|
||||||
|
commit()
|
||||||
|
|
||||||
@db_session
|
@db_session
|
||||||
def load(self, room_id: int):
|
def load(self, room_id: int):
|
||||||
@@ -156,9 +161,9 @@ class WebHostContext(Context):
|
|||||||
with db_session:
|
with db_session:
|
||||||
savegame_data = Room.get(id=self.room_id).multisave
|
savegame_data = Room.get(id=self.room_id).multisave
|
||||||
if savegame_data:
|
if savegame_data:
|
||||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
self.set_save(restricted_loads(savegame_data))
|
||||||
self._start_async_saving(atexit_save=False)
|
self._start_async_saving(atexit_save=False)
|
||||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
asyncio.create_task(self.listen_to_db_commands())
|
||||||
|
|
||||||
@db_session
|
@db_session
|
||||||
def _save(self, exit_save: bool = False) -> bool:
|
def _save(self, exit_save: bool = False) -> bool:
|
||||||
@@ -167,7 +172,7 @@ class WebHostContext(Context):
|
|||||||
room.multisave = pickle.dumps(self.get_save())
|
room.multisave = pickle.dumps(self.get_save())
|
||||||
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
|
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
|
||||||
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
|
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
|
||||||
room.last_activity = datetime.datetime.utcnow()
|
room.last_activity = Utils.utcnow()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_save(self) -> dict:
|
def get_save(self) -> dict:
|
||||||
@@ -229,6 +234,17 @@ def set_up_logging(room_id) -> logging.Logger:
|
|||||||
return logger
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def tear_down_logging(room_id):
|
||||||
|
"""Close logging handling for a room."""
|
||||||
|
logger_name = f"RoomLogger {room_id}"
|
||||||
|
if logger_name in logging.Logger.manager.loggerDict:
|
||||||
|
logger = logging.getLogger(logger_name)
|
||||||
|
for handler in logger.handlers[:]:
|
||||||
|
logger.removeHandler(handler)
|
||||||
|
handler.close()
|
||||||
|
del logging.Logger.manager.loggerDict[logger_name]
|
||||||
|
|
||||||
|
|
||||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||||
@@ -325,7 +341,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
if ctx.saving:
|
if ctx.saving:
|
||||||
ctx._save()
|
ctx._save(True)
|
||||||
setattr(asyncio.current_task(), "save", None)
|
setattr(asyncio.current_task(), "save", None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
with db_session:
|
with db_session:
|
||||||
@@ -336,19 +352,24 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
|||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
if ctx.saving:
|
if ctx.saving:
|
||||||
ctx._save()
|
ctx._save(True)
|
||||||
setattr(asyncio.current_task(), "save", None)
|
setattr(asyncio.current_task(), "save", None)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
|
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
|
||||||
ctx.exit_event.set() # make sure the saving thread stops at some point
|
ctx.exit_event.set() # make sure the saving thread stops at some point
|
||||||
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
|
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
|
||||||
|
|
||||||
|
if ctx.server and hasattr(ctx.server, "ws_server"):
|
||||||
|
ctx.server.ws_server.close()
|
||||||
|
await ctx.server.ws_server.wait_closed()
|
||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
room.last_activity = datetime.datetime.utcnow() - \
|
room.last_activity = Utils.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
|
||||||
del room
|
del room
|
||||||
|
tear_down_logging(room_id)
|
||||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||||
finally:
|
finally:
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from datetime import timedelta, datetime
|
from datetime import timedelta
|
||||||
|
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from pony.orm import count
|
from pony.orm import count
|
||||||
|
|
||||||
|
from Utils import utcnow
|
||||||
from WebHostLib import app, cache
|
from WebHostLib import app, cache
|
||||||
from .models import Room, Seed
|
from .models import Room, Seed
|
||||||
|
|
||||||
@@ -10,6 +11,6 @@ from .models import Room, Seed
|
|||||||
@app.route('/', methods=['GET', 'POST'])
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
@cache.cached(timeout=300) # cache has to appear under app route for caching to work
|
@cache.cached(timeout=300) # cache has to appear under app route for caching to work
|
||||||
def landing():
|
def landing():
|
||||||
rooms = count(room for room in Room if room.creation_time >= datetime.utcnow() - timedelta(days=7))
|
rooms = count(room for room in Room if room.creation_time >= utcnow() - timedelta(days=7))
|
||||||
seeds = count(seed for seed in Seed if seed.creation_time >= datetime.utcnow() - timedelta(days=7))
|
seeds = count(seed for seed in Seed if seed.creation_time >= utcnow() - timedelta(days=7))
|
||||||
return render_template("landing.html", rooms=rooms, seeds=seeds)
|
return render_template("landing.html", rooms=rooms, seeds=seeds)
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ from flask import request, redirect, url_for, render_template, Response, session
|
|||||||
from pony.orm import count, commit, db_session
|
from pony.orm import count, commit, db_session
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister, World
|
from worlds.AutoWorld import AutoWorldRegister, World
|
||||||
from . import app, cache
|
from . import app, cache
|
||||||
from .markdown import render_markdown
|
from .markdown import render_markdown
|
||||||
from .models import Seed, Room, Command, UUID, uuid4
|
from .models import Seed, Room, Command, UUID, uuid4
|
||||||
from Utils import title_sorted
|
from Utils import title_sorted, utcnow
|
||||||
|
|
||||||
class WebWorldTheme(StrEnum):
|
class WebWorldTheme(StrEnum):
|
||||||
DIRT = "dirt"
|
DIRT = "dirt"
|
||||||
@@ -128,8 +129,13 @@ def tutorial_landing():
|
|||||||
"authors": tutorial.authors,
|
"authors": tutorial.authors,
|
||||||
"language": tutorial.language
|
"language": tutorial.language
|
||||||
}
|
}
|
||||||
tutorials = {world_name: tutorials for world_name, tutorials in title_sorted(
|
|
||||||
tutorials.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game)}
|
worlds = dict(
|
||||||
|
title_sorted(
|
||||||
|
worlds.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return render_template("tutorialLanding.html", worlds=worlds, tutorials=tutorials)
|
return render_template("tutorialLanding.html", worlds=worlds, tutorials=tutorials)
|
||||||
|
|
||||||
|
|
||||||
@@ -228,11 +234,12 @@ def host_room(room: UUID):
|
|||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
|
||||||
now = datetime.datetime.utcnow()
|
now = utcnow()
|
||||||
# indicate that the page should reload to get the assigned port
|
# indicate that the page should reload to get the assigned port
|
||||||
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
should_refresh = (
|
||||||
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
|
(not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
|
||||||
|
or room.last_activity < now - datetime.timedelta(seconds=room.timeout)
|
||||||
|
)
|
||||||
if now - room.last_activity > datetime.timedelta(minutes=1):
|
if now - room.last_activity > datetime.timedelta(minutes=1):
|
||||||
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
|
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
|
||||||
# due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction"
|
# due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from datetime import datetime
|
|||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
|
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
|
||||||
|
|
||||||
|
from Utils import utcnow
|
||||||
|
|
||||||
db = Database()
|
db = Database()
|
||||||
|
|
||||||
STATE_QUEUED = 0
|
STATE_QUEUED = 0
|
||||||
@@ -20,8 +22,8 @@ class Slot(db.Entity):
|
|||||||
|
|
||||||
class Room(db.Entity):
|
class Room(db.Entity):
|
||||||
id = PrimaryKey(UUID, default=uuid4)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
|
last_activity: datetime = Required(datetime, default=lambda: utcnow(), index=True)
|
||||||
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
|
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
|
||||||
owner = Required(UUID, index=True)
|
owner = Required(UUID, index=True)
|
||||||
commands = Set('Command')
|
commands = Set('Command')
|
||||||
seed = Required('Seed', index=True)
|
seed = Required('Seed', index=True)
|
||||||
@@ -38,7 +40,7 @@ class Seed(db.Entity):
|
|||||||
rooms = Set(Room)
|
rooms = Set(Room)
|
||||||
multidata = Required(bytes, lazy=True)
|
multidata = Required(bytes, lazy=True)
|
||||||
owner = Required(UUID, index=True)
|
owner = Required(UUID, index=True)
|
||||||
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
|
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
|
||||||
slots = Set(Slot)
|
slots = Set(Slot)
|
||||||
spoiler = Optional(LongStr, lazy=True)
|
spoiler = Optional(LongStr, lazy=True)
|
||||||
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
|
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ waitress>=3.0.2
|
|||||||
Flask-Caching>=2.3.0
|
Flask-Caching>=2.3.0
|
||||||
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
|
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
|
||||||
Flask-Limiter>=3.12
|
Flask-Limiter>=3.12
|
||||||
|
Flask-Cors>=6.0.2
|
||||||
bokeh>=3.6.3
|
bokeh>=3.6.3
|
||||||
markupsafe>=3.0.2
|
markupsafe>=3.0.2
|
||||||
setproctitle>=1.3.5
|
setproctitle>=1.3.5
|
||||||
|
|||||||
@@ -55,6 +55,9 @@
|
|||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="named-range-container">
|
<div class="named-range-container">
|
||||||
<select id="{{ option_name }}-select" name="{{ option_name }}" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
<select id="{{ option_name }}-select" name="{{ option_name }}" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||||
|
{% if option.default not in option.special_range_names.values() %}
|
||||||
|
<option value="{{ option.default }}" selected>Default ({{ option.default }})</option>
|
||||||
|
{% endif %}
|
||||||
{% for key, val in option.special_range_names.items() %}
|
{% for key, val in option.special_range_names.items() %}
|
||||||
{% if option.default == val %}
|
{% if option.default == val %}
|
||||||
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
||||||
@@ -94,6 +97,9 @@
|
|||||||
<div class="text-choice-container">
|
<div class="text-choice-container">
|
||||||
<div class="text-choice-wrapper">
|
<div class="text-choice-wrapper">
|
||||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||||
|
{% if option.default not in option.options.values() %}
|
||||||
|
<option value="{{ option.default }}" selected>Default ({{ option.default }})</option>
|
||||||
|
{% endif %}
|
||||||
{% for id, name in option.name_lookup.items()|sort %}
|
{% for id, name in option.name_lookup.items()|sort %}
|
||||||
{% if name != "random" %}
|
{% if name != "random" %}
|
||||||
{% if option.default == id %}
|
{% if option.default == id %}
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
<h1>Currently Supported Games</h1>
|
<h1>Currently Supported Games</h1>
|
||||||
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
|
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
|
||||||
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
|
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
|
||||||
custom worlds</a> section of the setup guide.</p>
|
custom worlds</a> section of the setup guide and the
|
||||||
|
<a href="{{ url_for("tutorial", game="Archipelago", file="other_en") }}">other games and tools guide</a>
|
||||||
|
to find more.</p>
|
||||||
<div class="js-only">
|
<div class="js-only">
|
||||||
<label for="game-search">Search for your game below!</label><br />
|
<label for="game-search">Search for your game below!</label><br />
|
||||||
<div class="page-controls">
|
<div class="page-controls">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from werkzeug.exceptions import abort
|
|||||||
|
|
||||||
from MultiServer import Context, get_saving_second
|
from MultiServer import Context, get_saving_second
|
||||||
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
|
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
|
||||||
from Utils import restricted_loads, KeyedDefaultDict
|
from Utils import restricted_loads, KeyedDefaultDict, utcnow
|
||||||
from . import app, cache
|
from . import app, cache
|
||||||
from .models import GameDataPackage, Room
|
from .models import GameDataPackage, Room
|
||||||
|
|
||||||
@@ -273,9 +273,10 @@ class TrackerData:
|
|||||||
Does not include players who have no activity recorded.
|
Does not include players who have no activity recorded.
|
||||||
"""
|
"""
|
||||||
last_activity: Dict[TeamPlayer, datetime.timedelta] = {}
|
last_activity: Dict[TeamPlayer, datetime.timedelta] = {}
|
||||||
now = datetime.datetime.utcnow()
|
now = utcnow()
|
||||||
for (team, player), timestamp in self._multisave.get("client_activity_timers", []):
|
for (team, player), timestamp in self._multisave.get("client_activity_timers", []):
|
||||||
last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
from_timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).replace(tzinfo=None)
|
||||||
|
last_activity[team, player] = now - from_timestamp
|
||||||
|
|
||||||
return last_activity
|
return last_activity
|
||||||
|
|
||||||
|
|||||||
13
data/GLOBAL.apignore
Normal file
13
data/GLOBAL.apignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# This file specifies patterns that are ignored by default for any world built with the "Build APWorlds" component.
|
||||||
|
# These patterns can be overriden by a world-specific .apignore using !-prefixed patterns for negation.
|
||||||
|
|
||||||
|
# Auto-created folders
|
||||||
|
__MACOSX
|
||||||
|
.DS_Store
|
||||||
|
__pycache__
|
||||||
|
|
||||||
|
# Unneeded files
|
||||||
|
/archipelago.json
|
||||||
|
/.apignore
|
||||||
|
/.git
|
||||||
|
/.gitignore
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
name: Player{number}
|
name: Player{number}
|
||||||
|
|
||||||
# Used to describe your yaml. Useful if you have multiple files.
|
# Used to describe your yaml. Useful if you have multiple files.
|
||||||
description: {{ yaml_dump("Default %s Template" % game) }}
|
description: {{ yaml_dump("%s Preset for %s" % (preset_name, game)) if preset_name else yaml_dump("Default %s Template" % game) }}
|
||||||
|
|
||||||
game: {{ yaml_dump(game) }}
|
game: {{ yaml_dump(game) }}
|
||||||
requires:
|
requires:
|
||||||
@@ -38,11 +38,11 @@ requires:
|
|||||||
{{ yaml_dump(game) }}: {{ world_version }} # Version of the world required for this yaml to work as expected.
|
{{ yaml_dump(game) }}: {{ world_version }} # Version of the world required for this yaml to work as expected.
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- macro range_option(option) %}
|
{%- macro range_option(option, option_val) %}
|
||||||
# You can define additional values between the minimum and maximum values.
|
# You can define additional values between the minimum and maximum values.
|
||||||
# Minimum value is {{ option.range_start }}
|
# Minimum value is {{ option.range_start }}
|
||||||
# Maximum value is {{ option.range_end }}
|
# Maximum value is {{ option.range_end }}
|
||||||
{%- set data, notes = dictify_range(option) %}
|
{%- set data, notes = dictify_range(option, option_val) %}
|
||||||
{%- for entry, default in data.items() %}
|
{%- for entry, default in data.items() %}
|
||||||
{{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
|
{{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -56,6 +56,10 @@ requires:
|
|||||||
|
|
||||||
{%- for option_key, option in group_options.items() %}
|
{%- for option_key, option in group_options.items() %}
|
||||||
{{ option_key }}:
|
{{ option_key }}:
|
||||||
|
{%- set option_val = option.default %}
|
||||||
|
{%- if option_key in preset %}
|
||||||
|
{%- set option_val = preset[option_key] %}
|
||||||
|
{%- endif -%}
|
||||||
{%- if option.__doc__ %}
|
{%- if option.__doc__ %}
|
||||||
# {{ cleandoc(option.__doc__)
|
# {{ cleandoc(option.__doc__)
|
||||||
| trim
|
| trim
|
||||||
@@ -69,25 +73,25 @@ requires:
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if option.range_start is defined and option.range_start is number %}
|
{%- if option.range_start is defined and option.range_start is number %}
|
||||||
{{- range_option(option) -}}
|
{{- range_option(option, option_val) -}}
|
||||||
|
|
||||||
{%- elif option.options -%}
|
{%- elif option.options -%}
|
||||||
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
||||||
{{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
{{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option_val or sub_option_name == option_val %}50{% else %}0{% endif %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|
||||||
{%- if option.name_lookup[option.default] not in option.options %}
|
{%- if option.name_lookup[option_val] not in option.options and option_val not in option.options %}
|
||||||
{{ yaml_dump(option.default) }}: 50
|
{{ yaml_dump(option_val) }}: 50
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- elif option.default is string %}
|
{%- elif option_val is string %}
|
||||||
{{ yaml_dump(option.default) }}: 50
|
{{ yaml_dump(option_val) }}: 50
|
||||||
|
|
||||||
{%- elif option.default is iterable and option.default is not mapping %}
|
{%- elif option_val is iterable and option_val is not mapping %}
|
||||||
{{ option.default | list }}
|
{{ option_val | list }}
|
||||||
|
|
||||||
{%- else %}
|
{%- else %}
|
||||||
{{ yaml_dump(option.default) | indent(4, first=false) }}
|
{{ yaml_dump(option_val) | indent(4, first=false) }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{ "\n" }}
|
{{ "\n" }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|||||||
@@ -41,16 +41,8 @@ http {
|
|||||||
# server_name example.com www.example.com;
|
# server_name example.com www.example.com;
|
||||||
|
|
||||||
keepalive_timeout 5;
|
keepalive_timeout 5;
|
||||||
|
|
||||||
# path for static files
|
|
||||||
root /app/WebHostLib;
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# checks for static file, if not found proxy to app
|
|
||||||
try_files $uri @proxy_to_app;
|
|
||||||
}
|
|
||||||
|
|
||||||
location @proxy_to_app {
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
@@ -60,5 +52,15 @@ http {
|
|||||||
|
|
||||||
proxy_pass http://app_server;
|
proxy_pass http://app_server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
root /app/WebHostLib/;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /favicon.ico {
|
||||||
|
alias /app/WebHostLib/static/static/favicon.ico;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
||||||
/worlds/apquest/ @NewSoupVi
|
/worlds/apquest/ @NewSoupVi
|
||||||
|
|
||||||
# Sudoku (APSudoku)
|
|
||||||
/worlds/apsudoku/ @EmilyV99
|
|
||||||
|
|
||||||
# Aquaria
|
# Aquaria
|
||||||
/worlds/aquaria/ @tioui
|
/worlds/aquaria/ @tioui
|
||||||
@@ -70,6 +68,9 @@
|
|||||||
# DOOM II
|
# DOOM II
|
||||||
/worlds/doom_ii/ @Daivuk @KScl
|
/worlds/doom_ii/ @Daivuk @KScl
|
||||||
|
|
||||||
|
# EarthBound
|
||||||
|
/worlds/earthbound/ @PinkSwitch
|
||||||
|
|
||||||
# Factorio
|
# Factorio
|
||||||
/worlds/factorio/ @Berserker66
|
/worlds/factorio/ @Berserker66
|
||||||
|
|
||||||
@@ -131,6 +132,9 @@
|
|||||||
# Mega Man 2
|
# Mega Man 2
|
||||||
/worlds/mm2/ @Silvris
|
/worlds/mm2/ @Silvris
|
||||||
|
|
||||||
|
# Mega Man 3
|
||||||
|
/worlds/mm3/ @Silvris
|
||||||
|
|
||||||
# MegaMan Battle Network 3
|
# MegaMan Battle Network 3
|
||||||
/worlds/mmbn3/ @digiholic
|
/worlds/mmbn3/ @digiholic
|
||||||
|
|
||||||
@@ -176,8 +180,12 @@
|
|||||||
# Sonic Adventure 2 Battle
|
# Sonic Adventure 2 Battle
|
||||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||||
|
|
||||||
|
# Satisfactory
|
||||||
|
/worlds/satisfactory/ @Jarno458 @budak7273
|
||||||
|
|
||||||
# Starcraft 2
|
# Starcraft 2
|
||||||
/worlds/sc2/ @Ziktofel
|
# Note: @Ziktofel acts as a mentor
|
||||||
|
/worlds/sc2/ @MatthewMarinets @Snarkie @SirChuckOfTheChuckles
|
||||||
|
|
||||||
# Super Metroid
|
# Super Metroid
|
||||||
/worlds/sm/ @lordlou
|
/worlds/sm/ @lordlou
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ it will not be detailed here.
|
|||||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||||
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
|
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document. Additional help with specific game
|
||||||
|
engines and rom formats can be found in the #ap-modding-help channel in the [Discord](https://archipelago.gg/discord).
|
||||||
|
|
||||||
### Hard Requirements
|
### Hard Requirements
|
||||||
|
|
||||||
@@ -86,7 +87,8 @@ The world is your game integration for the Archipelago generator, webhost, and m
|
|||||||
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
||||||
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
||||||
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
||||||
repository and creating a new world package in `/worlds/`.
|
repository and creating a new world package in `/worlds/` (see [running from source](/docs/running%20from%20source.md)
|
||||||
|
for setup).
|
||||||
|
|
||||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||||
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||||
@@ -139,8 +141,8 @@ if possible.
|
|||||||
|
|
||||||
* An implementation of
|
* An implementation of
|
||||||
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
||||||
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
|
* By default, this function chooses any item name from `item_name_to_id`, which may include items you consider
|
||||||
filler items.
|
"non-repeatable".
|
||||||
* An `options_dataclass` defining the options players have available to them
|
* An `options_dataclass` defining the options players have available to them
|
||||||
* This should be accompanied by a type hint for `options` with the same class name
|
* This should be accompanied by a type hint for `options` with the same class name
|
||||||
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
||||||
|
|||||||
@@ -41,16 +41,18 @@ There are also the following optional fields:
|
|||||||
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
|
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
|
||||||
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
|
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
|
||||||
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
|
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
|
||||||
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
["Build APWorlds" launcher component](#build-apworlds-launcher-component),
|
||||||
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
||||||
|
|
||||||
### "Build APWorlds" Launcher Component
|
### "Build APWorlds" Launcher Component
|
||||||
|
|
||||||
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
|
In the Archipelago Launcher (on [source only](/docs/running%20from%20source.md)), there is a "Build APWorlds"
|
||||||
and add `archipelago.json` manifest files to them.
|
component that will package all world folders to `.apworld`, and add `archipelago.json` manifest files to them.
|
||||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||||
`version` and `compatible_version`.
|
`version` and `compatible_version`.
|
||||||
|
The component can also be called from the command line to allow for specifying a certain list of worlds to build.
|
||||||
|
For example, running `Launcher.py "Build APWorlds" -- "Game Name"` will build only the game called `Game Name`.
|
||||||
|
|
||||||
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
|
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
|
||||||
So, a world folder with an `archipelago.json` that looks like this:
|
So, a world folder with an `archipelago.json` that looks like this:
|
||||||
@@ -79,10 +81,26 @@ will be packaged into an `.apworld` with a manifest file inside of it that looks
|
|||||||
|
|
||||||
This is the recommended workflow for packaging your world to an `.apworld`.
|
This is the recommended workflow for packaging your world to an `.apworld`.
|
||||||
|
|
||||||
## Extra Data
|
### .apignore Exclusions
|
||||||
|
|
||||||
The zip can contain arbitrary files in addition what was specified above.
|
By default, any additional files inside of the world folder will be packaged into the resulting `.apworld` archive and
|
||||||
|
can then be read by the world. However, if there are any other files that aren't needed in the resulting `.apworld`, you
|
||||||
|
can automatically prevent the build component from including them by specifying them in a file called `.apignore` inside
|
||||||
|
the root of the world folder.
|
||||||
|
|
||||||
|
The `.apignore` file selects files in the same way as the `.gitignore` format with patterns separated by line describing
|
||||||
|
which files to ignore. For example, an `.apignore` like this:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
*.iso
|
||||||
|
scripts/
|
||||||
|
!scripts/needed.py
|
||||||
|
```
|
||||||
|
|
||||||
|
would ignore any `.iso` files and anything in the scripts folder except for `scripts/needed.py`.
|
||||||
|
|
||||||
|
Some exclusions are made by default for all worlds such as `__pycache__` folders. These are listed in the
|
||||||
|
`GLOBAL.apignore` file inside of the `data` directory.
|
||||||
|
|
||||||
## Caveats
|
## Caveats
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,49 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### I've never added a game to Archipelago before. Should I start with the APWorld or the game client?
|
||||||
|
|
||||||
|
Strictly speaking, this is a false dichotomy: we do *not* recommend doing 100% of client work before the APWorld,
|
||||||
|
or 100% of APWorld work before the client. It's important to iterate on both parts and test them together.
|
||||||
|
However, the early iterations tend to be very similar for most games,
|
||||||
|
so the typical recommendation for first-time AP developers is:
|
||||||
|
|
||||||
|
- Start with a proof-of-concept for [the game client](adding%20games.md#client)
|
||||||
|
- Figure out how to interface with the game. Whether that means "modding" the game, or patching a ROM file,
|
||||||
|
or developing a separate client program that edits the game's memory, or some other technique.
|
||||||
|
- Figure out how to give items and detect locations in the actual game. Not every item and location,
|
||||||
|
just one of each major type (e.g. opening a chest vs completing a sidequest) to prove all the items and locations
|
||||||
|
you want can actually be implemented.
|
||||||
|
- Figure out how to make a websocket connection to an AP server, possibly using a client library (see [Network Protocol](<network%20protocol.md>).
|
||||||
|
To make absolutely sure this part works, you may want to test the connection by generating a multiworld
|
||||||
|
with a different game, then making your client temporarily pretend to be that other game.
|
||||||
|
- Next, make a "trivial" APWorld, i.e. an APWorld that always generates the same items and locations
|
||||||
|
- If you've never done this before, likely the fastest approach is to copy-paste [APQuest](<../worlds/apquest>), and read the many
|
||||||
|
comments in there until you understand how to edit the items and locations.
|
||||||
|
- Then you can do your first "end-to-end test": generate a multiworld using your APWorld, [run a local server](<running%20from%20source.md>)
|
||||||
|
to host it, connect to that local server from your game client, actually check a location in the game,
|
||||||
|
and finally make sure the client successfully sent that location check to the AP server
|
||||||
|
as well as received an item from it.
|
||||||
|
|
||||||
|
That's about where general recommendations end. What you should do next will depend entirely on your game
|
||||||
|
(e.g. implement more items, write down logic rules, add client features, prototype a tracker, etc).
|
||||||
|
If you're not sure, then this would be a good time to re-read [Adding Games](<adding%20games.md>), and [World API](<world%20api.md>).
|
||||||
|
|
||||||
|
There are a few assumptions in this recommendation worth stating explicitly, namely:
|
||||||
|
|
||||||
|
- If something you want to do is infeasible, you want to find out that it's infeasible as soon as possible, before
|
||||||
|
you write a bunch of code assuming it could be done. That's why we recommend starting with the game client.
|
||||||
|
- Getting an APWorld to generate whatever items/locations you want is always feasible, since items/locations are
|
||||||
|
little more than id numbers and name strings during generation.
|
||||||
|
- You generally want to get to an "end-to-end playable" prototype quickly. On top of all the technical challenges these
|
||||||
|
docs describe, it's also important to check that a randomizer is *fun to play*, and figure out what features would be
|
||||||
|
essential for a public release.
|
||||||
|
- A first-time world developer may or may not be deeply familiar with Archipelago, but they're almost certainly familiar
|
||||||
|
with the game they want to randomize. So judging whether your game client is working correctly might be significantly
|
||||||
|
easier than judging if your APWorld is working.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### My game has a restrictive start that leads to fill errors
|
### My game has a restrictive start that leads to fill errors
|
||||||
|
|
||||||
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
||||||
@@ -140,3 +183,58 @@ So when the game itself does not follow this assumption, the options are:
|
|||||||
- For connections, any logical regions will still need to be reachable through other, *repeatable* connections
|
- For connections, any logical regions will still need to be reachable through other, *repeatable* connections
|
||||||
- For locations, this may require game changes to remove the vanilla item if it affects logic
|
- For locations, this may require game changes to remove the vanilla item if it affects logic
|
||||||
- Decide that resetting the save file is part of the game's logic, and warn players about that
|
- Decide that resetting the save file is part of the game's logic, and warn players about that
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### What are "local" vs "remote" items, and what are the pros and cons of each?
|
||||||
|
|
||||||
|
First off, these terms can be misleading. Since the whole point of a multi-game multiworld randomizer is that some items
|
||||||
|
are going to be placed in other slots (unless there's only one slot), the choice isn't really "local vs remote";
|
||||||
|
it's "mixed local/remote vs all remote". You have to get "remote items" working to be an AP implementation at all, and
|
||||||
|
it's often simpler to handle every item/location the same way, so you generally shouldn't worry about "local items"
|
||||||
|
until you've finished more critical features.
|
||||||
|
|
||||||
|
Next, "local" and "remote" items confusingly refer to multiple concepts, so it's important to clearly separate them:
|
||||||
|
|
||||||
|
- Whether an item happens to get placed in the same slot it originates from, or a different slot. I'll call these
|
||||||
|
"locally placed" and "remotely placed" items.
|
||||||
|
- Whether an AP client implements location checking for locally placed items by skipping the usual AP server roundtrip
|
||||||
|
(i.e. sending [LocationChecks](<network%20protocol.md#locationchecks>)
|
||||||
|
then receiving [ReceivedItems](<network%20protocol.md#receiveditems>)
|
||||||
|
) and directly giving the item to the player, or by doing the AP server roundtrip regardless. I'll call these
|
||||||
|
"locally implemented" items and "remotely implemented" items.
|
||||||
|
- Locally implementing items requires the AP client to know what the locally placed items were without asking an AP
|
||||||
|
server (or else you'd effectively be doing remote items with extra steps). Typically, it gets that information from
|
||||||
|
a patch file, which is one reason why games that already need a patch file are more likely to choose local items.
|
||||||
|
- If items are remotely implemented, the AP client can use [location scouts](<network%20protocol.md#LocationScouts>)
|
||||||
|
to learn what items are placed on what locations. Features that require this information are sometimes mistakenly
|
||||||
|
assumed to require locally implemented items, but location scouts work just as well as patch file data.
|
||||||
|
- [The `items_handling` bitflags in the Connect packet](<network%20protocol.md#items_handling-flags>).
|
||||||
|
AP clients with remotely implemented items will typically set all three flags, including "from your own world".
|
||||||
|
Clients with locally implemented items might set only the "from other worlds" flag.
|
||||||
|
- Whether a local items client sets the "starting inventory" flag likely depends on other details. For example, if a ROM
|
||||||
|
is being patched, and starting inventory can be added to that patch, then it makes sense to leave the flag unset.
|
||||||
|
|
||||||
|
When people talk about "local vs remote items" as a choice that world devs have to make, they mean deciding whether
|
||||||
|
your client will locally or remotely implement the items which happen to be locally placed (or make both
|
||||||
|
implementations, or let the player choose an implementation).
|
||||||
|
|
||||||
|
Theoretically, the biggest benefit of "local items" is that it allows a solo (single slot) multiworld to be played
|
||||||
|
entirely offline, with no AP server, from start to finish. This is similar to a "standalone"/non-AP randomizer,
|
||||||
|
except that you still get AP's player options, generation, etc. for free.
|
||||||
|
For some games, there are also technical constraints that make certain items easier to implement locally,
|
||||||
|
or less glitchy when implemented locally, as long as you're okay with never allowing these items to be placed remotely
|
||||||
|
(or offering the player even more options).
|
||||||
|
|
||||||
|
The main downside (besides more implementation work) is that "local items" can't support "same slot co-op".
|
||||||
|
That's when two players on two different machines connect to the same slot and play together.
|
||||||
|
This only works if both players receive all the items for that slot, including ones found by the other player,
|
||||||
|
which requires those items to be implemented remotely so the AP server can send them to all of that slot's clients.
|
||||||
|
|
||||||
|
So to recap:
|
||||||
|
|
||||||
|
- (All) remote items is often the simplest choice, since you have to implement remote items anyway.
|
||||||
|
- Remote items enable same slot co-op.
|
||||||
|
- Local items enable solo offline play.
|
||||||
|
- If you want to support both solo offline play and same slot co-op,
|
||||||
|
you might need to expose local vs remote items as an option to the player.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ game contributions:
|
|||||||
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
|
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
|
||||||
pushing.
|
pushing.
|
||||||
You can turn them on here:
|
You can turn them on here:
|
||||||

|

|
||||||
|
|
||||||
* **When reviewing PRs, please leave a message about what was done.**
|
* **When reviewing PRs, please leave a message about what was done.**
|
||||||
We don't have full test coverage, so manual testing can help.
|
We don't have full test coverage, so manual testing can help.
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ Sent to clients after a client requested this message be sent to them, more info
|
|||||||
| games | list\[str\] | Optional. Game names this message is targeting |
|
| games | list\[str\] | Optional. Game names this message is targeting |
|
||||||
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
|
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
|
||||||
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
|
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
|
||||||
| data | dict | The data in the [Bounce](#Bounce) package copied |
|
| data | dict | Optional. The data in the [Bounce](#Bounce) package copied |
|
||||||
|
|
||||||
### InvalidPacket
|
### InvalidPacket
|
||||||
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
|
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
|
||||||
@@ -425,7 +425,7 @@ the server will forward the message to all those targets to which any one requir
|
|||||||
| games | list\[str\] | Optional. Game names that should receive this message |
|
| games | list\[str\] | Optional. Game names that should receive this message |
|
||||||
| slots | list\[int\] | Optional. Player IDs that should receive this message |
|
| slots | list\[int\] | Optional. Player IDs that should receive this message |
|
||||||
| tags | list\[str\] | Optional. Client tags that should receive this message |
|
| tags | list\[str\] | Optional. Client tags that should receive this message |
|
||||||
| data | dict | Any data you want to send |
|
| data | dict | Optional. Any data you want to send |
|
||||||
|
|
||||||
### Get
|
### Get
|
||||||
Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package.
|
Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package.
|
||||||
|
|||||||
482
docs/rule builder.md
Normal file
482
docs/rule builder.md
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
# Rule Builder
|
||||||
|
|
||||||
|
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
|
||||||
|
|
||||||
|
- Rule classes that avoid all the common pitfalls
|
||||||
|
- Logic optimization
|
||||||
|
- Automatic result caching (opt-in)
|
||||||
|
- Serialization/deserialization
|
||||||
|
- Human-readable logic explanations for players
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The rule builder consists of 3 main parts:
|
||||||
|
|
||||||
|
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
|
||||||
|
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
|
||||||
|
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In your world's create_regions method
|
||||||
|
location = MyWorldLocation(...)
|
||||||
|
self.set_rule(location, Has("A Big Gun"))
|
||||||
|
```
|
||||||
|
|
||||||
|
The rule builder comes with a number of rules by default:
|
||||||
|
|
||||||
|
- `True_`: Always returns true
|
||||||
|
- `False_`: Always returns false
|
||||||
|
- `And`: Checks that all child rules are true (also provided by `&` operator)
|
||||||
|
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
|
||||||
|
- `Has`: Checks that the player has the given item with the given count (default 1)
|
||||||
|
- `HasAll`: Checks that the player has all given items
|
||||||
|
- `HasAny`: Checks that the player has at least one of the given items
|
||||||
|
- `HasAllCounts`: Checks that the player has all of the counts for the given items
|
||||||
|
- `HasAnyCount`: Checks that the player has any of the counts for the given items
|
||||||
|
- `HasFromList`: Checks that the player has some number of given items
|
||||||
|
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
|
||||||
|
- `HasGroup`: Checks that the player has some number of items from a given item group
|
||||||
|
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
|
||||||
|
- `CanReachLocation`: Checks that the player can logically reach the given location
|
||||||
|
- `CanReachRegion`: Checks that the player can logically reach the given region
|
||||||
|
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
|
||||||
|
|
||||||
|
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||||
|
|
||||||
|
```python
|
||||||
|
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
|
||||||
|
|
||||||
|
### Assigning rules
|
||||||
|
|
||||||
|
When assigning the rule you must use the `set_rule` helper to correctly resolve and register the rule.
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.set_rule(location_or_entrance, rule)
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.create_entrance(from_region, to_region, rule)
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||||
|
|
||||||
|
You can also set a rule for your world's completion condition:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.set_completion_rule(rule)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restricting options
|
||||||
|
|
||||||
|
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
|
||||||
|
|
||||||
|
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
|
||||||
|
|
||||||
|
- `eq`: `==`
|
||||||
|
- `ne`: `!=`
|
||||||
|
- `gt`: `>`
|
||||||
|
- `lt`: `<`
|
||||||
|
- `ge`: `>=`
|
||||||
|
- `le`: `<=`
|
||||||
|
- `contains`: `in`
|
||||||
|
|
||||||
|
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
|
||||||
|
|
||||||
|
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
|
||||||
|
|
||||||
|
```python
|
||||||
|
rule = (
|
||||||
|
Has("Red switch", options=[OptionFilter(SwitchRando, 1)])
|
||||||
|
| CanReachLocation("Red switch", options=[OptionFilter(SwitchRando, 0)])
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
To add an extra logic requirement on the easiest difficulty which is ignored for other difficulties:
|
||||||
|
|
||||||
|
```python
|
||||||
|
rule = (
|
||||||
|
# ...the rest of the logic
|
||||||
|
& Has("QoL item", options=[OptionFilter(Difficulty, Difficulty.option_easy)], filtered_resolution=True)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you would like to provide option filters when reusing or composing rules, you can use the `Filtered` helper rule:
|
||||||
|
|
||||||
|
```python
|
||||||
|
common_rule = Has("A") | HasAny("B", "C")
|
||||||
|
...
|
||||||
|
rule = (
|
||||||
|
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
|
||||||
|
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use the & and | operators to apply options to rules:
|
||||||
|
|
||||||
|
```python
|
||||||
|
common_rule = Has("A")
|
||||||
|
easy_filter = [OptionFilter(Difficulty, Difficulty.option_easy)]
|
||||||
|
common_rule_only_on_easy = common_rule & easy_filter
|
||||||
|
common_rule_skipped_on_easy = common_rule | easy_filter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enabling caching
|
||||||
|
|
||||||
|
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyWorld(CachedRuleBuilderWorld):
|
||||||
|
game = "My Game"
|
||||||
|
```
|
||||||
|
|
||||||
|
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||||
|
|
||||||
|
### Item name mapping
|
||||||
|
|
||||||
|
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
|
||||||
|
|
||||||
|
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyWorld(CachedRuleBuilderWorld):
|
||||||
|
item_mapping = {
|
||||||
|
"Currency x10": "Currency",
|
||||||
|
"Currency x50": "Currency",
|
||||||
|
"Currency x100": "Currency",
|
||||||
|
"Currency x500": "Currency",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defining custom rules
|
||||||
|
|
||||||
|
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||||
|
|
||||||
|
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
|
||||||
|
|
||||||
|
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class CanGoal(Rule["MyWorld"], game="My Game"):
|
||||||
|
@override
|
||||||
|
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||||
|
# caching_enabled only needs to be passed in when your world inherits from CachedRuleBuilderWorld
|
||||||
|
return self.Resolved(world.required_mcguffins, player=world.player, caching_enabled=True)
|
||||||
|
|
||||||
|
class Resolved(Rule.Resolved):
|
||||||
|
goal: int
|
||||||
|
|
||||||
|
@override
|
||||||
|
def _evaluate(self, state: CollectionState) -> bool:
|
||||||
|
return state.has("McGuffin", self.player, count=self.goal)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def item_dependencies(self) -> dict[str, set[int]]:
|
||||||
|
# this function is only required if you have caching enabled
|
||||||
|
return {"McGuffin": {id(self)}}
|
||||||
|
|
||||||
|
@override
|
||||||
|
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||||
|
# this method can be overridden to display custom explanations
|
||||||
|
return [
|
||||||
|
{"type": "text", "text": "Goal with "},
|
||||||
|
{"type": "color", "color": "green" if state and self(state) else "salmon", "text": str(self.goal)},
|
||||||
|
{"type": "text", "text": " McGuffins"},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Your custom rule can also resolve to builtin rules instead of needing to define your own:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
|
||||||
|
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||||
|
if world.some_precalculated_bool:
|
||||||
|
return Has("Item 1").resolve(world)
|
||||||
|
if world.options.some_option:
|
||||||
|
return CanReachRegion("Region 1").resolve(world)
|
||||||
|
return False_().resolve(world)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item dependencies
|
||||||
|
|
||||||
|
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||||
|
class Resolved(Rule.Resolved):
|
||||||
|
item_name: str
|
||||||
|
|
||||||
|
@override
|
||||||
|
def item_dependencies(self) -> dict[str, set[int]]:
|
||||||
|
return {self.item_name: {id(self)}}
|
||||||
|
```
|
||||||
|
|
||||||
|
All of the default `Has*` rules define this function already.
|
||||||
|
|
||||||
|
### Region dependencies
|
||||||
|
|
||||||
|
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||||
|
class Resolved(Rule.Resolved):
|
||||||
|
region_name: str
|
||||||
|
|
||||||
|
@override
|
||||||
|
def region_dependencies(self) -> dict[str, set[int]]:
|
||||||
|
return {self.region_name: {id(self)}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules define this function already.
|
||||||
|
|
||||||
|
### Location dependencies
|
||||||
|
|
||||||
|
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||||
|
class Resolved(Rule.Resolved):
|
||||||
|
location_name: str
|
||||||
|
|
||||||
|
@override
|
||||||
|
def location_dependencies(self) -> dict[str, set[int]]:
|
||||||
|
return {self.location_name: {id(self)}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The default `CanReachLocation` rule defines this function already.
|
||||||
|
|
||||||
|
### Entrance dependencies
|
||||||
|
|
||||||
|
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||||
|
class Resolved(Rule.Resolved):
|
||||||
|
entrance_name: str
|
||||||
|
|
||||||
|
@override
|
||||||
|
def entrance_dependencies(self) -> dict[str, set[int]]:
|
||||||
|
return {self.entrance_name: {id(self)}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The default `CanReachEntrance` rule defines this function already.
|
||||||
|
|
||||||
|
### Rule explanations
|
||||||
|
|
||||||
|
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||||
|
|
||||||
|
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyRule(Rule, game="My Game"):
|
||||||
|
class Resolved(Rule.Resolved):
|
||||||
|
@override
|
||||||
|
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||||
|
has_item = state and state.has("growth spurt", self.player)
|
||||||
|
color = "yellow"
|
||||||
|
start = "You must be "
|
||||||
|
if has_item:
|
||||||
|
start = "You are "
|
||||||
|
color = "green"
|
||||||
|
elif state is not None:
|
||||||
|
start = "You are not "
|
||||||
|
color = "salmon"
|
||||||
|
return [
|
||||||
|
{"type": "text", "text": start},
|
||||||
|
{"type": "color", "color": color, "text": "THIS"},
|
||||||
|
{"type": "text", "text": " tall to beat the game"},
|
||||||
|
]
|
||||||
|
|
||||||
|
@override
|
||||||
|
def explain_str(self, state: CollectionState | None = None) -> str:
|
||||||
|
if state is None:
|
||||||
|
return str(self)
|
||||||
|
if state.has("growth spurt", self.player):
|
||||||
|
return "You ARE this tall and can beat the game"
|
||||||
|
return "You are not THIS tall and cannot beat the game"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "You must be THIS tall to beat the game"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache control
|
||||||
|
|
||||||
|
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
|
||||||
|
|
||||||
|
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
|
||||||
|
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
|
||||||
|
|
||||||
|
### Caveats
|
||||||
|
|
||||||
|
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
|
||||||
|
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
|
||||||
|
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
|
||||||
|
|
||||||
|
## Serialization
|
||||||
|
|
||||||
|
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||||
|
|
||||||
|
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"rule": "Has",
|
||||||
|
"options": [],
|
||||||
|
"args": {
|
||||||
|
"item_name": "Some item",
|
||||||
|
"count": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"rule": "And",
|
||||||
|
"options": [],
|
||||||
|
"children": [
|
||||||
|
..., # each serialized rule
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A full example is as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
rule = And(
|
||||||
|
Has("a", options=[OptionFilter(ToggleOption, 0)]),
|
||||||
|
Or(Has("b", count=2), CanReachRegion("c"), options=[OptionFilter(ToggleOption, 1)]),
|
||||||
|
)
|
||||||
|
assert rule.to_dict() == {
|
||||||
|
"rule": "And",
|
||||||
|
"options": [],
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"rule": "Has",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"option": "worlds.my_world.options.ToggleOption",
|
||||||
|
"value": 0,
|
||||||
|
"operator": "eq",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"item_name": "a",
|
||||||
|
"count": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "Or",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"option": "worlds.my_world.options.ToggleOption",
|
||||||
|
"value": 1,
|
||||||
|
"operator": "eq",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"rule": "Has",
|
||||||
|
"options": [],
|
||||||
|
"args": {
|
||||||
|
"item_name": "b",
|
||||||
|
"count": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "CanReachRegion",
|
||||||
|
"options": [],
|
||||||
|
"args": {
|
||||||
|
"region_name": "c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom serialization
|
||||||
|
|
||||||
|
To define a different format for your custom rules, override the `to_dict` function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BasicLogicRule(Rule, game="My Game"):
|
||||||
|
items = ("one", "two")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
# Return whatever format works best for you
|
||||||
|
return {
|
||||||
|
"logic": "basic",
|
||||||
|
"items": self.items,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BasicLogicRule(Rule, game="My Game"):
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Mapping[str, Any], world_cls: type[World]) -> Self:
|
||||||
|
items = data.get("items", ())
|
||||||
|
return cls(*items)
|
||||||
|
```
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
This section is provided for reference, refer to the above sections for examples.
|
||||||
|
|
||||||
|
### World API
|
||||||
|
|
||||||
|
These are properties and helpers that are available to you in your world.
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
|
||||||
|
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
|
||||||
|
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
|
||||||
|
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
|
||||||
|
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
|
||||||
|
|
||||||
|
#### CachedRuleBuilderWorld Properties
|
||||||
|
|
||||||
|
The following property is only available when inheriting from `CachedRuleBuilderWorld`
|
||||||
|
|
||||||
|
- `item_mapping: dict[str, str]`: A mapping of actual item name to logical item name
|
||||||
|
|
||||||
|
### Rule API
|
||||||
|
|
||||||
|
These are properties and helpers that you can use or override for custom rules.
|
||||||
|
|
||||||
|
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
|
||||||
|
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
|
||||||
|
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
|
||||||
|
- `__str__()`: Basic string representation of a rule, useful for debugging
|
||||||
|
|
||||||
|
#### Resolved rule API
|
||||||
|
|
||||||
|
- `player: int`: The slot this rule is resolved for
|
||||||
|
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
|
||||||
|
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
|
||||||
|
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
|
||||||
|
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
|
||||||
|
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
|
||||||
|
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||||
|
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||||
|
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
|
||||||
@@ -7,10 +7,9 @@ use that version. These steps are for developers or platforms without compiled r
|
|||||||
## General
|
## General
|
||||||
|
|
||||||
What you'll need:
|
What you'll need:
|
||||||
* [Python 3.11.9 or newer](https://www.python.org/downloads/), not the Windows Store version
|
* [Python 3.11.9 or newer but less than 3.14](https://www.python.org/downloads/), not the Windows Store version
|
||||||
* On Windows, please consider only using the latest supported version in production environments since security
|
* On Windows, please consider only using the latest supported version in production environments since security
|
||||||
updates for older versions are not easily available.
|
updates for older versions are not easily available.
|
||||||
* Python 3.13.x is currently the newest supported version
|
|
||||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||||
* Matching C compiler
|
* Matching C compiler
|
||||||
* possibly optional, read operating system specific sections
|
* possibly optional, read operating system specific sections
|
||||||
@@ -53,6 +52,32 @@ Recommended steps
|
|||||||
Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md).
|
Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
If your Linux distribution ships a compatible Python version (see [General](#general)) and pip, you can use that,
|
||||||
|
otherwise you may need to install Python from a 3rd party. Refer to documentation of your Linux distribution.
|
||||||
|
|
||||||
|
Installing a C compiler is usually optional. The package is typically named `gcc`, sometimes another package with the
|
||||||
|
base build tools may be required, i.e. `build-essential` (Debian/Ubuntu) or `base-devel` (Arch).
|
||||||
|
|
||||||
|
After getting the source code, it is strongly recommended to create a
|
||||||
|
[venv](https://docs.python.org/3/tutorial/venv.html) (Virtual Environment)
|
||||||
|
by hand or using an IDE, such as PyCharm, because Archipelago requires specific versions of Python packages.
|
||||||
|
|
||||||
|
Run `python ModuleUpdate.py` in the project root to install packages, run `python Launcher.py` to run the Launcher.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
Builds contain (almost) all dependencies to run Archipelago on any Linux distribution that is as new or newer than the
|
||||||
|
one it was built on. Beware that currently only the oldest Ubuntu LTS available in GitHub actions is supported for that.
|
||||||
|
This means the easiest way to generate a build is by running the `Build` action from GitHub actions instead of building
|
||||||
|
locally. If you still want to, e.g. for local testing, you can by running
|
||||||
|
|
||||||
|
`python setup.py build_exe` to generate a binary distribution of Archipelago in `build/`. Or to generate an AppImage
|
||||||
|
first generate the binary distribution and then run `python setup.py bdist_appimage` to populate `dist/`. You need to
|
||||||
|
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
|
||||||
|
|
||||||
|
|
||||||
## Optional: A Link to the Past Enemizer
|
## Optional: A Link to the Past Enemizer
|
||||||
|
|
||||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||||
|
|||||||
@@ -47,21 +47,27 @@
|
|||||||
|
|
||||||
## HTML
|
## HTML
|
||||||
|
|
||||||
* Indent with 2 spaces for new code.
|
* Indent with 4 spaces for new code.
|
||||||
* kebab-case for ids and classes.
|
* kebab-case for ids and classes.
|
||||||
|
* Avoid using on* attributes (onclick, etc.).
|
||||||
|
|
||||||
## CSS
|
## CSS / SCSS
|
||||||
|
|
||||||
* Indent with 2 spaces for new code.
|
* Indent with 4 spaces for new code.
|
||||||
* `{` on the same line as the selector.
|
* `{` on the same line as the selector.
|
||||||
* No space between selector and `{`.
|
* Space between selector and `{`.
|
||||||
|
|
||||||
## JS
|
## JS
|
||||||
|
|
||||||
* Indent with 2 spaces.
|
* Indent with 4 spaces.
|
||||||
* Indent `case` inside `switch ` with 2 spaces.
|
* Indent `case` inside `switch ` with 4 spaces.
|
||||||
* Use single quotes.
|
* Prefer double quotation marks (`"`).
|
||||||
* Semicolons are required after every statement.
|
* Semicolons are required after every statement.
|
||||||
|
* Use [IIFEs](https://developer.mozilla.org/docs/Glossary/IIFE) to avoid polluting global scope.
|
||||||
|
* Prefer to use [defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer)
|
||||||
|
in script tags, which retains order of execution but does not block.
|
||||||
|
* Avoid `<script async ...` in most cases, see [async and defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#async_and_defer).
|
||||||
|
* Use addEventListener.
|
||||||
|
|
||||||
## KV
|
## KV
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Archipelago has a rudimentary API that can be queried by endpoints. The API is a
|
|||||||
|
|
||||||
The following API requests are formatted as: `https://<Archipelago URL>/api/<endpoint>`
|
The following API requests are formatted as: `https://<Archipelago URL>/api/<endpoint>`
|
||||||
|
|
||||||
The returned data will be formated in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
|
The returned data will be formatted in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
|
||||||
|
|
||||||
Current endpoints:
|
Current endpoints:
|
||||||
- Datapackage API
|
- Datapackage API
|
||||||
@@ -24,13 +24,21 @@ Current endpoints:
|
|||||||
- [`/get_rooms`](#getrooms)
|
- [`/get_rooms`](#getrooms)
|
||||||
- [`/get_seeds`](#getseeds)
|
- [`/get_seeds`](#getseeds)
|
||||||
|
|
||||||
|
## API Data Caching
|
||||||
|
To reduce the strain on an Archipelago WebHost, many API endpoints will cache their data and only poll new data in timed intervals. Each endpoint has their own caching time related to the type of data being served. More dynamic data is refreshed more frequently, while static data is cached for longer.
|
||||||
|
Each API endpoint will have their "Cache timer" listed under their definition (if any).
|
||||||
|
API calls to these endpoints should not be faster than the listed timer. This will result in wasted processing for your client and (more importantly) the Archipelago WebHost, as the data will not be refreshed by the WebHost until the internal timer has elapsed.
|
||||||
|
|
||||||
|
|
||||||
## Datapackage Endpoints
|
## Datapackage Endpoints
|
||||||
These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations.
|
These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations.
|
||||||
|
|
||||||
### `/datapackage`
|
### `/datapackage`
|
||||||
<a name="datapackage"></a>
|
<a name="datapackage"></a>
|
||||||
|
|
||||||
Fetches the current datapackage from the WebHost.
|
Fetches the current datapackage from the WebHost.
|
||||||
|
**Cache timer: None**
|
||||||
|
|
||||||
You'll receive a dict named `games` that contains a named dict of every game and its data currently supported by Archipelago.
|
You'll receive a dict named `games` that contains a named dict of every game and its data currently supported by Archipelago.
|
||||||
Each game will have:
|
Each game will have:
|
||||||
- A checksum `checksum`
|
- A checksum `checksum`
|
||||||
@@ -40,7 +48,7 @@ Each game will have:
|
|||||||
- Location name to AP ID dict `location_name_to_id`
|
- Location name to AP ID dict `location_name_to_id`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"games": {
|
"games": {
|
||||||
...
|
...
|
||||||
@@ -76,7 +84,10 @@ Example:
|
|||||||
|
|
||||||
### `/datapackage/<string:checksum>`
|
### `/datapackage/<string:checksum>`
|
||||||
<a name="datapackagestringchecksum"></a>
|
<a name="datapackagestringchecksum"></a>
|
||||||
Fetches a single datapackage by checksum.
|
|
||||||
|
Fetches a single datapackage by checksum.
|
||||||
|
**Cache timer: None**
|
||||||
|
|
||||||
Returns a dict of the game's data with:
|
Returns a dict of the game's data with:
|
||||||
- A checksum `checksum`
|
- A checksum `checksum`
|
||||||
- A dict of item groups `item_name_groups`
|
- A dict of item groups `item_name_groups`
|
||||||
@@ -88,10 +99,13 @@ Its format will be identical to the whole-datapackage endpoint (`/datapackage`),
|
|||||||
|
|
||||||
### `/datapackage_checksum`
|
### `/datapackage_checksum`
|
||||||
<a name="datapackagechecksum"></a>
|
<a name="datapackagechecksum"></a>
|
||||||
Fetches the checksums of the current static datapackages on the WebHost.
|
|
||||||
|
Fetches the checksums of the current static datapackages on the WebHost.
|
||||||
|
**Cache timer: None**
|
||||||
|
|
||||||
You'll receive a dict with `game:checksum` key-value pairs for all the current officially supported games.
|
You'll receive a dict with `game:checksum` key-value pairs for all the current officially supported games.
|
||||||
Example:
|
Example:
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
...
|
...
|
||||||
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
|
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
|
||||||
@@ -108,6 +122,7 @@ These endpoints are used internally for the WebHost to generate games and valida
|
|||||||
<a name="generate"></a>
|
<a name="generate"></a>
|
||||||
Submits a game to the WebHost for generation.
|
Submits a game to the WebHost for generation.
|
||||||
**This endpoint only accepts a POST HTTP request.**
|
**This endpoint only accepts a POST HTTP request.**
|
||||||
|
**Cache timer: None**
|
||||||
|
|
||||||
There are two ways to submit data for generation: With a file and with JSON.
|
There are two ways to submit data for generation: With a file and with JSON.
|
||||||
|
|
||||||
@@ -116,7 +131,7 @@ Have your ZIP of yaml(s) or a single yaml, and submit a POST request to the `/ge
|
|||||||
If the options are valid, you'll be returned a successful generation response. (see [Generation Response](#generation-response))
|
If the options are valid, you'll be returned a successful generation response. (see [Generation Response](#generation-response))
|
||||||
|
|
||||||
Example using the python requests library:
|
Example using the python requests library:
|
||||||
```
|
```python
|
||||||
file = {'file': open('Games.zip', 'rb')}
|
file = {'file': open('Games.zip', 'rb')}
|
||||||
req = requests.post("https://archipelago.gg/api/generate", files=file)
|
req = requests.post("https://archipelago.gg/api/generate", files=file)
|
||||||
```
|
```
|
||||||
@@ -127,7 +142,7 @@ Finally, submit a POST request to the `/generate` endpoint.
|
|||||||
If the weighted options are valid, you'll be returned a successful generation response (see [Generation Response](#generation-response))
|
If the weighted options are valid, you'll be returned a successful generation response (see [Generation Response](#generation-response))
|
||||||
|
|
||||||
Example using the python requests library:
|
Example using the python requests library:
|
||||||
```
|
```python
|
||||||
data = {"Test":{"game": "Factorio","name": "Test","Factorio": {}},}
|
data = {"Test":{"game": "Factorio","name": "Test","Factorio": {}},}
|
||||||
weights={"weights": data}
|
weights={"weights": data}
|
||||||
req = requests.post("https://archipelago.gg/api/generate", json=weights)
|
req = requests.post("https://archipelago.gg/api/generate", json=weights)
|
||||||
@@ -143,7 +158,7 @@ Upon successful generation, you'll be sent a JSON dict response detailing the ge
|
|||||||
- The API status page of the generation `wait_api_url` (see [Status Endpoint](#status))
|
- The API status page of the generation `wait_api_url` (see [Status Endpoint](#status))
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"detail": "19878f16-5a58-4b76-aab7-d6bf38be9463",
|
"detail": "19878f16-5a58-4b76-aab7-d6bf38be9463",
|
||||||
"encoded": "GYePFlpYS3aqt9a_OL6UYw",
|
"encoded": "GYePFlpYS3aqt9a_OL6UYw",
|
||||||
@@ -167,12 +182,14 @@ If the generation detects a issue in generation, you'll be sent a dict with two
|
|||||||
- Detailed issue in `detail`
|
- Detailed issue in `detail`
|
||||||
|
|
||||||
In the event of an unhandled server exception, you'll be provided a dict with a single key `text`:
|
In the event of an unhandled server exception, you'll be provided a dict with a single key `text`:
|
||||||
- Exception, `Uncought Exception: <error>` with a 500 status code
|
- Exception, `Uncaught Exception: <error>` with a 500 status code
|
||||||
|
|
||||||
### `/status/<suuid:seed>`
|
### `/status/<suuid:seed>`
|
||||||
<a name="status"></a>
|
<a name="status"></a>
|
||||||
Retrieves the status of the seed's generation.
|
Retrieves the status of the seed's generation.
|
||||||
This endpoint will return a dict with a single key-vlaue pair. The key will always be `text`
|
**Cache timer: None**
|
||||||
|
|
||||||
|
This endpoint will return a dict with a single key-value pair. The key will always be `text`
|
||||||
The value will tell you the status of the generation:
|
The value will tell you the status of the generation:
|
||||||
- Generation was completed: `Generation done` with a 201 status code
|
- Generation was completed: `Generation done` with a 201 status code
|
||||||
- Generation request was not found: `Generation not found` with a 404 status code
|
- Generation request was not found: `Generation not found` with a 404 status code
|
||||||
@@ -184,6 +201,8 @@ Endpoints to fetch information of the active WebHost room with the supplied room
|
|||||||
|
|
||||||
### `/room_status/<suuid:room_id>`
|
### `/room_status/<suuid:room_id>`
|
||||||
<a name="roomstatus"></a>
|
<a name="roomstatus"></a>
|
||||||
|
**Cache timer: None**
|
||||||
|
|
||||||
Will provide a dict of room data with the following keys:
|
Will provide a dict of room data with the following keys:
|
||||||
- Tracker SUUID (`tracker`)
|
- Tracker SUUID (`tracker`)
|
||||||
- A list of players (`players`)
|
- A list of players (`players`)
|
||||||
@@ -192,10 +211,10 @@ Will provide a dict of room data with the following keys:
|
|||||||
- Last activity timestamp (`last_activity`)
|
- Last activity timestamp (`last_activity`)
|
||||||
- The room timeout counter (`timeout`)
|
- The room timeout counter (`timeout`)
|
||||||
- A list of downloads for files required for gameplay (`downloads`)
|
- A list of downloads for files required for gameplay (`downloads`)
|
||||||
- Each item is a dict containings the download URL and slot (`slot`, `download`)
|
- Each item is a dict containing the download URL and slot (`slot`, `download`)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"downloads": [
|
"downloads": [
|
||||||
{
|
{
|
||||||
@@ -244,7 +263,7 @@ Example:
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"timeout": 7200,
|
"timeout": 7200,
|
||||||
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
|
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -254,17 +273,27 @@ can either be viewed while on a room tracker page, or from the [room's endpoint]
|
|||||||
|
|
||||||
### `/tracker/<suuid:tracker>`
|
### `/tracker/<suuid:tracker>`
|
||||||
<a name=tracker></a>
|
<a name=tracker></a>
|
||||||
|
**Cache timer: 60 seconds**
|
||||||
|
|
||||||
Will provide a dict of tracker data with the following keys:
|
Will provide a dict of tracker data with the following keys:
|
||||||
|
|
||||||
- Each player's current alias (`aliases`)
|
- A list of players current alias data (`aliases`)
|
||||||
- Will return the name if there is none
|
- Each item containing a dict with, their alias `alias`, their player number `player`, and their team `team`
|
||||||
- A list of items each player has received as a NetworkItem (`player_items_received`)
|
- `alias` will return `null` if there is no alias set
|
||||||
|
- A list of items each player has received as a [NetworkItem](network%20protocol.md#networkitem) (`player_items_received`)
|
||||||
|
- Each item containing a dict with, a list of NetworkItems `items`, their player number `player`, their team `team`
|
||||||
- A list of checks done by each player as a list of the location id's (`player_checks_done`)
|
- A list of checks done by each player as a list of the location id's (`player_checks_done`)
|
||||||
- The total number of checks done by all players (`total_checks_done`)
|
- Each item containing a dict with, a list of checked location id's `locations`, their player number `player`, and their team `team`
|
||||||
- Hints that players have used or received (`hints`)
|
- A list of the total number of checks done by all players (`total_checks_done`)
|
||||||
- The time of last activity of each player in RFC 1123 format (`activity_timers`)
|
- Each item will contain a dict with, the total checks done `checks_done`, and the team `team`
|
||||||
- The time of last active connection of each player in RFC 1123 format (`connection_timers`)
|
- A list of [Hints](network%20protocol.md#hint) data that players have used or received (`hints`)
|
||||||
- The current client status of each player (`player_status`)
|
- Each item containing a dict containing, a list of hint data `hints`, the player number `player`, and their team `team`
|
||||||
|
- A list containing the last activity time for each player, formatted in RFC 1123 format (`activity_timers`)
|
||||||
|
- Each item containing, last activity time `time`, their player number `player`, and their team `team`
|
||||||
|
- A list containing the last connection time for each player, formatted in RFC 1123 format (`connection_timers`)
|
||||||
|
- Each item containing, the time of their last connection `time`, their player number `player`, and their team `team`
|
||||||
|
- A list of the current [ClientStatus](network%20protocol.md#clientstatus) of each player (`player_status`)
|
||||||
|
- Each item will contain, their status `status`, their player number `player`, and their team `team`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```json
|
```json
|
||||||
@@ -279,7 +308,12 @@ Example:
|
|||||||
"team": 0,
|
"team": 0,
|
||||||
"player": 2,
|
"player": 2,
|
||||||
"alias": "Slot_Name_2"
|
"alias": "Slot_Name_2"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"team": 0,
|
||||||
|
"player": 3,
|
||||||
|
"alias": null
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"player_items_received": [
|
"player_items_received": [
|
||||||
{
|
{
|
||||||
@@ -378,12 +412,18 @@ Example:
|
|||||||
|
|
||||||
### `/static_tracker/<suuid:tracker>`
|
### `/static_tracker/<suuid:tracker>`
|
||||||
<a name=statictracker></a>
|
<a name=statictracker></a>
|
||||||
|
**Cache timer: 300 seconds**
|
||||||
|
|
||||||
Will provide a dict of static tracker data with the following keys:
|
Will provide a dict of static tracker data with the following keys:
|
||||||
|
|
||||||
- item_link groups and their players (`groups`)
|
- A list of item_link groups and their member players (`groups`)
|
||||||
- The datapackage hash for each game (`datapackage`)
|
- Each item containing a dict with, the slot registering the group `slot`, the item_link name `name`, and a list of members `members`
|
||||||
|
- A dict of datapackage hashes for each game (`datapackage`)
|
||||||
|
- Each item is a named dict of the game's name.
|
||||||
|
- Each game contains two keys, the datapackage's checksum hash `checksum`, and the version `version`
|
||||||
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
|
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
|
||||||
- The number of checks found vs. total checks available per player (`player_locations_total`)
|
- A list of number of checks found vs. total checks available per player (`player_locations_total`)
|
||||||
|
- Each list item contains a dict with three keys, the total locations for that slot `total_locations`, their player number `player`, and their team `team`
|
||||||
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
|
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
|
||||||
- The game each player is playing (`player_game`)
|
- The game each player is playing (`player_game`)
|
||||||
- Provided as a list of objects with `team`, `player`, and `game`.
|
- Provided as a list of objects with `team`, `player`, and `game`.
|
||||||
@@ -446,7 +486,12 @@ Example:
|
|||||||
|
|
||||||
### `/slot_data_tracker/<suuid:tracker>`
|
### `/slot_data_tracker/<suuid:tracker>`
|
||||||
<a name=slotdatatracker></a>
|
<a name=slotdatatracker></a>
|
||||||
Will provide a list of each player's slot_data.
|
Will provide a list of each player's slot_data.
|
||||||
|
**Cache timer: 300 seconds**
|
||||||
|
|
||||||
|
Each list item will contain a dict with the player's data:
|
||||||
|
- player slot number `player`
|
||||||
|
- A named dict `slot_data` containing any set slot data for that player
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```json
|
```json
|
||||||
@@ -474,6 +519,8 @@ User endpoints can get room and seed details from the current session tokens (co
|
|||||||
### `/get_rooms`
|
### `/get_rooms`
|
||||||
<a name="getrooms"></a>
|
<a name="getrooms"></a>
|
||||||
Retreives a list of all rooms currently owned by the session token.
|
Retreives a list of all rooms currently owned by the session token.
|
||||||
|
**Cache timer: None**
|
||||||
|
|
||||||
Each list item will contain a dict with the room's details:
|
Each list item will contain a dict with the room's details:
|
||||||
- Room SUUID (`room_id`)
|
- Room SUUID (`room_id`)
|
||||||
- Seed SUUID (`seed_id`)
|
- Seed SUUID (`seed_id`)
|
||||||
@@ -484,25 +531,25 @@ Each list item will contain a dict with the room's details:
|
|||||||
- Room tracker SUUID (`tracker`)
|
- Room tracker SUUID (`tracker`)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"creation_time": "Fri, 18 Apr 2025 19:46:53 GMT",
|
"creation_time": "Fri, 18 Apr 2025 19:46:53 GMT",
|
||||||
"last_activity": "Fri, 18 Apr 2025 21:16:02 GMT",
|
"last_activity": "Fri, 18 Apr 2025 21:16:02 GMT",
|
||||||
"last_port": 52122,
|
"last_port": 52122,
|
||||||
"room_id": "90ae5f9b-177c-4df8-ac53-9629fc3bff7a",
|
"room_id": "0D30FgQaRcWivFsw9o8qzw",
|
||||||
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6",
|
"seed_id": "TFjiarBgTsCj5-Jbe8u33A",
|
||||||
"timeout": 7200,
|
"timeout": 7200,
|
||||||
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
|
"tracker": "52BycvJhRe6knrYH8v4bag"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"creation_time": "Fri, 18 Apr 2025 20:36:42 GMT",
|
"creation_time": "Fri, 18 Apr 2025 20:36:42 GMT",
|
||||||
"last_activity": "Fri, 18 Apr 2025 20:36:46 GMT",
|
"last_activity": "Fri, 18 Apr 2025 20:36:46 GMT",
|
||||||
"last_port": 56884,
|
"last_port": 56884,
|
||||||
"room_id": "14465c05-d08e-4d28-96bd-916f994609d8",
|
"room_id": "LMCFchESSNyuqcY3GxkhwA",
|
||||||
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb",
|
"seed_id": "CENtJMXCTGmkIYCzjB5Csg",
|
||||||
"timeout": 7200,
|
"timeout": 7200,
|
||||||
"tracker": "4e624bd8-32b6-42e4-9178-aa407f72751c"
|
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -510,6 +557,8 @@ Example:
|
|||||||
### `/get_seeds`
|
### `/get_seeds`
|
||||||
<a name="getseeds"></a>
|
<a name="getseeds"></a>
|
||||||
Retreives a list of all seeds currently owned by the session token.
|
Retreives a list of all seeds currently owned by the session token.
|
||||||
|
**Cache timer: None**
|
||||||
|
|
||||||
Each item in the list will contain a dict with the seed's details:
|
Each item in the list will contain a dict with the seed's details:
|
||||||
- Seed SUUID (`seed_id`)
|
- Seed SUUID (`seed_id`)
|
||||||
- Creation timestamp (`creation_time`)
|
- Creation timestamp (`creation_time`)
|
||||||
@@ -517,7 +566,7 @@ Each item in the list will contain a dict with the seed's details:
|
|||||||
- Each item in the list will contain a list of the slot name and game
|
- Each item in the list will contain a list of the slot name and game
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"creation_time": "Fri, 18 Apr 2025 19:46:52 GMT",
|
"creation_time": "Fri, 18 Apr 2025 19:46:52 GMT",
|
||||||
@@ -543,7 +592,7 @@ Example:
|
|||||||
"Ocarina of Time"
|
"Ocarina of Time"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6"
|
"seed_id": "CENtJMXCTGmkIYCzjB5Csg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"creation_time": "Fri, 18 Apr 2025 20:36:39 GMT",
|
"creation_time": "Fri, 18 Apr 2025 20:36:39 GMT",
|
||||||
@@ -565,7 +614,7 @@ Example:
|
|||||||
"Archipelago"
|
"Archipelago"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb"
|
"seed_id": "TFjiarBgTsCj5-Jbe8u33A"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -225,7 +225,10 @@ and has a classification. The name needs to be unique within each game and must
|
|||||||
letter or symbol). The ID needs to be unique across all locations within the game.
|
letter or symbol). The ID needs to be unique across all locations within the game.
|
||||||
Locations and items can share IDs, and locations can share IDs with other games' locations.
|
Locations and items can share IDs, and locations can share IDs with other games' locations.
|
||||||
|
|
||||||
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
|
World-specific IDs **must** be in the range 1 to 2<sup>53</sup>-1 (the largest integer that is "[safe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER#description)"
|
||||||
|
to store in a 64-bit float, and thus all popular programming languages can handle). IDs ≤ 0 are global and reserved.
|
||||||
|
It's **recommended** to keep your IDs in the range 1 to 2<sup>31</sup>-1,
|
||||||
|
so only 32-bit integers are needed to hold your IDs.
|
||||||
|
|
||||||
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
||||||
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
||||||
@@ -488,9 +491,10 @@ class MyGameWorld(World):
|
|||||||
base_id = 1234
|
base_id = 1234
|
||||||
# instead of dynamic numbering, IDs could be part of data
|
# instead of dynamic numbering, IDs could be part of data
|
||||||
|
|
||||||
# The following two dicts are required for the generation to know which
|
# The following two dicts are required for the generation to know which items exist.
|
||||||
# items exist. They could be generated from json or something else. They can
|
# They can be generated with arbitrary code during world load, but keep in mind that
|
||||||
# include events, but don't have to since events will be placed manually.
|
# anything expensive (e.g. parsing non-python data files) will delay world loading.
|
||||||
|
# They can include events, but don't have to since events will be placed manually.
|
||||||
item_name_to_id = {name: id for
|
item_name_to_id = {name: id for
|
||||||
id, name in enumerate(mygame_items, base_id)}
|
id, name in enumerate(mygame_items, base_id)}
|
||||||
location_name_to_id = {name: id for
|
location_name_to_id = {name: id for
|
||||||
@@ -767,6 +771,7 @@ class MyGameState(LogicMixin):
|
|||||||
new_state.mygame_defeatable_enemies = {
|
new_state.mygame_defeatable_enemies = {
|
||||||
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
||||||
}
|
}
|
||||||
|
return new_state
|
||||||
```
|
```
|
||||||
|
|
||||||
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
||||||
|
|||||||
@@ -186,9 +186,20 @@ class ERPlacementState:
|
|||||||
self.pairings = []
|
self.pairings = []
|
||||||
self.world = world
|
self.world = world
|
||||||
self.coupled = coupled
|
self.coupled = coupled
|
||||||
self.collection_state = world.multiworld.get_all_state(False, True)
|
|
||||||
self.entrance_lookup = entrance_lookup
|
self.entrance_lookup = entrance_lookup
|
||||||
|
|
||||||
|
# Construct an 'all state', similar to MultiWorld.get_all_state(), but only for the world which is having its
|
||||||
|
# entrances randomized.
|
||||||
|
single_player_all_state = CollectionState(world.multiworld, True)
|
||||||
|
player = world.player
|
||||||
|
for item in world.multiworld.itempool:
|
||||||
|
if item.player == player:
|
||||||
|
world.collect(single_player_all_state, item)
|
||||||
|
for item in world.get_pre_fill_items():
|
||||||
|
world.collect(single_player_all_state, item)
|
||||||
|
single_player_all_state.sweep_for_advancements(world.get_locations())
|
||||||
|
self.collection_state = single_player_all_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def placed_regions(self) -> set[Region]:
|
def placed_regions(self) -> set[Region]:
|
||||||
return self.collection_state.reachable_regions[self.world.player]
|
return self.collection_state.reachable_regions[self.world.player]
|
||||||
@@ -226,7 +237,7 @@ class ERPlacementState:
|
|||||||
copied_state.blocked_connections[self.world.player].remove(source_exit)
|
copied_state.blocked_connections[self.world.player].remove(source_exit)
|
||||||
copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits)
|
copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits)
|
||||||
copied_state.update_reachable_regions(self.world.player)
|
copied_state.update_reachable_regions(self.world.player)
|
||||||
copied_state.sweep_for_advancements()
|
copied_state.sweep_for_advancements(self.world.get_locations())
|
||||||
# test that at there are newly reachable randomized exits that are ACTUALLY reachable
|
# test that at there are newly reachable randomized exits that are ACTUALLY reachable
|
||||||
available_randomized_exits = copied_state.blocked_connections[self.world.player]
|
available_randomized_exits = copied_state.blocked_connections[self.world.player]
|
||||||
for _exit in available_randomized_exits:
|
for _exit in available_randomized_exits:
|
||||||
@@ -402,7 +413,7 @@ def randomize_entrances(
|
|||||||
placed_exits, paired_entrances = er_state.connect(source_exit, target_entrance)
|
placed_exits, paired_entrances = er_state.connect(source_exit, target_entrance)
|
||||||
# propagate new connections
|
# propagate new connections
|
||||||
er_state.collection_state.update_reachable_regions(world.player)
|
er_state.collection_state.update_reachable_regions(world.player)
|
||||||
er_state.collection_state.sweep_for_advancements()
|
er_state.collection_state.sweep_for_advancements(world.get_locations())
|
||||||
if on_connect:
|
if on_connect:
|
||||||
change = on_connect(er_state, placed_exits, paired_entrances)
|
change = on_connect(er_state, placed_exits, paired_entrances)
|
||||||
if change:
|
if change:
|
||||||
|
|||||||
586
host.yaml
Normal file
586
host.yaml
Normal 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"
|
||||||
@@ -208,6 +208,16 @@ Root: HKCR; Subkey: "{#MyAppName}apcivvipatch"; ValueData: "
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
|
Root: HKCR; Subkey: ".apeb"; ValueData: "{#MyAppName}ebpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archipelago EarthBound Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
|
Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||||
|
|||||||
12
kvui.py
12
kvui.py
@@ -19,6 +19,7 @@ os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
|||||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||||
os.environ["KIVY_NO_ARGS"] = "1"
|
os.environ["KIVY_NO_ARGS"] = "1"
|
||||||
os.environ["KIVY_LOG_ENABLE"] = "0"
|
os.environ["KIVY_LOG_ENABLE"] = "0"
|
||||||
|
os.environ["SDL_MOUSE_FOCUS_CLICKTHROUGH"] = "1"
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
@@ -35,6 +36,17 @@ Config.set("input", "mouse", "mouse,disable_multitouch")
|
|||||||
Config.set("kivy", "exit_on_escape", "0")
|
Config.set("kivy", "exit_on_escape", "0")
|
||||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||||
|
|
||||||
|
# Workaround for Kivy issue #9226.
|
||||||
|
# caused by kivy by default using probesysfs,
|
||||||
|
# which assumes all multi touch deviecs are touch screens.
|
||||||
|
# workaround provided by Snu of the kivy commmunity c:
|
||||||
|
from kivy.utils import platform
|
||||||
|
if platform == "linux":
|
||||||
|
options = Config.options("input")
|
||||||
|
for option in options:
|
||||||
|
if Config.get("input", option) == "probesysfs":
|
||||||
|
Config.remove_option("input", option)
|
||||||
|
|
||||||
# Workaround for an issue where importing kivy.core.window before loading sounds
|
# Workaround for an issue where importing kivy.core.window before loading sounds
|
||||||
# will hang the whole application on Linux once the first sound is loaded.
|
# will hang the whole application on Linux once the first sound is loaded.
|
||||||
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.
|
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.
|
||||||
|
|||||||
@@ -13,5 +13,9 @@ cymem>=2.0.13
|
|||||||
orjson>=3.11.4
|
orjson>=3.11.4
|
||||||
typing_extensions>=4.15.0
|
typing_extensions>=4.15.0
|
||||||
pyshortcuts>=1.9.6
|
pyshortcuts>=1.9.6
|
||||||
|
pathspec>=0.12.1
|
||||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||||
kivymd>=2.0.1.dev0
|
kivymd>=2.0.1.dev0
|
||||||
|
|
||||||
|
# Legacy world dependencies that custom worlds rely on
|
||||||
|
Pymem>=1.13.0
|
||||||
|
|||||||
0
rule_builder/__init__.py
Normal file
0
rule_builder/__init__.py
Normal file
146
rule_builder/cached_world.py
Normal file
146
rule_builder/cached_world.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import ClassVar, cast
|
||||||
|
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from BaseClasses import CollectionState, Item, MultiWorld, Region
|
||||||
|
from worlds.AutoWorld import LogicMixin, World
|
||||||
|
|
||||||
|
from .rules import Rule
|
||||||
|
|
||||||
|
|
||||||
|
class CachedRuleBuilderWorld(World):
|
||||||
|
"""A World subclass that provides helpers for interacting with the rule builder"""
|
||||||
|
|
||||||
|
rule_item_dependencies: dict[str, set[int]]
|
||||||
|
"""A mapping of item name to set of rule ids"""
|
||||||
|
|
||||||
|
rule_region_dependencies: dict[str, set[int]]
|
||||||
|
"""A mapping of region name to set of rule ids"""
|
||||||
|
|
||||||
|
rule_location_dependencies: dict[str, set[int]]
|
||||||
|
"""A mapping of location name to set of rule ids"""
|
||||||
|
|
||||||
|
rule_entrance_dependencies: dict[str, set[int]]
|
||||||
|
"""A mapping of entrance name to set of rule ids"""
|
||||||
|
|
||||||
|
item_mapping: ClassVar[dict[str, str]] = {}
|
||||||
|
"""A mapping of actual item name to logical item name.
|
||||||
|
Useful when there are multiple versions of a collected item but the logic only uses one. For example:
|
||||||
|
item = Item("Currency x500"), rule = Has("Currency", count=1000), item_mapping = {"Currency x500": "Currency"}"""
|
||||||
|
|
||||||
|
rule_caching_enabled: ClassVar[bool] = True
|
||||||
|
"""Flag to inform rules that the caching system for this world is enabled. It should not be overridden."""
|
||||||
|
|
||||||
|
def __init__(self, multiworld: MultiWorld, player: int) -> None:
|
||||||
|
super().__init__(multiworld, player)
|
||||||
|
self.rule_item_dependencies = defaultdict(set)
|
||||||
|
self.rule_region_dependencies = defaultdict(set)
|
||||||
|
self.rule_location_dependencies = defaultdict(set)
|
||||||
|
self.rule_entrance_dependencies = defaultdict(set)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def register_rule_dependencies(self, resolved_rule: Rule.Resolved) -> None:
|
||||||
|
for item_name, rule_ids in resolved_rule.item_dependencies().items():
|
||||||
|
self.rule_item_dependencies[item_name] |= rule_ids
|
||||||
|
for region_name, rule_ids in resolved_rule.region_dependencies().items():
|
||||||
|
self.rule_region_dependencies[region_name] |= rule_ids
|
||||||
|
for location_name, rule_ids in resolved_rule.location_dependencies().items():
|
||||||
|
self.rule_location_dependencies[location_name] |= rule_ids
|
||||||
|
for entrance_name, rule_ids in resolved_rule.entrance_dependencies().items():
|
||||||
|
self.rule_entrance_dependencies[entrance_name] |= rule_ids
|
||||||
|
|
||||||
|
def register_rule_builder_dependencies(self) -> None:
|
||||||
|
"""Register all rules that depend on locations or entrances with their dependencies"""
|
||||||
|
for location_name, rule_ids in self.rule_location_dependencies.items():
|
||||||
|
try:
|
||||||
|
location = self.get_location(location_name)
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
if not isinstance(location.access_rule, Rule.Resolved):
|
||||||
|
continue
|
||||||
|
for item_name in location.access_rule.item_dependencies():
|
||||||
|
self.rule_item_dependencies[item_name] |= rule_ids
|
||||||
|
for region_name in location.access_rule.region_dependencies():
|
||||||
|
self.rule_region_dependencies[region_name] |= rule_ids
|
||||||
|
|
||||||
|
for entrance_name, rule_ids in self.rule_entrance_dependencies.items():
|
||||||
|
try:
|
||||||
|
entrance = self.get_entrance(entrance_name)
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
if not isinstance(entrance.access_rule, Rule.Resolved):
|
||||||
|
continue
|
||||||
|
for item_name in entrance.access_rule.item_dependencies():
|
||||||
|
self.rule_item_dependencies[item_name] |= rule_ids
|
||||||
|
for region_name in entrance.access_rule.region_dependencies():
|
||||||
|
self.rule_region_dependencies[region_name] |= rule_ids
|
||||||
|
|
||||||
|
@override
|
||||||
|
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||||
|
changed = super().collect(state, item)
|
||||||
|
if changed and self.rule_item_dependencies:
|
||||||
|
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||||
|
mapped_name = self.item_mapping.get(item.name, "")
|
||||||
|
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
||||||
|
for rule_id in rule_ids:
|
||||||
|
if player_results.get(rule_id, None) is False:
|
||||||
|
del player_results[rule_id]
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
@override
|
||||||
|
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||||
|
changed = super().remove(state, item)
|
||||||
|
if not changed:
|
||||||
|
return changed
|
||||||
|
|
||||||
|
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||||
|
if self.rule_item_dependencies:
|
||||||
|
mapped_name = self.item_mapping.get(item.name, "")
|
||||||
|
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
||||||
|
for rule_id in rule_ids:
|
||||||
|
player_results.pop(rule_id, None)
|
||||||
|
|
||||||
|
# clear all region dependent caches as none can be trusted
|
||||||
|
if self.rule_region_dependencies:
|
||||||
|
for rule_ids in self.rule_region_dependencies.values():
|
||||||
|
for rule_id in rule_ids:
|
||||||
|
player_results.pop(rule_id, None)
|
||||||
|
|
||||||
|
# clear all location dependent caches as they may have lost region access
|
||||||
|
if self.rule_location_dependencies:
|
||||||
|
for rule_ids in self.rule_location_dependencies.values():
|
||||||
|
for rule_id in rule_ids:
|
||||||
|
player_results.pop(rule_id, None)
|
||||||
|
|
||||||
|
# clear all entrance dependent caches as they may have lost region access
|
||||||
|
if self.rule_entrance_dependencies:
|
||||||
|
for rule_ids in self.rule_entrance_dependencies.values():
|
||||||
|
for rule_id in rule_ids:
|
||||||
|
player_results.pop(rule_id, None)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
@override
|
||||||
|
def reached_region(self, state: CollectionState, region: Region) -> None:
|
||||||
|
super().reached_region(state, region)
|
||||||
|
if self.rule_region_dependencies:
|
||||||
|
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||||
|
for rule_id in self.rule_region_dependencies[region.name]:
|
||||||
|
player_results.pop(rule_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
class CachedRuleBuilderLogicMixin(LogicMixin):
|
||||||
|
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
|
||||||
|
rule_builder_cache: dict[int, dict[int, bool]] # pyright: ignore[reportUninitializedInstanceVariable]
|
||||||
|
|
||||||
|
def init_mixin(self, multiworld: "MultiWorld") -> None:
|
||||||
|
players = multiworld.get_all_ids()
|
||||||
|
self.rule_builder_cache = {player: {} for player in players}
|
||||||
|
|
||||||
|
def copy_mixin(self, new_state: "CachedRuleBuilderLogicMixin") -> "CachedRuleBuilderLogicMixin":
|
||||||
|
new_state.rule_builder_cache = {
|
||||||
|
player: player_results.copy() for player, player_results in self.rule_builder_cache.items()
|
||||||
|
}
|
||||||
|
return new_state
|
||||||
91
rule_builder/options.py
Normal file
91
rule_builder/options.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import dataclasses
|
||||||
|
import importlib
|
||||||
|
import operator
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
|
from typing import Any, Final, Literal, Self, cast
|
||||||
|
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from Options import CommonOptions, Option
|
||||||
|
|
||||||
|
Operator = Literal["eq", "ne", "gt", "lt", "ge", "le", "contains", "in"]
|
||||||
|
|
||||||
|
OPERATORS: Final[dict[Operator, Callable[..., bool]]] = {
|
||||||
|
"eq": operator.eq,
|
||||||
|
"ne": operator.ne,
|
||||||
|
"gt": operator.gt,
|
||||||
|
"lt": operator.lt,
|
||||||
|
"ge": operator.ge,
|
||||||
|
"le": operator.le,
|
||||||
|
"contains": operator.contains,
|
||||||
|
"in": operator.contains,
|
||||||
|
}
|
||||||
|
OPERATOR_STRINGS: Final[dict[Operator, str]] = {
|
||||||
|
"eq": "==",
|
||||||
|
"ne": "!=",
|
||||||
|
"gt": ">",
|
||||||
|
"lt": "<",
|
||||||
|
"ge": ">=",
|
||||||
|
"le": "<=",
|
||||||
|
}
|
||||||
|
REVERSE_OPERATORS: Final[tuple[Operator, ...]] = ("in",)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class OptionFilter:
|
||||||
|
option: type[Option[Any]]
|
||||||
|
value: Any
|
||||||
|
operator: Operator = "eq"
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
"""Returns a JSON compatible dict representation of this option filter"""
|
||||||
|
return {
|
||||||
|
"option": f"{self.option.__module__}.{self.option.__name__}",
|
||||||
|
"value": self.value,
|
||||||
|
"operator": self.operator,
|
||||||
|
}
|
||||||
|
|
||||||
|
def check(self, options: CommonOptions) -> bool:
|
||||||
|
"""Tests the given options dataclass to see if it passes this option filter"""
|
||||||
|
option_name = next(
|
||||||
|
(name for name, cls in 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 {options.__class__.__name__}")
|
||||||
|
opt = cast(Option[Any] | None, getattr(options, option_name, None))
|
||||||
|
if opt is None:
|
||||||
|
raise ValueError(f"Invalid option: {option_name}")
|
||||||
|
|
||||||
|
fn = OPERATORS[self.operator]
|
||||||
|
return fn(self.value, opt) if self.operator in REVERSE_OPERATORS else fn(opt, self.value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||||
|
"""Returns a new OptionFilter instance from a dict representation"""
|
||||||
|
if "option" not in data or "value" not in data:
|
||||||
|
raise ValueError("Missing required value and/or 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")
|
||||||
|
|
||||||
|
value = data["value"]
|
||||||
|
operator = data.get("operator", "eq")
|
||||||
|
return cls(option=cast(type[Option[Any]], option), value=value, operator=operator)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def multiple_from_dict(cls, data: Iterable[dict[str, Any]]) -> tuple[Self, ...]:
|
||||||
|
"""Returns a tuple of OptionFilters instances from an iterable of dict representations"""
|
||||||
|
return tuple(cls.from_dict(o) for o in data)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
op = OPERATOR_STRINGS.get(self.operator, self.operator)
|
||||||
|
return f"{self.option.__name__} {op} {self.value}"
|
||||||
1822
rule_builder/rules.py
Normal file
1822
rule_builder/rules.py
Normal file
File diff suppressed because it is too large
Load Diff
1
setup.py
1
setup.py
@@ -71,7 +71,6 @@ non_apworlds: set[str] = {
|
|||||||
"Ocarina of Time",
|
"Ocarina of Time",
|
||||||
"Overcooked! 2",
|
"Overcooked! 2",
|
||||||
"Raft",
|
"Raft",
|
||||||
"Sudoku",
|
|
||||||
"Super Mario 64",
|
"Super Mario 64",
|
||||||
"VVVVVV",
|
"VVVVVV",
|
||||||
"Wargroove",
|
"Wargroove",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
call_all(self.multiworld, "post_fill")
|
call_all(self.multiworld, "post_fill")
|
||||||
|
call_all(self.multiworld, "finalize_multiworld")
|
||||||
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
||||||
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
||||||
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from typing import Callable, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from BaseClasses import CollectionState, MultiWorld, Region
|
from BaseClasses import CollectionRule, MultiWorld, Region
|
||||||
|
from rule_builder.rules import Has, Rule
|
||||||
|
from test.general import TestWorld
|
||||||
|
|
||||||
|
|
||||||
class TestHelpers(unittest.TestCase):
|
class TestHelpers(unittest.TestCase):
|
||||||
@@ -16,6 +18,7 @@ class TestHelpers(unittest.TestCase):
|
|||||||
self.multiworld.game[self.player] = "helper_test_game"
|
self.multiworld.game[self.player] = "helper_test_game"
|
||||||
self.multiworld.player_name = {1: "Tester"}
|
self.multiworld.player_name = {1: "Tester"}
|
||||||
self.multiworld.set_seed()
|
self.multiworld.set_seed()
|
||||||
|
self.multiworld.worlds[self.player] = TestWorld(self.multiworld, self.player)
|
||||||
|
|
||||||
def test_region_helpers(self) -> None:
|
def test_region_helpers(self) -> None:
|
||||||
"""Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
|
"""Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
|
||||||
@@ -46,8 +49,9 @@ class TestHelpers(unittest.TestCase):
|
|||||||
"TestRegion1": {"TestRegion3"}
|
"TestRegion1": {"TestRegion3"}
|
||||||
}
|
}
|
||||||
|
|
||||||
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
exit_rules: Dict[str, CollectionRule | Rule[Any]] = {
|
||||||
"TestRegion1": lambda state: state.has("test_item", self.player)
|
"TestRegion1": lambda state: state.has("test_item", self.player),
|
||||||
|
"TestRegion2": Has("test_item2"),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
|
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
|
||||||
@@ -74,13 +78,17 @@ class TestHelpers(unittest.TestCase):
|
|||||||
self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names)
|
self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names)
|
||||||
if exit_reg in exit_rules:
|
if exit_reg in exit_rules:
|
||||||
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
|
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
|
||||||
self.assertEqual(exit_rules[exit_reg],
|
rule = exit_rules[exit_reg]
|
||||||
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
if isinstance(rule, Rule):
|
||||||
|
self.assertEqual(rule.resolve(self.multiworld.worlds[self.player]),
|
||||||
|
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||||
|
else:
|
||||||
|
self.assertEqual(rule, self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||||
|
|
||||||
for region in reg_exit_set:
|
for region, exit_set in reg_exit_set.items():
|
||||||
current_region = self.multiworld.get_region(region, self.player)
|
current_region = self.multiworld.get_region(region, self.player)
|
||||||
current_region.add_exits(reg_exit_set[region])
|
current_region.add_exits(exit_set)
|
||||||
exit_names = {_exit.name for _exit in current_region.exits}
|
exit_names = {_exit.name for _exit in current_region.exits}
|
||||||
for reg_exit in reg_exit_set[region]:
|
for reg_exit in exit_set:
|
||||||
self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
|
self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
|
||||||
f"{region} -> {reg_exit} not in {exit_names}")
|
f"{region} -> {reg_exit} not in {exit_names}")
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class TestIDs(unittest.TestCase):
|
|||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
||||||
distribute_items_restrictive(multiworld)
|
distribute_items_restrictive(multiworld)
|
||||||
call_all(multiworld, "post_fill")
|
call_all(multiworld, "post_fill")
|
||||||
|
call_all(multiworld, "finalize_multiworld")
|
||||||
datapackage = world_type.get_data_package_data()
|
datapackage = world_type.get_data_package_data()
|
||||||
for item_group, item_names in datapackage["item_name_groups"].items():
|
for item_group, item_names in datapackage["item_name_groups"].items():
|
||||||
self.assertIsInstance(item_group, str,
|
self.assertIsInstance(item_group, str,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class TestImplemented(unittest.TestCase):
|
|||||||
def test_completion_condition(self):
|
def test_completion_condition(self):
|
||||||
"""Ensure a completion condition is set that has requirements."""
|
"""Ensure a completion condition is set that has requirements."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden and game_name not in {"Sudoku"}:
|
if not world_type.hidden:
|
||||||
with self.subTest(game_name):
|
with self.subTest(game_name):
|
||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
||||||
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
||||||
@@ -46,6 +46,8 @@ class TestImplemented(unittest.TestCase):
|
|||||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||||
distribute_items_restrictive(multiworld)
|
distribute_items_restrictive(multiworld)
|
||||||
call_all(multiworld, "post_fill")
|
call_all(multiworld, "post_fill")
|
||||||
|
call_all(multiworld, "finalize_multiworld")
|
||||||
|
call_all(multiworld, "pre_output")
|
||||||
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
for key, data in multiworld.worlds[1].fill_slot_data().items():
|
||||||
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
self.assertIsInstance(key, str, "keys in slot data must be a string")
|
||||||
convert_to_base_types(data) # only put base data types into slot data
|
convert_to_base_types(data) # only put base data types into slot data
|
||||||
@@ -57,7 +59,7 @@ class TestImplemented(unittest.TestCase):
|
|||||||
def test_prefill_items(self):
|
def test_prefill_items(self):
|
||||||
"""Test that every world can reach every location from allstate before pre_fill."""
|
"""Test that every world can reach every location from allstate before pre_fill."""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"):
|
if gamename not in ("Archipelago", "Final Fantasy", "Test Game"):
|
||||||
with self.subTest(gamename):
|
with self.subTest(gamename):
|
||||||
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
|
||||||
"set_rules", "connect_entrances", "generate_basic"))
|
"set_rules", "connect_entrances", "generate_basic"))
|
||||||
@@ -93,6 +95,7 @@ class TestImplemented(unittest.TestCase):
|
|||||||
with self.subTest(game=game_name, seed=multiworld.seed):
|
with self.subTest(game=game_name, seed=multiworld.seed):
|
||||||
distribute_items_restrictive(multiworld)
|
distribute_items_restrictive(multiworld)
|
||||||
call_all(multiworld, "post_fill")
|
call_all(multiworld, "post_fill")
|
||||||
|
call_all(multiworld, "finalize_multiworld")
|
||||||
|
|
||||||
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
|
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
|
||||||
# is nondeterministic and may vary between runs with the same seed.
|
# is nondeterministic and may vary between runs with the same seed.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from collections import ChainMap
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from BaseClasses import CollectionState, MultiWorld
|
from BaseClasses import CollectionState, MultiWorld
|
||||||
@@ -82,12 +83,13 @@ class TestBase(unittest.TestCase):
|
|||||||
|
|
||||||
def test_items_in_datapackage(self):
|
def test_items_in_datapackage(self):
|
||||||
"""Test that any created items in the itempool are in the datapackage"""
|
"""Test that any created items in the itempool are in the datapackage"""
|
||||||
|
archipelago = AutoWorldRegister.world_types["Archipelago"]
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest("Game", game=game_name):
|
with self.subTest("Game", game=game_name):
|
||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
||||||
for item in multiworld.itempool:
|
for item in multiworld.itempool:
|
||||||
self.assertIn(item.name, world_type.item_name_to_id)
|
self.assertIn(item.name, ChainMap(world_type.item_name_to_id, archipelago.item_name_to_id))
|
||||||
|
|
||||||
def test_item_links(self) -> None:
|
def test_item_links(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tests item link creation by creating a multiworld of 2 worlds for every game and linking their items together.
|
Tests item link creation by creating a multiworld of 2 worlds for every game and linking their items together.
|
||||||
@@ -121,6 +123,7 @@ class TestBase(unittest.TestCase):
|
|||||||
call_all(multiworld, "pre_fill")
|
call_all(multiworld, "pre_fill")
|
||||||
distribute_items_restrictive(multiworld)
|
distribute_items_restrictive(multiworld)
|
||||||
call_all(multiworld, "post_fill")
|
call_all(multiworld, "post_fill")
|
||||||
|
call_all(multiworld, "finalize_multiworld")
|
||||||
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
|
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
|
||||||
|
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from Options import Choice, ItemLinks, PlandoConnections, PlandoItems, PlandoTexts
|
from Options import Choice, TextChoice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
|
||||||
from Utils import restricted_dumps
|
from Utils import restricted_dumps
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +16,29 @@ class TestOptions(unittest.TestCase):
|
|||||||
with self.subTest(game=gamename, option=option_key):
|
with self.subTest(game=gamename, option=option_key):
|
||||||
self.assertTrue(option.__doc__)
|
self.assertTrue(option.__doc__)
|
||||||
|
|
||||||
|
def test_option_defaults(self):
|
||||||
|
"""Test that defaults for submitted options are valid."""
|
||||||
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
if not world_type.hidden:
|
||||||
|
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||||
|
with self.subTest(game=gamename, option=option_key):
|
||||||
|
if issubclass(option, TextChoice):
|
||||||
|
self.assertTrue(option.default in option.name_lookup,
|
||||||
|
f"Default value {option.default} for TextChoice option {option.__name__} in"
|
||||||
|
f" {gamename} does not resolve to a listed value!"
|
||||||
|
)
|
||||||
|
# Standard "can default generate" test
|
||||||
|
err_raised = None
|
||||||
|
try:
|
||||||
|
option.from_any(option.default)
|
||||||
|
except Exception as ex:
|
||||||
|
err_raised = ex
|
||||||
|
self.assertIsNone(err_raised,
|
||||||
|
f"Default value {option.default} for option {option.__name__} in {gamename}"
|
||||||
|
f" is not valid! Exception: {err_raised}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_options_are_not_set_by_world(self):
|
def test_options_are_not_set_by_world(self):
|
||||||
"""Test that options attribute is not already set"""
|
"""Test that options attribute is not already set"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
@@ -81,6 +105,19 @@ class TestOptions(unittest.TestCase):
|
|||||||
restricted_dumps(option.from_any(option.default))
|
restricted_dumps(option.from_any(option.default))
|
||||||
if issubclass(option, Choice) and option.default in option.name_lookup:
|
if issubclass(option, Choice) and option.default in option.name_lookup:
|
||||||
restricted_dumps(option.from_text(option.name_lookup[option.default]))
|
restricted_dumps(option.from_text(option.name_lookup[option.default]))
|
||||||
|
|
||||||
|
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", "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):
|
||||||
|
self.assertFalse(any(random_key in option.valid_keys for random_key in ("random",
|
||||||
|
"random-high",
|
||||||
|
"random-low")))
|
||||||
|
for key in option.valid_keys:
|
||||||
|
self.assertFalse("random-range" in key)
|
||||||
|
|
||||||
def test_pickle_dumps_plando(self):
|
def test_pickle_dumps_plando(self):
|
||||||
"""Test that plando options using containers of a custom type can be pickled"""
|
"""Test that plando options using containers of a custom type can be pickled"""
|
||||||
|
|||||||
1344
test/general/test_rule_builder.py
Normal file
1344
test/general/test_rule_builder.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import zipfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Iterable, Optional, cast
|
from typing import TYPE_CHECKING, Iterable, Optional, cast
|
||||||
|
|
||||||
|
from Utils import utcnow
|
||||||
from WebHostLib import to_python
|
from WebHostLib import to_python
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -133,7 +134,7 @@ def stop_room(app_client: "FlaskClient",
|
|||||||
room_id: str,
|
room_id: str,
|
||||||
timeout: Optional[float] = None,
|
timeout: Optional[float] = None,
|
||||||
simulate_idle: bool = True) -> None:
|
simulate_idle: bool = True) -> None:
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from pony.orm import db_session
|
from pony.orm import db_session
|
||||||
@@ -151,10 +152,11 @@ def stop_room(app_client: "FlaskClient",
|
|||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
room: Room = Room.get(id=room_uuid)
|
room: Room = Room.get(id=room_uuid)
|
||||||
|
now = utcnow()
|
||||||
if simulate_idle:
|
if simulate_idle:
|
||||||
new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
|
new_last_activity = now - timedelta(seconds=room.timeout + 5)
|
||||||
else:
|
else:
|
||||||
new_last_activity = datetime.utcnow() - timedelta(days=3)
|
new_last_activity = now - timedelta(days=3)
|
||||||
room.last_activity = new_last_activity
|
room.last_activity = new_last_activity
|
||||||
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
|
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
|
||||||
if address:
|
if address:
|
||||||
@@ -188,6 +190,7 @@ def stop_room(app_client: "FlaskClient",
|
|||||||
if address:
|
if address:
|
||||||
room.timeout = original_timeout
|
room.timeout = original_timeout
|
||||||
room.last_activity = new_last_activity
|
room.last_activity = new_last_activity
|
||||||
|
room.commands.clear() # make sure there is no leftover /exit
|
||||||
print("timeout restored")
|
print("timeout restored")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
|||||||
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
call_all(self.multiworld, "post_fill")
|
call_all(self.multiworld, "post_fill")
|
||||||
|
call_all(self.multiworld, "finalize_multiworld")
|
||||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||||
|
|
||||||
|
|
||||||
@@ -78,4 +79,5 @@ class TestTwoPlayerMulti(MultiworldTestBase):
|
|||||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
call_all(self.multiworld, "post_fill")
|
call_all(self.multiworld, "post_fill")
|
||||||
|
call_all(self.multiworld, "finalize_multiworld")
|
||||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||||
|
|||||||
@@ -25,31 +25,41 @@ class TestGenerateYamlTemplates(unittest.TestCase):
|
|||||||
if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types:
|
if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types:
|
||||||
del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"]
|
del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"]
|
||||||
|
|
||||||
|
|
||||||
def test_name_with_colon(self) -> None:
|
def test_name_with_colon(self) -> None:
|
||||||
from Options import generate_yaml_templates
|
from Options import generate_yaml_templates
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
from worlds.AutoWorld import World
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
|
||||||
|
class WebWorldWithColon(WebWorld):
|
||||||
|
options_presets = {
|
||||||
|
"Generic": {
|
||||||
|
"progression_balancing": "disabled",
|
||||||
|
"accessibility": "minimal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class WorldWithColon(World):
|
class WorldWithColon(World):
|
||||||
game = "World: with colon"
|
game = "World: with colon"
|
||||||
item_name_to_id = {}
|
item_name_to_id = {}
|
||||||
location_name_to_id = {}
|
location_name_to_id = {}
|
||||||
|
web = WebWorldWithColon()
|
||||||
|
|
||||||
AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon}
|
AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon}
|
||||||
with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir:
|
with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir:
|
||||||
generate_yaml_templates(temp_dir)
|
generate_yaml_templates(temp_dir)
|
||||||
path: Path
|
path: Path
|
||||||
for path in Path(temp_dir).iterdir():
|
for path in Path(temp_dir).rglob("*"):
|
||||||
self.assertTrue(path.is_file())
|
if path.is_file():
|
||||||
self.assertTrue(path.suffix == ".yaml")
|
self.assertTrue(path.suffix == ".yaml")
|
||||||
with path.open(encoding="utf-8") as f:
|
with path.open(encoding="utf-8") as f:
|
||||||
try:
|
try:
|
||||||
data = parse_yaml(f)
|
data = parse_yaml(f)
|
||||||
except:
|
except:
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
print(f"Error in {path.name}:\n{f.read()}")
|
print(f"Error in {path.name}:\n{f.read()}")
|
||||||
raise
|
raise
|
||||||
self.assertIn("game", data)
|
self.assertIn("game", data)
|
||||||
self.assertIn(":", data["game"])
|
self.assertIn(":", data["game"])
|
||||||
self.assertIn(data["game"], data)
|
self.assertIn(data["game"], data)
|
||||||
self.assertIsInstance(data[data["game"]], dict)
|
self.assertIsInstance(data[data["game"]], dict)
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from uuid import UUID, uuid4, uuid5
|
from uuid import UUID, uuid4, uuid5
|
||||||
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
|
from WebHostLib.customserver import set_up_logging, tear_down_logging
|
||||||
from . import TestBase
|
from . import TestBase
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_logger(room_id: UUID) -> None:
|
||||||
|
from Utils import user_path
|
||||||
|
tear_down_logging(room_id)
|
||||||
|
try:
|
||||||
|
os.unlink(user_path("logs", f"{room_id}.txt"))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestHostFakeRoom(TestBase):
|
class TestHostFakeRoom(TestBase):
|
||||||
room_id: UUID
|
room_id: UUID
|
||||||
log_filename: str
|
log_filename: str
|
||||||
@@ -39,7 +50,7 @@ class TestHostFakeRoom(TestBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
os.unlink(self.log_filename)
|
os.unlink(self.log_filename)
|
||||||
except FileNotFoundError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_display_log_missing_full(self) -> None:
|
def test_display_log_missing_full(self) -> None:
|
||||||
@@ -191,3 +202,27 @@ class TestHostFakeRoom(TestBase):
|
|||||||
with db_session:
|
with db_session:
|
||||||
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
||||||
self.assertNotIn("/help", (command.commandtext for command in commands))
|
self.assertNotIn("/help", (command.commandtext for command in commands))
|
||||||
|
|
||||||
|
def test_logger_teardown(self) -> None:
|
||||||
|
"""Verify that room loggers are removed from the global logging manager."""
|
||||||
|
from WebHostLib.customserver import tear_down_logging
|
||||||
|
room_id = uuid4()
|
||||||
|
self.addCleanup(_cleanup_logger, room_id)
|
||||||
|
set_up_logging(room_id)
|
||||||
|
self.assertIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
|
||||||
|
tear_down_logging(room_id)
|
||||||
|
self.assertNotIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
|
||||||
|
|
||||||
|
def test_handler_teardown(self) -> None:
|
||||||
|
"""Verify that handlers for room loggers are closed by tear_down_logging."""
|
||||||
|
from WebHostLib.customserver import tear_down_logging
|
||||||
|
room_id = uuid4()
|
||||||
|
self.addCleanup(_cleanup_logger, room_id)
|
||||||
|
logger = set_up_logging(room_id)
|
||||||
|
handlers = logger.handlers[:]
|
||||||
|
self.assertGreater(len(handlers), 0)
|
||||||
|
|
||||||
|
tear_down_logging(room_id)
|
||||||
|
for handler in handlers:
|
||||||
|
if isinstance(handler, logging.FileHandler):
|
||||||
|
self.assertTrue(handler.stream is None or handler.stream.closed)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import unittest
|
|||||||
|
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from worlds import AutoWorldRegister
|
from worlds import AutoWorldRegister
|
||||||
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
|
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet, Visibility
|
||||||
|
|
||||||
|
|
||||||
class TestOptionPresets(unittest.TestCase):
|
class TestOptionPresets(unittest.TestCase):
|
||||||
@@ -19,6 +19,9 @@ class TestOptionPresets(unittest.TestCase):
|
|||||||
# pass in all plando options in case a preset wants to require certain plando options
|
# pass in all plando options in case a preset wants to require certain plando options
|
||||||
# for some reason
|
# for some reason
|
||||||
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
||||||
|
if not (Visibility.complex_ui in option.visibility or Visibility.simple_ui in option.visibility):
|
||||||
|
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' is not "
|
||||||
|
f"visible in any supported UI.")
|
||||||
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
|
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
|
||||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
||||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||||
|
|||||||
82
typings/kivy/clock.pyi
Normal file
82
typings/kivy/clock.pyi
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from _typeshed import Incomplete
|
||||||
|
from kivy._clock import (
|
||||||
|
ClockEvent as ClockEvent,
|
||||||
|
ClockNotRunningError as ClockNotRunningError,
|
||||||
|
CyClockBase as CyClockBase,
|
||||||
|
CyClockBaseFree as CyClockBaseFree,
|
||||||
|
FreeClockEvent as FreeClockEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Clock",
|
||||||
|
"ClockNotRunningError",
|
||||||
|
"ClockEvent",
|
||||||
|
"FreeClockEvent",
|
||||||
|
"CyClockBase",
|
||||||
|
"CyClockBaseFree",
|
||||||
|
"triggered",
|
||||||
|
"ClockBaseBehavior",
|
||||||
|
"ClockBaseInterruptBehavior",
|
||||||
|
"ClockBaseInterruptFreeBehavior",
|
||||||
|
"ClockBase",
|
||||||
|
"ClockBaseInterrupt",
|
||||||
|
"ClockBaseFreeInterruptAll",
|
||||||
|
"ClockBaseFreeInterruptOnly",
|
||||||
|
"mainthread",
|
||||||
|
]
|
||||||
|
|
||||||
|
class ClockBaseBehavior:
|
||||||
|
MIN_SLEEP: float
|
||||||
|
SLEEP_UNDERSHOOT: Incomplete
|
||||||
|
def __init__(self, async_lib: str = "asyncio", **kwargs) -> None: ...
|
||||||
|
def init_async_lib(self, lib) -> None: ...
|
||||||
|
@property
|
||||||
|
def frametime(self): ...
|
||||||
|
@property
|
||||||
|
def frames(self): ...
|
||||||
|
@property
|
||||||
|
def frames_displayed(self): ...
|
||||||
|
def usleep(self, microseconds) -> None: ...
|
||||||
|
def idle(self): ...
|
||||||
|
async def async_idle(self): ...
|
||||||
|
def tick(self) -> None: ...
|
||||||
|
async def async_tick(self) -> None: ...
|
||||||
|
def pre_idle(self) -> None: ...
|
||||||
|
def post_idle(self, ts, current): ...
|
||||||
|
def tick_draw(self) -> None: ...
|
||||||
|
def get_fps(self): ...
|
||||||
|
def get_rfps(self): ...
|
||||||
|
def get_time(self): ...
|
||||||
|
def get_boottime(self): ...
|
||||||
|
time: Incomplete
|
||||||
|
def handle_exception(self, e) -> None: ...
|
||||||
|
|
||||||
|
class ClockBaseInterruptBehavior(ClockBaseBehavior):
|
||||||
|
interupt_next_only: bool
|
||||||
|
def __init__(self, interupt_next_only: bool = False, **kwargs) -> None: ...
|
||||||
|
def init_async_lib(self, lib) -> None: ...
|
||||||
|
def usleep(self, microseconds) -> None: ...
|
||||||
|
async def async_usleep(self, microseconds) -> None: ...
|
||||||
|
def on_schedule(self, event) -> None: ...
|
||||||
|
def idle(self): ...
|
||||||
|
async def async_idle(self): ...
|
||||||
|
|
||||||
|
class ClockBaseInterruptFreeBehavior(ClockBaseInterruptBehavior):
|
||||||
|
def __init__(self, **kwargs) -> None: ...
|
||||||
|
def on_schedule(self, event): ...
|
||||||
|
|
||||||
|
class ClockBase(ClockBaseBehavior, CyClockBase):
|
||||||
|
def __init__(self, **kwargs) -> None: ...
|
||||||
|
def usleep(self, microseconds) -> None: ...
|
||||||
|
|
||||||
|
class ClockBaseInterrupt(ClockBaseInterruptBehavior, CyClockBase): ...
|
||||||
|
class ClockBaseFreeInterruptAll(ClockBaseInterruptFreeBehavior, CyClockBaseFree): ...
|
||||||
|
|
||||||
|
class ClockBaseFreeInterruptOnly(ClockBaseInterruptFreeBehavior, CyClockBaseFree):
|
||||||
|
def idle(self): ...
|
||||||
|
async def async_idle(self): ...
|
||||||
|
|
||||||
|
def mainthread(func): ...
|
||||||
|
def triggered(timeout: int = 0, interval: bool = False): ...
|
||||||
|
|
||||||
|
Clock: ClockBase
|
||||||
@@ -5,17 +5,18 @@ import logging
|
|||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Callable, Iterable, Mapping
|
||||||
from random import Random
|
from random import Random
|
||||||
from dataclasses import make_dataclass
|
from typing import (Any, ClassVar, Dict, FrozenSet, List, Optional, Self, Set, TextIO, Tuple,
|
||||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, TextIO, Tuple,
|
|
||||||
TYPE_CHECKING, Type, Union)
|
TYPE_CHECKING, Type, Union)
|
||||||
|
|
||||||
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
|
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState, Entrance
|
||||||
|
from rule_builder.rules import CustomRuleRegister, Rule
|
||||||
from Utils import Version
|
from Utils import Version
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
from BaseClasses import CollectionRule, Item, Location, MultiWorld, Region, Tutorial
|
||||||
from NetUtils import GamesPackage, MultiData
|
from NetUtils import GamesPackage, MultiData
|
||||||
from settings import Group
|
from settings import Group
|
||||||
|
|
||||||
@@ -47,27 +48,31 @@ class AutoWorldRegister(type):
|
|||||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||||
if "web" in dct:
|
if "web" in dct:
|
||||||
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
|
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
|
||||||
# filter out any events
|
|
||||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
|
||||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
|
||||||
# build reverse lookups
|
|
||||||
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
|
|
||||||
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
|
|
||||||
|
|
||||||
# build rest
|
|
||||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
|
||||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
|
||||||
in dct.get("item_name_groups", {}).items()}
|
|
||||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
|
||||||
|
|
||||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
|
||||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
|
||||||
in dct.get("location_name_groups", {}).items()}
|
|
||||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
|
||||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
|
||||||
|
|
||||||
# move away from get_required_client_version function
|
|
||||||
if "game" in dct:
|
if "game" in dct:
|
||||||
|
assert "item_name_to_id" in dct, f"{name}: item_name_to_id is required"
|
||||||
|
assert "location_name_to_id" in dct, f"{name}: location_name_to_id is required"
|
||||||
|
|
||||||
|
# filter out any events
|
||||||
|
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||||
|
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||||
|
# build reverse lookups
|
||||||
|
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
|
||||||
|
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
|
||||||
|
|
||||||
|
# build rest
|
||||||
|
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||||
|
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||||
|
in dct.get("item_name_groups", {}).items()}
|
||||||
|
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||||
|
|
||||||
|
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||||
|
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||||
|
in dct.get("location_name_groups", {}).items()}
|
||||||
|
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||||
|
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||||
|
|
||||||
|
# move away from get_required_client_version function
|
||||||
assert "get_required_client_version" not in dct, f"{name}: required_client_version is an attribute now"
|
assert "get_required_client_version" not in dct, f"{name}: required_client_version is an attribute now"
|
||||||
# set minimum required_client_version from bases
|
# set minimum required_client_version from bases
|
||||||
if "required_client_version" in dct and bases:
|
if "required_client_version" in dct and bases:
|
||||||
@@ -173,7 +178,8 @@ def _timed_call(method: Callable[..., Any], *args: Any,
|
|||||||
|
|
||||||
|
|
||||||
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
||||||
method = getattr(multiworld.worlds[player], method_name)
|
world = multiworld.worlds[player]
|
||||||
|
method = getattr(world, method_name)
|
||||||
try:
|
try:
|
||||||
ret = _timed_call(method, *args, multiworld=multiworld, player=player)
|
ret = _timed_call(method, *args, multiworld=multiworld, player=player)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -184,6 +190,10 @@ def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args:
|
|||||||
logging.error(message)
|
logging.error(message)
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
|
# Convenience for CachedRuleBuilderWorld users: Ensure that caching setup function is called
|
||||||
|
# Can be removed once dependency system is improved
|
||||||
|
if method_name == "set_rules" and hasattr(world, "register_rule_builder_dependencies"):
|
||||||
|
call_single(multiworld, "register_rule_builder_dependencies", player)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@@ -353,7 +363,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
|
|
||||||
def __getattr__(self, item: str) -> Any:
|
def __getattr__(self, item: str) -> Any:
|
||||||
if item == "settings":
|
if item == "settings":
|
||||||
return self.__class__.settings
|
return getattr(self.__class__, item)
|
||||||
raise AttributeError
|
raise AttributeError
|
||||||
|
|
||||||
# overridable methods that get called by Main.py, sorted by execution order
|
# overridable methods that get called by Main.py, sorted by execution order
|
||||||
@@ -420,6 +430,23 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
This happens before progression balancing, so the items may not be in their final locations yet.
|
This happens before progression balancing, so the items may not be in their final locations yet.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def finalize_multiworld(self) -> None:
|
||||||
|
"""
|
||||||
|
Optional Method that is called after fill and progression balancing.
|
||||||
|
This is the last stage of generation where worlds may change logically relevant data,
|
||||||
|
such as item placements and connections. To not break assumptions,
|
||||||
|
only ever increase accessibility, never decrease it.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pre_output(self):
|
||||||
|
"""
|
||||||
|
Optional method that is called before output generation.
|
||||||
|
Items and connections are not meant to be moved anymore,
|
||||||
|
anything that would affect logical spheres is forbidden at this point.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def generate_output(self, output_directory: str) -> None:
|
def generate_output(self, output_directory: str) -> None:
|
||||||
"""
|
"""
|
||||||
This method gets called from a threadpool, do not use multiworld.random here.
|
This method gets called from a threadpool, do not use multiworld.random here.
|
||||||
@@ -484,7 +511,14 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
"""Called when the item pool needs to be filled with additional items to match location count."""
|
"""
|
||||||
|
Called when the item pool needs to be filled with additional items to match location count.
|
||||||
|
|
||||||
|
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
|
||||||
|
For most worlds this will be one or more of your filler items, but the classification of these items
|
||||||
|
does not need to be ItemClassification.filler.
|
||||||
|
The item name returned can be for a trap, useful, and/or progression item as long as it's repeatable.
|
||||||
|
"""
|
||||||
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
||||||
return self.random.choice(tuple(self.item_name_to_id.keys()))
|
return self.random.choice(tuple(self.item_name_to_id.keys()))
|
||||||
|
|
||||||
@@ -538,6 +572,10 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def reached_region(self, state: "CollectionState", region: "Region") -> None:
|
||||||
|
"""Called when a region is newly reachable by the state."""
|
||||||
|
pass
|
||||||
|
|
||||||
# following methods should not need to be overridden.
|
# following methods should not need to be overridden.
|
||||||
def create_filler(self) -> "Item":
|
def create_filler(self) -> "Item":
|
||||||
return self.create_item(self.get_filler_item_name())
|
return self.create_item(self.get_filler_item_name())
|
||||||
@@ -586,6 +624,64 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
res["checksum"] = data_package_checksum(res)
|
res["checksum"] = data_package_checksum(res)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_rule_cls(cls, name: str) -> type[Rule[Self]]:
|
||||||
|
"""Returns the world-registered or default rule with the given name"""
|
||||||
|
return CustomRuleRegister.get_rule_cls(cls.game, name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def rule_from_dict(cls, data: Mapping[str, Any]) -> Rule[Self]:
|
||||||
|
"""Create a rule instance from a serialized dict representation"""
|
||||||
|
name = data.get("rule", "")
|
||||||
|
rule_class = cls.get_rule_cls(name)
|
||||||
|
return rule_class.from_dict(data, cls)
|
||||||
|
|
||||||
|
def set_rule(self, spot: Location | Entrance, rule: CollectionRule | Rule[Any]) -> None:
|
||||||
|
"""Sets an access rule for a location or entrance"""
|
||||||
|
if isinstance(rule, Rule):
|
||||||
|
rule = rule.resolve(self)
|
||||||
|
self.register_rule_dependencies(rule)
|
||||||
|
if isinstance(spot, Entrance):
|
||||||
|
self._register_rule_indirects(rule, spot)
|
||||||
|
spot.access_rule = rule
|
||||||
|
|
||||||
|
def set_completion_rule(self, rule: CollectionRule | Rule[Any]) -> None:
|
||||||
|
"""Set the completion rule for this world"""
|
||||||
|
if isinstance(rule, Rule):
|
||||||
|
rule = rule.resolve(self)
|
||||||
|
self.register_rule_dependencies(rule)
|
||||||
|
self.multiworld.completion_condition[self.player] = rule
|
||||||
|
|
||||||
|
def create_entrance(
|
||||||
|
self,
|
||||||
|
from_region: Region,
|
||||||
|
to_region: Region,
|
||||||
|
rule: CollectionRule | Rule[Any] | None = None,
|
||||||
|
name: str | None = None,
|
||||||
|
force_creation: bool = False,
|
||||||
|
) -> Entrance | None:
|
||||||
|
"""Try to create an entrance between regions with the given rule,
|
||||||
|
skipping it if the rule resolves to False (unless force_creation is True)"""
|
||||||
|
if rule is not None and isinstance(rule, Rule):
|
||||||
|
rule = rule.resolve(self)
|
||||||
|
if rule.always_false and not force_creation:
|
||||||
|
return None
|
||||||
|
self.register_rule_dependencies(rule)
|
||||||
|
|
||||||
|
entrance = from_region.connect(to_region, name, rule=rule)
|
||||||
|
if rule and isinstance(rule, Rule.Resolved):
|
||||||
|
self._register_rule_indirects(rule, entrance)
|
||||||
|
return entrance
|
||||||
|
|
||||||
|
def register_rule_dependencies(self, resolved_rule: Rule.Resolved) -> None:
|
||||||
|
"""Hook for registering dependencies when a rule is assigned for this world"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _register_rule_indirects(self, resolved_rule: Rule.Resolved, entrance: Entrance) -> None:
|
||||||
|
if self.explicit_indirect_conditions:
|
||||||
|
for indirect_region in resolved_rule.region_dependencies().keys():
|
||||||
|
self.multiworld.register_indirect_condition(self.get_region(indirect_region), entrance)
|
||||||
|
|
||||||
|
|
||||||
# any methods attached to this can be used as part of CollectionState,
|
# any methods attached to this can be used as part of CollectionState,
|
||||||
# please use a prefix as all of them get clobbered together
|
# please use a prefix as all of them get clobbered together
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import weakref
|
|||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Optional, Callable, List, Iterable, Tuple
|
from typing import Optional, Callable, List, Iterable, Tuple
|
||||||
|
|
||||||
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path
|
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore
|
||||||
|
|
||||||
|
|
||||||
class Type(Enum):
|
class Type(Enum):
|
||||||
@@ -247,7 +247,8 @@ components: List[Component] = [
|
|||||||
# MegaMan Battle Network 3
|
# MegaMan Battle Network 3
|
||||||
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')),
|
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')),
|
||||||
|
|
||||||
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL),
|
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL,
|
||||||
|
description="Write item/location data for installed worlds to a file and open it."),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -278,6 +279,10 @@ if not is_frozen():
|
|||||||
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
|
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
|
||||||
if not worldtype.zip_path]
|
if not worldtype.zip_path]
|
||||||
|
|
||||||
|
global_apignores = read_apignore(local_path("data", "GLOBAL.apignore"))
|
||||||
|
if not global_apignores:
|
||||||
|
raise RuntimeError("Could not read global apignore file for build component")
|
||||||
|
|
||||||
apworlds_folder = os.path.join("build", "apworlds")
|
apworlds_folder = os.path.join("build", "apworlds")
|
||||||
os.makedirs(apworlds_folder, exist_ok=True)
|
os.makedirs(apworlds_folder, exist_ok=True)
|
||||||
for worldname, worldtype in games:
|
for worldname, worldtype in games:
|
||||||
@@ -305,18 +310,17 @@ if not is_frozen():
|
|||||||
apworld = APWorldContainer(str(zip_path))
|
apworld = APWorldContainer(str(zip_path))
|
||||||
apworld.game = worldtype.game
|
apworld.game = worldtype.game
|
||||||
manifest.update(apworld.get_manifest())
|
manifest.update(apworld.get_manifest())
|
||||||
apworld.manifest_path = f"{file_name}/archipelago.json"
|
apworld.manifest_path = os.path.join(file_name, "archipelago.json")
|
||||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
|
|
||||||
compresslevel=9) as zf:
|
local_ignores = read_apignore(pathlib.Path(world_directory, ".apignore"))
|
||||||
for path in pathlib.Path(world_directory).rglob("*"):
|
apignores = global_apignores + local_ignores if local_ignores else global_apignores
|
||||||
relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:])
|
|
||||||
if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path:
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||||
continue
|
for file in apignores.match_tree_files(world_directory, negate=True):
|
||||||
if not relative_path.endswith("archipelago.json"):
|
zf.write(pathlib.Path(world_directory, file), pathlib.Path(file_name, file))
|
||||||
zf.write(path, relative_path)
|
|
||||||
zf.writestr(apworld.manifest_path, json.dumps(manifest))
|
zf.writestr(apworld.manifest_path, json.dumps(manifest))
|
||||||
open_folder(apworlds_folder)
|
open_folder(apworlds_folder)
|
||||||
|
|
||||||
|
|
||||||
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
||||||
description="Build APWorlds from loose-file world folders."))
|
description="Build APWorlds from loose-file world folders."))
|
||||||
|
|||||||
429
worlds/PokemonStadium/Client.py
Normal file
429
worlds/PokemonStadium/Client.py
Normal 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
worlds/PokemonStadium/Items.py
Normal file
132
worlds/PokemonStadium/Items.py
Normal 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
worlds/PokemonStadium/Locations.py
Normal file
193
worlds/PokemonStadium/Locations.py
Normal 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
worlds/PokemonStadium/Options.py
Normal file
342
worlds/PokemonStadium/Options.py
Normal 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
worlds/PokemonStadium/Regions.py
Normal file
49
worlds/PokemonStadium/Regions.py
Normal 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
worlds/PokemonStadium/Rom.py
Normal file
136
worlds/PokemonStadium/Rom.py
Normal 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
worlds/PokemonStadium/Rules.py
Normal file
132
worlds/PokemonStadium/Rules.py
Normal 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
worlds/PokemonStadium/Types.py
Normal file
18
worlds/PokemonStadium/Types.py
Normal 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
worlds/PokemonStadium/__init__.py
Normal file
148
worlds/PokemonStadium/__init__.py
Normal 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
worlds/PokemonStadium/docs/setup_en.md
Normal file
1
worlds/PokemonStadium/docs/setup_en.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
stop making me do this
|
||||||
674
worlds/PokemonStadium/randomizer/LICENSE
Normal file
674
worlds/PokemonStadium/randomizer/LICENSE
Normal 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>.
|
||||||
18
worlds/PokemonStadium/randomizer/README.md
Normal file
18
worlds/PokemonStadium/randomizer/README.md
Normal file
@@ -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
|
||||||
898
worlds/PokemonStadium/randomizer/constants.py
Normal file
898
worlds/PokemonStadium/randomizer/constants.py
Normal file
@@ -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]
|
||||||
|
]
|
||||||
22
worlds/PokemonStadium/randomizer/levelFunctions.py
Normal file
22
worlds/PokemonStadium/randomizer/levelFunctions.py
Normal file
@@ -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
|
||||||
144
worlds/PokemonStadium/randomizer/randomMovesetGenerator.py
Normal file
144
worlds/PokemonStadium/randomizer/randomMovesetGenerator.py
Normal file
@@ -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]
|
||||||
1542
worlds/PokemonStadium/randomizer/stadium_randomizer.py
Normal file
1542
worlds/PokemonStadium/randomizer/stadium_randomizer.py
Normal file
File diff suppressed because it is too large
Load Diff
25
worlds/PokemonStadium/randomizer/util.py
Normal file
25
worlds/PokemonStadium/randomizer/util.py
Normal 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
|
||||||
23
worlds/PokemonStadium/randomizer/writeDisplayData.py
Normal file
23
worlds/PokemonStadium/randomizer/writeDisplayData.py
Normal file
@@ -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
worlds/Schedule_I/__init__.py
Normal file
11
worlds/Schedule_I/__init__.py
Normal 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
worlds/Schedule_I/archipelago.json
Normal file
1
worlds/Schedule_I/archipelago.json
Normal 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}
|
||||||
1289
worlds/Schedule_I/data/items.json
Normal file
1289
worlds/Schedule_I/data/items.json
Normal file
File diff suppressed because it is too large
Load Diff
1761
worlds/Schedule_I/data/locations.json
Normal file
1761
worlds/Schedule_I/data/locations.json
Normal file
File diff suppressed because it is too large
Load Diff
250
worlds/Schedule_I/data/regions.json
Normal file
250
worlds/Schedule_I/data/regions.json
Normal 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
worlds/Schedule_I/data/victory.json
Normal file
93
worlds/Schedule_I/data/victory.json
Normal 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}}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user