Compare commits

..

10 Commits

Author SHA1 Message Date
Chris Wilson
c562c5cde9 Merge branch 'main' into archipidle 2024-06-07 17:16:14 -04:00
Chris Wilson
fc46eb4329 Merge branch 'main' into archipidle 2024-06-07 17:14:18 -04:00
Chris Wilson
5aa137be52 Fix imports to allow compressing for distribution as a .apworld 2024-04-10 00:21:52 -04:00
Chris Wilson
37fbe9fe8f Merge branch 'main' into archipidle 2024-04-10 00:12:03 -04:00
Chris Wilson
4b65469dbb Adjust item pool to have 40 of both jokes and videos 2024-04-02 00:16:07 -04:00
Chris Wilson
f735776143 Merge branch 'main' into archipidle 2024-04-01 19:33:33 -04:00
Chris Wilson
96b0a604a2 Change item pool to have 50 jokes and 20 motivational videos 2024-03-30 00:23:41 -04:00
Chris Wilson
2f6dfd5d29 Remove unnecessary import 2024-03-28 00:18:54 -04:00
Chris Wilson
5d5a5fd705 Fix a bug in Items.py causing item names to be appended inappropriately 2024-03-27 23:16:20 -04:00
Chris Wilson
3d06b1798a Update item pool to include 25 jokes and videos as progression items, as well as a progression GeroCities profile 2024-03-27 22:58:35 -04:00
53 changed files with 175 additions and 780 deletions

View File

@@ -40,10 +40,6 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python setup.py build_exe --yes python setup.py build_exe --yes
if ( $? -eq $false ) {
Write-Error "setup.py failed!"
exit 1
}
$NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1] $NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1]
$ZIP_NAME="Archipelago_$NAME.7z" $ZIP_NAME="Archipelago_$NAME.7z"
echo "$NAME -> $ZIP_NAME" echo "$NAME -> $ZIP_NAME"
@@ -53,6 +49,12 @@ jobs:
Rename-Item "exe.$NAME" Archipelago Rename-Item "exe.$NAME" Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago 7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name
- name: Store 7z
uses: actions/upload-artifact@v4
with:
name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
retention-days: 7 # keep for 7 days, should be enough
- name: Build Setup - name: Build Setup
run: | run: |
& "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL & "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL
@@ -63,38 +65,11 @@ jobs:
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse $contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
$SETUP_NAME=$contents[0].Name $SETUP_NAME=$contents[0].Name
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
- name: Check build loads expected worlds
shell: bash
run: |
cd build/exe*
mv Players/Templates/meta.yaml .
ls -1 Players/Templates | sort > setup-player-templates.txt
rm -R Players/Templates
timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true
ls -1 Players/Templates | sort > generated-player-templates.txt
cmp setup-player-templates.txt generated-player-templates.txt \
|| diff setup-player-templates.txt generated-player-templates.txt
mv meta.yaml Players/Templates/
- name: Test Generate
shell: bash
run: |
cd build/exe*
cp Players/Templates/Clique.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store 7z
uses: actions/upload-artifact@v4
with:
name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
compression-level: 0 # .7z is incompressible by zip
if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough
- name: Store Setup - name: Store Setup
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.SETUP_NAME }} name: ${{ env.SETUP_NAME }}
path: setups/${{ env.SETUP_NAME }} path: setups/${{ env.SETUP_NAME }}
if-no-files-found: error
retention-days: 7 # keep for 7 days, should be enough retention-days: 7 # keep for 7 days, should be enough
build-ubuntu2004: build-ubuntu2004:
@@ -135,7 +110,7 @@ jobs:
echo -e "setup.py dist output:\n `ls dist`" echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz" export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME") (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - copy code above to release.yml - # - copy code above to release.yml -
@@ -143,36 +118,15 @@ jobs:
run: | run: |
source venv/bin/activate source venv/bin/activate
python setup.py build_exe --yes python setup.py build_exe --yes
- name: Check build loads expected worlds
shell: bash
run: |
cd build/exe*
mv Players/Templates/meta.yaml .
ls -1 Players/Templates | sort > setup-player-templates.txt
rm -R Players/Templates
timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true
ls -1 Players/Templates | sort > generated-player-templates.txt
cmp setup-player-templates.txt generated-player-templates.txt \
|| diff setup-player-templates.txt generated-player-templates.txt
mv meta.yaml Players/Templates/
- name: Test Generate
shell: bash
run: |
cd build/exe*
cp Players/Templates/Clique.yaml Players/
timeout 30 ./ArchipelagoGenerate
- name: Store AppImage - name: Store AppImage
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.APPIMAGE_NAME }} name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }} path: dist/${{ env.APPIMAGE_NAME }}
if-no-files-found: error
retention-days: 7 retention-days: 7
- name: Store .tar.gz - name: Store .tar.gz
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.TAR_NAME }} name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }} path: dist/${{ env.TAR_NAME }}
compression-level: 0 # .gz is incompressible by zip
if-no-files-found: error
retention-days: 7 retention-days: 7

View File

@@ -1,54 +0,0 @@
# Run CMake / CTest C++ unit tests
name: ctest
on:
push:
paths:
- '**.cc?'
- '**.cpp'
- '**.cxx'
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '.github/workflows/ctest.yml'
pull_request:
paths:
- '**.cc?'
- '**.cpp'
- '**.cxx'
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '.github/workflows/ctest.yml'
jobs:
ctest:
runs-on: ${{ matrix.os }}
name: Test C++ ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
if: startsWith(matrix.os,'windows')
- uses: Bacondish2023/setup-googletest@v1
with:
build-type: 'Release'
- name: Build tests
run: |
cd test/cpp
mkdir build
cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release
cmake --build build/ --config Release
ls
- name: Run tests
run: |
cd test/cpp
ctest --test-dir build/ -C Release --output-on-failure

View File

@@ -69,7 +69,7 @@ jobs:
echo -e "setup.py dist output:\n `ls dist`" echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz" export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME") (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - code above copied from build.yml - # - code above copied from build.yml -

1
.gitignore vendored
View File

@@ -178,7 +178,6 @@ dmypy.json
cython_debug/ cython_debug/
# Cython intermediates # Cython intermediates
_speedups.c
_speedups.cpp _speedups.cpp
_speedups.html _speedups.html

View File

@@ -80,7 +80,7 @@ class AdventureContext(CommonContext):
self.local_item_locations = {} self.local_item_locations = {}
self.dragon_speed_info = {} self.dragon_speed_info = {}
options = Utils.get_settings() options = Utils.get_options()
self.display_msgs = options["adventure_options"]["display_msgs"] self.display_msgs = options["adventure_options"]["display_msgs"]
async def server_auth(self, password_requested: bool = False): async def server_auth(self, password_requested: bool = False):
@@ -102,7 +102,7 @@ class AdventureContext(CommonContext):
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd == 'Connected': if cmd == 'Connected':
self.locations_array = None self.locations_array = None
if Utils.get_settings()["adventure_options"].get("death_link", False): if Utils.get_options()["adventure_options"].get("death_link", False):
self.set_deathlink = True self.set_deathlink = True
async_start(self.get_freeincarnates_used()) async_start(self.get_freeincarnates_used())
elif cmd == "RoomInfo": elif cmd == "RoomInfo":
@@ -415,8 +415,8 @@ async def atari_sync_task(ctx: AdventureContext):
async def run_game(romfile): async def run_game(romfile):
auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True) auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
rom_args = Utils.get_settings()["adventure_options"].get("rom_args") rom_args = Utils.get_options()["adventure_options"].get("rom_args")
if auto_start is True: if auto_start is True:
import webbrowser import webbrowser
webbrowser.open(romfile) webbrowser.open(romfile)

View File

@@ -493,11 +493,6 @@ class CommonContext:
"""Gets called before sending a Say to the server from the user. """Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned.""" Returned text is sent, or sending is aborted if None is returned."""
return text return text
def on_ui_command(self, text: str) -> None:
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
The command processor is still called; this is just intended for command echoing."""
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
def update_permissions(self, permissions: typing.Dict[str, int]): def update_permissions(self, permissions: typing.Dict[str, int]):
for permission_name, permission_flag in permissions.items(): for permission_name, permission_flag in permissions.items():

View File

@@ -483,15 +483,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if panic_method == "swap": if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=True, swap=True,
name="Progression", single_player_placement=multiworld.players == 1) on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "raise": elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, swap=False,
name="Progression", single_player_placement=multiworld.players == 1) on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "start_inventory": elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool,
swap=False, allow_partial=True, swap=False, allow_partial=True,
name="Progression", single_player_placement=multiworld.players == 1) on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
if progitempool: if progitempool:
for item in progitempool: for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")

View File

@@ -1,12 +1,10 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import copy
import logging import logging
import os import os
import random import random
import string import string
import sys
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from collections import Counter from collections import Counter
@@ -17,16 +15,21 @@ import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
import copy
import Utils import Utils
import Options import Options
from BaseClasses import seeddigits, get_seed, PlandoOptions from BaseClasses import seeddigits, get_seed, PlandoOptions
from Main import main as ERmain
from settings import get_settings
from Utils import parse_yamls, version_tuple, __version__, tuplize_version from Utils import parse_yamls, version_tuple, __version__, tuplize_version
from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.AutoWorld import AutoWorldRegister
from worlds import failed_world_loads
def mystery_argparse(): def mystery_argparse():
from settings import get_settings options = get_settings()
settings = get_settings() defaults = options.generator
defaults = settings.generator
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.") parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights_file_path', default=defaults.weights_file_path, parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
@@ -38,7 +41,7 @@ def mystery_argparse():
parser.add_argument('--seed', help='Define seed number to generate.', type=int) parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults.players, type=lambda value: max(int(value), 1)) parser.add_argument('--multi', default=defaults.players, type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults.spoiler) parser.add_argument('--spoiler', type=int, default=defaults.spoiler)
parser.add_argument('--outputpath', default=settings.general_options.output_path, parser.add_argument('--outputpath', default=options.general_options.output_path,
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path) parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
@@ -58,23 +61,20 @@ def mystery_argparse():
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 = PlandoOptions.from_option_string(args.plando)
return args return args, options
def get_seed_name(random_source) -> str: def get_seed_name(random_source) -> str:
return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits) return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def main(args=None): def main(args=None, callback=ERmain):
# __name__ == "__main__" check so unittests that already imported worlds don't trip this.
if __name__ == "__main__" and "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded before logging init.")
if not args: if not args:
args = mystery_argparse() args, options = mystery_argparse()
else:
options = get_settings()
seed = get_seed(args.seed) seed = get_seed(args.seed)
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
random.seed(seed) random.seed(seed)
seed_name = get_seed_name(random) seed_name = get_seed_name(random)
@@ -143,9 +143,6 @@ def main(args=None):
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.")
from worlds.AutoWorld import AutoWorldRegister
from worlds.alttp.EntranceRandomizer import parse_arguments
erargs = parse_arguments(['--multi', str(args.multi)]) erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed erargs.seed = seed
erargs.plando_options = args.plando erargs.plando_options = args.plando
@@ -237,8 +234,7 @@ def main(args=None):
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f) yaml.dump(important, f)
from Main import main as ERmain return callback(erargs, seed)
return ERmain(erargs, seed)
def read_weights_yamls(path) -> Tuple[Any, ...]: def read_weights_yamls(path) -> Tuple[Any, ...]:
@@ -363,8 +359,6 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
from worlds import AutoWorldRegister
if not game: if not game:
return get_choice(option_key, category_dict) return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types: if game in AutoWorldRegister.world_types:
@@ -442,13 +436,10 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
except Exception as e: except Exception as e:
raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e raise Options.OptionError(f"Error generating option {option_key} in {ret.game}") from e
else: else:
from worlds import AutoWorldRegister
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options) player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses): def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
from worlds import AutoWorldRegister
if "linked_options" in weights: if "linked_options" in weights:
weights = roll_linked_options(weights) weights = roll_linked_options(weights)
@@ -475,7 +466,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
ret.game = get_choice("game", weights) ret.game = get_choice("game", weights)
if ret.game not in AutoWorldRegister.world_types: if ret.game not in AutoWorldRegister.world_types:
from worlds import failed_world_loads
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
if picks[0] in failed_world_loads: if picks[0] in failed_world_loads:
raise Exception(f"No functional world found to handle game {ret.game}. " raise Exception(f"No functional world found to handle game {ret.game}. "

View File

@@ -198,8 +198,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
"slateblue": "6D8BE8", "slateblue": "6D8BE8",
"plum": "AF99EF", "plum": "AF99EF",
"salmon": "FA8072", "salmon": "FA8072",
"white": "FFFFFF", "white": "FFFFFF"
"orange": "FF7700",
} }
def __init__(self, ctx): def __init__(self, ctx):

View File

@@ -553,7 +553,6 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
f"Archipelago ({__version__}) logging initialized" f"Archipelago ({__version__}) logging initialized"
f" on {platform.platform()}" f" on {platform.platform()}"
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
f"{' (frozen)' if is_frozen() else ''}"
) )

View File

@@ -58,7 +58,6 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
worlds[game] = world worlds[game] = world
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs") base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
shutil.rmtree(base_target_path, ignore_errors=True)
for game, world in worlds.items(): for game, world in worlds.items():
# copy files from world's docs folder to the generated folder # copy files from world's docs folder to the generated folder
target_path = os.path.join(base_target_path, game) target_path = os.path.join(base_target_path, game)

View File

@@ -5,6 +5,7 @@ from uuid import UUID
from flask import Blueprint, abort, url_for from flask import Blueprint, abort, url_for
import worlds.Files import worlds.Files
from .. import cache
from ..models import Room, Seed from ..models import Room, Seed
api_endpoints = Blueprint('api', __name__, url_prefix="/api") api_endpoints = Blueprint('api', __name__, url_prefix="/api")
@@ -48,4 +49,21 @@ def room_info(room: UUID):
} }
from . import generate, user, datapackage # trigger registration @api_endpoints.route('/datapackage')
@cache.cached()
def get_datapackage():
from worlds import network_data_package
return network_data_package
@api_endpoints.route('/datapackage_checksum')
@cache.cached()
def get_datapackage_checksums():
from worlds import network_data_package
version_package = {
game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
}
return version_package
from . import generate, user # trigger registration

View File

@@ -1,32 +0,0 @@
from flask import abort
from Utils import restricted_loads
from WebHostLib import cache
from WebHostLib.models import GameDataPackage
from . import api_endpoints
@api_endpoints.route('/datapackage')
@cache.cached()
def get_datapackage():
from worlds import network_data_package
return network_data_package
@api_endpoints.route('/datapackage/<string:checksum>')
@cache.memoize(timeout=3600)
def get_datapackage_by_checksum(checksum: str):
package = GameDataPackage.get(checksum=checksum)
if package:
return restricted_loads(package.data)
return abort(404)
@api_endpoints.route('/datapackage_checksum')
@cache.cached()
def get_datapackage_checksums():
from worlds import network_data_package
version_package = {
game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
}
return version_package

View File

@@ -18,11 +18,7 @@
<tbody> <tbody>
{% for id, name in option.name_lookup.items() %} {% for id, name in option.name_lookup.items() %}
{% if name != 'random' %} {% if name != 'random' %}
{% if option.default != 'random' %} {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name|lower else None) }}
{{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
{% else %}
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{{ RandomRow(option_name, option) }} {{ RandomRow(option_name, option) }}
@@ -38,7 +34,7 @@
Normal range: {{ option.range_start }} - {{ option.range_end }} Normal range: {{ option.range_start }} - {{ option.range_end }}
{% if option.special_range_names %} {% if option.special_range_names %}
<br /><br /> <br /><br />
The following values have special meanings, and may fall outside the normal range. The following values has special meaning, and may fall outside the normal range.
<ul> <ul>
{% for name, value in option.special_range_names.items() %} {% for name, value in option.special_range_names.items() %}
<li>{{ value }}: {{ name }}</li> <li>{{ value }}: {{ name }}</li>
@@ -47,7 +43,7 @@
{% endif %} {% endif %}
<div class="add-option-div"> <div class="add-option-div">
<input type="number" class="range-option-value" data-option="{{ option_name }}" /> <input type="number" class="range-option-value" data-option="{{ option_name }}" />
<button type="button" class="add-range-option-button" data-option="{{ option_name }}">Add</button> <button class="add-range-option-button" data-option="{{ option_name }}">Add</button>
</div> </div>
</div> </div>
<table class="range-rows" data-option="{{ option_name }}"> <table class="range-rows" data-option="{{ option_name }}">
@@ -72,7 +68,7 @@
This option allows custom values only. Please enter your desired values below. This option allows custom values only. Please enter your desired values below.
<div class="custom-value-wrapper"> <div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" /> <input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" data-option="{{ option_name }}">Add</button> <button data-option="{{ option_name }}">Add</button>
</div> </div>
<table> <table>
<tbody> <tbody>
@@ -89,18 +85,14 @@
Custom values are also allowed for this option. To create one, enter it into the input box below. Custom values are also allowed for this option. To create one, enter it into the input box below.
<div class="custom-value-wrapper"> <div class="custom-value-wrapper">
<input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" /> <input class="custom-value" data-option="{{ option_name }}" placeholder="Custom Value" />
<button type="button" data-option="{{ option_name }}">Add</button> <button data-option="{{ option_name }}">Add</button>
</div> </div>
</div> </div>
<table> <table>
<tbody> <tbody>
{% for id, name in option.name_lookup.items() %} {% for id, name in option.name_lookup.items() %}
{% if name != 'random' %} {% if name != 'random' %}
{% if option.default != 'random' %} {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
{{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
{% else %}
{{ RangeRow(option_name, option, option.get_option_name(id), name) }}
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{{ RandomRow(option_name, option) }} {{ RandomRow(option_name, option) }}

View File

@@ -1,6 +1,5 @@
#cython: language_level=3 #cython: language_level=3
#distutils: language = c #distutils: language = c++
#distutils: depends = intset.h
""" """
Provides faster implementation of some core parts. Provides faster implementation of some core parts.
@@ -14,6 +13,7 @@ from cpython cimport PyObject
from typing import Any, Dict, Iterable, Iterator, Generator, Sequence, Tuple, TypeVar, Union, Set, List, TYPE_CHECKING from typing import Any, Dict, Iterable, Iterator, Generator, Sequence, Tuple, TypeVar, Union, Set, List, TYPE_CHECKING
from cymem.cymem cimport Pool from cymem.cymem cimport Pool
from libc.stdint cimport int64_t, uint32_t from libc.stdint cimport int64_t, uint32_t
from libcpp.set cimport set as std_set
from collections import defaultdict from collections import defaultdict
cdef extern from *: cdef extern from *:
@@ -31,27 +31,6 @@ ctypedef int64_t ap_id_t
cdef ap_player_t MAX_PLAYER_ID = 1000000 # limit the size of indexing array cdef ap_player_t MAX_PLAYER_ID = 1000000 # limit the size of indexing array
cdef size_t INVALID_SIZE = <size_t>(-1) # this is all 0xff... adding 1 results in 0, but it's not negative cdef size_t INVALID_SIZE = <size_t>(-1) # this is all 0xff... adding 1 results in 0, but it's not negative
# configure INTSET for player
cdef extern from *:
"""
#define INTSET_NAME ap_player_set
#define INTSET_TYPE uint32_t // has to match ap_player_t
"""
# create INTSET for player
cdef extern from "intset.h":
"""
#undef INTSET_NAME
#undef INTSET_TYPE
"""
ctypedef struct ap_player_set:
pass
ap_player_set* ap_player_set_new(size_t bucket_count) nogil
void ap_player_set_free(ap_player_set* set) nogil
bint ap_player_set_add(ap_player_set* set, ap_player_t val) nogil
bint ap_player_set_contains(ap_player_set* set, ap_player_t val) nogil
cdef struct LocationEntry: cdef struct LocationEntry:
# layout is so that # layout is so that
@@ -206,7 +185,7 @@ cdef class LocationStore:
def find_item(self, slots: Set[int], seeked_item_id: int) -> Generator[Tuple[int, int, int, int, int], None, None]: def find_item(self, slots: Set[int], seeked_item_id: int) -> Generator[Tuple[int, int, int, int, int], None, None]:
cdef ap_id_t item = seeked_item_id cdef ap_id_t item = seeked_item_id
cdef ap_player_t receiver cdef ap_player_t receiver
cdef ap_player_set* receivers cdef std_set[ap_player_t] receivers
cdef size_t slot_count = len(slots) cdef size_t slot_count = len(slots)
if slot_count == 1: if slot_count == 1:
# specialized implementation for single slot # specialized implementation for single slot
@@ -218,20 +197,13 @@ cdef class LocationStore:
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
elif slot_count: elif slot_count:
# generic implementation with lookup in set # generic implementation with lookup in set
receivers = ap_player_set_new(min(1023, slot_count)) # limit top level struct to 16KB for receiver in slots:
if not receivers: receivers.insert(receiver)
raise MemoryError() with nogil:
try: for entry in self.entries[:self.entry_count]:
for receiver in slots: if entry.item == item and receivers.count(entry.receiver):
if not ap_player_set_add(receivers, receiver): with gil:
raise MemoryError() yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
with nogil:
for entry in self.entries[:self.entry_count]:
if entry.item == item and ap_player_set_contains(receivers, entry.receiver):
with gil:
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
finally:
ap_player_set_free(receivers)
def get_for_player(self, slot: int) -> Dict[int, Set[int]]: def get_for_player(self, slot: int) -> Dict[int, Set[int]]:
cdef ap_player_t receiver = slot cdef ap_player_t receiver = slot

View File

@@ -1,10 +1,8 @@
# This file is used when doing pyximport # This file is required to get pyximport to work with C++.
import os # Switching from std::set to a pure C implementation is still on the table to simplify everything.
def make_ext(modname, pyxfilename): def make_ext(modname, pyxfilename):
from distutils.extension import Extension from distutils.extension import Extension
return Extension(name=modname, return Extension(name=modname,
sources=[pyxfilename], sources=[pyxfilename],
depends=["intset.h"], language='c++')
include_dirs=[os.getcwd()],
language="c")

View File

@@ -13,7 +13,6 @@
plum: "AF99EF" # typically progression item plum: "AF99EF" # typically progression item
salmon: "FA8072" # typically trap item salmon: "FA8072" # typically trap item
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
orange: "FF7700" # Used for command echo
<Label>: <Label>:
color: "FFFFFF" color: "FFFFFF"
<TabbedPanel>: <TabbedPanel>:

View File

@@ -70,7 +70,7 @@
/worlds/heretic/ @Daivuk /worlds/heretic/ @Daivuk
# Hollow Knight # Hollow Knight
/worlds/hk/ @BadMagic100 @qwint /worlds/hk/ @BadMagic100 @ThePhar
# Hylics 2 # Hylics 2
/worlds/hylics2/ @TRPG0 /worlds/hylics2/ @TRPG0

View File

@@ -89,9 +89,6 @@ Type: files; Name: "{app}\ArchipelagoPokemonClient.exe"
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy" Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy"
Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy" Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy"
Type: files; Name: "{app}\lib\worlds\sc2wol.apworld"
Type: filesandordirs; Name: "{app}\lib\worlds\sc2wol"
Type: dirifempty; Name: "{app}\lib\worlds\sc2wol"
Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku" Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku"
Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku" Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku"
Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe" Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe"

135
intset.h
View File

@@ -1,135 +0,0 @@
/* A specialized unordered_set implementation for literals, where bucket_count
* is defined at initialization rather than increased automatically.
*/
#include <stddef.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#ifndef INTSET_NAME
#error "Please #define INTSET_NAME ... before including intset.h"
#endif
#ifndef INTSET_TYPE
#error "Please #define INTSET_TYPE ... before including intset.h"
#endif
/* macros to generate unique names from INTSET_NAME */
#ifndef INTSET_CONCAT
#define INTSET_CONCAT_(a, b) a ## b
#define INTSET_CONCAT(a, b) INTSET_CONCAT_(a, b)
#define INTSET_FUNC_(a, b) INTSET_CONCAT(a, _ ## b)
#endif
#define INTSET_FUNC(name) INTSET_FUNC_(INTSET_NAME, name)
#define INTSET_BUCKET INTSET_CONCAT(INTSET_NAME, Bucket)
#define INTSET_UNION INTSET_CONCAT(INTSET_NAME, Union)
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable : 4200)
#endif
typedef struct {
size_t count;
union INTSET_UNION {
INTSET_TYPE val;
INTSET_TYPE *data;
} v;
} INTSET_BUCKET;
typedef struct {
size_t bucket_count;
INTSET_BUCKET buckets[];
} INTSET_NAME;
static INTSET_NAME *INTSET_FUNC(new)(size_t buckets)
{
size_t i, size;
INTSET_NAME *set;
if (buckets < 1)
buckets = 1;
if ((SIZE_MAX - sizeof(INTSET_NAME)) / sizeof(INTSET_BUCKET) < buckets)
return NULL;
size = sizeof(INTSET_NAME) + buckets * sizeof(INTSET_BUCKET);
set = (INTSET_NAME*)malloc(size);
if (!set)
return NULL;
memset(set, 0, size); /* gcc -fanalyzer does not understand this sets all buckets' count to 0 */
for (i = 0; i < buckets; i++) {
set->buckets[i].count = 0;
}
set->bucket_count = buckets;
return set;
}
static void INTSET_FUNC(free)(INTSET_NAME *set)
{
size_t i;
if (!set)
return;
for (i = 0; i < set->bucket_count; i++) {
if (set->buckets[i].count > 1)
free(set->buckets[i].v.data);
}
free(set);
}
static bool INTSET_FUNC(contains)(INTSET_NAME *set, INTSET_TYPE val)
{
size_t i;
INTSET_BUCKET* bucket = &set->buckets[(size_t)val % set->bucket_count];
if (bucket->count == 1)
return bucket->v.val == val;
for (i = 0; i < bucket->count; ++i) {
if (bucket->v.data[i] == val)
return true;
}
return false;
}
static bool INTSET_FUNC(add)(INTSET_NAME *set, INTSET_TYPE val)
{
INTSET_BUCKET* bucket;
if (INTSET_FUNC(contains)(set, val))
return true; /* ok */
bucket = &set->buckets[(size_t)val % set->bucket_count];
if (bucket->count == 0) {
bucket->v.val = val;
bucket->count = 1;
} else if (bucket->count == 1) {
INTSET_TYPE old = bucket->v.val;
bucket->v.data = (INTSET_TYPE*)malloc(2 * sizeof(INTSET_TYPE));
if (!bucket->v.data) {
bucket->v.val = old;
return false; /* error */
}
bucket->v.data[0] = old;
bucket->v.data[1] = val;
bucket->count = 2;
} else {
size_t new_bucket_size;
INTSET_TYPE* new_bucket_data;
new_bucket_size = (bucket->count + 1) * sizeof(INTSET_TYPE);
new_bucket_data = (INTSET_TYPE*)realloc(bucket->v.data, new_bucket_size);
if (!new_bucket_data)
return false; /* error */
bucket->v.data = new_bucket_data;
bucket->v.data[bucket->count++] = val;
}
return true; /* success */
}
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
#undef INTSET_FUNC
#undef INTSET_BUCKET
#undef INTSET_UNION

62
kvui.py
View File

@@ -3,7 +3,6 @@ import logging
import sys import sys
import typing import typing
import re import re
from collections import deque
if sys.platform == "win32": if sys.platform == "win32":
import ctypes import ctypes
@@ -381,57 +380,6 @@ class ConnectBarTextInput(TextInput):
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo) return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
def is_command_input(string: str) -> bool:
return len(string) > 0 and string[0] in "/!"
class CommandPromptTextInput(TextInput):
MAXIMUM_HISTORY_MESSAGES = 50
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._command_history_index = -1
self._command_history: typing.Deque[str] = deque(maxlen=CommandPromptTextInput.MAXIMUM_HISTORY_MESSAGES)
def update_history(self, new_entry: str) -> None:
self._command_history_index = -1
if is_command_input(new_entry):
self._command_history.appendleft(new_entry)
def keyboard_on_key_down(
self,
window,
keycode: typing.Tuple[int, str],
text: typing.Optional[str],
modifiers: typing.List[str]
) -> bool:
"""
:param window: The kivy window object
:param keycode: A tuple of (keycode, keyname). Keynames are always lowercase
:param text: The text printed by this key, not accounting for modifiers, or `None` if no text.
Seems to pretty naively interpret the keycode as unicode, so numlock can return odd characters.
:param modifiers: A list of string modifiers, like `ctrl` or `numlock`
"""
if keycode[1] == 'up':
self._change_to_history_text_if_available(self._command_history_index + 1)
return True
if keycode[1] == 'down':
self._change_to_history_text_if_available(self._command_history_index - 1)
return True
return super().keyboard_on_key_down(window, keycode, text, modifiers)
def _change_to_history_text_if_available(self, new_index: int) -> None:
if new_index < -1:
return
if new_index >= len(self._command_history):
return
self._command_history_index = new_index
if new_index == -1:
self.text = ""
return
self.text = self._command_history[self._command_history_index]
class MessageBox(Popup): class MessageBox(Popup):
class MessageBoxLabel(Label): class MessageBoxLabel(Label):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -467,7 +415,7 @@ class GameManager(App):
self.commandprocessor = ctx.command_processor(ctx) self.commandprocessor = ctx.command_processor(ctx)
self.icon = r"data/icon.png" self.icon = r"data/icon.png"
self.json_to_kivy_parser = KivyJSONtoTextParser(ctx) self.json_to_kivy_parser = KivyJSONtoTextParser(ctx)
self.log_panels: typing.Dict[str, Widget] = {} self.log_panels = {}
# keep track of last used command to autofill on click # keep track of last used command to autofill on click
self.last_autofillable_command = "hint" self.last_autofillable_command = "hint"
@@ -551,7 +499,7 @@ class GameManager(App):
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None) info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
info_button.bind(on_release=self.command_button_action) info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button) bottom_layout.add_widget(info_button)
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False) self.textinput = TextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message) self.textinput.bind(on_text_validate=self.on_message)
self.textinput.text_validate_unfocus = False self.textinput.text_validate_unfocus = False
bottom_layout.add_widget(self.textinput) bottom_layout.add_widget(self.textinput)
@@ -609,18 +557,14 @@ class GameManager(App):
self.ctx.exit_event.set() self.ctx.exit_event.set()
def on_message(self, textinput: CommandPromptTextInput): def on_message(self, textinput: TextInput):
try: try:
input_text = textinput.text.strip() input_text = textinput.text.strip()
textinput.text = "" textinput.text = ""
textinput.update_history(input_text)
if self.ctx.input_requests > 0: if self.ctx.input_requests > 0:
self.ctx.input_requests -= 1 self.ctx.input_requests -= 1
self.ctx.input_queue.put_nowait(input_text) self.ctx.input_queue.put_nowait(input_text)
elif is_command_input(input_text):
self.ctx.on_ui_command(input_text)
self.commandprocessor(input_text)
elif input_text: elif input_text:
self.commandprocessor(input_text) self.commandprocessor(input_text)

View File

@@ -1,49 +0,0 @@
cmake_minimum_required(VERSION 3.5)
project(ap-cpp-tests)
enable_testing()
find_package(GTest REQUIRED)
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_definitions("/source-charset:utf-8")
set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# enable static analysis for gcc
add_compile_options(-fanalyzer -Werror)
# disable stuff that gets triggered by googletest
add_compile_options(-Wno-analyzer-malloc-leak)
# enable asan for gcc
add_compile_options(-fsanitize=address)
add_link_options(-fsanitize=address)
endif ()
add_executable(test_default)
target_include_directories(test_default
PRIVATE
${GTEST_INCLUDE_DIRS}
)
target_link_libraries(test_default
${GTEST_BOTH_LIBRARIES}
)
add_test(
NAME test_default
COMMAND test_default
)
set_property(
TEST test_default
PROPERTY ENVIRONMENT "ASAN_OPTIONS=allocator_may_return_null=1"
)
file(GLOB ITEMS *)
foreach(item ${ITEMS})
if(IS_DIRECTORY ${item} AND EXISTS ${item}/CMakeLists.txt)
message(${item})
add_subdirectory(${item})
endif()
endforeach()

View File

@@ -1,32 +0,0 @@
# C++ tests
Test framework for C and C++ code in AP.
## Adding a Test
### GoogleTest
Adding GoogleTests is as simple as creating a directory with
* one or more `test_*.cpp` files that define tests using
[GoogleTest API](https://google.github.io/googletest/)
* a `CMakeLists.txt` that adds the .cpp files to `test_default` target using
[target_sources](https://cmake.org/cmake/help/latest/command/target_sources.html)
### CTest
If either GoogleTest is not suitable for the test or the build flags / sources / libraries are incompatible,
you can add another CTest to the project using add_target and add_test, similar to how it's done for `test_default`.
## Running Tests
* Install [CMake](https://cmake.org/).
* Build and/or install GoogleTest and make sure
[CMake can find it](https://cmake.org/cmake/help/latest/module/FindGTest.html), or
[create a parent `CMakeLists.txt` that fetches GoogleTest](https://google.github.io/googletest/quickstart-cmake.html).
* Enter the directory with the top-most `CMakeLists.txt` and run
```sh
mkdir build
cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release
cmake --build build/ --config Release && \
ctest --test-dir build/ -C Release --output-on-failure
```

View File

@@ -1,4 +0,0 @@
target_sources(test_default
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/test_intset.cpp
)

View File

@@ -1,105 +0,0 @@
#include <limits>
#include <cstdint>
#include <gtest/gtest.h>
// uint32Set
#define INTSET_NAME uint32Set
#define INTSET_TYPE uint32_t
#include "../../../intset.h"
#undef INTSET_NAME
#undef INTSET_TYPE
// int64Set
#define INTSET_NAME int64Set
#define INTSET_TYPE int64_t
#include "../../../intset.h"
TEST(IntsetTest, ZeroBuckets)
{
// trying to allocate with zero buckets has to either fail or be functioning
uint32Set *set = uint32Set_new(0);
if (!set)
return; // failed -> OK
EXPECT_FALSE(uint32Set_contains(set, 1));
EXPECT_TRUE(uint32Set_add(set, 1));
EXPECT_TRUE(uint32Set_contains(set, 1));
uint32Set_free(set);
}
TEST(IntsetTest, Duplicate)
{
// adding the same number again can't fail
uint32Set *set = uint32Set_new(2);
ASSERT_TRUE(set);
EXPECT_TRUE(uint32Set_add(set, 0));
EXPECT_TRUE(uint32Set_add(set, 0));
EXPECT_TRUE(uint32Set_contains(set, 0));
uint32Set_free(set);
}
TEST(IntsetTest, SetAllocFailure)
{
// try to allocate 100TB of RAM, should fail and return NULL
if (sizeof(size_t) < 8)
GTEST_SKIP() << "Alloc error not testable on 32bit";
int64Set *set = int64Set_new(6250000000000ULL);
EXPECT_FALSE(set);
int64Set_free(set);
}
TEST(IntsetTest, SetAllocOverflow)
{
// try to overflow argument passed to malloc
int64Set *set = int64Set_new(std::numeric_limits<size_t>::max());
EXPECT_FALSE(set);
int64Set_free(set);
}
TEST(IntsetTest, NullFree)
{
// free(NULL) should not try to free buckets
uint32Set_free(NULL);
int64Set_free(NULL);
}
TEST(IntsetTest, BucketRealloc)
{
// add a couple of values to the same bucket to test growing the bucket
uint32Set* set = uint32Set_new(1);
ASSERT_TRUE(set);
EXPECT_FALSE(uint32Set_contains(set, 0));
EXPECT_TRUE(uint32Set_add(set, 0));
EXPECT_TRUE(uint32Set_contains(set, 0));
for (uint32_t i = 1; i < 32; ++i) {
EXPECT_TRUE(uint32Set_add(set, i));
EXPECT_TRUE(uint32Set_contains(set, i - 1));
EXPECT_TRUE(uint32Set_contains(set, i));
EXPECT_FALSE(uint32Set_contains(set, i + 1));
}
uint32Set_free(set);
}
TEST(IntSet, Max)
{
constexpr auto n = std::numeric_limits<uint32_t>::max();
uint32Set *set = uint32Set_new(1);
ASSERT_TRUE(set);
EXPECT_FALSE(uint32Set_contains(set, n));
EXPECT_TRUE(uint32Set_add(set, n));
EXPECT_TRUE(uint32Set_contains(set, n));
uint32Set_free(set);
}
TEST(InsetTest, Negative)
{
constexpr auto n = std::numeric_limits<int64_t>::min();
static_assert(n < 0, "n not negative");
int64Set *set = int64Set_new(3);
ASSERT_TRUE(set);
EXPECT_FALSE(int64Set_contains(set, n));
EXPECT_TRUE(int64Set_add(set, n));
EXPECT_TRUE(int64Set_contains(set, n));
int64Set_free(set);
}

View File

@@ -2,7 +2,6 @@ from argparse import Namespace
from typing import List, Optional, Tuple, Type, Union from typing import List, Optional, Tuple, Type, Union
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
from worlds import network_data_package
from worlds.AutoWorld import World, call_all from worlds.AutoWorld import World, call_all
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
@@ -61,10 +60,6 @@ class TestWorld(World):
hidden = True hidden = True
# add our test world to the data package, so we can test it later
network_data_package["games"][TestWorld.game] = TestWorld.get_data_package_data()
def generate_test_multiworld(players: int = 1) -> MultiWorld: def generate_test_multiworld(players: int = 1) -> MultiWorld:
""" """
Generates a multiworld using a special Test Case World class, and seed of 0. Generates a multiworld using a special Test Case World class, and seed of 0.

View File

@@ -1,7 +1,6 @@
import unittest import unittest
from Fill import distribute_items_restrictive from Fill import distribute_items_restrictive
from worlds import network_data_package
from worlds.AutoWorld import AutoWorldRegister, call_all from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld from . import setup_solo_multiworld
@@ -85,4 +84,3 @@ class TestIDs(unittest.TestCase):
f"{loc_name} is not a valid item name for location_name_to_id") f"{loc_name} is not a valid item name for location_name_to_id")
self.assertIsInstance(loc_id, int, self.assertIsInstance(loc_id, int,
f"{loc_id} for {loc_name} should be an int") f"{loc_id} for {loc_name} should be an int")
self.assertEqual(datapackage["checksum"], network_data_package["games"][gamename]["checksum"])

View File

@@ -66,19 +66,12 @@ def create_room(app_client: "FlaskClient", seed: str, auto_start: bool = False)
def start_room(app_client: "FlaskClient", room_id: str, timeout: float = 30) -> str: def start_room(app_client: "FlaskClient", room_id: str, timeout: float = 30) -> str:
from time import sleep from time import sleep
import pony.orm
poll_interval = .2 poll_interval = .2
print(f"Starting room {room_id}") print(f"Starting room {room_id}")
no_timeout = timeout <= 0 no_timeout = timeout <= 0
while no_timeout or timeout > 0: while no_timeout or timeout > 0:
try: response = app_client.get(f"/room/{room_id}")
response = app_client.get(f"/room/{room_id}")
except pony.orm.core.OptimisticCheckError:
# hoster wrote to room during our transaction
continue
assert response.status_code == 200, f"Starting room for {room_id} failed: status {response.status_code}" assert response.status_code == 200, f"Starting room for {room_id} failed: status {response.status_code}"
match = re.search(r"/connect ([\w:.\-]+)", response.text) match = re.search(r"/connect ([\w:.\-]+)", response.text)
if match: if match:

View File

@@ -1,5 +1,4 @@
# Tests for _speedups.LocationStore and NetUtils._LocationStore # Tests for _speedups.LocationStore and NetUtils._LocationStore
import os
import typing import typing
import unittest import unittest
import warnings import warnings
@@ -8,8 +7,6 @@ from NetUtils import LocationStore, _LocationStore
State = typing.Dict[typing.Tuple[int, int], typing.Set[int]] State = typing.Dict[typing.Tuple[int, int], typing.Set[int]]
RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
ci = bool(os.environ.get("CI")) # always set in GitHub actions
sample_data: RawLocations = { sample_data: RawLocations = {
1: { 1: {
11: (21, 2, 7), 11: (21, 2, 7),
@@ -27,9 +24,6 @@ sample_data: RawLocations = {
3: { 3: {
9: (99, 4, 0), 9: (99, 4, 0),
}, },
5: {
9: (99, 5, 0),
}
} }
empty_state: State = { empty_state: State = {
@@ -51,14 +45,14 @@ class Base:
store: typing.Union[LocationStore, _LocationStore] store: typing.Union[LocationStore, _LocationStore]
def test_len(self) -> None: def test_len(self) -> None:
self.assertEqual(len(self.store), 5) self.assertEqual(len(self.store), 4)
self.assertEqual(len(self.store[1]), 3) self.assertEqual(len(self.store[1]), 3)
def test_key_error(self) -> None: def test_key_error(self) -> None:
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
_ = self.store[0] _ = self.store[0]
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
_ = self.store[6] _ = self.store[5]
locations = self.store[1] # no Exception locations = self.store[1] # no Exception
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
_ = locations[7] _ = locations[7]
@@ -77,7 +71,7 @@ class Base:
self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None)) self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None))
def test_iter(self) -> None: def test_iter(self) -> None:
self.assertEqual(sorted(self.store), [1, 2, 3, 4, 5]) self.assertEqual(sorted(self.store), [1, 2, 3, 4])
self.assertEqual(len(self.store), len(sample_data)) self.assertEqual(len(self.store), len(sample_data))
self.assertEqual(list(self.store[1]), [11, 12, 13]) self.assertEqual(list(self.store[1]), [11, 12, 13])
self.assertEqual(len(self.store[1]), len(sample_data[1])) self.assertEqual(len(self.store[1]), len(sample_data[1]))
@@ -91,26 +85,13 @@ class Base:
self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11]) self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11])
def test_find_item(self) -> None: def test_find_item(self) -> None:
# empty player set
self.assertEqual(sorted(self.store.find_item(set(), 99)), []) self.assertEqual(sorted(self.store.find_item(set(), 99)), [])
# no such player, single
self.assertEqual(sorted(self.store.find_item({6}, 99)), [])
# no such player, set
self.assertEqual(sorted(self.store.find_item({7, 8, 9}, 99)), [])
# no such item
self.assertEqual(sorted(self.store.find_item({3}, 1)), []) self.assertEqual(sorted(self.store.find_item({3}, 1)), [])
# valid matches self.assertEqual(sorted(self.store.find_item({5}, 99)), [])
self.assertEqual(sorted(self.store.find_item({3}, 99)), self.assertEqual(sorted(self.store.find_item({3}, 99)),
[(4, 9, 99, 3, 0)]) [(4, 9, 99, 3, 0)])
self.assertEqual(sorted(self.store.find_item({3, 4}, 99)), self.assertEqual(sorted(self.store.find_item({3, 4}, 99)),
[(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)]) [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
self.assertEqual(sorted(self.store.find_item({2, 3, 4}, 99)),
[(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
# test hash collision in set
self.assertEqual(sorted(self.store.find_item({3, 5}, 99)),
[(4, 9, 99, 3, 0), (5, 9, 99, 5, 0)])
self.assertEqual(sorted(self.store.find_item(set(range(2048)), 13)),
[(1, 13, 13, 1, 0)])
def test_get_for_player(self) -> None: def test_get_for_player(self) -> None:
self.assertEqual(self.store.get_for_player(3), {4: {9}}) self.assertEqual(self.store.get_for_player(3), {4: {9}})
@@ -215,20 +196,18 @@ class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
super().setUp() super().setUp()
@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available") @unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStore(Base.TestLocationStore): class TestSpeedupsLocationStore(Base.TestLocationStore):
"""Run base method tests for cython implementation.""" """Run base method tests for cython implementation."""
def setUp(self) -> None: def setUp(self) -> None:
self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups")
self.store = LocationStore(sample_data) self.store = LocationStore(sample_data)
super().setUp() super().setUp()
@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available") @unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor): class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor):
"""Run base constructor tests and tests the additional constraints for cython implementation.""" """Run base constructor tests and tests the additional constraints for cython implementation."""
def setUp(self) -> None: def setUp(self) -> None:
self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups")
self.type = LocationStore self.type = LocationStore
super().setUp() super().setUp()

View File

@@ -123,8 +123,8 @@ class WebWorldRegister(type):
assert group.options, "A custom defined Option Group must contain at least one Option." assert group.options, "A custom defined Option Group must contain at least one Option."
# catch incorrectly titled versions of the prebuilt groups so they don't create extra groups # catch incorrectly titled versions of the prebuilt groups so they don't create extra groups
title_name = group.name.title() title_name = group.name.title()
assert title_name not in prebuilt_options or title_name == group.name, \ if title_name in prebuilt_options:
f"Prebuilt group name \"{group.name}\" must be \"{title_name}\"" group.name = title_name
if group.name == "Item & Location Options": if group.name == "Item & Location Options":
assert not any(option in item_and_loc_options for option in group.options), \ assert not any(option in item_and_loc_options for option in group.options), \

View File

@@ -1,5 +1,4 @@
import importlib import importlib
import importlib.util
import logging import logging
import os import os
import sys import sys
@@ -108,9 +107,8 @@ for folder in (folder for folder in (user_folder, local_folder) if folder):
if not entry.name.startswith(("_", ".")): if not entry.name.startswith(("_", ".")):
file_name = entry.name if relative else os.path.join(folder, entry.name) file_name = entry.name if relative else os.path.join(folder, entry.name)
if entry.is_dir(): if entry.is_dir():
if os.path.isfile(os.path.join(entry.path, '__init__.py')): init_file_path = os.path.join(entry.path, '__init__.py')
world_sources.append(WorldSource(file_name, relative=relative)) if os.path.isfile(init_file_path):
elif os.path.isfile(os.path.join(entry.path, '__init__.pyc')):
world_sources.append(WorldSource(file_name, relative=relative)) world_sources.append(WorldSource(file_name, relative=relative))
else: else:
logging.warning(f"excluding {entry.name} from world sources because it has no __init__.py") logging.warning(f"excluding {entry.name} from world sources because it has no __init__.py")

View File

@@ -28,11 +28,6 @@ class kill_switch:
logger.debug("kill_switch: Add switch") logger.debug("kill_switch: Add switch")
cls._to_kill.append(value) cls._to_kill.append(value)
@classmethod
def kill(cls, value):
logger.info(f"kill_switch: Process cleanup for 1 process")
value._clean(verbose=False)
@classmethod @classmethod
def kill_all(cls): def kill_all(cls):
logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes") logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes")
@@ -121,7 +116,7 @@ class SC2Process:
async def __aexit__(self, *args): async def __aexit__(self, *args):
logger.exception("async exit") logger.exception("async exit")
await self._close_connection() await self._close_connection()
kill_switch.kill(self) kill_switch.kill_all()
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
@property @property

View File

@@ -2,8 +2,7 @@ from __future__ import annotations
from typing import Dict from typing import Dict
from dataclasses import dataclass from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
class FreeincarnateMax(Range): class FreeincarnateMax(Range):
@@ -224,22 +223,22 @@ class StartCastle(Choice):
option_white = 2 option_white = 2
default = option_yellow default = option_yellow
@dataclass
class AdventureOptions(PerGameCommonOptions):
dragon_slay_check: DragonSlayCheck
death_link: DeathLink
bat_logic: BatLogic
freeincarnate_max: FreeincarnateMax
dragon_rando_type: DragonRandoType
connector_multi_slot: ConnectorMultiSlot
yorgle_speed: YorgleStartingSpeed
yorgle_min_speed: YorgleMinimumSpeed
grundle_speed: GrundleStartingSpeed
grundle_min_speed: GrundleMinimumSpeed
rhindle_speed: RhindleStartingSpeed
rhindle_min_speed: RhindleMinimumSpeed
difficulty_switch_a: DifficultySwitchA
difficulty_switch_b: DifficultySwitchB
start_castle: StartCastle
adventure_option_definitions: Dict[str, type(Option)] = {
"dragon_slay_check": DragonSlayCheck,
"death_link": DeathLink,
"bat_logic": BatLogic,
"freeincarnate_max": FreeincarnateMax,
"dragon_rando_type": DragonRandoType,
"connector_multi_slot": ConnectorMultiSlot,
"yorgle_speed": YorgleStartingSpeed,
"yorgle_min_speed": YorgleMinimumSpeed,
"grundle_speed": GrundleStartingSpeed,
"grundle_min_speed": GrundleMinimumSpeed,
"rhindle_speed": RhindleStartingSpeed,
"rhindle_min_speed": RhindleMinimumSpeed,
"difficulty_switch_a": DifficultySwitchA,
"difficulty_switch_b": DifficultySwitchB,
"start_castle": StartCastle,
}

View File

@@ -1,5 +1,4 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
from Options import PerGameCommonOptions
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
@@ -25,7 +24,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
connect(world, player, target, source, rule, True) connect(world, player, target, source, rule, True)
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None: def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
menu = Region("Menu", player, multiworld) menu = Region("Menu", player, multiworld)
@@ -75,7 +74,7 @@ def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side)) credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
multiworld.regions.append(credits_room_far_side) multiworld.regions.append(credits_room_far_side)
dragon_slay_check = options.dragon_slay_check.value dragon_slay_check = multiworld.dragon_slay_check[player].value
priority_locations = determine_priority_locations(multiworld, dragon_slay_check) priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
for name, location_data in location_table.items(): for name, location_data in location_table.items():

View File

@@ -6,7 +6,7 @@ from BaseClasses import LocationProgressType
def set_rules(self) -> None: def set_rules(self) -> None:
world = self.multiworld world = self.multiworld
use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
set_rule(world.get_entrance("YellowCastlePort", self.player), set_rule(world.get_entrance("YellowCastlePort", self.player),
lambda state: state.has("Yellow Key", self.player)) lambda state: state.has("Yellow Key", self.player))
@@ -28,7 +28,7 @@ def set_rules(self) -> None:
lambda state: state.has("Bridge", self.player) or lambda state: state.has("Bridge", self.player) or
state.has("Magnet", self.player)) state.has("Magnet", self.player))
dragon_slay_check = self.options.dragon_slay_check.value dragon_slay_check = world.dragon_slay_check[self.player].value
if dragon_slay_check: if dragon_slay_check:
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
set_rule(world.get_location("Slay Yorgle", self.player), set_rule(world.get_location("Slay Yorgle", self.player),

View File

@@ -15,8 +15,7 @@ from Options import AssembleOptions
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from Fill import fill_restrictive from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule from worlds.generic.Rules import add_rule, set_rule
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \ from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
AdventureOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \ from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation AdventureAutoCollectLocation
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
@@ -110,7 +109,7 @@ class AdventureWorld(World):
game: ClassVar[str] = "Adventure" game: ClassVar[str] = "Adventure"
web: ClassVar[WebWorld] = AdventureWeb() web: ClassVar[WebWorld] = AdventureWeb()
options_dataclass = AdventureOptions option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
settings: ClassVar[AdventureSettings] settings: ClassVar[AdventureSettings]
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
@@ -150,18 +149,18 @@ class AdventureWorld(World):
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
self.rom_name.extend([0] * (21 - len(self.rom_name))) self.rom_name.extend([0] * (21 - len(self.rom_name)))
self.dragon_rando_type = self.options.dragon_rando_type.value self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
self.dragon_slay_check = self.options.dragon_slay_check.value self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
self.connector_multi_slot = self.options.connector_multi_slot.value self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
self.yorgle_speed = self.options.yorgle_speed.value self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
self.yorgle_min_speed = self.options.yorgle_min_speed.value self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
self.grundle_speed = self.options.grundle_speed.value self.grundle_speed = self.multiworld.grundle_speed[self.player].value
self.grundle_min_speed = self.options.grundle_min_speed.value self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
self.rhindle_speed = self.options.rhindle_speed.value self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
self.rhindle_min_speed = self.options.rhindle_min_speed.value self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
self.difficulty_switch_a = self.options.difficulty_switch_a.value self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
self.difficulty_switch_b = self.options.difficulty_switch_b.value self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
self.start_castle = self.options.start_castle.value self.start_castle = self.multiworld.start_castle[self.player].value
self.created_items = 0 self.created_items = 0
if self.dragon_slay_check == 0: if self.dragon_slay_check == 0:
@@ -228,7 +227,7 @@ class AdventureWorld(World):
extra_filler_count = num_locations - self.created_items extra_filler_count = num_locations - self.created_items
# traps would probably go here, if enabled # traps would probably go here, if enabled
freeincarnate_max = self.options.freeincarnate_max.value freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
actual_freeincarnates = min(extra_filler_count, freeincarnate_max) actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)] self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
self.created_items += actual_freeincarnates self.created_items += actual_freeincarnates
@@ -248,7 +247,7 @@ class AdventureWorld(World):
self.created_items += 1 self.created_items += 1
def create_regions(self) -> None: def create_regions(self) -> None:
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms) create_regions(self.multiworld, self.player, self.dragon_rooms)
set_rules = set_rules set_rules = set_rules
@@ -355,7 +354,7 @@ class AdventureWorld(World):
auto_collect_locations: [AdventureAutoCollectLocation] = [] auto_collect_locations: [AdventureAutoCollectLocation] = []
local_item_to_location: {int, int} = {} local_item_to_location: {int, int} = {}
bat_no_touch_locs: [LocationData] = [] bat_no_touch_locs: [LocationData] = []
bat_logic: int = self.options.bat_logic.value bat_logic: int = self.multiworld.bat_logic[self.player].value
try: try:
rom_deltas: { int, int } = {} rom_deltas: { int, int } = {}
self.place_dragons(rom_deltas) self.place_dragons(rom_deltas)
@@ -422,7 +421,7 @@ class AdventureWorld(World):
item_position_data_start = get_item_position_data_start(unplaced_item.table_index) item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
rom_deltas[item_position_data_start] = 0xff rom_deltas[item_position_data_start] = 0xff
if self.options.connector_multi_slot.value: if self.multiworld.connector_multi_slot[self.player].value:
rom_deltas[connector_port_offset] = (self.player & 0xff) rom_deltas[connector_port_offset] = (self.player & 0xff)
else: else:
rom_deltas[connector_port_offset] = 0 rom_deltas[connector_port_offset] = 0

View File

@@ -185,7 +185,7 @@ class AquariaLocations:
"Mithalas City, second bulb at the end of the top path": 698040, "Mithalas City, second bulb at the end of the top path": 698040,
"Mithalas City, bulb in the top path": 698036, "Mithalas City, bulb in the top path": 698036,
"Mithalas City, Mithalas Pot": 698174, "Mithalas City, Mithalas Pot": 698174,
"Mithalas City, urn in the Castle flower tube entrance": 698128, "Mithalas City, urn in the Cathedral flower tube entrance": 698128,
} }
locations_mithalas_city_fishpass = { locations_mithalas_city_fishpass = {
@@ -246,7 +246,7 @@ class AquariaLocations:
"Kelp Forest top left area, bulb in the bottom left clearing": 698044, "Kelp Forest top left area, bulb in the bottom left clearing": 698044,
"Kelp Forest top left area, bulb in the path down from the top left clearing": 698045, "Kelp Forest top left area, bulb in the path down from the top left clearing": 698045,
"Kelp Forest top left area, bulb in the top left clearing": 698046, "Kelp Forest top left area, bulb in the top left clearing": 698046,
"Kelp Forest top left area, Jelly Egg": 698185, "Kelp Forest top left, Jelly Egg": 698185,
} }
locations_forest_tl_fp = { locations_forest_tl_fp = {
@@ -332,7 +332,7 @@ class AquariaLocations:
} }
locations_veil_tr_l = { locations_veil_tr_l = {
"The Veil top right area, bulb at the top of the waterfall": 698080, "The Veil top right area, bulb in the top of the waterfall": 698080,
"The Veil top right area, Transturtle": 698210, "The Veil top right area, Transturtle": 698210,
} }

View File

@@ -771,7 +771,6 @@ class AquariaRegions:
self.__connect_regions("Sunken City left area", "Sunken City boss area", self.__connect_regions("Sunken City left area", "Sunken City boss area",
self.sunken_city_l, self.sunken_city_boss, self.sunken_city_l, self.sunken_city_boss,
lambda state: _has_beast_form(state, self.player) and lambda state: _has_beast_form(state, self.player) and
_has_sun_form(state, self.player) and
_has_energy_form(state, self.player) and _has_energy_form(state, self.player) and
_has_bind_song(state, self.player)) _has_bind_song(state, self.player))
@@ -984,7 +983,7 @@ class AquariaRegions:
lambda state: _has_damaging_item(state, self.player)) lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player), add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player),
lambda state: _has_damaging_item(state, self.player)) lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player), add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player),
lambda state: _has_damaging_item(state, self.player)) lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player), add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player),
lambda state: _has_damaging_item(state, self.player)) lambda state: _has_damaging_item(state, self.player))
@@ -1024,7 +1023,7 @@ class AquariaRegions:
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player),
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player), add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player),
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
def __adjusting_under_rock_location(self) -> None: def __adjusting_under_rock_location(self) -> None:
@@ -1176,7 +1175,7 @@ class AquariaRegions:
self.multiworld.get_location("Sun Worm path, second cliff bulb", self.multiworld.get_location("Sun Worm path, second cliff bulb",
self.player).item_rule =\ self.player).item_rule =\
lambda item: item.classification != ItemClassification.progression lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall",
self.player).item_rule =\ self.player).item_rule =\
lambda item: item.classification != ItemClassification.progression lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",

View File

@@ -167,10 +167,14 @@ class AquariaWorld(World):
self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected)
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
for name, data in item_table.items(): for name, data in item_table.items():
if name not in self.exclude: if name in precollected:
for i in range(data.count): precollected.remove(name)
item = self.create_item(name) self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
self.multiworld.itempool.append(item) else:
if name not in self.exclude:
for i in range(data.count):
item = self.create_item(name)
self.multiworld.itempool.append(item)
def set_rules(self) -> None: def set_rules(self) -> None:
""" """

View File

@@ -56,7 +56,7 @@ after_home_water_locations = [
"Mithalas City, second bulb at the end of the top path", "Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path", "Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot", "Mithalas City, Mithalas Pot",
"Mithalas City, urn in the Castle flower tube entrance", "Mithalas City, urn in the Cathedral flower tube entrance",
"Mithalas City, Doll", "Mithalas City, Doll",
"Mithalas City, urn inside a home fish pass", "Mithalas City, urn inside a home fish pass",
"Mithalas City Castle, bulb in the flesh hole", "Mithalas City Castle, bulb in the flesh hole",
@@ -93,7 +93,7 @@ after_home_water_locations = [
"Kelp Forest top left area, bulb in the bottom left clearing", "Kelp Forest top left area, bulb in the bottom left clearing",
"Kelp Forest top left area, bulb in the path down from the top left clearing", "Kelp Forest top left area, bulb in the path down from the top left clearing",
"Kelp Forest top left area, bulb in the top left clearing", "Kelp Forest top left area, bulb in the top left clearing",
"Kelp Forest top left area, Jelly Egg", "Kelp Forest top left, Jelly Egg",
"Kelp Forest top left area, bulb close to the Verse Egg", "Kelp Forest top left area, bulb close to the Verse Egg",
"Kelp Forest top left area, Verse Egg", "Kelp Forest top left area, Verse Egg",
"Kelp Forest top right area, bulb under the rock in the right path", "Kelp Forest top right area, bulb under the rock in the right path",
@@ -125,7 +125,7 @@ after_home_water_locations = [
"Turtle cave, Urchin Costume", "Turtle cave, Urchin Costume",
"The Veil top right area, bulb in the middle of the wall jump cliff", "The Veil top right area, bulb in the middle of the wall jump cliff",
"The Veil top right area, Golden Starfish", "The Veil top right area, Golden Starfish",
"The Veil top right area, bulb at the top of the waterfall", "The Veil top right area, bulb in the top of the waterfall",
"The Veil top right area, Transturtle", "The Veil top right area, Transturtle",
"The Veil bottom area, bulb in the left path", "The Veil bottom area, bulb in the left path",
"The Veil bottom area, bulb in the spirit path", "The Veil bottom area, bulb in the spirit path",

View File

@@ -20,14 +20,14 @@ class BeastFormAccessTest(AquariaTestBase):
"Mithalas City, second bulb at the end of the top path", "Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path", "Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot", "Mithalas City, Mithalas Pot",
"Mithalas City, urn in the Castle flower tube entrance", "Mithalas City, urn in the Cathedral flower tube entrance",
"Mermog cave, Piranha Egg", "Mermog cave, Piranha Egg",
"Mithalas Cathedral, Mithalan Dress", "Mithalas Cathedral, Mithalan Dress",
"Turtle cave, bulb in Bubble Cliff", "Turtle cave, bulb in Bubble Cliff",
"Turtle cave, Urchin Costume", "Turtle cave, Urchin Costume",
"Sun Worm path, first cliff bulb", "Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb", "Sun Worm path, second cliff bulb",
"The Veil top right area, bulb at the top of the waterfall", "The Veil top right area, bulb in the top of the waterfall",
"Bubble Cave, bulb in the left cave wall", "Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg", "Bubble Cave, Verse Egg",

View File

@@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
"Final Boss area, bulb in the boss third form room", "Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb", "Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb", "Sun Worm path, second cliff bulb",
"The Veil top right area, bulb at the top of the waterfall", "The Veil top right area, bulb in the top of the waterfall",
"Bubble Cave, bulb in the left cave wall", "Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg", "Bubble Cave, Verse Egg",

View File

@@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
"Final Boss area, bulb in the boss third form room", "Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb", "Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb", "Sun Worm path, second cliff bulb",
"The Veil top right area, bulb at the top of the waterfall", "The Veil top right area, bulb in the top of the waterfall",
"Bubble Cave, bulb in the left cave wall", "Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg", "Bubble Cave, Verse Egg",

View File

@@ -18,9 +18,6 @@ class SunFormAccessTest(AquariaTestBase):
"Abyss right area, bulb behind the rock in the whale room", "Abyss right area, bulb behind the rock in the whale room",
"Octopus Cave, Dumbo Egg", "Octopus Cave, Dumbo Egg",
"Beating Octopus Prime", "Beating Octopus Prime",
"Sunken City, bulb on top of the boss area",
"Beating the Golem",
"Sunken City cleared",
"Final Boss area, bulb in the boss third form room", "Final Boss area, bulb in the boss third form room",
"Objective complete" "Objective complete"
] ]

View File

@@ -64,4 +64,3 @@ item_name_groups = ({
}) })
item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash'] item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash']
item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'} item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'}
item_name_groups['Skills'] |= item_name_groups['Vertical'] | item_name_groups['Horizontal']

View File

@@ -5,7 +5,7 @@ from schema import And, Optional, Or, Schema
from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \
PlandoConnections, Range, StartInventoryPool, Toggle, Visibility PlandoConnections, Range, StartInventoryPool, Toggle, Visibility
from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS
class MessengerAccessibility(Accessibility): class MessengerAccessibility(Accessibility):

View File

@@ -306,7 +306,8 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
if world.options.scale_stats: if world.options.scale_stats:
patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1])) patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1]))
patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value])) if world.options.xp_multiplier:
patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
if world.options.tattle_hp: if world.options.tattle_hp:
patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1])) patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1]))

View File

@@ -249,7 +249,9 @@ class MuseDashWorld(World):
def create_regions(self) -> None: def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld) menu_region = Region("Menu", self.player, self.multiworld)
self.multiworld.regions += [menu_region] song_select_region = Region("Song Select", self.player, self.multiworld)
self.multiworld.regions += [menu_region, song_select_region]
menu_region.connect(song_select_region)
# Make a collection of all songs available for this rando. # Make a collection of all songs available for this rando.
# 1. All starting songs # 1. All starting songs
@@ -263,16 +265,18 @@ class MuseDashWorld(World):
self.random.shuffle(included_song_copy) self.random.shuffle(included_song_copy)
all_selected_locations.extend(included_song_copy) all_selected_locations.extend(included_song_copy)
# Adds 2 item locations per song/album to the menu region. # Make a region per song/album, then adds 1-2 item locations to them
for i in range(0, len(all_selected_locations)): for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i] name = all_selected_locations[i]
loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region) region = Region(name, self.player, self.multiworld)
loc1.access_rule = lambda state, place=name: state.has(place, self.player) self.multiworld.regions.append(region)
menu_region.locations.append(loc1) song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region) # Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler.
loc2.access_rule = lambda state, place=name: state.has(place, self.player) region.add_locations({
menu_region.locations.append(loc2) name + "-0": self.md_collection.song_locations[name + "-0"],
name + "-1": self.md_collection.song_locations[name + "-1"]
}, MuseDashLocation)
def set_rules(self) -> None: def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: \ self.multiworld.completion_condition[self.player] = lambda state: \

View File

@@ -107,10 +107,10 @@ class ColouredMessage:
def coloured(self, text: str, colour: str) -> 'ColouredMessage': def coloured(self, text: str, colour: str) -> 'ColouredMessage':
add_json_text(self.parts, text, type="color", color=colour) add_json_text(self.parts, text, type="color", color=colour)
return self return self
def location(self, location_id: int, player_id: int) -> 'ColouredMessage': def location(self, location_id: int, player_id: int = 0) -> 'ColouredMessage':
add_json_location(self.parts, location_id, player_id) add_json_location(self.parts, location_id, player_id)
return self return self
def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage': def item(self, item_id: int, player_id: int = 0, flags: int = 0) -> 'ColouredMessage':
add_json_item(self.parts, item_id, player_id, flags) add_json_item(self.parts, item_id, player_id, flags)
return self return self
def player(self, player_id: int) -> 'ColouredMessage': def player(self, player_id: int) -> 'ColouredMessage':
@@ -122,6 +122,7 @@ class ColouredMessage:
class StarcraftClientProcessor(ClientCommandProcessor): class StarcraftClientProcessor(ClientCommandProcessor):
ctx: SC2Context ctx: SC2Context
echo_commands = True
def formatted_print(self, text: str) -> None: def formatted_print(self, text: str) -> None:
"""Prints with kivy formatting to the GUI, and also prints to command-line and to all logs""" """Prints with kivy formatting to the GUI, and also prints to command-line and to all logs"""
@@ -256,7 +257,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
for item in received_items_of_this_type: for item in received_items_of_this_type:
print_faction_title() print_faction_title()
has_printed_faction_title = True has_printed_faction_title = True
(ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags) (ColouredMessage('* ').item(item.item, flags=item.flags)
(" from ").location(item.location, self.ctx.slot) (" from ").location(item.location, self.ctx.slot)
(" by ").player(item.player) (" by ").player(item.player)
).send(self.ctx) ).send(self.ctx)
@@ -277,7 +278,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
received_items_of_this_type = items_received.get(child_item, []) received_items_of_this_type = items_received.get(child_item, [])
for item in received_items_of_this_type: for item in received_items_of_this_type:
filter_match_count += len(received_items_of_this_type) filter_match_count += len(received_items_of_this_type)
(ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags) (ColouredMessage(' * ').item(item.item, flags=item.flags)
(" from ").location(item.location, self.ctx.slot) (" from ").location(item.location, self.ctx.slot)
(" by ").player(item.player) (" by ").player(item.player)
).send(self.ctx) ).send(self.ctx)

View File

@@ -1,6 +1,5 @@
from worlds.generic.Rules import forbid_items_for_player, add_rule from worlds.generic.Rules import forbid_items_for_player, add_rule
from .Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic from worlds.shorthike.Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic
def create_rules(self, location_table): def create_rules(self, location_table):
multiworld = self.multiworld multiworld = self.multiworld

View File

@@ -70,7 +70,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
logic = TimespinnerLogic(world, player, precalculated_weights) logic = TimespinnerLogic(world, player, precalculated_weights)
connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player))
connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state)) connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state))
connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Upper lake desolation', 'Lake desolation') connect(world, player, 'Upper lake desolation', 'Lake desolation')
@@ -80,7 +80,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Eastern lake desolation', 'Library') connect(world, player, 'Eastern lake desolation', 'Library')
connect(world, player, 'Eastern lake desolation', 'Lower lake desolation') connect(world, player, 'Eastern lake desolation', 'Lower lake desolation')
connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
connect(world, player, 'Library', 'Eastern lake desolation') connect(world, player, 'Library', 'Eastern lake desolation')
connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player))
connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D) connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D)
@@ -185,7 +185,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
if is_option_enabled(world, player, "GyreArchives"): if is_option_enabled(world, player, "GyreArchives"):
connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player))
connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)')
connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp") connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player))
connect(world, player, 'Ifrit\'s Lair', 'Library top') connect(world, player, 'Ifrit\'s Lair', 'Library top')
@@ -242,19 +242,11 @@ def connectStartingRegion(world: MultiWorld, player: int):
def connect(world: MultiWorld, player: int, source: str, target: str, def connect(world: MultiWorld, player: int, source: str, target: str,
rule: Optional[Callable[[CollectionState], bool]] = None, rule: Optional[Callable[[CollectionState], bool]] = None):
indirect: str = ""):
sourceRegion = world.get_region(source, player) sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player) targetRegion = world.get_region(target, player)
entrance = sourceRegion.connect(targetRegion, rule=rule) sourceRegion.connect(targetRegion, rule=rule)
if indirect:
indirectRegion = world.get_region(indirect, player)
if indirectRegion in world.indirect_connections:
world.indirect_connections[indirectRegion].add(entrance)
else:
world.indirect_connections[indirectRegion] = {entrance}
def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]: def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]:

View File

@@ -5,7 +5,7 @@ from NetUtils import ClientStatus, NetworkItem
import worlds._bizhawk as bizhawk import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient from worlds._bizhawk.client import BizHawkClient
from . import item_to_index from worlds.yugioh06 import item_to_index
if TYPE_CHECKING: if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext from worlds._bizhawk.context import BizHawkClientContext

View File

@@ -3,8 +3,8 @@ from typing import Dict, List, NamedTuple, Optional, Union
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from worlds.generic.Rules import CollectionRule from worlds.generic.Rules import CollectionRule
from . import item_to_index, tier_1_opponents, yugioh06_difficulty from worlds.yugioh06 import item_to_index, tier_1_opponents, yugioh06_difficulty
from .locations import special from worlds.yugioh06.locations import special
class OpponentData(NamedTuple): class OpponentData(NamedTuple):