mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 01:23:25 -07:00
Compare commits
79 Commits
archipidle
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b819aa0a4 | ||
|
|
93617fa546 | ||
|
|
b6925c593e | ||
|
|
401606e8e3 | ||
|
|
e95bb5ea56 | ||
|
|
52a13d38e9 | ||
|
|
31bd5e3ebc | ||
|
|
192f1b3fae | ||
|
|
55cb81d487 | ||
|
|
2424fb0c5b | ||
|
|
6191ff4b47 | ||
|
|
1c817e1eb7 | ||
|
|
d4c00ed267 | ||
|
|
e07a2667ae | ||
|
|
b8f78af506 | ||
|
|
77304a8743 | ||
|
|
5882ce7380 | ||
|
|
6c54b3596b | ||
|
|
07dd8f0671 | ||
|
|
935c94dc80 | ||
|
|
1ab1aeff15 | ||
|
|
5ca31533dc | ||
|
|
60a26920e1 | ||
|
|
d00abe7b8e | ||
|
|
40c9dfd3bf | ||
|
|
ce37bed7c6 | ||
|
|
4f514e5944 | ||
|
|
f515a085db | ||
|
|
903a0bab1a | ||
|
|
9bb3947d7e | ||
|
|
240d1a3bbf | ||
|
|
b6191ff7ca | ||
|
|
19d00547c2 | ||
|
|
67a0a04917 | ||
|
|
af213c9e5d | ||
|
|
898509e7ee | ||
|
|
1f685b4272 | ||
|
|
c622240730 | ||
|
|
1d314374d7 | ||
|
|
753eb8683f | ||
|
|
e8542b8acd | ||
|
|
2a11d610b6 | ||
|
|
92023a2cb5 | ||
|
|
df94271d30 | ||
|
|
0354315c22 | ||
|
|
e796f0ae64 | ||
|
|
c61505baf6 | ||
|
|
3972b1b257 | ||
|
|
1fe3d842c8 | ||
|
|
e9ad7cb797 | ||
|
|
533395d336 | ||
|
|
2ae51364d9 | ||
|
|
f6e3113af6 | ||
|
|
da34800f43 | ||
|
|
c108845d1f | ||
|
|
acf85eb9ab | ||
|
|
2daccded36 | ||
|
|
3b9b9353b7 | ||
|
|
b9e454ab4e | ||
|
|
7299891bdf | ||
|
|
e755f1a0b5 | ||
|
|
87d24eb38a | ||
|
|
54531c6eba | ||
|
|
ccfffa1147 | ||
|
|
75bef3ddb1 | ||
|
|
484082616f | ||
|
|
35617bdac5 | ||
|
|
0a912808e3 | ||
|
|
84a6d50ae7 | ||
|
|
5f8a8e6dad | ||
|
|
2198a70251 | ||
|
|
c478e55d7a | ||
|
|
76804d295b | ||
|
|
0d9fce29c6 | ||
|
|
302017c69e | ||
|
|
a0653cdfe0 | ||
|
|
89d584e474 | ||
|
|
39deef5d09 | ||
|
|
b3a2473853 |
61
.github/workflows/build.yml
vendored
61
.github/workflows/build.yml
vendored
@@ -36,10 +36,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||||
|
choco install innosetup --version=6.2.2 --allow-downgrade
|
||||||
- name: Build
|
- name: Build
|
||||||
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"
|
||||||
@@ -49,12 +54,6 @@ 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
|
||||||
@@ -65,11 +64,38 @@ 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:
|
||||||
@@ -110,7 +136,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 -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
|
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && 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 -
|
||||||
@@ -118,15 +144,36 @@ 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
|
||||||
|
|||||||
54
.github/workflows/ctest.yml
vendored
Normal file
54
.github/workflows/ctest.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 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
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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 -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
|
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && 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
1
.gitignore
vendored
@@ -178,6 +178,7 @@ dmypy.json
|
|||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
# Cython intermediates
|
# Cython intermediates
|
||||||
|
_speedups.c
|
||||||
_speedups.cpp
|
_speedups.cpp
|
||||||
_speedups.html
|
_speedups.html
|
||||||
|
|
||||||
|
|||||||
@@ -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_options()
|
options = Utils.get_settings()
|
||||||
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_options()["adventure_options"].get("death_link", False):
|
if Utils.get_settings()["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":
|
||||||
@@ -112,7 +112,7 @@ class AdventureContext(CommonContext):
|
|||||||
if ': !' not in msg:
|
if ': !' not in msg:
|
||||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||||
elif cmd == "ReceivedItems":
|
elif cmd == "ReceivedItems":
|
||||||
msg = f"Received {', '.join([self.item_names.lookup_in_slot(item.item) for item in args['items']])}"
|
msg = f"Received {', '.join([self.item_names.lookup_in_game(item.item) for item in args['items']])}"
|
||||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||||
elif cmd == "Retrieved":
|
elif cmd == "Retrieved":
|
||||||
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
|
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
|
||||||
@@ -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_options()["adventure_options"].get("rom_start", True)
|
auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True)
|
||||||
rom_args = Utils.get_options()["adventure_options"].get("rom_args")
|
rom_args = Utils.get_settings()["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)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
from MultiServer import CommandProcessor
|
from MultiServer import CommandProcessor
|
||||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes)
|
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType)
|
||||||
from Utils import Version, stream_input, async_start
|
from Utils import Version, stream_input, async_start
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import network_data_package, AutoWorldRegister
|
||||||
import os
|
import os
|
||||||
@@ -225,6 +225,9 @@ class CommonContext:
|
|||||||
def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str:
|
def lookup_in_slot(self, code: int, slot: typing.Optional[int] = None) -> str:
|
||||||
"""Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is
|
"""Returns the name for an item/location id in the context of a specific slot or own slot if `slot` is
|
||||||
omitted.
|
omitted.
|
||||||
|
|
||||||
|
Use of `lookup_in_slot` should not be used when not connected to a server. If looking in own game, set
|
||||||
|
`ctx.game` and use `lookup_in_game` method instead.
|
||||||
"""
|
"""
|
||||||
if slot is None:
|
if slot is None:
|
||||||
slot = self.ctx.slot
|
slot = self.ctx.slot
|
||||||
@@ -493,6 +496,11 @@ 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():
|
||||||
@@ -854,7 +862,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
ctx.team = args["team"]
|
ctx.team = args["team"]
|
||||||
ctx.slot = args["slot"]
|
ctx.slot = args["slot"]
|
||||||
# int keys get lost in JSON transfer
|
# int keys get lost in JSON transfer
|
||||||
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
|
ctx.slot_info = {0: NetworkSlot("Archipelago", "Archipelago", SlotType.player)}
|
||||||
|
ctx.slot_info.update({int(pid): data for pid, data in args["slot_info"].items()})
|
||||||
ctx.hint_points = args.get("hint_points", 0)
|
ctx.hint_points = args.get("hint_points", 0)
|
||||||
ctx.consume_players_package(args["players"])
|
ctx.consume_players_package(args["players"])
|
||||||
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
|
ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}")
|
||||||
|
|||||||
6
Fill.py
6
Fill.py
@@ -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,
|
||||||
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
|
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,
|
||||||
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
|
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,
|
||||||
on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1)
|
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.")
|
||||||
|
|||||||
43
Generate.py
43
Generate.py
@@ -1,10 +1,12 @@
|
|||||||
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
|
||||||
@@ -15,21 +17,16 @@ 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():
|
||||||
options = get_settings()
|
from settings import get_settings
|
||||||
defaults = options.generator
|
settings = get_settings()
|
||||||
|
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,
|
||||||
@@ -41,7 +38,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=options.general_options.output_path,
|
parser.add_argument('--outputpath', default=settings.general_options.output_path,
|
||||||
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
|
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)
|
||||||
@@ -61,20 +58,23 @@ 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, options
|
return args
|
||||||
|
|
||||||
|
|
||||||
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, callback=ERmain):
|
def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||||
|
# __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, options = mystery_argparse()
|
args = 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,6 +143,9 @@ def main(args=None, callback=ERmain):
|
|||||||
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
|
||||||
@@ -234,7 +237,7 @@ def main(args=None, callback=ERmain):
|
|||||||
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)
|
||||||
|
|
||||||
return callback(erargs, seed)
|
return erargs, seed
|
||||||
|
|
||||||
|
|
||||||
def read_weights_yamls(path) -> Tuple[Any, ...]:
|
def read_weights_yamls(path) -> Tuple[Any, ...]:
|
||||||
@@ -359,6 +362,8 @@ 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:
|
||||||
@@ -436,10 +441,13 @@ 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)
|
||||||
|
|
||||||
@@ -466,6 +474,7 @@ 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}. "
|
||||||
@@ -537,7 +546,9 @@ def roll_alttp_settings(ret: argparse.Namespace, weights):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import atexit
|
import atexit
|
||||||
confirmation = atexit.register(input, "Press enter to close.")
|
confirmation = atexit.register(input, "Press enter to close.")
|
||||||
multiworld = main()
|
erargs, seed = main()
|
||||||
|
from Main import main as ERmain
|
||||||
|
multiworld = ERmain(erargs, seed)
|
||||||
if __debug__:
|
if __debug__:
|
||||||
import gc
|
import gc
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@@ -198,7 +198,8 @@ 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):
|
||||||
|
|||||||
75
Options.py
75
Options.py
@@ -53,8 +53,8 @@ class AssembleOptions(abc.ABCMeta):
|
|||||||
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
||||||
options.update(new_options)
|
options.update(new_options)
|
||||||
# apply aliases, without name_lookup
|
# apply aliases, without name_lookup
|
||||||
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
aliases = attrs["aliases"] = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||||
name.startswith("alias_")}
|
name.startswith("alias_")}
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
name in {"Option", "VerifyKeys"} or # base abstract classes don't need default
|
name in {"Option", "VerifyKeys"} or # base abstract classes don't need default
|
||||||
@@ -126,10 +126,28 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
|||||||
# can be weighted between selections
|
# can be weighted between selections
|
||||||
supports_weighting = True
|
supports_weighting = True
|
||||||
|
|
||||||
|
rich_text_doc: typing.Optional[bool] = None
|
||||||
|
"""Whether the WebHost should render the Option's docstring as rich text.
|
||||||
|
|
||||||
|
If this is True, the Option's docstring is interpreted as reStructuredText_,
|
||||||
|
the standard Python markup format. In the WebHost, it's rendered to HTML so
|
||||||
|
that lists, emphasis, and other rich text features are displayed properly.
|
||||||
|
|
||||||
|
If this is False, the docstring is instead interpreted as plain text, and
|
||||||
|
displayed as-is on the WebHost with whitespace preserved.
|
||||||
|
|
||||||
|
If this is None, it inherits the value of `World.rich_text_options_doc`. For
|
||||||
|
backwards compatibility, this defaults to False, but worlds are encouraged to
|
||||||
|
set it to True and use reStructuredText for their Option documentation.
|
||||||
|
|
||||||
|
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
|
||||||
|
"""
|
||||||
|
|
||||||
# filled by AssembleOptions:
|
# filled by AssembleOptions:
|
||||||
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
|
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
|
||||||
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
|
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
|
||||||
options: typing.ClassVar[typing.Dict[str, int]]
|
options: typing.ClassVar[typing.Dict[str, int]]
|
||||||
|
aliases: typing.ClassVar[typing.Dict[str, int]]
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.__class__.__name__}({self.current_option_name})"
|
return f"{self.__class__.__name__}({self.current_option_name})"
|
||||||
@@ -735,6 +753,12 @@ class NamedRange(Range):
|
|||||||
elif value > self.range_end and value not in self.special_range_names.values():
|
elif value > self.range_end and value not in self.special_range_names.values():
|
||||||
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
|
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
|
||||||
f"and is also not one of the supported named special values: {self.special_range_names}")
|
f"and is also not one of the supported named special values: {self.special_range_names}")
|
||||||
|
|
||||||
|
# See docstring
|
||||||
|
for key in self.special_range_names:
|
||||||
|
if key != key.lower():
|
||||||
|
raise Exception(f"{self.__class__.__name__} has an invalid special_range_names key: {key}. "
|
||||||
|
f"NamedRange keys must use only lowercase letters, and ideally should be snake_case.")
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1121,10 +1145,13 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
|||||||
|
|
||||||
class Accessibility(Choice):
|
class Accessibility(Choice):
|
||||||
"""Set rules for reachability of your items/locations.
|
"""Set rules for reachability of your items/locations.
|
||||||
Locations: ensure everything can be reached and acquired.
|
|
||||||
Items: ensure all logically relevant items can be acquired.
|
- **Locations:** ensure everything can be reached and acquired.
|
||||||
Minimal: ensure what is needed to reach your goal can be acquired."""
|
- **Items:** ensure all logically relevant items can be acquired.
|
||||||
|
- **Minimal:** ensure what is needed to reach your goal can be acquired.
|
||||||
|
"""
|
||||||
display_name = "Accessibility"
|
display_name = "Accessibility"
|
||||||
|
rich_text_doc = True
|
||||||
option_locations = 0
|
option_locations = 0
|
||||||
option_items = 1
|
option_items = 1
|
||||||
option_minimal = 2
|
option_minimal = 2
|
||||||
@@ -1133,14 +1160,15 @@ class Accessibility(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class ProgressionBalancing(NamedRange):
|
class ProgressionBalancing(NamedRange):
|
||||||
"""
|
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||||
A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
|
||||||
A lower setting means more getting stuck. A higher setting means less getting stuck.
|
A lower setting means more getting stuck. A higher setting means less getting stuck.
|
||||||
"""
|
"""
|
||||||
default = 50
|
default = 50
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 99
|
range_end = 99
|
||||||
display_name = "Progression Balancing"
|
display_name = "Progression Balancing"
|
||||||
|
rich_text_doc = True
|
||||||
special_range_names = {
|
special_range_names = {
|
||||||
"disabled": 0,
|
"disabled": 0,
|
||||||
"normal": 50,
|
"normal": 50,
|
||||||
@@ -1205,29 +1233,36 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
|||||||
class LocalItems(ItemSet):
|
class LocalItems(ItemSet):
|
||||||
"""Forces these items to be in their native world."""
|
"""Forces these items to be in their native world."""
|
||||||
display_name = "Local Items"
|
display_name = "Local Items"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class NonLocalItems(ItemSet):
|
class NonLocalItems(ItemSet):
|
||||||
"""Forces these items to be outside their native world."""
|
"""Forces these items to be outside their native world."""
|
||||||
display_name = "Non-local Items"
|
display_name = "Non-local Items"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class StartInventory(ItemDict):
|
class StartInventory(ItemDict):
|
||||||
"""Start with these items."""
|
"""Start with these items."""
|
||||||
verify_item_name = True
|
verify_item_name = True
|
||||||
display_name = "Start Inventory"
|
display_name = "Start Inventory"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class StartInventoryPool(StartInventory):
|
class StartInventoryPool(StartInventory):
|
||||||
"""Start with these items and don't place them in the world.
|
"""Start with these items and don't place them in the world.
|
||||||
The game decides what the replacement items will be."""
|
|
||||||
|
The game decides what the replacement items will be.
|
||||||
|
"""
|
||||||
verify_item_name = True
|
verify_item_name = True
|
||||||
display_name = "Start Inventory from Pool"
|
display_name = "Start Inventory from Pool"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class StartHints(ItemSet):
|
class StartHints(ItemSet):
|
||||||
"""Start with these item's locations prefilled into the !hint command."""
|
"""Start with these item's locations prefilled into the ``!hint`` command."""
|
||||||
display_name = "Start Hints"
|
display_name = "Start Hints"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class LocationSet(OptionSet):
|
class LocationSet(OptionSet):
|
||||||
@@ -1236,28 +1271,33 @@ class LocationSet(OptionSet):
|
|||||||
|
|
||||||
|
|
||||||
class StartLocationHints(LocationSet):
|
class StartLocationHints(LocationSet):
|
||||||
"""Start with these locations and their item prefilled into the !hint command"""
|
"""Start with these locations and their item prefilled into the ``!hint`` command."""
|
||||||
display_name = "Start Location Hints"
|
display_name = "Start Location Hints"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class ExcludeLocations(LocationSet):
|
class ExcludeLocations(LocationSet):
|
||||||
"""Prevent these locations from having an important item"""
|
"""Prevent these locations from having an important item."""
|
||||||
display_name = "Excluded Locations"
|
display_name = "Excluded Locations"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class PriorityLocations(LocationSet):
|
class PriorityLocations(LocationSet):
|
||||||
"""Prevent these locations from having an unimportant item"""
|
"""Prevent these locations from having an unimportant item."""
|
||||||
display_name = "Priority Locations"
|
display_name = "Priority Locations"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class DeathLink(Toggle):
|
class DeathLink(Toggle):
|
||||||
"""When you die, everyone dies. Of course the reverse is true too."""
|
"""When you die, everyone dies. Of course the reverse is true too."""
|
||||||
display_name = "Death Link"
|
display_name = "Death Link"
|
||||||
|
rich_text_doc = True
|
||||||
|
|
||||||
|
|
||||||
class ItemLinks(OptionList):
|
class ItemLinks(OptionList):
|
||||||
"""Share part of your item pool with other players."""
|
"""Share part of your item pool with other players."""
|
||||||
display_name = "Item Links"
|
display_name = "Item Links"
|
||||||
|
rich_text_doc = True
|
||||||
default = []
|
default = []
|
||||||
schema = Schema([
|
schema = Schema([
|
||||||
{
|
{
|
||||||
@@ -1324,6 +1364,7 @@ class ItemLinks(OptionList):
|
|||||||
|
|
||||||
class Removed(FreeText):
|
class Removed(FreeText):
|
||||||
"""This Option has been Removed."""
|
"""This Option has been Removed."""
|
||||||
|
rich_text_doc = True
|
||||||
default = ""
|
default = ""
|
||||||
visibility = Visibility.none
|
visibility = Visibility.none
|
||||||
|
|
||||||
@@ -1426,14 +1467,18 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
|
|
||||||
return data, notes
|
return data, notes
|
||||||
|
|
||||||
|
def yaml_dump_scalar(scalar) -> str:
|
||||||
|
# yaml dump may add end of document marker and newlines.
|
||||||
|
return yaml.dump(scalar).replace("...\n", "").strip()
|
||||||
|
|
||||||
for game_name, world in AutoWorldRegister.world_types.items():
|
for game_name, world in AutoWorldRegister.world_types.items():
|
||||||
if not world.hidden or generate_hidden:
|
if not world.hidden or generate_hidden:
|
||||||
grouped_options = get_option_groups(world)
|
option_groups = get_option_groups(world)
|
||||||
with open(local_path("data", "options.yaml")) as f:
|
with open(local_path("data", "options.yaml")) as f:
|
||||||
file_data = f.read()
|
file_data = f.read()
|
||||||
res = Template(file_data).render(
|
res = Template(file_data).render(
|
||||||
option_groups=grouped_options,
|
option_groups=option_groups,
|
||||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
__version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar,
|
||||||
dictify_range=dictify_range,
|
dictify_range=dictify_range,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
toDraw = ""
|
toDraw = ""
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
if i < len(str(ctx.item_names.lookup_in_slot(l.item))):
|
if i < len(str(ctx.item_names.lookup_in_game(l.item))):
|
||||||
toDraw += str(ctx.item_names.lookup_in_slot(l.item))[i]
|
toDraw += str(ctx.item_names.lookup_in_game(l.item))[i]
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
f.write(toDraw)
|
f.write(toDraw)
|
||||||
|
|||||||
1
Utils.py
1
Utils.py
@@ -553,6 +553,7 @@ 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 ''}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class WargrooveContext(CommonContext):
|
|||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
open(path, 'w').close()
|
open(path, 'w').close()
|
||||||
# Announcing commander unlocks
|
# Announcing commander unlocks
|
||||||
item_name = self.item_names.lookup_in_slot(network_item.item)
|
item_name = self.item_names.lookup_in_game(network_item.item)
|
||||||
if item_name in faction_table.keys():
|
if item_name in faction_table.keys():
|
||||||
for commander in faction_table[item_name]:
|
for commander in faction_table[item_name]:
|
||||||
logger.info(f"{commander.name} has been unlocked!")
|
logger.info(f"{commander.name} has been unlocked!")
|
||||||
@@ -197,7 +197,7 @@ class WargrooveContext(CommonContext):
|
|||||||
open(print_path, 'w').close()
|
open(print_path, 'w').close()
|
||||||
with open(print_path, 'w') as f:
|
with open(print_path, 'w') as f:
|
||||||
f.write("Received " +
|
f.write("Received " +
|
||||||
self.item_names.lookup_in_slot(network_item.item) +
|
self.item_names.lookup_in_game(network_item.item) +
|
||||||
" from " +
|
" from " +
|
||||||
self.player_names[network_item.player])
|
self.player_names[network_item.player])
|
||||||
f.close()
|
f.close()
|
||||||
@@ -342,7 +342,7 @@ class WargrooveContext(CommonContext):
|
|||||||
faction_items = 0
|
faction_items = 0
|
||||||
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
|
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
|
||||||
for network_item in self.items_received:
|
for network_item in self.items_received:
|
||||||
if self.item_names.lookup_in_slot(network_item.item) in faction_item_names:
|
if self.item_names.lookup_in_game(network_item.item) in faction_item_names:
|
||||||
faction_items += 1
|
faction_items += 1
|
||||||
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
|
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
|
||||||
# Must be an integer larger than 0
|
# Must be an integer larger than 0
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ 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)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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")
|
||||||
@@ -49,21 +48,4 @@ def room_info(room: UUID):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/datapackage')
|
from . import generate, user, datapackage # trigger registration
|
||||||
@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
|
|
||||||
|
|||||||
32
WebHostLib/api/datapackage.py
Normal file
32
WebHostLib/api/datapackage.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Union
|
from typing import Any, IO, Dict, Iterator, List, Tuple, Union
|
||||||
|
|
||||||
import jinja2.exceptions
|
import jinja2.exceptions
|
||||||
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||||
@@ -97,25 +97,37 @@ def new_room(seed: UUID):
|
|||||||
return redirect(url_for("host_room", room=room.id))
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
|
|
||||||
def _read_log(path: str):
|
def _read_log(log: IO[Any], offset: int = 0) -> Iterator[bytes]:
|
||||||
if os.path.exists(path):
|
marker = log.read(3) # skip optional BOM
|
||||||
with open(path, encoding="utf-8-sig") as log:
|
if marker != b'\xEF\xBB\xBF':
|
||||||
yield from log
|
log.seek(0, os.SEEK_SET)
|
||||||
else:
|
log.seek(offset, os.SEEK_CUR)
|
||||||
yield f"Logfile {path} does not exist. " \
|
yield from log
|
||||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
log.close() # free file handle as soon as possible
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log/<suuid:room>')
|
@app.route('/log/<suuid:room>')
|
||||||
def display_log(room: UUID):
|
def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
|
||||||
room = Room.get(id=room)
|
room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
if room.owner == session["_id"]:
|
if room.owner == session["_id"]:
|
||||||
file_path = os.path.join("logs", str(room.id) + ".txt")
|
file_path = os.path.join("logs", str(room.id) + ".txt")
|
||||||
if os.path.exists(file_path):
|
try:
|
||||||
return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8")
|
log = open(file_path, "rb")
|
||||||
return "Log File does not exist."
|
range_header = request.headers.get("Range")
|
||||||
|
if range_header:
|
||||||
|
range_type, range_values = range_header.split('=')
|
||||||
|
start, end = map(str.strip, range_values.split('-', 1))
|
||||||
|
if range_type != "bytes" or end != "":
|
||||||
|
return "Unsupported range", 500
|
||||||
|
# NOTE: we skip Content-Range in the response here, which isn't great but works for our JS
|
||||||
|
return Response(_read_log(log, int(start)), mimetype="text/plain", status=206)
|
||||||
|
return Response(_read_log(log), mimetype="text/plain")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return Response(f"Logfile {file_path} does not exist. "
|
||||||
|
f"Likely a crash during spinup of multiworld instance or it is still spinning up.",
|
||||||
|
mimetype="text/plain")
|
||||||
|
|
||||||
return "Access Denied", 403
|
return "Access Denied", 403
|
||||||
|
|
||||||
@@ -139,7 +151,22 @@ def host_room(room: UUID):
|
|||||||
with db_session:
|
with db_session:
|
||||||
room.last_activity = now # will trigger a spinup, if it's not already running
|
room.last_activity = now # will trigger a spinup, if it's not already running
|
||||||
|
|
||||||
return render_template("hostRoom.html", room=room, should_refresh=should_refresh)
|
def get_log(max_size: int = 1024000) -> str:
|
||||||
|
try:
|
||||||
|
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
|
||||||
|
raw_size = 0
|
||||||
|
fragments: List[str] = []
|
||||||
|
for block in _read_log(log):
|
||||||
|
if raw_size + len(block) > max_size:
|
||||||
|
fragments.append("…")
|
||||||
|
break
|
||||||
|
raw_size += len(block)
|
||||||
|
fragments.append(block.decode("utf-8"))
|
||||||
|
return "".join(fragments)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union
|
||||||
|
from docutils.core import publish_parts
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from flask import redirect, render_template, request, Response
|
from flask import redirect, render_template, request, Response
|
||||||
@@ -66,6 +67,22 @@ def filter_dedent(text: str) -> str:
|
|||||||
return dedent(text).strip("\n ")
|
return dedent(text).strip("\n ")
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter("rst_to_html")
|
||||||
|
def filter_rst_to_html(text: str) -> str:
|
||||||
|
"""Converts reStructuredText (such as a Python docstring) to HTML."""
|
||||||
|
if text.startswith(" ") or text.startswith("\t"):
|
||||||
|
text = dedent(text)
|
||||||
|
elif "\n" in text:
|
||||||
|
lines = text.splitlines()
|
||||||
|
text = lines[0] + "\n" + dedent("\n".join(lines[1:]))
|
||||||
|
|
||||||
|
return publish_parts(text, writer_name='html', settings=None, settings_overrides={
|
||||||
|
'raw_enable': False,
|
||||||
|
'file_insertion_enabled': False,
|
||||||
|
'output_encoding': 'unicode'
|
||||||
|
})['body']
|
||||||
|
|
||||||
|
|
||||||
@app.template_test("ordered")
|
@app.template_test("ordered")
|
||||||
def test_ordered(obj):
|
def test_ordered(obj):
|
||||||
return isinstance(obj, collections.abc.Sequence)
|
return isinstance(obj, collections.abc.Sequence)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from . import cache
|
|||||||
def robots():
|
def robots():
|
||||||
# If this host is not official, do not allow search engine crawling
|
# If this host is not official, do not allow search engine crawling
|
||||||
if not app.config["ASSET_RIGHTS"]:
|
if not app.config["ASSET_RIGHTS"]:
|
||||||
return app.send_static_file('robots.txt')
|
# filename changed in case the path is intercepted and served by an outside service
|
||||||
|
return app.send_static_file('robots_file.txt')
|
||||||
|
|
||||||
# Send 404 if the host has affirmed this to be the official WebHost
|
# Send 404 if the host has affirmed this to be the official WebHost
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* Base styles for the element that has a tooltip */
|
/* Base styles for the element that has a tooltip */
|
||||||
[data-tooltip], .tooltip {
|
[data-tooltip], .tooltip-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base styles for the entire tooltip */
|
/* Base styles for the entire tooltip */
|
||||||
[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after {
|
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -39,14 +39,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{
|
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
|
||||||
|
.tooltip-container:hover .tooltip {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Directional arrow styles */
|
/** Directional arrow styles */
|
||||||
.tooltip:before, [data-tooltip]:before {
|
[data-tooltip]:before, .tooltip-container:before {
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
border: 6px solid transparent;
|
border: 6px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -54,7 +55,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Content styles */
|
/** Content styles */
|
||||||
.tooltip:after, [data-tooltip]:after {
|
[data-tooltip]:after, .tooltip {
|
||||||
width: 260px;
|
width: 260px;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -63,24 +64,26 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
background-color: hsla(0, 0%, 20%, 0.9);
|
background-color: hsla(0, 0%, 20%, 0.9);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
content: attr(data-tooltip);
|
content: attr(data-tooltip);
|
||||||
white-space: pre-wrap;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:before, [data-tooltip]:after{
|
[data-tooltip]:after {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:before, [data-tooltip]:after, .tooltip:before, .tooltip:after,
|
[data-tooltip]:before, [data-tooltip]:after, .tooltip-container:before, .tooltip {
|
||||||
.tooltip-top:before, .tooltip-top:after {
|
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:before, .tooltip:before, .tooltip-top:before {
|
[data-tooltip]:before, .tooltip-container:before {
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
margin-bottom: -12px;
|
margin-bottom: -12px;
|
||||||
border-top-color: #000;
|
border-top-color: #000;
|
||||||
@@ -88,19 +91,19 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Horizontally align tooltips on the top and bottom */
|
/** Horizontally align tooltips on the top and bottom */
|
||||||
[data-tooltip]:after, .tooltip:after, .tooltip-top:after {
|
[data-tooltip]:after, .tooltip {
|
||||||
margin-left: -80px;
|
margin-left: -80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after,
|
[data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip-container:hover:before,
|
||||||
.tooltip-top:hover:before, .tooltip-top:hover:after {
|
.tooltip-container:hover .tooltip {
|
||||||
-webkit-transform: translateY(-12px);
|
-webkit-transform: translateY(-12px);
|
||||||
-moz-transform: translateY(-12px);
|
-moz-transform: translateY(-12px);
|
||||||
transform: translateY(-12px);
|
transform: translateY(-12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tooltips on the left */
|
/** Tooltips on the left */
|
||||||
.tooltip-left:before, .tooltip-left:after {
|
.tooltip-left:before, [data-tooltip].tooltip-left:after, .tooltip-left .tooltip {
|
||||||
right: 100%;
|
right: 100%;
|
||||||
bottom: 50%;
|
bottom: 50%;
|
||||||
left: auto;
|
left: auto;
|
||||||
@@ -115,14 +118,14 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
border-left-color: hsla(0, 0%, 20%, 0.9);
|
border-left-color: hsla(0, 0%, 20%, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-left:hover:before, .tooltip-left:hover:after {
|
.tooltip-left:hover:before, [data-tooltip].tooltip-left:hover:after, .tooltip-left:hover .tooltip {
|
||||||
-webkit-transform: translateX(-12px);
|
-webkit-transform: translateX(-12px);
|
||||||
-moz-transform: translateX(-12px);
|
-moz-transform: translateX(-12px);
|
||||||
transform: translateX(-12px);
|
transform: translateX(-12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tooltips on the bottom */
|
/** Tooltips on the bottom */
|
||||||
.tooltip-bottom:before, .tooltip-bottom:after {
|
.tooltip-bottom:before, [data-tooltip].tooltip-bottom:after, .tooltip-bottom .tooltip {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -136,14 +139,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
border-bottom-color: hsla(0, 0%, 20%, 0.9);
|
border-bottom-color: hsla(0, 0%, 20%, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom:hover:before, .tooltip-bottom:hover:after {
|
.tooltip-bottom:hover:before, [data-tooltip].tooltip-bottom:hover:after,
|
||||||
|
.tooltip-bottom:hover .tooltip {
|
||||||
-webkit-transform: translateY(12px);
|
-webkit-transform: translateY(12px);
|
||||||
-moz-transform: translateY(12px);
|
-moz-transform: translateY(12px);
|
||||||
transform: translateY(12px);
|
transform: translateY(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tooltips on the right */
|
/** Tooltips on the right */
|
||||||
.tooltip-right:before, .tooltip-right:after {
|
.tooltip-right:before, [data-tooltip].tooltip-right:after, .tooltip-right .tooltip {
|
||||||
bottom: 50%;
|
bottom: 50%;
|
||||||
left: 100%;
|
left: 100%;
|
||||||
}
|
}
|
||||||
@@ -156,7 +160,8 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
border-right-color: hsla(0, 0%, 20%, 0.9);
|
border-right-color: hsla(0, 0%, 20%, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-right:hover:before, .tooltip-right:hover:after {
|
.tooltip-right:hover:before, [data-tooltip].tooltip-right:hover:after,
|
||||||
|
.tooltip-right:hover .tooltip {
|
||||||
-webkit-transform: translateX(12px);
|
-webkit-transform: translateX(12px);
|
||||||
-moz-transform: translateX(12px);
|
-moz-transform: translateX(12px);
|
||||||
transform: translateX(12px);
|
transform: translateX(12px);
|
||||||
@@ -168,7 +173,16 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Center content vertically for tooltips ont he left and right */
|
/** Center content vertically for tooltips ont he left and right */
|
||||||
.tooltip-left:after, .tooltip-right:after {
|
[data-tooltip].tooltip-left:after, [data-tooltip].tooltip-right:after,
|
||||||
|
.tooltip-left .tooltip, .tooltip-right .tooltip {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-bottom: -16px;
|
margin-bottom: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip ul, .tooltip ol {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<form method=post style="flex-grow: 1; margin-right: 1em;">
|
<form method="post" id="command-form" style="flex-grow: 1; margin-right: 1em;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cmd"></label>
|
<label for="cmd"></label>
|
||||||
<input class="form-control" type="text" id="cmd" name="cmd"
|
<input class="form-control" type="text" id="cmd" name="cmd"
|
||||||
@@ -55,24 +55,89 @@
|
|||||||
Open Log File...
|
Open Log File...
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="logger"></div>
|
{% set log = get_log() -%}
|
||||||
<script type="application/ecmascript">
|
{%- set log_len = log | length - 1 if log.endswith("…") else log | length -%}
|
||||||
let xmlhttp = new XMLHttpRequest();
|
<div id="logger" style="white-space: pre">{{ log }}</div>
|
||||||
let url = '{{ url_for('display_log', room = room.id) }}';
|
<script>
|
||||||
|
let url = '{{ url_for('display_log', room = room.id) }}';
|
||||||
|
let bytesReceived = {{ log_len }};
|
||||||
|
let updateLogTimeout;
|
||||||
|
let awaitingCommandResponse = false;
|
||||||
|
let logger = document.getElementById("logger");
|
||||||
|
|
||||||
xmlhttp.onreadystatechange = function () {
|
function scrollToBottom(el) {
|
||||||
if (this.readyState === 4 && this.status === 200) {
|
let bot = el.scrollHeight - el.clientHeight;
|
||||||
document.getElementById("logger").innerText = this.responseText;
|
el.scrollTop += Math.ceil((bot - el.scrollTop)/10);
|
||||||
}
|
if (bot - el.scrollTop >= 1) {
|
||||||
};
|
window.clearTimeout(el.scrollTimer);
|
||||||
|
el.scrollTimer = window.setTimeout(() => {
|
||||||
function request_new() {
|
scrollToBottom(el)
|
||||||
xmlhttp.open("GET", url, true);
|
}, 16);
|
||||||
xmlhttp.send();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.setTimeout(request_new, 1000);
|
async function updateLog() {
|
||||||
window.setInterval(request_new, 10000);
|
try {
|
||||||
|
let res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Range': `bytes=${bytesReceived}-`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
let text = await res.text();
|
||||||
|
if (text.length > 0) {
|
||||||
|
awaitingCommandResponse = false;
|
||||||
|
if (bytesReceived === 0 || res.status !== 206) {
|
||||||
|
logger.innerHTML = '';
|
||||||
|
}
|
||||||
|
if (res.status !== 206) {
|
||||||
|
bytesReceived = 0;
|
||||||
|
} else {
|
||||||
|
bytesReceived += new Blob([text]).size;
|
||||||
|
}
|
||||||
|
if (logger.innerHTML.endsWith('…')) {
|
||||||
|
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
|
||||||
|
}
|
||||||
|
logger.appendChild(document.createTextNode(text));
|
||||||
|
scrollToBottom(logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
window.clearTimeout(updateLogTimeout);
|
||||||
|
updateLogTimeout = window.setTimeout(updateLog, awaitingCommandResponse ? 500 : 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postForm(ev) {
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
|
let cmd = document.getElementById("cmd");
|
||||||
|
if (cmd.value === "") {
|
||||||
|
ev.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/** @type {HTMLFormElement} */
|
||||||
|
let form = document.getElementById("command-form");
|
||||||
|
let req = fetch(form.action || window.location.href, {
|
||||||
|
method: form.method,
|
||||||
|
body: new FormData(form),
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
ev.preventDefault(); // has to happen before first await
|
||||||
|
form.reset();
|
||||||
|
let res = await req;
|
||||||
|
if (res.ok || res.type === 'opaqueredirect') {
|
||||||
|
awaitingCommandResponse = true;
|
||||||
|
window.clearTimeout(updateLogTimeout);
|
||||||
|
updateLogTimeout = window.setTimeout(updateLog, 100);
|
||||||
|
} else {
|
||||||
|
window.alert(res.statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("command-form").addEventListener("submit", postForm);
|
||||||
|
updateLogTimeout = window.setTimeout(updateLog, 1000);
|
||||||
|
logger.scrollTop = logger.scrollHeight;
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,9 +57,9 @@
|
|||||||
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||||
{% for key, val in option.special_range_names.items() %}
|
{% for key, val in option.special_range_names.items() %}
|
||||||
{% if option.default == val %}
|
{% if option.default == val %}
|
||||||
<option value="{{ val }}" selected>{{ key }} ({{ val }})</option>
|
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
||||||
{% else %}
|
{% else %}
|
||||||
<option value="{{ val }}">{{ key }} ({{ val }})</option>
|
<option value="{{ val }}">{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<option value="custom" hidden>Custom</option>
|
<option value="custom" hidden>Custom</option>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro ItemDict(option_name, option, world) %}
|
{% macro ItemDict(option_name, option) %}
|
||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro LocationSet(option_name, option, world) %}
|
{% macro LocationSet(option_name, option) %}
|
||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
{% for group_name in world.location_name_groups.keys()|sort %}
|
{% for group_name in world.location_name_groups.keys()|sort %}
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro ItemSet(option_name, option, world) %}
|
{% macro ItemSet(option_name, option) %}
|
||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
{% for group_name in world.item_name_groups.keys()|sort %}
|
{% for group_name in world.item_name_groups.keys()|sort %}
|
||||||
@@ -196,7 +196,18 @@
|
|||||||
{% macro OptionTitle(option_name, option) %}
|
{% macro OptionTitle(option_name, option) %}
|
||||||
<label for="{{ option_name }}">
|
<label for="{{ option_name }}">
|
||||||
{{ option.display_name|default(option_name) }}:
|
{{ option.display_name|default(option_name) }}:
|
||||||
<span class="interactive" data-tooltip="{% filter dedent %}{{(option.__doc__ | default("Please document me!"))|escape }}{% endfilter %}">(?)</span>
|
<span
|
||||||
|
class="interactive tooltip-container"
|
||||||
|
{% if not (option.rich_text_doc | default(world.web.rich_text_options_doc, true)) %}
|
||||||
|
data-tooltip="{{(option.__doc__ | default("Please document me!"))|replace('\n ', '\n')|escape|trim}}"
|
||||||
|
{% endif %}>
|
||||||
|
(?)
|
||||||
|
{% if option.rich_text_doc | default(world.web.rich_text_options_doc, true) %}
|
||||||
|
<div class="tooltip">
|
||||||
|
{{ option.__doc__ | default("**Please document me!**") | rst_to_html | safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import 'playerOptions/macros.html' as inputs %}
|
{% import 'playerOptions/macros.html' as inputs with context %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>{{ world_name }} Options</title>
|
<title>{{ world_name }} Options</title>
|
||||||
@@ -94,16 +94,16 @@
|
|||||||
{{ inputs.FreeText(option_name, option) }}
|
{{ inputs.FreeText(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||||
{{ inputs.ItemDict(option_name, option, world) }}
|
{{ inputs.ItemDict(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
{{ inputs.OptionList(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||||
{{ inputs.LocationSet(option_name, option, world) }}
|
{{ inputs.LocationSet(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||||
{{ inputs.ItemSet(option_name, option, world) }}
|
{{ inputs.ItemSet(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||||
{{ inputs.OptionSet(option_name, option) }}
|
{{ inputs.OptionSet(option_name, option) }}
|
||||||
@@ -134,16 +134,16 @@
|
|||||||
{{ inputs.FreeText(option_name, option) }}
|
{{ inputs.FreeText(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
||||||
{{ inputs.ItemDict(option_name, option, world) }}
|
{{ inputs.ItemDict(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
{{ inputs.OptionList(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
{% elif issubclass(option, Options.LocationSet) and option.verify_location_name %}
|
||||||
{{ inputs.LocationSet(option_name, option, world) }}
|
{{ inputs.LocationSet(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
{% elif issubclass(option, Options.ItemSet) and option.verify_item_name %}
|
||||||
{{ inputs.ItemSet(option_name, option, world) }}
|
{{ inputs.ItemSet(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
{% elif issubclass(option, Options.OptionSet) and option.valid_keys %}
|
||||||
{{ inputs.OptionSet(option_name, option) }}
|
{{ inputs.OptionSet(option_name, option) }}
|
||||||
|
|||||||
@@ -18,7 +18,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for id, name in option.name_lookup.items() %}
|
{% for id, name in option.name_lookup.items() %}
|
||||||
{% if name != 'random' %}
|
{% if name != '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) }}
|
{% if option.default != 'random' %}
|
||||||
|
{{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id 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) }}
|
||||||
@@ -34,16 +38,16 @@
|
|||||||
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 has special meaning, and may fall outside the normal range.
|
The following values have special meanings, 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|replace("_", " ")|title }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% 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 class="add-range-option-button" data-option="{{ option_name }}">Add</button>
|
<button type="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 }}">
|
||||||
@@ -68,7 +72,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 data-option="{{ option_name }}">Add</button>
|
<button type="button" data-option="{{ option_name }}">Add</button>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -85,14 +89,18 @@
|
|||||||
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 data-option="{{ option_name }}">Add</button>
|
<button type="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' %}
|
||||||
{{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }}
|
{% if option.default != 'random' %}
|
||||||
|
{{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id 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) }}
|
||||||
|
|||||||
@@ -1366,28 +1366,28 @@ if "Starcraft 2" in network_data_package["games"]:
|
|||||||
organics_icon_base_url = "https://0rganics.org/archipelago/sc2wol/"
|
organics_icon_base_url = "https://0rganics.org/archipelago/sc2wol/"
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
|
"Starting Minerals": github_icon_base_url + "blizzard/icon-mineral-nobg.png",
|
||||||
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
|
"Starting Vespene": github_icon_base_url + "blizzard/icon-gas-terran-nobg.png",
|
||||||
"Starting Supply": github_icon_base_url + "blizzard/icon-supply-terran_nobg.png",
|
"Starting Supply": github_icon_base_url + "blizzard/icon-supply-terran_nobg.png",
|
||||||
|
|
||||||
"Terran Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png",
|
"Terran Infantry Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel1.png",
|
||||||
"Terran Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png",
|
"Terran Infantry Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel2.png",
|
||||||
"Terran Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png",
|
"Terran Infantry Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryweaponslevel3.png",
|
||||||
"Terran Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png",
|
"Terran Infantry Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel1.png",
|
||||||
"Terran Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png",
|
"Terran Infantry Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel2.png",
|
||||||
"Terran Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png",
|
"Terran Infantry Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-infantryarmorlevel3.png",
|
||||||
"Terran Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png",
|
"Terran Vehicle Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel1.png",
|
||||||
"Terran Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png",
|
"Terran Vehicle Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel2.png",
|
||||||
"Terran Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png",
|
"Terran Vehicle Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleweaponslevel3.png",
|
||||||
"Terran Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png",
|
"Terran Vehicle Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel1.png",
|
||||||
"Terran Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png",
|
"Terran Vehicle Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel2.png",
|
||||||
"Terran Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png",
|
"Terran Vehicle Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-vehicleplatinglevel3.png",
|
||||||
"Terran Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png",
|
"Terran Ship Weapons Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel1.png",
|
||||||
"Terran Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png",
|
"Terran Ship Weapons Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel2.png",
|
||||||
"Terran Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png",
|
"Terran Ship Weapons Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipweaponslevel3.png",
|
||||||
"Terran Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png",
|
"Terran Ship Armor Level 1": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel1.png",
|
||||||
"Terran Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png",
|
"Terran Ship Armor Level 2": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel2.png",
|
||||||
"Terran Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png",
|
"Terran Ship Armor Level 3": github_icon_base_url + "blizzard/btn-upgrade-terran-shipplatinglevel3.png",
|
||||||
|
|
||||||
"Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg",
|
"Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg",
|
||||||
"Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg",
|
"Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg",
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ def get_payload(ctx: ZeldaContext):
|
|||||||
|
|
||||||
|
|
||||||
def reconcile_shops(ctx: ZeldaContext):
|
def reconcile_shops(ctx: ZeldaContext):
|
||||||
checked_location_names = [ctx.location_names.lookup_in_slot(location) for location in ctx.checked_locations]
|
checked_location_names = [ctx.location_names.lookup_in_game(location) for location in ctx.checked_locations]
|
||||||
shops = [location for location in checked_location_names if "Shop" in location]
|
shops = [location for location in checked_location_names if "Shop" in location]
|
||||||
left_slots = [shop for shop in shops if "Left" in shop]
|
left_slots = [shop for shop in shops if "Left" in shop]
|
||||||
middle_slots = [shop for shop in shops if "Middle" in shop]
|
middle_slots = [shop for shop in shops if "Middle" in shop]
|
||||||
@@ -190,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone=
|
|||||||
locations_checked = []
|
locations_checked = []
|
||||||
location = None
|
location = None
|
||||||
for location in ctx.missing_locations:
|
for location in ctx.missing_locations:
|
||||||
location_name = ctx.location_names.lookup_in_slot(location)
|
location_name = ctx.location_names.lookup_in_game(location)
|
||||||
|
|
||||||
if location_name in Locations.overworld_locations and zone == "overworld":
|
if location_name in Locations.overworld_locations and zone == "overworld":
|
||||||
status = locations_array[Locations.major_location_offsets[location_name]]
|
status = locations_array[Locations.major_location_offsets[location_name]]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#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.
|
||||||
@@ -13,7 +14,6 @@ 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,6 +31,27 @@ 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
|
||||||
@@ -185,7 +206,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 std_set[ap_player_t] receivers
|
cdef ap_player_set* 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
|
||||||
@@ -197,13 +218,20 @@ 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
|
||||||
for receiver in slots:
|
receivers = ap_player_set_new(min(1023, slot_count)) # limit top level struct to 16KB
|
||||||
receivers.insert(receiver)
|
if not receivers:
|
||||||
with nogil:
|
raise MemoryError()
|
||||||
for entry in self.entries[:self.entry_count]:
|
try:
|
||||||
if entry.item == item and receivers.count(entry.receiver):
|
for receiver in slots:
|
||||||
with gil:
|
if not ap_player_set_add(receivers, receiver):
|
||||||
yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags
|
raise MemoryError()
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# This file is required to get pyximport to work with C++.
|
# This file is used when doing pyximport
|
||||||
# Switching from std::set to a pure C implementation is still on the table to simplify everything.
|
import os
|
||||||
|
|
||||||
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],
|
||||||
language='c++')
|
depends=["intset.h"],
|
||||||
|
include_dirs=[os.getcwd()],
|
||||||
|
language="c")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
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>:
|
||||||
|
|||||||
@@ -68,21 +68,21 @@ requires:
|
|||||||
|
|
||||||
{%- elif option.options -%}
|
{%- elif option.options -%}
|
||||||
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
||||||
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
{{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|
||||||
{%- if option.name_lookup[option.default] not in option.options %}
|
{%- if option.name_lookup[option.default] not in option.options %}
|
||||||
{{ option.default }}: 50
|
{{ yaml_dump(option.default) }}: 50
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- elif option.default is string %}
|
{%- elif option.default is string %}
|
||||||
{{ option.default }}: 50
|
{{ yaml_dump(option.default) }}: 50
|
||||||
|
|
||||||
{%- elif option.default is iterable and option.default is not mapping %}
|
{%- elif option.default is iterable and option.default is not mapping %}
|
||||||
{{ option.default | list }}
|
{{ option.default | list }}
|
||||||
|
|
||||||
{%- else %}
|
{%- else %}
|
||||||
{{ yaml_dump(option.default) | trim | indent(4, first=false) }}
|
{{ yaml_dump(option.default) | indent(4, first=false) }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{ "\n" }}
|
{{ "\n" }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
/worlds/heretic/ @Daivuk
|
/worlds/heretic/ @Daivuk
|
||||||
|
|
||||||
# Hollow Knight
|
# Hollow Knight
|
||||||
/worlds/hk/ @BadMagic100 @ThePhar
|
/worlds/hk/ @BadMagic100 @qwint
|
||||||
|
|
||||||
# Hylics 2
|
# Hylics 2
|
||||||
/worlds/hylics2/ @TRPG0
|
/worlds/hylics2/ @TRPG0
|
||||||
@@ -87,9 +87,6 @@
|
|||||||
# Lingo
|
# Lingo
|
||||||
/worlds/lingo/ @hatkirby
|
/worlds/lingo/ @hatkirby
|
||||||
|
|
||||||
# Links Awakening DX
|
|
||||||
/worlds/ladx/ @zig-for
|
|
||||||
|
|
||||||
# Lufia II Ancient Cave
|
# Lufia II Ancient Cave
|
||||||
/worlds/lufia2ac/ @el-u
|
/worlds/lufia2ac/ @el-u
|
||||||
/worlds/lufia2ac/docs/ @wordfcuk @el-u
|
/worlds/lufia2ac/docs/ @wordfcuk @el-u
|
||||||
@@ -218,6 +215,8 @@
|
|||||||
# Final Fantasy (1)
|
# Final Fantasy (1)
|
||||||
# /worlds/ff1/
|
# /worlds/ff1/
|
||||||
|
|
||||||
|
# Links Awakening DX
|
||||||
|
# /worlds/ladx/
|
||||||
|
|
||||||
## Disabled Unmaintained Worlds
|
## Disabled Unmaintained Worlds
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,50 @@ class ExampleWorld(World):
|
|||||||
options: ExampleGameOptions
|
options: ExampleGameOptions
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Option Documentation
|
||||||
|
|
||||||
|
Options' [docstrings] are used as their user-facing documentation. They're displayed on the WebHost setup page when a
|
||||||
|
user hovers over the yellow "(?)" icon, and included in the YAML templates generated for each game.
|
||||||
|
|
||||||
|
[docstrings]: /docs/world%20api.md#docstrings
|
||||||
|
|
||||||
|
The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base
|
||||||
|
indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the
|
||||||
|
default for backwards compatibility, world authors are encouraged to write their Option documentation as
|
||||||
|
reStructuredText and enable rich text rendering by setting `World.rich_text_options_doc = True`.
|
||||||
|
|
||||||
|
[reStructuredText]: https://docutils.sourceforge.io/rst.html
|
||||||
|
|
||||||
|
```python
|
||||||
|
from worlds.AutoWorld import WebWorld
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleWebWorld(WebWorld):
|
||||||
|
# Render all this world's options as rich text.
|
||||||
|
rich_text_options_doc = True
|
||||||
|
```
|
||||||
|
|
||||||
|
You can set a single option to use rich or plain text by setting
|
||||||
|
`Option.rich_text_doc`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from Options import Toggle, Range, Choice, PerGameCommonOptions
|
||||||
|
|
||||||
|
|
||||||
|
class Difficulty(Choice):
|
||||||
|
"""Sets overall game difficulty.
|
||||||
|
|
||||||
|
- **Easy:** All enemies die in one hit.
|
||||||
|
- **Normal:** Enemies and the player both have normal health bars.
|
||||||
|
- **Hard:** The player dies in one hit."""
|
||||||
|
display_name = "Difficulty"
|
||||||
|
rich_text_doc = True
|
||||||
|
option_easy = 0
|
||||||
|
option_normal = 1
|
||||||
|
option_hard = 2
|
||||||
|
default = 1
|
||||||
|
```
|
||||||
|
|
||||||
### Option Groups
|
### Option Groups
|
||||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
||||||
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ webhost:
|
|||||||
|
|
||||||
* `options_page` can be changed to a link instead of an AP-generated options page.
|
* `options_page` can be changed to a link instead of an AP-generated options page.
|
||||||
|
|
||||||
|
* `rich_text_options_doc` controls whether [Option documentation] uses plain text (`False`) or rich text (`True`). It
|
||||||
|
defaults to `False`, but world authors are encouraged to set it to `True` for nicer-looking documentation that looks
|
||||||
|
good on both the WebHost and the YAML template.
|
||||||
|
|
||||||
|
[Option documentation]: /docs/options%20api.md#option-documentation
|
||||||
|
|
||||||
* `theme` to be used for your game-specific AP pages. Available themes:
|
* `theme` to be used for your game-specific AP pages. Available themes:
|
||||||
|
|
||||||
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|
| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone |
|
||||||
@@ -450,8 +456,9 @@ In addition, the following methods can be implemented and are called in this ord
|
|||||||
called to place player's regions and their locations into the MultiWorld's regions list.
|
called to place player's regions and their locations into the MultiWorld's regions list.
|
||||||
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
If it's hard to separate, this can be done during `generate_early` or `create_items` as well.
|
||||||
* `create_items(self)`
|
* `create_items(self)`
|
||||||
called to place player's items into the MultiWorld's itempool. After this step all regions
|
called to place player's items into the MultiWorld's itempool. By the end of this step all regions, locations and
|
||||||
and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterward.
|
items have to be in the MultiWorld's regions and itempool. You cannot add or remove items, locations, or regions
|
||||||
|
after this step. Locations cannot be moved to different regions after this step.
|
||||||
* `set_rules(self)`
|
* `set_rules(self)`
|
||||||
called to set access and item rules on locations and entrances.
|
called to set access and item rules on locations and entrances.
|
||||||
* `generate_basic(self)`
|
* `generate_basic(self)`
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ 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"
|
||||||
@@ -216,7 +219,7 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{
|
|||||||
Root: HKCR; Subkey: ".apworld"; ValueData: "{#MyAppName}worlddata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: ".apworld"; ValueData: "{#MyAppName}worlddata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}worlddata"; ValueData: "Archipelago World Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}worlddata"; ValueData: "Archipelago World Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}worlddata\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
Root: HKCR; Subkey: "{#MyAppName}worlddata\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
||||||
Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||||
|
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||||
|
|||||||
135
intset.h
Normal file
135
intset.h
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/* 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
62
kvui.py
@@ -3,6 +3,7 @@ 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
|
||||||
@@ -380,6 +381,57 @@ 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):
|
||||||
@@ -415,7 +467,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 = {}
|
self.log_panels: typing.Dict[str, Widget] = {}
|
||||||
|
|
||||||
# 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"
|
||||||
@@ -499,7 +551,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 = TextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
self.textinput = CommandPromptTextInput(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)
|
||||||
@@ -557,14 +609,18 @@ class GameManager(App):
|
|||||||
|
|
||||||
self.ctx.exit_event.set()
|
self.ctx.exit_event.set()
|
||||||
|
|
||||||
def on_message(self, textinput: TextInput):
|
def on_message(self, textinput: CommandPromptTextInput):
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -292,12 +292,12 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
"""Ensure all state can reach everything and complete the game with the defined options"""
|
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||||
if not (self.run_default_tests and self.constructed):
|
if not (self.run_default_tests and self.constructed):
|
||||||
return
|
return
|
||||||
with self.subTest("Game", game=self.game):
|
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||||
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
|
excluded = self.multiworld.worlds[self.player].options.exclude_locations.value
|
||||||
state = self.multiworld.get_all_state(False)
|
state = self.multiworld.get_all_state(False)
|
||||||
for location in self.multiworld.get_locations():
|
for location in self.multiworld.get_locations():
|
||||||
if location.name not in excluded:
|
if location.name not in excluded:
|
||||||
with self.subTest("Location should be reached", location=location):
|
with self.subTest("Location should be reached", location=location.name):
|
||||||
reachable = location.can_reach(state)
|
reachable = location.can_reach(state)
|
||||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||||
with self.subTest("Beatable"):
|
with self.subTest("Beatable"):
|
||||||
@@ -308,7 +308,7 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
"""Ensure empty state can reach at least one location with the defined options"""
|
"""Ensure empty state can reach at least one location with the defined options"""
|
||||||
if not (self.run_default_tests and self.constructed):
|
if not (self.run_default_tests and self.constructed):
|
||||||
return
|
return
|
||||||
with self.subTest("Game", game=self.game):
|
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||||
state = CollectionState(self.multiworld)
|
state = CollectionState(self.multiworld)
|
||||||
locations = self.multiworld.get_reachable_locations(state, self.player)
|
locations = self.multiworld.get_reachable_locations(state, self.player)
|
||||||
self.assertGreater(len(locations), 0,
|
self.assertGreater(len(locations), 0,
|
||||||
@@ -329,7 +329,7 @@ class WorldTestBase(unittest.TestCase):
|
|||||||
for n in range(len(locations) - 1, -1, -1):
|
for n in range(len(locations) - 1, -1, -1):
|
||||||
if locations[n].can_reach(state):
|
if locations[n].can_reach(state):
|
||||||
sphere.append(locations.pop(n))
|
sphere.append(locations.pop(n))
|
||||||
self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal",
|
self.assertTrue(sphere or self.multiworld.worlds[1].options.accessibility == "minimal",
|
||||||
f"Unreachable locations: {locations}")
|
f"Unreachable locations: {locations}")
|
||||||
if not sphere:
|
if not sphere:
|
||||||
break
|
break
|
||||||
|
|||||||
49
test/cpp/CMakeLists.txt
Normal file
49
test/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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()
|
||||||
32
test/cpp/README.md
Normal file
32
test/cpp/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
4
test/cpp/intset/CMakeLists.txt
Normal file
4
test/cpp/intset/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
target_sources(test_default
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/test_intset.cpp
|
||||||
|
)
|
||||||
105
test/cpp/intset/test_intset.cpp
Normal file
105
test/cpp/intset/test_intset.cpp
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ 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")
|
||||||
@@ -60,6 +61,10 @@ 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.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -84,3 +85,4 @@ 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"])
|
||||||
|
|||||||
@@ -41,15 +41,15 @@ class TestBase(unittest.TestCase):
|
|||||||
state = multiworld.get_all_state(False)
|
state = multiworld.get_all_state(False)
|
||||||
for location in multiworld.get_locations():
|
for location in multiworld.get_locations():
|
||||||
if location.name not in excluded:
|
if location.name not in excluded:
|
||||||
with self.subTest("Location should be reached", location=location):
|
with self.subTest("Location should be reached", location=location.name):
|
||||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||||
|
|
||||||
for region in multiworld.get_regions():
|
for region in multiworld.get_regions():
|
||||||
if region.name in unreachable_regions:
|
if region.name in unreachable_regions:
|
||||||
with self.subTest("Region should be unreachable", region=region):
|
with self.subTest("Region should be unreachable", region=region.name):
|
||||||
self.assertFalse(region.can_reach(state))
|
self.assertFalse(region.can_reach(state))
|
||||||
else:
|
else:
|
||||||
with self.subTest("Region should be reached", region=region):
|
with self.subTest("Region should be reached", region=region.name):
|
||||||
self.assertTrue(region.can_reach(state))
|
self.assertTrue(region.can_reach(state))
|
||||||
|
|
||||||
with self.subTest("Completion Condition"):
|
with self.subTest("Completion Condition"):
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ def _generate_local_inner(games: Iterable[str],
|
|||||||
with TemporaryDirectory() as players_dir:
|
with TemporaryDirectory() as players_dir:
|
||||||
with TemporaryDirectory() as output_dir:
|
with TemporaryDirectory() as output_dir:
|
||||||
import Generate
|
import Generate
|
||||||
|
import Main
|
||||||
|
|
||||||
for n, game in enumerate(games, 1):
|
for n, game in enumerate(games, 1):
|
||||||
player_path = Path(players_dir) / f"{n}.yaml"
|
player_path = Path(players_dir) / f"{n}.yaml"
|
||||||
@@ -42,7 +43,7 @@ def _generate_local_inner(games: Iterable[str],
|
|||||||
sys.argv = [sys.argv[0], "--seed", str(hash(tuple(games))),
|
sys.argv = [sys.argv[0], "--seed", str(hash(tuple(games))),
|
||||||
"--player_files_path", players_dir,
|
"--player_files_path", players_dir,
|
||||||
"--outputpath", output_dir]
|
"--outputpath", output_dir]
|
||||||
Generate.main()
|
Main.main(*Generate.main())
|
||||||
output_files = list(Path(output_dir).glob('*.zip'))
|
output_files = list(Path(output_dir).glob('*.zip'))
|
||||||
assert len(output_files) == 1
|
assert len(output_files) == 1
|
||||||
final_file = dest / output_files[0].name
|
final_file = dest / output_files[0].name
|
||||||
|
|||||||
@@ -66,12 +66,19 @@ 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:
|
||||||
response = app_client.get(f"/room/{room_id}")
|
try:
|
||||||
|
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:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# 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
|
||||||
@@ -7,6 +8,8 @@ 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),
|
||||||
@@ -24,6 +27,9 @@ sample_data: RawLocations = {
|
|||||||
3: {
|
3: {
|
||||||
9: (99, 4, 0),
|
9: (99, 4, 0),
|
||||||
},
|
},
|
||||||
|
5: {
|
||||||
|
9: (99, 5, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
empty_state: State = {
|
empty_state: State = {
|
||||||
@@ -45,14 +51,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), 4)
|
self.assertEqual(len(self.store), 5)
|
||||||
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[5]
|
_ = self.store[6]
|
||||||
locations = self.store[1] # no Exception
|
locations = self.store[1] # no Exception
|
||||||
with self.assertRaises(KeyError):
|
with self.assertRaises(KeyError):
|
||||||
_ = locations[7]
|
_ = locations[7]
|
||||||
@@ -71,7 +77,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])
|
self.assertEqual(sorted(self.store), [1, 2, 3, 4, 5])
|
||||||
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]))
|
||||||
@@ -85,13 +91,26 @@ 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)), [])
|
||||||
self.assertEqual(sorted(self.store.find_item({5}, 99)), [])
|
# valid matches
|
||||||
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}})
|
||||||
@@ -196,18 +215,20 @@ class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
|
@unittest.skipIf(LocationStore is _LocationStore and not ci, "_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, "_speedups not available")
|
@unittest.skipIf(LocationStore is _LocationStore and not ci, "_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()
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from pathlib import Path
|
|||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
import Generate
|
import Generate
|
||||||
|
import Main
|
||||||
|
|
||||||
|
|
||||||
class TestGenerateMain(unittest.TestCase):
|
class TestGenerateMain(unittest.TestCase):
|
||||||
@@ -58,7 +59,7 @@ class TestGenerateMain(unittest.TestCase):
|
|||||||
'--player_files_path', str(self.abs_input_dir),
|
'--player_files_path', str(self.abs_input_dir),
|
||||||
'--outputpath', self.output_tempdir.name]
|
'--outputpath', self.output_tempdir.name]
|
||||||
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
|
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
|
||||||
Generate.main()
|
Main.main(*Generate.main())
|
||||||
|
|
||||||
self.assertOutput(self.output_tempdir.name)
|
self.assertOutput(self.output_tempdir.name)
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ class TestGenerateMain(unittest.TestCase):
|
|||||||
'--player_files_path', str(self.rel_input_dir),
|
'--player_files_path', str(self.rel_input_dir),
|
||||||
'--outputpath', self.output_tempdir.name]
|
'--outputpath', self.output_tempdir.name]
|
||||||
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
|
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}')
|
||||||
Generate.main()
|
Main.main(*Generate.main())
|
||||||
|
|
||||||
self.assertOutput(self.output_tempdir.name)
|
self.assertOutput(self.output_tempdir.name)
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ class TestGenerateMain(unittest.TestCase):
|
|||||||
sys.argv = [sys.argv[0], '--seed', '0',
|
sys.argv = [sys.argv[0], '--seed', '0',
|
||||||
'--outputpath', self.output_tempdir.name]
|
'--outputpath', self.output_tempdir.name]
|
||||||
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
|
print(f'Testing Generate.py {sys.argv} in {os.getcwd()}, player_files_path={self.yaml_input_dir}')
|
||||||
Generate.main()
|
Main.main(*Generate.main())
|
||||||
finally:
|
finally:
|
||||||
user_path.cached_path = user_path_backup
|
user_path.cached_path = user_path_backup
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
if title_name in prebuilt_options:
|
assert title_name not in prebuilt_options or title_name == group.name, \
|
||||||
group.name = title_name
|
f"Prebuilt group name \"{group.name}\" must be \"{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), \
|
||||||
@@ -223,6 +223,21 @@ class WebWorld(metaclass=WebWorldRegister):
|
|||||||
option_groups: ClassVar[List[OptionGroup]] = []
|
option_groups: ClassVar[List[OptionGroup]] = []
|
||||||
"""Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options"."""
|
"""Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options"."""
|
||||||
|
|
||||||
|
rich_text_options_doc = False
|
||||||
|
"""Whether the WebHost should render Options' docstrings as rich text.
|
||||||
|
|
||||||
|
If this is True, Options' docstrings are interpreted as reStructuredText_,
|
||||||
|
the standard Python markup format. In the WebHost, they're rendered to HTML
|
||||||
|
so that lists, emphasis, and other rich text features are displayed
|
||||||
|
properly.
|
||||||
|
|
||||||
|
If this is False, the docstrings are instead interpreted as plain text, and
|
||||||
|
displayed as-is on the WebHost with whitespace preserved. For backwards
|
||||||
|
compatibility, this is the default.
|
||||||
|
|
||||||
|
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
|
||||||
|
"""
|
||||||
|
|
||||||
location_descriptions: Dict[str, str] = {}
|
location_descriptions: Dict[str, str] = {}
|
||||||
"""An optional map from location names (or location group names) to brief descriptions for users."""
|
"""An optional map from location names (or location group names) to brief descriptions for users."""
|
||||||
|
|
||||||
@@ -265,7 +280,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required_server_version: Tuple[int, int, int] = (0, 2, 4)
|
required_server_version: Tuple[int, int, int] = (0, 5, 0)
|
||||||
"""update this if the resulting multidata breaks forward-compatibility of the server"""
|
"""update this if the resulting multidata breaks forward-compatibility of the server"""
|
||||||
|
|
||||||
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
|
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
import importlib.util
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -107,8 +108,9 @@ 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():
|
||||||
init_file_path = os.path.join(entry.path, '__init__.py')
|
if os.path.isfile(os.path.join(entry.path, '__init__.py')):
|
||||||
if os.path.isfile(init_file_path):
|
world_sources.append(WorldSource(file_name, relative=relative))
|
||||||
|
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")
|
||||||
@@ -126,3 +128,4 @@ from .AutoWorld import AutoWorldRegister
|
|||||||
network_data_package: DataPackage = {
|
network_data_package: DataPackage = {
|
||||||
"games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},
|
"games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ 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")
|
||||||
@@ -116,7 +121,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_all()
|
kill_switch.kill(self)
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
|
from dataclasses import dataclass
|
||||||
|
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
|
||||||
|
|
||||||
|
|
||||||
class FreeincarnateMax(Range):
|
class FreeincarnateMax(Range):
|
||||||
@@ -223,22 +224,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,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +25,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(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||||
|
|
||||||
menu = Region("Menu", player, multiworld)
|
menu = Region("Menu", player, multiworld)
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
|
|||||||
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 = multiworld.dragon_slay_check[player].value
|
dragon_slay_check = options.dragon_slay_check.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():
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from worlds.adventure import location_table
|
from .Options import BatLogic, DifficultySwitchB
|
||||||
from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA
|
|
||||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||||
from BaseClasses import LocationProgressType
|
|
||||||
|
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
def set_rules(self) -> None:
|
||||||
world = self.multiworld
|
world = self.multiworld
|
||||||
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
|
use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic
|
||||||
|
|
||||||
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
||||||
lambda state: state.has("Yellow Key", self.player))
|
lambda state: state.has("Yellow Key", self.player))
|
||||||
@@ -28,7 +26,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 = world.dragon_slay_check[self.player].value
|
dragon_slay_check = self.options.dragon_slay_check.value
|
||||||
if dragon_slay_check:
|
if dragon_slay_check:
|
||||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ 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 adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
from .Options import 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
|
||||||
@@ -109,7 +110,7 @@ class AdventureWorld(World):
|
|||||||
game: ClassVar[str] = "Adventure"
|
game: ClassVar[str] = "Adventure"
|
||||||
web: ClassVar[WebWorld] = AdventureWeb()
|
web: ClassVar[WebWorld] = AdventureWeb()
|
||||||
|
|
||||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
|
options_dataclass = AdventureOptions
|
||||||
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()}
|
||||||
@@ -149,18 +150,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.multiworld.dragon_rando_type[self.player].value
|
self.dragon_rando_type = self.options.dragon_rando_type.value
|
||||||
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
|
self.dragon_slay_check = self.options.dragon_slay_check.value
|
||||||
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
|
self.connector_multi_slot = self.options.connector_multi_slot.value
|
||||||
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
|
self.yorgle_speed = self.options.yorgle_speed.value
|
||||||
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
|
self.yorgle_min_speed = self.options.yorgle_min_speed.value
|
||||||
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
|
self.grundle_speed = self.options.grundle_speed.value
|
||||||
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
|
self.grundle_min_speed = self.options.grundle_min_speed.value
|
||||||
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
|
self.rhindle_speed = self.options.rhindle_speed.value
|
||||||
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
|
self.rhindle_min_speed = self.options.rhindle_min_speed.value
|
||||||
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
|
self.difficulty_switch_a = self.options.difficulty_switch_a.value
|
||||||
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
|
self.difficulty_switch_b = self.options.difficulty_switch_b.value
|
||||||
self.start_castle = self.multiworld.start_castle[self.player].value
|
self.start_castle = self.options.start_castle.value
|
||||||
self.created_items = 0
|
self.created_items = 0
|
||||||
|
|
||||||
if self.dragon_slay_check == 0:
|
if self.dragon_slay_check == 0:
|
||||||
@@ -227,7 +228,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.multiworld.freeincarnate_max[self.player].value
|
freeincarnate_max = self.options.freeincarnate_max.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
|
||||||
@@ -247,7 +248,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.multiworld, self.player, self.dragon_rooms)
|
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
||||||
|
|
||||||
set_rules = set_rules
|
set_rules = set_rules
|
||||||
|
|
||||||
@@ -354,7 +355,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.multiworld.bat_logic[self.player].value
|
bat_logic: int = self.options.bat_logic.value
|
||||||
try:
|
try:
|
||||||
rom_deltas: { int, int } = {}
|
rom_deltas: { int, int } = {}
|
||||||
self.place_dragons(rom_deltas)
|
self.place_dragons(rom_deltas)
|
||||||
@@ -421,7 +422,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.multiworld.connector_multi_slot[self.player].value:
|
if self.options.connector_multi_slot.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
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
|||||||
def new_check(location_id):
|
def new_check(location_id):
|
||||||
new_locations.append(location_id)
|
new_locations.append(location_id)
|
||||||
ctx.locations_checked.add(location_id)
|
ctx.locations_checked.add(location_id)
|
||||||
location = ctx.location_names.lookup_in_slot(location_id)
|
location = ctx.location_names.lookup_in_game(location_id)
|
||||||
snes_logger.info(
|
snes_logger.info(
|
||||||
f'New Check: {location} ' +
|
f'New Check: {location} ' +
|
||||||
f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' +
|
f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' +
|
||||||
@@ -552,7 +552,7 @@ class ALTTPSNIClient(SNIClient):
|
|||||||
item = ctx.items_received[recv_index]
|
item = ctx.items_received[recv_index]
|
||||||
recv_index += 1
|
recv_index += 1
|
||||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'),
|
color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
|
||||||
color(ctx.player_names[item.player], 'yellow'),
|
color(ctx.player_names[item.player], 'yellow'),
|
||||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
||||||
|
|
||||||
@@ -682,7 +682,7 @@ def get_alttp_settings(romfile: str):
|
|||||||
|
|
||||||
if 'yes' in choice:
|
if 'yes' in choice:
|
||||||
import LttPAdjuster
|
import LttPAdjuster
|
||||||
from worlds.alttp.Rom import get_base_rom_path
|
from .Rom import get_base_rom_path
|
||||||
last_settings.rom = romfile
|
last_settings.rom = romfile
|
||||||
last_settings.baserom = get_base_rom_path()
|
last_settings.baserom = get_base_rom_path()
|
||||||
last_settings.world = None
|
last_settings.world = None
|
||||||
|
|||||||
@@ -1437,7 +1437,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
|
|||||||
invalid_cave_connections = defaultdict(set)
|
invalid_cave_connections = defaultdict(set)
|
||||||
|
|
||||||
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
||||||
from worlds.alttp import OverworldGlitchRules
|
from . import OverworldGlitchRules
|
||||||
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
|
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
|
||||||
invalid_connections[entrance] = set()
|
invalid_connections[entrance] = set()
|
||||||
if entrance in must_be_exits:
|
if entrance in must_be_exits:
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ class LTTPBosses(PlandoBosses):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_place_boss(cls, boss: str, location: str) -> bool:
|
def can_place_boss(cls, boss: str, location: str) -> bool:
|
||||||
from worlds.alttp.Bosses import can_place_boss
|
from .Bosses import can_place_boss
|
||||||
level = ''
|
level = ''
|
||||||
words = location.split(" ")
|
words = location.split(" ")
|
||||||
if words[-1] in ("top", "middle", "bottom"):
|
if words[-1] in ("top", "middle", "bottom"):
|
||||||
|
|||||||
@@ -220,26 +220,7 @@ def get_invalid_bunny_revival_dungeons():
|
|||||||
yield 'Sanctuary'
|
yield 'Sanctuary'
|
||||||
|
|
||||||
|
|
||||||
def no_logic_rules(world, player):
|
|
||||||
"""
|
|
||||||
Add OWG transitions to no logic player's world
|
|
||||||
"""
|
|
||||||
create_no_logic_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
|
|
||||||
create_no_logic_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
|
|
||||||
|
|
||||||
# Glitched speed drops.
|
|
||||||
create_no_logic_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'))
|
|
||||||
|
|
||||||
# Mirror clip spots.
|
|
||||||
if world.mode[player] != 'inverted':
|
|
||||||
create_no_logic_connections(player, world, get_mirror_clip_spots_dw())
|
|
||||||
create_no_logic_connections(player, world, get_mirror_offset_spots_dw())
|
|
||||||
else:
|
|
||||||
create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player))
|
|
||||||
|
|
||||||
|
|
||||||
def overworld_glitch_connections(world, player):
|
def overworld_glitch_connections(world, player):
|
||||||
|
|
||||||
# Boots-accessible locations.
|
# Boots-accessible locations.
|
||||||
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
|
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
|
||||||
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
|
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str,
|
|||||||
|
|
||||||
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
|
||||||
exits=None):
|
exits=None):
|
||||||
from worlds.alttp.SubClasses import ALttPLocation
|
from .SubClasses import ALttPLocation
|
||||||
ret = LTTPRegion(name, type, hint, player, world)
|
ret = LTTPRegion(name, type, hint, player, world)
|
||||||
if exits:
|
if exits:
|
||||||
for exit in exits:
|
for exit in exits:
|
||||||
@@ -760,7 +760,7 @@ location_table: typing.Dict[str,
|
|||||||
'Turtle Rock - Prize': (
|
'Turtle Rock - Prize': (
|
||||||
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||||
|
|
||||||
from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
|
from .Shops import shop_table_by_location_id, shop_table_by_location
|
||||||
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
||||||
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
|
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
|
||||||
lookup_id_to_name.update(shop_table_by_location_id)
|
lookup_id_to_name.update(shop_table_by_location_id)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from . import OverworldGlitchRules
|
|||||||
from .Bosses import GanonDefeatRule
|
from .Bosses import GanonDefeatRule
|
||||||
from .Items import item_factory, item_name_groups, item_table, progression_items
|
from .Items import item_factory, item_name_groups, item_table, progression_items
|
||||||
from .Options import small_key_shuffle
|
from .Options import small_key_shuffle
|
||||||
from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules
|
from .OverworldGlitchRules import overworld_glitches_rules
|
||||||
from .Regions import LTTPRegionType, location_table
|
from .Regions import LTTPRegionType, location_table
|
||||||
from .StateHelpers import (can_extend_magic, can_kill_most_things,
|
from .StateHelpers import (can_extend_magic, can_kill_most_things,
|
||||||
can_lift_heavy_rocks, can_lift_rocks,
|
can_lift_heavy_rocks, can_lift_rocks,
|
||||||
@@ -33,7 +33,6 @@ def set_rules(world):
|
|||||||
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
||||||
|
|
||||||
if world.players == 1:
|
if world.players == 1:
|
||||||
no_logic_rules(world, player)
|
|
||||||
for exit in world.get_region('Menu', player).exits:
|
for exit in world.get_region('Menu', player).exits:
|
||||||
exit.hide_path = True
|
exit.hide_path = True
|
||||||
return
|
return
|
||||||
@@ -406,16 +405,14 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||||
|
|
||||||
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||||
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
||||||
|
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
|
||||||
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
||||||
|
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
||||||
|
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||||
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
||||||
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||||
|
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ class TestThievesTown(TestDungeon):
|
|||||||
|
|
||||||
["Thieves' Town - Blind's Cell", False, []],
|
["Thieves' Town - Blind's Cell", False, []],
|
||||||
["Thieves' Town - Blind's Cell", False, [], ['Big Key (Thieves Town)']],
|
["Thieves' Town - Blind's Cell", False, [], ['Big Key (Thieves Town)']],
|
||||||
["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)']],
|
["Thieves' Town - Blind's Cell", False, [], ['Small Key (Thieves Town)']],
|
||||||
|
["Thieves' Town - Blind's Cell", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']],
|
||||||
|
|
||||||
["Thieves' Town - Boss", False, []],
|
["Thieves' Town - Boss", False, []],
|
||||||
["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']],
|
["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']],
|
||||||
|
|||||||
@@ -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 Cathedral flower tube entrance": 698128,
|
"Mithalas City, urn in the Castle 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, Jelly Egg": 698185,
|
"Kelp Forest top left area, 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 in the top of the waterfall": 698080,
|
"The Veil top right area, bulb at the top of the waterfall": 698080,
|
||||||
"The Veil top right area, Transturtle": 698210,
|
"The Veil top right area, Transturtle": 698210,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -771,6 +771,7 @@ 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))
|
||||||
|
|
||||||
@@ -983,7 +984,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 Cathedral flower tube entrance", self.player),
|
add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle 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))
|
||||||
@@ -1023,7 +1024,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 in the top of the waterfall", self.player),
|
add_rule(self.multiworld.get_location("The Veil top right area, bulb at 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:
|
||||||
@@ -1175,7 +1176,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 in the top of the waterfall",
|
self.multiworld.get_location("The Veil top right area, bulb at 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",
|
||||||
|
|||||||
@@ -167,14 +167,10 @@ 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 in precollected:
|
if name not in self.exclude:
|
||||||
precollected.remove(name)
|
for i in range(data.count):
|
||||||
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
item = self.create_item(name)
|
||||||
else:
|
self.multiworld.itempool.append(item)
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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 Cathedral flower tube entrance",
|
"Mithalas City, urn in the Castle 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, Jelly Egg",
|
"Kelp Forest top left area, 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 in the top of the waterfall",
|
"The Veil top right area, bulb at 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",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the beast form
|
Description: Unit test used to test accessibility of locations with and without the beast form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class BeastFormAccessTest(AquariaTestBase):
|
class BeastFormAccessTest(AquariaTestBase):
|
||||||
@@ -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 Cathedral flower tube entrance",
|
"Mithalas City, urn in the Castle 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 in the top of the waterfall",
|
"The Veil top right area, bulb at 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",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Unit test used to test accessibility of locations with and without
|
|||||||
under rock needing bind song option)
|
under rock needing bind song option)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase, after_home_water_locations
|
from . import AquariaTestBase, after_home_water_locations
|
||||||
|
|
||||||
|
|
||||||
class BindSongAccessTest(AquariaTestBase):
|
class BindSongAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ Description: Unit test used to test accessibility of locations with and without
|
|||||||
under rock needing bind song option)
|
under rock needing bind song option)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
from worlds.aquaria.test.test_bind_song_access import after_home_water_locations
|
from .test_bind_song_access import after_home_water_locations
|
||||||
|
|
||||||
|
|
||||||
class BindSongOptionAccessTest(AquariaTestBase):
|
class BindSongOptionAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test accessibility of region with the home water confine via option
|
Description: Unit test used to test accessibility of region with the home water confine via option
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class ConfinedHomeWaterAccessTest(AquariaTestBase):
|
class ConfinedHomeWaterAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the dual song
|
Description: Unit test used to test accessibility of locations with and without the dual song
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class LiAccessTest(AquariaTestBase):
|
class LiAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Unit test used to test accessibility of locations with and without
|
|||||||
energy form option)
|
energy form option)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class EnergyFormAccessTest(AquariaTestBase):
|
class EnergyFormAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the fish form
|
Description: Unit test used to test accessibility of locations with and without the fish form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class FishFormAccessTest(AquariaTestBase):
|
class FishFormAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without Li
|
Description: Unit test used to test accessibility of locations with and without Li
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class LiAccessTest(AquariaTestBase):
|
class LiAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
|
Description: Unit test used to test accessibility of locations with and without a light (Dumbo pet or sun form)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class LightAccessTest(AquariaTestBase):
|
class LightAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the nature form
|
Description: Unit test used to test accessibility of locations with and without the nature form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class NatureFormAccessTest(AquariaTestBase):
|
class NatureFormAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
|
Description: Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
from BaseClasses import ItemClassification
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
|
|
||||||
@@ -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 in the top of the waterfall",
|
"The Veil top right area, bulb at 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",
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
|
Description: Unit test used to test that progression items can be put in hard or hidden locations when option disabled
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
from BaseClasses import ItemClassification
|
|
||||||
|
|
||||||
|
|
||||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||||
@@ -30,7 +29,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 in the top of the waterfall",
|
"The Veil top right area, bulb at 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",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the spirit form
|
Description: Unit test used to test accessibility of locations with and without the spirit form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class SpiritFormAccessTest(AquariaTestBase):
|
class SpiritFormAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Thu, 18 Apr 2024 18:45:56 +0000
|
|||||||
Description: Unit test used to test accessibility of locations with and without the sun form
|
Description: Unit test used to test accessibility of locations with and without the sun form
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class SunFormAccessTest(AquariaTestBase):
|
class SunFormAccessTest(AquariaTestBase):
|
||||||
@@ -18,6 +18,9 @@ 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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Description: Unit test used to test accessibility of region with the unconfined
|
|||||||
turtle and energy door
|
turtle and energy door
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
|
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door
|
Description: Unit test used to test accessibility of region with the unconfined home water option via the energy door
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
|
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Date: Fri, 03 May 2024 14:07:35 +0000
|
|||||||
Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle
|
Description: Unit test used to test accessibility of region with the unconfined home water option via transturtle
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from worlds.aquaria.test import AquariaTestBase
|
from . import AquariaTestBase
|
||||||
|
|
||||||
|
|
||||||
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
|
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class Castlevania64Client(BizHawkClient):
|
|||||||
text_color = bytearray([0xA2, 0x0B])
|
text_color = bytearray([0xA2, 0x0B])
|
||||||
else:
|
else:
|
||||||
text_color = bytearray([0xA2, 0x02])
|
text_color = bytearray([0xA2, 0x02])
|
||||||
received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_slot(next_item.item)}\n"
|
received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_game(next_item.item)}\n"
|
||||||
f"from {ctx.player_names[next_item.player]}", 96)
|
f"from {ctx.player_names[next_item.player]}", 96)
|
||||||
await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
||||||
[(0x389BE1, [next_item.item & 0xFF], "RDRAM"),
|
[(0x389BE1, [next_item.item & 0xFF], "RDRAM"),
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class DKC3SNIClient(SNIClient):
|
|||||||
return
|
return
|
||||||
|
|
||||||
new_checks = []
|
new_checks = []
|
||||||
from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
from .Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
||||||
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
|
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
|
||||||
for loc_id, loc_data in location_rom_data.items():
|
for loc_id, loc_data in location_rom_data.items():
|
||||||
if loc_id not in ctx.locations_checked:
|
if loc_id not in ctx.locations_checked:
|
||||||
@@ -86,7 +86,7 @@ class DKC3SNIClient(SNIClient):
|
|||||||
|
|
||||||
for new_check_id in new_checks:
|
for new_check_id in new_checks:
|
||||||
ctx.locations_checked.add(new_check_id)
|
ctx.locations_checked.add(new_check_id)
|
||||||
location = ctx.location_names.lookup_in_slot(new_check_id)
|
location = ctx.location_names.lookup_in_game(new_check_id)
|
||||||
snes_logger.info(
|
snes_logger.info(
|
||||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
||||||
@@ -99,7 +99,7 @@ class DKC3SNIClient(SNIClient):
|
|||||||
item = ctx.items_received[recv_index]
|
item = ctx.items_received[recv_index]
|
||||||
recv_index += 1
|
recv_index += 1
|
||||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'),
|
color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
|
||||||
color(ctx.player_names[item.player], 'yellow'),
|
color(ctx.player_names[item.player], 'yellow'),
|
||||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
||||||
|
|
||||||
|
|||||||
@@ -60,17 +60,18 @@ class DOOM2World(World):
|
|||||||
# Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1.
|
# Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1.
|
||||||
# The ratio have been tweaked seem, and feel good.
|
# The ratio have been tweaked seem, and feel good.
|
||||||
items_ratio: Dict[str, float] = {
|
items_ratio: Dict[str, float] = {
|
||||||
"Armor": 41,
|
"Armor": 39,
|
||||||
"Mega Armor": 25,
|
"Mega Armor": 23,
|
||||||
"Berserk": 12,
|
"Berserk": 11,
|
||||||
"Invulnerability": 10,
|
"Invulnerability": 10,
|
||||||
"Partial invisibility": 18,
|
"Partial invisibility": 18,
|
||||||
"Supercharge": 28,
|
"Supercharge": 26,
|
||||||
"Medikit": 15,
|
"Medikit": 15,
|
||||||
"Box of bullets": 13,
|
"Box of bullets": 13,
|
||||||
"Box of rockets": 13,
|
"Box of rockets": 13,
|
||||||
"Box of shotgun shells": 13,
|
"Box of shotgun shells": 13,
|
||||||
"Energy cell pack": 10
|
"Energy cell pack": 10,
|
||||||
|
"Megasphere": 7
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, multiworld: MultiWorld, player: int):
|
def __init__(self, multiworld: MultiWorld, player: int):
|
||||||
@@ -233,6 +234,7 @@ class DOOM2World(World):
|
|||||||
self.create_ratioed_items("Invulnerability", itempool)
|
self.create_ratioed_items("Invulnerability", itempool)
|
||||||
self.create_ratioed_items("Partial invisibility", itempool)
|
self.create_ratioed_items("Partial invisibility", itempool)
|
||||||
self.create_ratioed_items("Supercharge", itempool)
|
self.create_ratioed_items("Supercharge", itempool)
|
||||||
|
self.create_ratioed_items("Megasphere", itempool)
|
||||||
|
|
||||||
while len(itempool) < self.location_count:
|
while len(itempool) < self.location_count:
|
||||||
itempool.append(self.create_item(self.get_filler_item_name()))
|
itempool.append(self.create_item(self.get_filler_item_name()))
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ async def game_watcher(ctx: FactorioContext):
|
|||||||
if ctx.locations_checked != research_data:
|
if ctx.locations_checked != research_data:
|
||||||
bridge_logger.debug(
|
bridge_logger.debug(
|
||||||
f"New researches done: "
|
f"New researches done: "
|
||||||
f"{[ctx.location_names.lookup_in_slot(rid) for rid in research_data - ctx.locations_checked]}")
|
f"{[ctx.location_names.lookup_in_game(rid) for rid in research_data - ctx.locations_checked]}")
|
||||||
ctx.locations_checked = research_data
|
ctx.locations_checked = research_data
|
||||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||||
death_link_tick = data.get("death_link_tick", 0)
|
death_link_tick = data.get("death_link_tick", 0)
|
||||||
@@ -360,7 +360,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||||
item_id = transfer_item.item
|
item_id = transfer_item.item
|
||||||
player_name = ctx.player_names[transfer_item.player]
|
player_name = ctx.player_names[transfer_item.player]
|
||||||
item_name = ctx.item_names.lookup_in_slot(item_id)
|
item_name = ctx.item_names.lookup_in_game(item_id)
|
||||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
||||||
commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}"
|
commands[ctx.send_index] = f"/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}"
|
||||||
ctx.send_index += 1
|
ctx.send_index += 1
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ Some steps also assume use of Windows, so may vary with your OS.
|
|||||||
|
|
||||||
## Installing the Archipelago software
|
## Installing the Archipelago software
|
||||||
|
|
||||||
The most recent public release of Archipelago can be found on the GitHub Releases page:
|
The most recent public release of Archipelago can be found on GitHub:
|
||||||
[Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases).
|
[Archipelago Lastest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
|
||||||
|
|
||||||
Run the exe file, and after accepting the license agreement you will be asked which components you would like to
|
Run the exe file, and after accepting the license agreement you will be asked which components you would like to
|
||||||
install.
|
install.
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class HereticWorld(World):
|
|||||||
"Tome of Power": 16,
|
"Tome of Power": 16,
|
||||||
"Silver Shield": 10,
|
"Silver Shield": 10,
|
||||||
"Enchanted Shield": 5,
|
"Enchanted Shield": 5,
|
||||||
|
"Torch": 5,
|
||||||
"Morph Ovum": 3,
|
"Morph Ovum": 3,
|
||||||
"Mystic Urn": 2,
|
"Mystic Urn": 2,
|
||||||
"Chaos Device": 1,
|
"Chaos Device": 1,
|
||||||
@@ -242,6 +243,7 @@ class HereticWorld(World):
|
|||||||
self.create_ratioed_items("Mystic Urn", itempool)
|
self.create_ratioed_items("Mystic Urn", itempool)
|
||||||
self.create_ratioed_items("Ring of Invincibility", itempool)
|
self.create_ratioed_items("Ring of Invincibility", itempool)
|
||||||
self.create_ratioed_items("Shadowsphere", itempool)
|
self.create_ratioed_items("Shadowsphere", itempool)
|
||||||
|
self.create_ratioed_items("Torch", itempool)
|
||||||
self.create_ratioed_items("Timebomb of the Ancients", itempool)
|
self.create_ratioed_items("Timebomb of the Ancients", itempool)
|
||||||
self.create_ratioed_items("Tome of Power", itempool)
|
self.create_ratioed_items("Tome of Power", itempool)
|
||||||
self.create_ratioed_items("Silver Shield", itempool)
|
self.create_ratioed_items("Silver Shield", itempool)
|
||||||
|
|||||||
@@ -64,3 +64,4 @@ 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']
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ class KDL3SNIClient(SNIClient):
|
|||||||
item = ctx.items_received[recv_amount]
|
item = ctx.items_received[recv_amount]
|
||||||
recv_amount += 1
|
recv_amount += 1
|
||||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'),
|
color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
|
||||||
color(ctx.player_names[item.player], 'yellow'),
|
color(ctx.player_names[item.player], 'yellow'),
|
||||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
|
ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
|
||||||
|
|
||||||
@@ -415,7 +415,7 @@ class KDL3SNIClient(SNIClient):
|
|||||||
|
|
||||||
for new_check_id in new_checks:
|
for new_check_id in new_checks:
|
||||||
ctx.locations_checked.add(new_check_id)
|
ctx.locations_checked.add(new_check_id)
|
||||||
location = ctx.location_names.lookup_in_slot(new_check_id)
|
location = ctx.location_names.lookup_in_game(new_check_id)
|
||||||
snes_logger.info(
|
snes_logger.info(
|
||||||
f'New Check: {location} ({len(ctx.locations_checked)}/'
|
f'New Check: {location} ({len(ctx.locations_checked)}/'
|
||||||
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import importlib.machinery
|
|||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .romTables import ROMWithTables
|
from .romTables import ROMWithTables
|
||||||
from . import assembler
|
from . import assembler
|
||||||
@@ -67,10 +68,14 @@ from BaseClasses import ItemClassification
|
|||||||
from ..Locations import LinksAwakeningLocation
|
from ..Locations import LinksAwakeningLocation
|
||||||
from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
|
from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import LinksAwakeningWorld
|
||||||
|
|
||||||
|
|
||||||
# Function to generate a final rom, this patches the rom with all required patches
|
# Function to generate a final rom, this patches the rom with all required patches
|
||||||
def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0):
|
def generateRom(args, world: "LinksAwakeningWorld"):
|
||||||
rom_patches = []
|
rom_patches = []
|
||||||
|
player_names = list(world.multiworld.player_name.values())
|
||||||
|
|
||||||
rom = ROMWithTables(args.input_filename, rom_patches)
|
rom = ROMWithTables(args.input_filename, rom_patches)
|
||||||
rom.player_names = player_names
|
rom.player_names = player_names
|
||||||
@@ -84,10 +89,10 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
for pymod in pymods:
|
for pymod in pymods:
|
||||||
pymod.prePatch(rom)
|
pymod.prePatch(rom)
|
||||||
|
|
||||||
if settings.gfxmod:
|
if world.ladxr_settings.gfxmod:
|
||||||
patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod))
|
patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", world.ladxr_settings.gfxmod))
|
||||||
|
|
||||||
item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)]
|
item_list = [item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]
|
||||||
|
|
||||||
assembler.resetConsts()
|
assembler.resetConsts()
|
||||||
assembler.const("INV_SIZE", 16)
|
assembler.const("INV_SIZE", 16)
|
||||||
@@ -116,7 +121,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
assembler.const("wLinkSpawnDelay", 0xDE13)
|
assembler.const("wLinkSpawnDelay", 0xDE13)
|
||||||
|
|
||||||
#assembler.const("HARDWARE_LINK", 1)
|
#assembler.const("HARDWARE_LINK", 1)
|
||||||
assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0)
|
assembler.const("HARD_MODE", 1 if world.ladxr_settings.hardmode != "none" else 0)
|
||||||
|
|
||||||
patches.core.cleanup(rom)
|
patches.core.cleanup(rom)
|
||||||
patches.save.singleSaveSlot(rom)
|
patches.save.singleSaveSlot(rom)
|
||||||
@@ -130,7 +135,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
patches.core.easyColorDungeonAccess(rom)
|
patches.core.easyColorDungeonAccess(rom)
|
||||||
patches.owl.removeOwlEvents(rom)
|
patches.owl.removeOwlEvents(rom)
|
||||||
patches.enemies.fixArmosKnightAsMiniboss(rom)
|
patches.enemies.fixArmosKnightAsMiniboss(rom)
|
||||||
patches.bank3e.addBank3E(rom, auth, player_id, player_names)
|
patches.bank3e.addBank3E(rom, world.multi_key, world.player, player_names)
|
||||||
patches.bank3f.addBank3F(rom)
|
patches.bank3f.addBank3F(rom)
|
||||||
patches.bank34.addBank34(rom, item_list)
|
patches.bank34.addBank34(rom, item_list)
|
||||||
patches.core.removeGhost(rom)
|
patches.core.removeGhost(rom)
|
||||||
@@ -141,10 +146,11 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
|
|
||||||
from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys
|
from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys
|
||||||
|
|
||||||
if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon:
|
if world.options.shuffle_small_keys != ShuffleSmallKeys.option_original_dungeon or\
|
||||||
|
world.options.shuffle_nightmare_keys != ShuffleNightmareKeys.option_original_dungeon:
|
||||||
patches.inventory.advancedInventorySubscreen(rom)
|
patches.inventory.advancedInventorySubscreen(rom)
|
||||||
patches.inventory.moreSlots(rom)
|
patches.inventory.moreSlots(rom)
|
||||||
if settings.witch:
|
if world.ladxr_settings.witch:
|
||||||
patches.witch.updateWitch(rom)
|
patches.witch.updateWitch(rom)
|
||||||
patches.softlock.fixAll(rom)
|
patches.softlock.fixAll(rom)
|
||||||
patches.maptweaks.tweakMap(rom)
|
patches.maptweaks.tweakMap(rom)
|
||||||
@@ -158,9 +164,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
patches.tarin.updateTarin(rom)
|
patches.tarin.updateTarin(rom)
|
||||||
patches.fishingMinigame.updateFinishingMinigame(rom)
|
patches.fishingMinigame.updateFinishingMinigame(rom)
|
||||||
patches.health.upgradeHealthContainers(rom)
|
patches.health.upgradeHealthContainers(rom)
|
||||||
if settings.owlstatues in ("dungeon", "both"):
|
if world.ladxr_settings.owlstatues in ("dungeon", "both"):
|
||||||
patches.owl.upgradeDungeonOwlStatues(rom)
|
patches.owl.upgradeDungeonOwlStatues(rom)
|
||||||
if settings.owlstatues in ("overworld", "both"):
|
if world.ladxr_settings.owlstatues in ("overworld", "both"):
|
||||||
patches.owl.upgradeOverworldOwlStatues(rom)
|
patches.owl.upgradeOverworldOwlStatues(rom)
|
||||||
patches.goldenLeaf.fixGoldenLeaf(rom)
|
patches.goldenLeaf.fixGoldenLeaf(rom)
|
||||||
patches.heartPiece.fixHeartPiece(rom)
|
patches.heartPiece.fixHeartPiece(rom)
|
||||||
@@ -170,106 +176,110 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
patches.songs.upgradeMarin(rom)
|
patches.songs.upgradeMarin(rom)
|
||||||
patches.songs.upgradeManbo(rom)
|
patches.songs.upgradeManbo(rom)
|
||||||
patches.songs.upgradeMamu(rom)
|
patches.songs.upgradeMamu(rom)
|
||||||
if settings.tradequest:
|
if world.ladxr_settings.tradequest:
|
||||||
patches.tradeSequence.patchTradeSequence(rom, settings.boomerang)
|
patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings.boomerang)
|
||||||
else:
|
else:
|
||||||
# Monkey bridge patch, always have the bridge there.
|
# Monkey bridge patch, always have the bridge there.
|
||||||
rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True)
|
rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True)
|
||||||
patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal')
|
patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal')
|
||||||
if settings.bowwow != 'normal':
|
if world.ladxr_settings.bowwow != 'normal':
|
||||||
patches.bowwow.bowwowMapPatches(rom)
|
patches.bowwow.bowwowMapPatches(rom)
|
||||||
patches.desert.desertAccess(rom)
|
patches.desert.desertAccess(rom)
|
||||||
if settings.overworld == 'dungeondive':
|
if world.ladxr_settings.overworld == 'dungeondive':
|
||||||
patches.overworld.patchOverworldTilesets(rom)
|
patches.overworld.patchOverworldTilesets(rom)
|
||||||
patches.overworld.createDungeonOnlyOverworld(rom)
|
patches.overworld.createDungeonOnlyOverworld(rom)
|
||||||
elif settings.overworld == 'nodungeons':
|
elif world.ladxr_settings.overworld == 'nodungeons':
|
||||||
patches.dungeon.patchNoDungeons(rom)
|
patches.dungeon.patchNoDungeons(rom)
|
||||||
elif settings.overworld == 'random':
|
elif world.ladxr_settings.overworld == 'random':
|
||||||
patches.overworld.patchOverworldTilesets(rom)
|
patches.overworld.patchOverworldTilesets(rom)
|
||||||
mapgen.store_map(rom, logic.world.map)
|
mapgen.store_map(rom, world.ladxr_logic.world.map)
|
||||||
#if settings.dungeon_items == 'keysy':
|
#if settings.dungeon_items == 'keysy':
|
||||||
# patches.dungeon.removeKeyDoors(rom)
|
# patches.dungeon.removeKeyDoors(rom)
|
||||||
# patches.reduceRNG.slowdownThreeOfAKind(rom)
|
# patches.reduceRNG.slowdownThreeOfAKind(rom)
|
||||||
patches.reduceRNG.fixHorseHeads(rom)
|
patches.reduceRNG.fixHorseHeads(rom)
|
||||||
patches.bomb.onlyDropBombsWhenHaveBombs(rom)
|
patches.bomb.onlyDropBombsWhenHaveBombs(rom)
|
||||||
if ap_settings['music_change_condition'] == MusicChangeCondition.option_always:
|
if world.options.music_change_condition == MusicChangeCondition.option_always:
|
||||||
patches.aesthetics.noSwordMusic(rom)
|
patches.aesthetics.noSwordMusic(rom)
|
||||||
patches.aesthetics.reduceMessageLengths(rom, rnd)
|
patches.aesthetics.reduceMessageLengths(rom, world.random)
|
||||||
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
|
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
|
||||||
if settings.music == 'random':
|
if world.ladxr_settings.music == 'random':
|
||||||
patches.music.randomizeMusic(rom, rnd)
|
patches.music.randomizeMusic(rom, world.random)
|
||||||
elif settings.music == 'off':
|
elif world.ladxr_settings.music == 'off':
|
||||||
patches.music.noMusic(rom)
|
patches.music.noMusic(rom)
|
||||||
if settings.noflash:
|
if world.ladxr_settings.noflash:
|
||||||
patches.aesthetics.removeFlashingLights(rom)
|
patches.aesthetics.removeFlashingLights(rom)
|
||||||
if settings.hardmode == "oracle":
|
if world.ladxr_settings.hardmode == "oracle":
|
||||||
patches.hardMode.oracleMode(rom)
|
patches.hardMode.oracleMode(rom)
|
||||||
elif settings.hardmode == "hero":
|
elif world.ladxr_settings.hardmode == "hero":
|
||||||
patches.hardMode.heroMode(rom)
|
patches.hardMode.heroMode(rom)
|
||||||
elif settings.hardmode == "ohko":
|
elif world.ladxr_settings.hardmode == "ohko":
|
||||||
patches.hardMode.oneHitKO(rom)
|
patches.hardMode.oneHitKO(rom)
|
||||||
if settings.superweapons:
|
if world.ladxr_settings.superweapons:
|
||||||
patches.weapons.patchSuperWeapons(rom)
|
patches.weapons.patchSuperWeapons(rom)
|
||||||
if settings.textmode == 'fast':
|
if world.ladxr_settings.textmode == 'fast':
|
||||||
patches.aesthetics.fastText(rom)
|
patches.aesthetics.fastText(rom)
|
||||||
if settings.textmode == 'none':
|
if world.ladxr_settings.textmode == 'none':
|
||||||
patches.aesthetics.fastText(rom)
|
patches.aesthetics.fastText(rom)
|
||||||
patches.aesthetics.noText(rom)
|
patches.aesthetics.noText(rom)
|
||||||
if not settings.nagmessages:
|
if not world.ladxr_settings.nagmessages:
|
||||||
patches.aesthetics.removeNagMessages(rom)
|
patches.aesthetics.removeNagMessages(rom)
|
||||||
if settings.lowhpbeep == 'slow':
|
if world.ladxr_settings.lowhpbeep == 'slow':
|
||||||
patches.aesthetics.slowLowHPBeep(rom)
|
patches.aesthetics.slowLowHPBeep(rom)
|
||||||
if settings.lowhpbeep == 'none':
|
if world.ladxr_settings.lowhpbeep == 'none':
|
||||||
patches.aesthetics.removeLowHPBeep(rom)
|
patches.aesthetics.removeLowHPBeep(rom)
|
||||||
if 0 <= int(settings.linkspalette):
|
if 0 <= int(world.ladxr_settings.linkspalette):
|
||||||
patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette))
|
patches.aesthetics.forceLinksPalette(rom, int(world.ladxr_settings.linkspalette))
|
||||||
if args.romdebugmode:
|
if args.romdebugmode:
|
||||||
# The default rom has this build in, just need to set a flag and we get this save.
|
# The default rom has this build in, just need to set a flag and we get this save.
|
||||||
rom.patch(0, 0x0003, "00", "01")
|
rom.patch(0, 0x0003, "00", "01")
|
||||||
|
|
||||||
# Patch the sword check on the shopkeeper turning around.
|
# Patch the sword check on the shopkeeper turning around.
|
||||||
if settings.steal == 'never':
|
if world.ladxr_settings.steal == 'never':
|
||||||
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
|
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
|
||||||
elif settings.steal == 'always':
|
elif world.ladxr_settings.steal == 'always':
|
||||||
rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
|
rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
|
||||||
|
|
||||||
if settings.hpmode == 'inverted':
|
if world.ladxr_settings.hpmode == 'inverted':
|
||||||
patches.health.setStartHealth(rom, 9)
|
patches.health.setStartHealth(rom, 9)
|
||||||
elif settings.hpmode == '1':
|
elif world.ladxr_settings.hpmode == '1':
|
||||||
patches.health.setStartHealth(rom, 1)
|
patches.health.setStartHealth(rom, 1)
|
||||||
|
|
||||||
patches.inventory.songSelectAfterOcarinaSelect(rom)
|
patches.inventory.songSelectAfterOcarinaSelect(rom)
|
||||||
if settings.quickswap == 'a':
|
if world.ladxr_settings.quickswap == 'a':
|
||||||
patches.core.quickswap(rom, 1)
|
patches.core.quickswap(rom, 1)
|
||||||
elif settings.quickswap == 'b':
|
elif world.ladxr_settings.quickswap == 'b':
|
||||||
patches.core.quickswap(rom, 0)
|
patches.core.quickswap(rom, 0)
|
||||||
|
|
||||||
patches.core.addBootsControls(rom, ap_settings['boots_controls'])
|
patches.core.addBootsControls(rom, world.options.boots_controls)
|
||||||
|
|
||||||
|
|
||||||
world_setup = logic.world_setup
|
world_setup = world.ladxr_logic.world_setup
|
||||||
|
|
||||||
JUNK_HINT = 0.33
|
JUNK_HINT = 0.33
|
||||||
RANDOM_HINT= 0.66
|
RANDOM_HINT= 0.66
|
||||||
# USEFUL_HINT = 1.0
|
# USEFUL_HINT = 1.0
|
||||||
# TODO: filter events, filter unshuffled keys
|
# TODO: filter events, filter unshuffled keys
|
||||||
all_items = multiworld.get_items()
|
all_items = world.multiworld.get_items()
|
||||||
our_items = [item for item in all_items if item.player == player_id and item.location and item.code is not None and item.location.show_in_spoiler]
|
our_items = [item for item in all_items
|
||||||
|
if item.player == world.player
|
||||||
|
and item.location
|
||||||
|
and item.code is not None
|
||||||
|
and item.location.show_in_spoiler]
|
||||||
our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification]
|
our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification]
|
||||||
|
|
||||||
def gen_hint():
|
def gen_hint():
|
||||||
chance = rnd.uniform(0, 1)
|
chance = world.random.uniform(0, 1)
|
||||||
if chance < JUNK_HINT:
|
if chance < JUNK_HINT:
|
||||||
return None
|
return None
|
||||||
elif chance < RANDOM_HINT:
|
elif chance < RANDOM_HINT:
|
||||||
location = rnd.choice(our_items).location
|
location = world.random.choice(our_items).location
|
||||||
else: # USEFUL_HINT
|
else: # USEFUL_HINT
|
||||||
location = rnd.choice(our_useful_items).location
|
location = world.random.choice(our_useful_items).location
|
||||||
|
|
||||||
if location.item.player == player_id:
|
if location.item.player == world.player:
|
||||||
name = "Your"
|
name = "Your"
|
||||||
else:
|
else:
|
||||||
name = f"{multiworld.player_name[location.item.player]}'s"
|
name = f"{world.multiworld.player_name[location.item.player]}'s"
|
||||||
|
|
||||||
if isinstance(location, LinksAwakeningLocation):
|
if isinstance(location, LinksAwakeningLocation):
|
||||||
location_name = location.ladxr_item.metadata.name
|
location_name = location.ladxr_item.metadata.name
|
||||||
@@ -277,8 +287,8 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
location_name = location.name
|
location_name = location.name
|
||||||
|
|
||||||
hint = f"{name} {location.item} is at {location_name}"
|
hint = f"{name} {location.item} is at {location_name}"
|
||||||
if location.player != player_id:
|
if location.player != world.player:
|
||||||
hint += f" in {multiworld.player_name[location.player]}'s world"
|
hint += f" in {world.multiworld.player_name[location.player]}'s world"
|
||||||
|
|
||||||
# Cap hint size at 85
|
# Cap hint size at 85
|
||||||
# Realistically we could go bigger but let's be safe instead
|
# Realistically we could go bigger but let's be safe instead
|
||||||
@@ -286,7 +296,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
|
|
||||||
return hint
|
return hint
|
||||||
|
|
||||||
hints.addHints(rom, rnd, gen_hint)
|
hints.addHints(rom, world.random, gen_hint)
|
||||||
|
|
||||||
if world_setup.goal == "raft":
|
if world_setup.goal == "raft":
|
||||||
patches.goal.setRaftGoal(rom)
|
patches.goal.setRaftGoal(rom)
|
||||||
@@ -299,7 +309,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
|
|
||||||
# Patch the generated logic into the rom
|
# Patch the generated logic into the rom
|
||||||
patches.chest.setMultiChest(rom, world_setup.multichest)
|
patches.chest.setMultiChest(rom, world_setup.multichest)
|
||||||
if settings.overworld not in {"dungeondive", "random"}:
|
if world.ladxr_settings.overworld not in {"dungeondive", "random"}:
|
||||||
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
|
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
|
||||||
for spot in item_list:
|
for spot in item_list:
|
||||||
if spot.item and spot.item.startswith("*"):
|
if spot.item and spot.item.startswith("*"):
|
||||||
@@ -318,15 +328,16 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
patches.core.addFrameCounter(rom, len(item_list))
|
patches.core.addFrameCounter(rom, len(item_list))
|
||||||
|
|
||||||
patches.core.warpHome(rom) # Needs to be done after setting the start location.
|
patches.core.warpHome(rom) # Needs to be done after setting the start location.
|
||||||
patches.titleScreen.setRomInfo(rom, auth, seed_name, settings, player_name, player_id)
|
patches.titleScreen.setRomInfo(rom, world.multi_key, world.multiworld.seed_name, world.ladxr_settings,
|
||||||
if ap_settings["ap_title_screen"]:
|
world.player_name, world.player)
|
||||||
|
if world.options.ap_title_screen:
|
||||||
patches.titleScreen.setTitleGraphics(rom)
|
patches.titleScreen.setTitleGraphics(rom)
|
||||||
patches.endscreen.updateEndScreen(rom)
|
patches.endscreen.updateEndScreen(rom)
|
||||||
patches.aesthetics.updateSpriteData(rom)
|
patches.aesthetics.updateSpriteData(rom)
|
||||||
if args.doubletrouble:
|
if args.doubletrouble:
|
||||||
patches.enemies.doubleTrouble(rom)
|
patches.enemies.doubleTrouble(rom)
|
||||||
|
|
||||||
if ap_settings["text_shuffle"]:
|
if world.options.text_shuffle:
|
||||||
buckets = defaultdict(list)
|
buckets = defaultdict(list)
|
||||||
# For each ROM bank, shuffle text within the bank
|
# For each ROM bank, shuffle text within the bank
|
||||||
for n, data in enumerate(rom.texts._PointerTable__data):
|
for n, data in enumerate(rom.texts._PointerTable__data):
|
||||||
@@ -336,20 +347,20 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
for bucket in buckets.values():
|
for bucket in buckets.values():
|
||||||
# For each bucket, make a copy and shuffle
|
# For each bucket, make a copy and shuffle
|
||||||
shuffled = bucket.copy()
|
shuffled = bucket.copy()
|
||||||
rnd.shuffle(shuffled)
|
world.random.shuffle(shuffled)
|
||||||
# Then put new text in
|
# Then put new text in
|
||||||
for bucket_idx, (orig_idx, data) in enumerate(bucket):
|
for bucket_idx, (orig_idx, data) in enumerate(bucket):
|
||||||
rom.texts[shuffled[bucket_idx][0]] = data
|
rom.texts[shuffled[bucket_idx][0]] = data
|
||||||
|
|
||||||
|
|
||||||
if ap_settings["trendy_game"] != TrendyGame.option_normal:
|
if world.options.trendy_game != TrendyGame.option_normal:
|
||||||
|
|
||||||
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
|
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
|
||||||
|
|
||||||
|
|
||||||
room_editor = RoomEditor(rom, 0x2A0)
|
room_editor = RoomEditor(rom, 0x2A0)
|
||||||
|
|
||||||
if ap_settings["trendy_game"] == TrendyGame.option_easy:
|
if world.options.trendy_game == TrendyGame.option_easy:
|
||||||
# Set physics flag on all objects
|
# Set physics flag on all objects
|
||||||
for i in range(0, 6):
|
for i in range(0, 6):
|
||||||
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
|
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
|
||||||
@@ -360,7 +371,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
# Add new conveyor to "push" yoshi (it's only a visual)
|
# Add new conveyor to "push" yoshi (it's only a visual)
|
||||||
room_editor.objects.append(Object(5, 3, 0xD0))
|
room_editor.objects.append(Object(5, 3, 0xD0))
|
||||||
|
|
||||||
if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder:
|
if world.options.trendy_game >= TrendyGame.option_harder:
|
||||||
"""
|
"""
|
||||||
Data_004_76A0::
|
Data_004_76A0::
|
||||||
db $FC, $00, $04, $00, $00
|
db $FC, $00, $04, $00, $00
|
||||||
@@ -374,12 +385,12 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
TrendyGame.option_impossible: (3, 16),
|
TrendyGame.option_impossible: (3, 16),
|
||||||
}
|
}
|
||||||
def speed():
|
def speed():
|
||||||
return rnd.randint(*speeds[ap_settings["trendy_game"]])
|
return world.random.randint(*speeds[world.options.trendy_game])
|
||||||
rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
|
rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
|
||||||
rom.banks[0x4][0x76A2-0x4000] = speed()
|
rom.banks[0x4][0x76A2-0x4000] = speed()
|
||||||
rom.banks[0x4][0x76A6-0x4000] = speed()
|
rom.banks[0x4][0x76A6-0x4000] = speed()
|
||||||
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
|
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
|
||||||
if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest:
|
if world.options.trendy_game >= TrendyGame.option_hardest:
|
||||||
rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
|
rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
|
||||||
rom.banks[0x4][0x76A3-0x4000] = speed()
|
rom.banks[0x4][0x76A3-0x4000] = speed()
|
||||||
rom.banks[0x4][0x76A5-0x4000] = speed()
|
rom.banks[0x4][0x76A5-0x4000] = speed()
|
||||||
@@ -403,10 +414,10 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
for channel in range(3):
|
for channel in range(3):
|
||||||
color[channel] = color[channel] * 31 // 0xbc
|
color[channel] = color[channel] * 31 // 0xbc
|
||||||
|
|
||||||
if ap_settings["warp_improvements"]:
|
if world.options.warp_improvements:
|
||||||
patches.core.addWarpImprovements(rom, ap_settings["additional_warp_points"])
|
patches.core.addWarpImprovements(rom, world.options.additional_warp_points)
|
||||||
|
|
||||||
palette = ap_settings["palette"]
|
palette = world.options.palette
|
||||||
if palette != Palette.option_normal:
|
if palette != Palette.option_normal:
|
||||||
ranges = {
|
ranges = {
|
||||||
# Object palettes
|
# Object palettes
|
||||||
@@ -472,8 +483,8 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
|||||||
|
|
||||||
SEED_LOCATION = 0x0134
|
SEED_LOCATION = 0x0134
|
||||||
# Patch over the title
|
# Patch over the title
|
||||||
assert(len(auth) == 12)
|
assert(len(world.multi_key) == 12)
|
||||||
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth))
|
rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(world.multi_key))
|
||||||
|
|
||||||
for pymod in pymods:
|
for pymod in pymods:
|
||||||
pymod.postPatch(rom)
|
pymod.postPatch(rom)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class DroppedKey(ItemInfo):
|
|||||||
extra = 0x01F8
|
extra = 0x01F8
|
||||||
super().__init__(room, extra)
|
super().__init__(room, extra)
|
||||||
def patch(self, rom, option, *, multiworld=None):
|
def patch(self, rom, option, *, multiworld=None):
|
||||||
if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or option.startswith(STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY )or (option.startswith(KEY) and option != KEY):
|
if (option.startswith(MAP) and option != MAP) or (option.startswith(COMPASS) and option != COMPASS) or (option.startswith(STONE_BEAK) and option != STONE_BEAK) or (option.startswith(NIGHTMARE_KEY) and option != NIGHTMARE_KEY) or (option.startswith(KEY) and option != KEY):
|
||||||
if option[-1] == 'P':
|
if option[-1] == 'P':
|
||||||
print(option)
|
print(option)
|
||||||
if self._location.dungeon == int(option[-1]) and multiworld is None and self.room not in {0x166, 0x223}:
|
if self._location.dungeon == int(option[-1]) and multiworld is None and self.room not in {0x166, 0x223}:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import typing
|
import typing
|
||||||
import logging
|
import logging
|
||||||
from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText
|
from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ class LADXROption:
|
|||||||
def to_ladxr_option(self, all_options):
|
def to_ladxr_option(self, all_options):
|
||||||
if not self.ladxr_name:
|
if not self.ladxr_name:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
return (self.ladxr_name, self.name_lookup[self.value].replace("_", ""))
|
return (self.ladxr_name, self.name_lookup[self.value].replace("_", ""))
|
||||||
|
|
||||||
|
|
||||||
@@ -32,9 +34,10 @@ class Logic(Choice, LADXROption):
|
|||||||
option_hard = 2
|
option_hard = 2
|
||||||
option_glitched = 3
|
option_glitched = 3
|
||||||
option_hell = 4
|
option_hell = 4
|
||||||
|
|
||||||
default = option_normal
|
default = option_normal
|
||||||
|
|
||||||
|
|
||||||
class TradeQuest(DefaultOffToggle, LADXROption):
|
class TradeQuest(DefaultOffToggle, LADXROption):
|
||||||
"""
|
"""
|
||||||
[On] adds the trade items to the pool (the trade locations will always be local items)
|
[On] adds the trade items to the pool (the trade locations will always be local items)
|
||||||
@@ -43,11 +46,14 @@ class TradeQuest(DefaultOffToggle, LADXROption):
|
|||||||
display_name = "Trade Quest"
|
display_name = "Trade Quest"
|
||||||
ladxr_name = "tradequest"
|
ladxr_name = "tradequest"
|
||||||
|
|
||||||
|
|
||||||
class TextShuffle(DefaultOffToggle):
|
class TextShuffle(DefaultOffToggle):
|
||||||
"""
|
"""
|
||||||
[On] Shuffles all the text in the game
|
[On] Shuffles all the text in the game
|
||||||
[Off] (default) doesn't shuffle them.
|
[Off] (default) doesn't shuffle them.
|
||||||
"""
|
"""
|
||||||
|
display_name = "Text Shuffle"
|
||||||
|
|
||||||
|
|
||||||
class Rooster(DefaultOnToggle, LADXROption):
|
class Rooster(DefaultOnToggle, LADXROption):
|
||||||
"""
|
"""
|
||||||
@@ -57,16 +63,19 @@ class Rooster(DefaultOnToggle, LADXROption):
|
|||||||
display_name = "Rooster"
|
display_name = "Rooster"
|
||||||
ladxr_name = "rooster"
|
ladxr_name = "rooster"
|
||||||
|
|
||||||
|
|
||||||
class Boomerang(Choice):
|
class Boomerang(Choice):
|
||||||
"""
|
"""
|
||||||
[Normal] requires Magnifying Lens to get the boomerang.
|
[Normal] requires Magnifying Lens to get the boomerang.
|
||||||
[Gift] The boomerang salesman will give you a random item, and the boomerang is shuffled.
|
[Gift] The boomerang salesman will give you a random item, and the boomerang is shuffled.
|
||||||
"""
|
"""
|
||||||
|
display_name = "Boomerang"
|
||||||
|
|
||||||
normal = 0
|
normal = 0
|
||||||
gift = 1
|
gift = 1
|
||||||
default = gift
|
default = gift
|
||||||
|
|
||||||
|
|
||||||
class EntranceShuffle(Choice, LADXROption):
|
class EntranceShuffle(Choice, LADXROption):
|
||||||
"""
|
"""
|
||||||
[WARNING] Experimental, may fail to fill
|
[WARNING] Experimental, may fail to fill
|
||||||
@@ -75,19 +84,20 @@ class EntranceShuffle(Choice, LADXROption):
|
|||||||
If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool.
|
If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool.
|
||||||
Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this."""
|
Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this."""
|
||||||
|
|
||||||
#[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well.
|
# [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well.
|
||||||
#[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool.
|
# [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool.
|
||||||
#[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool.
|
# [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool.
|
||||||
|
|
||||||
option_none = 0
|
option_none = 0
|
||||||
option_simple = 1
|
option_simple = 1
|
||||||
#option_advanced = 2
|
# option_advanced = 2
|
||||||
#option_expert = 3
|
# option_expert = 3
|
||||||
#option_insanity = 4
|
# option_insanity = 4
|
||||||
default = option_none
|
default = option_none
|
||||||
display_name = "Experimental Entrance Shuffle"
|
display_name = "Experimental Entrance Shuffle"
|
||||||
ladxr_name = "entranceshuffle"
|
ladxr_name = "entranceshuffle"
|
||||||
|
|
||||||
|
|
||||||
class DungeonShuffle(DefaultOffToggle, LADXROption):
|
class DungeonShuffle(DefaultOffToggle, LADXROption):
|
||||||
"""
|
"""
|
||||||
[WARNING] Experimental, may fail to fill
|
[WARNING] Experimental, may fail to fill
|
||||||
@@ -96,13 +106,16 @@ class DungeonShuffle(DefaultOffToggle, LADXROption):
|
|||||||
display_name = "Experimental Dungeon Shuffle"
|
display_name = "Experimental Dungeon Shuffle"
|
||||||
ladxr_name = "dungeonshuffle"
|
ladxr_name = "dungeonshuffle"
|
||||||
|
|
||||||
|
|
||||||
class APTitleScreen(DefaultOnToggle):
|
class APTitleScreen(DefaultOnToggle):
|
||||||
"""
|
"""
|
||||||
Enables AP specific title screen and disables the intro cutscene
|
Enables AP specific title screen and disables the intro cutscene
|
||||||
"""
|
"""
|
||||||
display_name = "AP Title Screen"
|
display_name = "AP Title Screen"
|
||||||
|
|
||||||
|
|
||||||
class BossShuffle(Choice):
|
class BossShuffle(Choice):
|
||||||
|
display_name = "Boss Shuffle"
|
||||||
none = 0
|
none = 0
|
||||||
shuffle = 1
|
shuffle = 1
|
||||||
random = 2
|
random = 2
|
||||||
@@ -110,15 +123,18 @@ class BossShuffle(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class DungeonItemShuffle(Choice):
|
class DungeonItemShuffle(Choice):
|
||||||
|
display_name = "Dungeon Item Shuffle"
|
||||||
option_original_dungeon = 0
|
option_original_dungeon = 0
|
||||||
option_own_dungeons = 1
|
option_own_dungeons = 1
|
||||||
option_own_world = 2
|
option_own_world = 2
|
||||||
option_any_world = 3
|
option_any_world = 3
|
||||||
option_different_world = 4
|
option_different_world = 4
|
||||||
#option_delete = 5
|
# option_delete = 5
|
||||||
#option_start_with = 6
|
# option_start_with = 6
|
||||||
alias_true = 3
|
alias_true = 3
|
||||||
alias_false = 0
|
alias_false = 0
|
||||||
|
ladxr_item: str
|
||||||
|
|
||||||
|
|
||||||
class ShuffleNightmareKeys(DungeonItemShuffle):
|
class ShuffleNightmareKeys(DungeonItemShuffle):
|
||||||
"""
|
"""
|
||||||
@@ -132,6 +148,7 @@ class ShuffleNightmareKeys(DungeonItemShuffle):
|
|||||||
display_name = "Shuffle Nightmare Keys"
|
display_name = "Shuffle Nightmare Keys"
|
||||||
ladxr_item = "NIGHTMARE_KEY"
|
ladxr_item = "NIGHTMARE_KEY"
|
||||||
|
|
||||||
|
|
||||||
class ShuffleSmallKeys(DungeonItemShuffle):
|
class ShuffleSmallKeys(DungeonItemShuffle):
|
||||||
"""
|
"""
|
||||||
Shuffle Small Keys
|
Shuffle Small Keys
|
||||||
@@ -143,6 +160,8 @@ class ShuffleSmallKeys(DungeonItemShuffle):
|
|||||||
"""
|
"""
|
||||||
display_name = "Shuffle Small Keys"
|
display_name = "Shuffle Small Keys"
|
||||||
ladxr_item = "KEY"
|
ladxr_item = "KEY"
|
||||||
|
|
||||||
|
|
||||||
class ShuffleMaps(DungeonItemShuffle):
|
class ShuffleMaps(DungeonItemShuffle):
|
||||||
"""
|
"""
|
||||||
Shuffle Dungeon Maps
|
Shuffle Dungeon Maps
|
||||||
@@ -155,6 +174,7 @@ class ShuffleMaps(DungeonItemShuffle):
|
|||||||
display_name = "Shuffle Maps"
|
display_name = "Shuffle Maps"
|
||||||
ladxr_item = "MAP"
|
ladxr_item = "MAP"
|
||||||
|
|
||||||
|
|
||||||
class ShuffleCompasses(DungeonItemShuffle):
|
class ShuffleCompasses(DungeonItemShuffle):
|
||||||
"""
|
"""
|
||||||
Shuffle Dungeon Compasses
|
Shuffle Dungeon Compasses
|
||||||
@@ -167,6 +187,7 @@ class ShuffleCompasses(DungeonItemShuffle):
|
|||||||
display_name = "Shuffle Compasses"
|
display_name = "Shuffle Compasses"
|
||||||
ladxr_item = "COMPASS"
|
ladxr_item = "COMPASS"
|
||||||
|
|
||||||
|
|
||||||
class ShuffleStoneBeaks(DungeonItemShuffle):
|
class ShuffleStoneBeaks(DungeonItemShuffle):
|
||||||
"""
|
"""
|
||||||
Shuffle Owl Beaks
|
Shuffle Owl Beaks
|
||||||
@@ -179,6 +200,7 @@ class ShuffleStoneBeaks(DungeonItemShuffle):
|
|||||||
display_name = "Shuffle Stone Beaks"
|
display_name = "Shuffle Stone Beaks"
|
||||||
ladxr_item = "STONE_BEAK"
|
ladxr_item = "STONE_BEAK"
|
||||||
|
|
||||||
|
|
||||||
class ShuffleInstruments(DungeonItemShuffle):
|
class ShuffleInstruments(DungeonItemShuffle):
|
||||||
"""
|
"""
|
||||||
Shuffle Instruments
|
Shuffle Instruments
|
||||||
@@ -195,6 +217,7 @@ class ShuffleInstruments(DungeonItemShuffle):
|
|||||||
option_vanilla = 100
|
option_vanilla = 100
|
||||||
alias_false = 100
|
alias_false = 100
|
||||||
|
|
||||||
|
|
||||||
class Goal(Choice, LADXROption):
|
class Goal(Choice, LADXROption):
|
||||||
"""
|
"""
|
||||||
The Goal of the game
|
The Goal of the game
|
||||||
@@ -207,7 +230,7 @@ class Goal(Choice, LADXROption):
|
|||||||
option_instruments = 1
|
option_instruments = 1
|
||||||
option_seashells = 2
|
option_seashells = 2
|
||||||
option_open = 3
|
option_open = 3
|
||||||
|
|
||||||
default = option_instruments
|
default = option_instruments
|
||||||
|
|
||||||
def to_ladxr_option(self, all_options):
|
def to_ladxr_option(self, all_options):
|
||||||
@@ -216,6 +239,7 @@ class Goal(Choice, LADXROption):
|
|||||||
else:
|
else:
|
||||||
return LADXROption.to_ladxr_option(self, all_options)
|
return LADXROption.to_ladxr_option(self, all_options)
|
||||||
|
|
||||||
|
|
||||||
class InstrumentCount(Range, LADXROption):
|
class InstrumentCount(Range, LADXROption):
|
||||||
"""
|
"""
|
||||||
Sets the number of instruments required to open the Egg
|
Sets the number of instruments required to open the Egg
|
||||||
@@ -226,6 +250,7 @@ class InstrumentCount(Range, LADXROption):
|
|||||||
range_end = 8
|
range_end = 8
|
||||||
default = 8
|
default = 8
|
||||||
|
|
||||||
|
|
||||||
class NagMessages(DefaultOffToggle, LADXROption):
|
class NagMessages(DefaultOffToggle, LADXROption):
|
||||||
"""
|
"""
|
||||||
Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else.
|
Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else.
|
||||||
@@ -233,6 +258,7 @@ class NagMessages(DefaultOffToggle, LADXROption):
|
|||||||
display_name = "Nag Messages"
|
display_name = "Nag Messages"
|
||||||
ladxr_name = "nagmessages"
|
ladxr_name = "nagmessages"
|
||||||
|
|
||||||
|
|
||||||
class MusicChangeCondition(Choice):
|
class MusicChangeCondition(Choice):
|
||||||
"""
|
"""
|
||||||
Controls how the music changes.
|
Controls how the music changes.
|
||||||
@@ -243,6 +269,8 @@ class MusicChangeCondition(Choice):
|
|||||||
option_sword = 0
|
option_sword = 0
|
||||||
option_always = 1
|
option_always = 1
|
||||||
default = option_always
|
default = option_always
|
||||||
|
|
||||||
|
|
||||||
# Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default',
|
# Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default',
|
||||||
# description="""
|
# description="""
|
||||||
# [Normal} health works as you would expect.
|
# [Normal} health works as you would expect.
|
||||||
@@ -267,10 +295,12 @@ class Bowwow(Choice):
|
|||||||
[Normal] BowWow is in the item pool, but can be logically expected as a damage source.
|
[Normal] BowWow is in the item pool, but can be logically expected as a damage source.
|
||||||
[Swordless] The progressive swords are removed from the item pool.
|
[Swordless] The progressive swords are removed from the item pool.
|
||||||
"""
|
"""
|
||||||
|
display_name = "BowWow"
|
||||||
normal = 0
|
normal = 0
|
||||||
swordless = 1
|
swordless = 1
|
||||||
default = normal
|
default = normal
|
||||||
|
|
||||||
|
|
||||||
class Overworld(Choice, LADXROption):
|
class Overworld(Choice, LADXROption):
|
||||||
"""
|
"""
|
||||||
[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld.
|
[Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld.
|
||||||
@@ -284,9 +314,10 @@ class Overworld(Choice, LADXROption):
|
|||||||
# option_shuffled = 3
|
# option_shuffled = 3
|
||||||
default = option_normal
|
default = option_normal
|
||||||
|
|
||||||
#Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
|
|
||||||
|
# Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
|
||||||
# description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
|
# description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
|
||||||
#Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
|
# Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
|
||||||
# description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
|
# description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
|
||||||
# aesthetic=True),
|
# aesthetic=True),
|
||||||
# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
|
# Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast',
|
||||||
@@ -329,7 +360,7 @@ class BootsControls(Choice):
|
|||||||
option_bracelet = 1
|
option_bracelet = 1
|
||||||
option_press_a = 2
|
option_press_a = 2
|
||||||
option_press_b = 3
|
option_press_b = 3
|
||||||
|
|
||||||
|
|
||||||
class LinkPalette(Choice, LADXROption):
|
class LinkPalette(Choice, LADXROption):
|
||||||
"""
|
"""
|
||||||
@@ -352,6 +383,7 @@ class LinkPalette(Choice, LADXROption):
|
|||||||
def to_ladxr_option(self, all_options):
|
def to_ladxr_option(self, all_options):
|
||||||
return self.ladxr_name, str(self.value)
|
return self.ladxr_name, str(self.value)
|
||||||
|
|
||||||
|
|
||||||
class TrendyGame(Choice):
|
class TrendyGame(Choice):
|
||||||
"""
|
"""
|
||||||
[Easy] All of the items hold still for you
|
[Easy] All of the items hold still for you
|
||||||
@@ -370,6 +402,7 @@ class TrendyGame(Choice):
|
|||||||
option_impossible = 5
|
option_impossible = 5
|
||||||
default = option_normal
|
default = option_normal
|
||||||
|
|
||||||
|
|
||||||
class GfxMod(FreeText, LADXROption):
|
class GfxMod(FreeText, LADXROption):
|
||||||
"""
|
"""
|
||||||
Sets the sprite for link, among other things
|
Sets the sprite for link, among other things
|
||||||
@@ -380,7 +413,7 @@ class GfxMod(FreeText, LADXROption):
|
|||||||
normal = ''
|
normal = ''
|
||||||
default = 'Link'
|
default = 'Link'
|
||||||
|
|
||||||
__spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx'))
|
__spriteDir: str = Utils.local_path(os.path.join('data', 'sprites', 'ladx'))
|
||||||
__spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list)
|
__spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list)
|
||||||
|
|
||||||
extensions = [".bin", ".bdiff", ".png", ".bmp"]
|
extensions = [".bin", ".bdiff", ".png", ".bmp"]
|
||||||
@@ -389,16 +422,15 @@ class GfxMod(FreeText, LADXROption):
|
|||||||
name, extension = os.path.splitext(file)
|
name, extension = os.path.splitext(file)
|
||||||
if extension in extensions:
|
if extension in extensions:
|
||||||
__spriteFiles[name].append(file)
|
__spriteFiles[name].append(file)
|
||||||
|
|
||||||
def __init__(self, value: str):
|
def __init__(self, value: str):
|
||||||
super().__init__(value)
|
super().__init__(value)
|
||||||
|
|
||||||
|
|
||||||
def verify(self, world, player_name: str, plando_options) -> None:
|
def verify(self, world, player_name: str, plando_options) -> None:
|
||||||
if self.value == "Link" or self.value in GfxMod.__spriteFiles:
|
if self.value == "Link" or self.value in GfxMod.__spriteFiles:
|
||||||
return
|
return
|
||||||
raise Exception(f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}")
|
raise Exception(
|
||||||
|
f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}")
|
||||||
|
|
||||||
def to_ladxr_option(self, all_options):
|
def to_ladxr_option(self, all_options):
|
||||||
if self.value == -1 or self.value == "Link":
|
if self.value == -1 or self.value == "Link":
|
||||||
@@ -407,10 +439,12 @@ class GfxMod(FreeText, LADXROption):
|
|||||||
assert self.value in GfxMod.__spriteFiles
|
assert self.value in GfxMod.__spriteFiles
|
||||||
|
|
||||||
if len(GfxMod.__spriteFiles[self.value]) > 1:
|
if len(GfxMod.__spriteFiles[self.value]) > 1:
|
||||||
logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}")
|
logger.warning(
|
||||||
|
f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}")
|
||||||
|
|
||||||
return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0]
|
return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0]
|
||||||
|
|
||||||
|
|
||||||
class Palette(Choice):
|
class Palette(Choice):
|
||||||
"""
|
"""
|
||||||
Sets the palette for the game.
|
Sets the palette for the game.
|
||||||
@@ -430,18 +464,19 @@ class Palette(Choice):
|
|||||||
option_pink = 4
|
option_pink = 4
|
||||||
option_inverted = 5
|
option_inverted = 5
|
||||||
|
|
||||||
|
|
||||||
class Music(Choice, LADXROption):
|
class Music(Choice, LADXROption):
|
||||||
"""
|
"""
|
||||||
[Vanilla] Regular Music
|
[Vanilla] Regular Music
|
||||||
[Shuffled] Shuffled Music
|
[Shuffled] Shuffled Music
|
||||||
[Off] No music
|
[Off] No music
|
||||||
"""
|
"""
|
||||||
|
display_name = "Music"
|
||||||
ladxr_name = "music"
|
ladxr_name = "music"
|
||||||
option_vanilla = 0
|
option_vanilla = 0
|
||||||
option_shuffled = 1
|
option_shuffled = 1
|
||||||
option_off = 2
|
option_off = 2
|
||||||
|
|
||||||
|
|
||||||
def to_ladxr_option(self, all_options):
|
def to_ladxr_option(self, all_options):
|
||||||
s = ""
|
s = ""
|
||||||
if self.value == self.option_shuffled:
|
if self.value == self.option_shuffled:
|
||||||
@@ -450,55 +485,97 @@ class Music(Choice, LADXROption):
|
|||||||
s = "off"
|
s = "off"
|
||||||
return self.ladxr_name, s
|
return self.ladxr_name, s
|
||||||
|
|
||||||
|
|
||||||
class WarpImprovements(DefaultOffToggle):
|
class WarpImprovements(DefaultOffToggle):
|
||||||
"""
|
"""
|
||||||
[On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select.
|
[On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select.
|
||||||
[Off] No change
|
[Off] No change
|
||||||
"""
|
"""
|
||||||
|
display_name = "Warp Improvements"
|
||||||
|
|
||||||
|
|
||||||
class AdditionalWarpPoints(DefaultOffToggle):
|
class AdditionalWarpPoints(DefaultOffToggle):
|
||||||
"""
|
"""
|
||||||
[On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower
|
[On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower
|
||||||
[Off] No change
|
[Off] No change
|
||||||
"""
|
"""
|
||||||
|
display_name = "Additional Warp Points"
|
||||||
|
|
||||||
links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
|
ladx_option_groups = [
|
||||||
'logic': Logic,
|
OptionGroup("Goal Options", [
|
||||||
|
Goal,
|
||||||
|
InstrumentCount,
|
||||||
|
]),
|
||||||
|
OptionGroup("Shuffles", [
|
||||||
|
ShuffleInstruments,
|
||||||
|
ShuffleNightmareKeys,
|
||||||
|
ShuffleSmallKeys,
|
||||||
|
ShuffleMaps,
|
||||||
|
ShuffleCompasses,
|
||||||
|
ShuffleStoneBeaks
|
||||||
|
]),
|
||||||
|
OptionGroup("Warp Points", [
|
||||||
|
WarpImprovements,
|
||||||
|
AdditionalWarpPoints,
|
||||||
|
]),
|
||||||
|
OptionGroup("Miscellaneous", [
|
||||||
|
TradeQuest,
|
||||||
|
Rooster,
|
||||||
|
TrendyGame,
|
||||||
|
NagMessages,
|
||||||
|
BootsControls
|
||||||
|
]),
|
||||||
|
OptionGroup("Experimental", [
|
||||||
|
DungeonShuffle,
|
||||||
|
EntranceShuffle
|
||||||
|
]),
|
||||||
|
OptionGroup("Visuals & Sound", [
|
||||||
|
LinkPalette,
|
||||||
|
Palette,
|
||||||
|
TextShuffle,
|
||||||
|
APTitleScreen,
|
||||||
|
GfxMod,
|
||||||
|
Music,
|
||||||
|
MusicChangeCondition
|
||||||
|
])
|
||||||
|
]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LinksAwakeningOptions(PerGameCommonOptions):
|
||||||
|
logic: Logic
|
||||||
# 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'),
|
# 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'),
|
||||||
# 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'),
|
# 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'),
|
||||||
# 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'),
|
# 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'),
|
||||||
# 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'),
|
# 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'),
|
||||||
'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
|
tradequest: TradeQuest # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'),
|
||||||
# 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'),
|
# 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'),
|
||||||
'rooster': Rooster, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'),
|
rooster: Rooster # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'),
|
||||||
# 'boomerang': Boomerang,
|
# 'boomerang': Boomerang,
|
||||||
# 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'),
|
# 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'),
|
||||||
'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'),
|
experimental_dungeon_shuffle: DungeonShuffle # 'Randomizes the dungeon that each dungeon entrance leads to'),
|
||||||
'experimental_entrance_shuffle': EntranceShuffle,
|
experimental_entrance_shuffle: EntranceShuffle
|
||||||
# 'bossshuffle': BossShuffle,
|
# 'bossshuffle': BossShuffle,
|
||||||
# 'minibossshuffle': BossShuffle,
|
# 'minibossshuffle': BossShuffle,
|
||||||
'goal': Goal,
|
goal: Goal
|
||||||
'instrument_count': InstrumentCount,
|
instrument_count: InstrumentCount
|
||||||
# 'itempool': ItemPool,
|
# 'itempool': ItemPool,
|
||||||
# 'bowwow': Bowwow,
|
# 'bowwow': Bowwow,
|
||||||
# 'overworld': Overworld,
|
# 'overworld': Overworld,
|
||||||
'link_palette': LinkPalette,
|
link_palette: LinkPalette
|
||||||
'warp_improvements': WarpImprovements,
|
warp_improvements: WarpImprovements
|
||||||
'additional_warp_points': AdditionalWarpPoints,
|
additional_warp_points: AdditionalWarpPoints
|
||||||
'trendy_game': TrendyGame,
|
trendy_game: TrendyGame
|
||||||
'gfxmod': GfxMod,
|
gfxmod: GfxMod
|
||||||
'palette': Palette,
|
palette: Palette
|
||||||
'text_shuffle': TextShuffle,
|
text_shuffle: TextShuffle
|
||||||
'shuffle_nightmare_keys': ShuffleNightmareKeys,
|
shuffle_nightmare_keys: ShuffleNightmareKeys
|
||||||
'shuffle_small_keys': ShuffleSmallKeys,
|
shuffle_small_keys: ShuffleSmallKeys
|
||||||
'shuffle_maps': ShuffleMaps,
|
shuffle_maps: ShuffleMaps
|
||||||
'shuffle_compasses': ShuffleCompasses,
|
shuffle_compasses: ShuffleCompasses
|
||||||
'shuffle_stone_beaks': ShuffleStoneBeaks,
|
shuffle_stone_beaks: ShuffleStoneBeaks
|
||||||
'music': Music,
|
music: Music
|
||||||
'shuffle_instruments': ShuffleInstruments,
|
shuffle_instruments: ShuffleInstruments
|
||||||
'music_change_condition': MusicChangeCondition,
|
music_change_condition: MusicChangeCondition
|
||||||
'nag_messages': NagMessages,
|
nag_messages: NagMessages
|
||||||
'ap_title_screen': APTitleScreen,
|
ap_title_screen: APTitleScreen
|
||||||
'boots_controls': BootsControls,
|
boots_controls: BootsControls
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
import settings
|
||||||
import worlds.Files
|
import worlds.Files
|
||||||
import hashlib
|
import hashlib
|
||||||
import Utils
|
import Utils
|
||||||
@@ -32,7 +32,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def get_base_rom_path(file_name: str = "") -> str:
|
def get_base_rom_path(file_name: str = "") -> str:
|
||||||
options = Utils.get_options()
|
options = settings.get_settings()
|
||||||
if not file_name:
|
if not file_name:
|
||||||
file_name = options["ladx_options"]["rom_file"]
|
file_name = options["ladx_options"]["rom_file"]
|
||||||
if not os.path.exists(file_name):
|
if not os.path.exists(file_name):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
from .LADXR.checkMetadata import checkMetadataTable
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import websockets
|
import websockets
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import binascii
|
import binascii
|
||||||
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -7,7 +8,7 @@ import typing
|
|||||||
import bsdiff4
|
import bsdiff4
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial
|
from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial, MultiWorld
|
||||||
from Fill import fill_restrictive
|
from Fill import fill_restrictive
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from .Common import *
|
from .Common import *
|
||||||
@@ -17,14 +18,14 @@ from .LADXR import generator
|
|||||||
from .LADXR.itempool import ItemPool as LADXRItemPool
|
from .LADXR.itempool import ItemPool as LADXRItemPool
|
||||||
from .LADXR.locations.constants import CHEST_ITEMS
|
from .LADXR.locations.constants import CHEST_ITEMS
|
||||||
from .LADXR.locations.instrument import Instrument
|
from .LADXR.locations.instrument import Instrument
|
||||||
from .LADXR.logic import Logic as LAXDRLogic
|
from .LADXR.logic import Logic as LADXRLogic
|
||||||
from .LADXR.main import get_parser
|
from .LADXR.main import get_parser
|
||||||
from .LADXR.settings import Settings as LADXRSettings
|
from .LADXR.settings import Settings as LADXRSettings
|
||||||
from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup
|
from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup
|
||||||
from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion,
|
from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion,
|
||||||
create_regions_from_ladxr, get_locations_to_id)
|
create_regions_from_ladxr, get_locations_to_id)
|
||||||
from .Options import DungeonItemShuffle, links_awakening_options, ShuffleInstruments
|
from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups
|
||||||
from .Rom import LADXDeltaPatch
|
from .Rom import LADXDeltaPatch, get_base_rom_path
|
||||||
|
|
||||||
DEVELOPER_MODE = False
|
DEVELOPER_MODE = False
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ class LinksAwakeningWebWorld(WebWorld):
|
|||||||
["zig"]
|
["zig"]
|
||||||
)]
|
)]
|
||||||
theme = "dirt"
|
theme = "dirt"
|
||||||
|
option_groups = ladx_option_groups
|
||||||
|
|
||||||
class LinksAwakeningWorld(World):
|
class LinksAwakeningWorld(World):
|
||||||
"""
|
"""
|
||||||
@@ -73,8 +74,9 @@ class LinksAwakeningWorld(World):
|
|||||||
"""
|
"""
|
||||||
game = LINKS_AWAKENING # name of the game/world
|
game = LINKS_AWAKENING # name of the game/world
|
||||||
web = LinksAwakeningWebWorld()
|
web = LinksAwakeningWebWorld()
|
||||||
|
|
||||||
option_definitions = links_awakening_options # options the player can set
|
options_dataclass = LinksAwakeningOptions
|
||||||
|
options: LinksAwakeningOptions
|
||||||
settings: typing.ClassVar[LinksAwakeningSettings]
|
settings: typing.ClassVar[LinksAwakeningSettings]
|
||||||
topology_present = True # show path to required location checks in spoiler
|
topology_present = True # show path to required location checks in spoiler
|
||||||
|
|
||||||
@@ -102,7 +104,11 @@ class LinksAwakeningWorld(World):
|
|||||||
|
|
||||||
prefill_dungeon_items = None
|
prefill_dungeon_items = None
|
||||||
|
|
||||||
player_options = None
|
ladxr_settings: LADXRSettings
|
||||||
|
ladxr_logic: LADXRLogic
|
||||||
|
ladxr_itempool: LADXRItemPool
|
||||||
|
|
||||||
|
multi_key: bytearray
|
||||||
|
|
||||||
rupees = {
|
rupees = {
|
||||||
ItemName.RUPEES_20: 20,
|
ItemName.RUPEES_20: 20,
|
||||||
@@ -113,17 +119,13 @@ class LinksAwakeningWorld(World):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def convert_ap_options_to_ladxr_logic(self):
|
def convert_ap_options_to_ladxr_logic(self):
|
||||||
self.player_options = {
|
self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options))
|
||||||
option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions
|
|
||||||
}
|
|
||||||
|
|
||||||
self.laxdr_options = LADXRSettings(self.player_options)
|
self.ladxr_settings.validate()
|
||||||
|
|
||||||
self.laxdr_options.validate()
|
|
||||||
world_setup = LADXRWorldSetup()
|
world_setup = LADXRWorldSetup()
|
||||||
world_setup.randomize(self.laxdr_options, self.multiworld.random)
|
world_setup.randomize(self.ladxr_settings, self.random)
|
||||||
self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup)
|
self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup)
|
||||||
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict()
|
self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict()
|
||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
# Initialize
|
# Initialize
|
||||||
@@ -180,8 +182,8 @@ class LinksAwakeningWorld(World):
|
|||||||
# For any and different world, set item rule instead
|
# For any and different world, set item rule instead
|
||||||
|
|
||||||
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
||||||
option = "shuffle_" + dungeon_item_type
|
option_name = "shuffle_" + dungeon_item_type
|
||||||
option = self.player_options[option]
|
option: DungeonItemShuffle = getattr(self.options, option_name)
|
||||||
|
|
||||||
dungeon_item_types[option.ladxr_item] = option.value
|
dungeon_item_types[option.ladxr_item] = option.value
|
||||||
|
|
||||||
@@ -189,11 +191,11 @@ class LinksAwakeningWorld(World):
|
|||||||
num_items = 8 if dungeon_item_type == "instruments" else 9
|
num_items = 8 if dungeon_item_type == "instruments" else 9
|
||||||
|
|
||||||
if option.value == DungeonItemShuffle.option_own_world:
|
if option.value == DungeonItemShuffle.option_own_world:
|
||||||
self.multiworld.local_items[self.player].value |= {
|
self.options.local_items.value |= {
|
||||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||||
}
|
}
|
||||||
elif option.value == DungeonItemShuffle.option_different_world:
|
elif option.value == DungeonItemShuffle.option_different_world:
|
||||||
self.multiworld.non_local_items[self.player].value |= {
|
self.options.non_local_items.value |= {
|
||||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||||
}
|
}
|
||||||
# option_original_dungeon = 0
|
# option_original_dungeon = 0
|
||||||
@@ -215,7 +217,7 @@ class LinksAwakeningWorld(World):
|
|||||||
else:
|
else:
|
||||||
item = self.create_item(item_name)
|
item = self.create_item(item_name)
|
||||||
|
|
||||||
if not self.multiworld.tradequest[self.player] and isinstance(item.item_data, TradeItemData):
|
if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
|
||||||
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
|
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
|
||||||
location.place_locked_item(item)
|
location.place_locked_item(item)
|
||||||
location.show_in_spoiler = False
|
location.show_in_spoiler = False
|
||||||
@@ -287,7 +289,7 @@ class LinksAwakeningWorld(World):
|
|||||||
if item.player == self.player
|
if item.player == self.player
|
||||||
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location]
|
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location]
|
||||||
if possible_start_items:
|
if possible_start_items:
|
||||||
index = self.multiworld.random.choice(possible_start_items)
|
index = self.random.choice(possible_start_items)
|
||||||
start_item = self.multiworld.itempool.pop(index)
|
start_item = self.multiworld.itempool.pop(index)
|
||||||
start_loc.place_locked_item(start_item)
|
start_loc.place_locked_item(start_item)
|
||||||
|
|
||||||
@@ -336,7 +338,7 @@ class LinksAwakeningWorld(World):
|
|||||||
# Get the list of locations and shuffle
|
# Get the list of locations and shuffle
|
||||||
all_dungeon_locs_to_fill = sorted(all_dungeon_locs)
|
all_dungeon_locs_to_fill = sorted(all_dungeon_locs)
|
||||||
|
|
||||||
self.multiworld.random.shuffle(all_dungeon_locs_to_fill)
|
self.random.shuffle(all_dungeon_locs_to_fill)
|
||||||
|
|
||||||
# Get the list of items and sort by priority
|
# Get the list of items and sort by priority
|
||||||
def priority(item):
|
def priority(item):
|
||||||
@@ -433,6 +435,12 @@ class LinksAwakeningWorld(World):
|
|||||||
|
|
||||||
return "TRADING_ITEM_LETTER"
|
return "TRADING_ITEM_LETTER"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||||
|
rom_file = get_base_rom_path()
|
||||||
|
if not os.path.exists(rom_file):
|
||||||
|
raise FileNotFoundError(rom_file)
|
||||||
|
|
||||||
def generate_output(self, output_directory: str):
|
def generate_output(self, output_directory: str):
|
||||||
# copy items back to locations
|
# copy items back to locations
|
||||||
for r in self.multiworld.get_regions(self.player):
|
for r in self.multiworld.get_regions(self.player):
|
||||||
@@ -459,34 +467,19 @@ class LinksAwakeningWorld(World):
|
|||||||
loc.ladxr_item.location_owner = self.player
|
loc.ladxr_item.location_owner = self.player
|
||||||
|
|
||||||
rom_name = Rom.get_base_rom_path()
|
rom_name = Rom.get_base_rom_path()
|
||||||
out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc"
|
out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.player_name}.gbc"
|
||||||
out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc")
|
out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc")
|
||||||
|
|
||||||
parser = get_parser()
|
parser = get_parser()
|
||||||
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
|
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
|
||||||
|
|
||||||
name_for_rom = self.multiworld.player_name[self.player]
|
rom = generator.generateRom(args, self)
|
||||||
|
|
||||||
all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))]
|
|
||||||
|
|
||||||
rom = generator.generateRom(
|
|
||||||
args,
|
|
||||||
self.laxdr_options,
|
|
||||||
self.player_options,
|
|
||||||
self.multi_key,
|
|
||||||
self.multiworld.seed_name,
|
|
||||||
self.ladxr_logic,
|
|
||||||
rnd=self.multiworld.per_slot_randoms[self.player],
|
|
||||||
player_name=name_for_rom,
|
|
||||||
player_names=all_names,
|
|
||||||
player_id = self.player,
|
|
||||||
multiworld=self.multiworld)
|
|
||||||
|
|
||||||
with open(out_path, "wb") as handle:
|
with open(out_path, "wb") as handle:
|
||||||
rom.save(handle, name="LADXR")
|
rom.save(handle, name="LADXR")
|
||||||
|
|
||||||
# Write title screen after everything else is done - full gfxmods may stomp over the egg tiles
|
# Write title screen after everything else is done - full gfxmods may stomp over the egg tiles
|
||||||
if self.player_options["ap_title_screen"]:
|
if self.options.ap_title_screen:
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
|
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
|
||||||
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
|
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
|
||||||
|
|
||||||
@@ -494,16 +487,16 @@ class LinksAwakeningWorld(World):
|
|||||||
os.unlink(title_patch.name)
|
os.unlink(title_patch.name)
|
||||||
|
|
||||||
patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player,
|
patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player,
|
||||||
player_name=self.multiworld.player_name[self.player], patched_path=out_path)
|
player_name=self.player_name, patched_path=out_path)
|
||||||
patch.write()
|
patch.write()
|
||||||
if not DEVELOPER_MODE:
|
if not DEVELOPER_MODE:
|
||||||
os.unlink(out_path)
|
os.unlink(out_path)
|
||||||
|
|
||||||
def generate_multi_key(self):
|
def generate_multi_key(self):
|
||||||
return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
|
return bytearray(self.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big')
|
||||||
|
|
||||||
def modify_multidata(self, multidata: dict):
|
def modify_multidata(self, multidata: dict):
|
||||||
multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.player_name]
|
||||||
|
|
||||||
def collect(self, state, item: Item) -> bool:
|
def collect(self, state, item: Item) -> bool:
|
||||||
change = super().collect(state, item)
|
change = super().collect(state, item)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .regions import create_regions
|
|||||||
|
|
||||||
class LingoWebWorld(WebWorld):
|
class LingoWebWorld(WebWorld):
|
||||||
option_groups = lingo_option_groups
|
option_groups = lingo_option_groups
|
||||||
|
rich_text_options_doc = True
|
||||||
theme = "grass"
|
theme = "grass"
|
||||||
tutorials = [Tutorial(
|
tutorials = [Tutorial(
|
||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ from .items import TRAP_ITEMS
|
|||||||
|
|
||||||
class ShuffleDoors(Choice):
|
class ShuffleDoors(Choice):
|
||||||
"""If on, opening doors will require their respective "keys".
|
"""If on, opening doors will require their respective "keys".
|
||||||
In "simple", doors are sorted into logical groups, which are all opened by receiving an item.
|
|
||||||
In "complex", the items are much more granular, and will usually only open a single door each."""
|
- **Simple:** Doors are sorted into logical groups, which are all opened by
|
||||||
|
receiving an item.
|
||||||
|
- **Complex:** The items are much more granular, and will usually only open
|
||||||
|
a single door each.
|
||||||
|
"""
|
||||||
display_name = "Shuffle Doors"
|
display_name = "Shuffle Doors"
|
||||||
option_none = 0
|
option_none = 0
|
||||||
option_simple = 1
|
option_simple = 1
|
||||||
@@ -19,24 +23,37 @@ class ShuffleDoors(Choice):
|
|||||||
|
|
||||||
class ProgressiveOrangeTower(DefaultOnToggle):
|
class ProgressiveOrangeTower(DefaultOnToggle):
|
||||||
"""When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up.
|
"""When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up.
|
||||||
If off, there is an item for each floor of the tower, and each floor's item is the only one needed to access that floor.
|
|
||||||
If on, there are six progressive items, which open up the tower from the bottom floor upward.
|
- **Off:** There is an item for each floor of the tower, and each floor's
|
||||||
|
item is the only one needed to access that floor.
|
||||||
|
- **On:** There are six progressive items, which open up the tower from the
|
||||||
|
bottom floor upward.
|
||||||
"""
|
"""
|
||||||
display_name = "Progressive Orange Tower"
|
display_name = "Progressive Orange Tower"
|
||||||
|
|
||||||
|
|
||||||
class ProgressiveColorful(DefaultOnToggle):
|
class ProgressiveColorful(DefaultOnToggle):
|
||||||
"""When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up.
|
"""When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up.
|
||||||
If off, there is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you access to them.
|
|
||||||
If on, there are ten progressive items, which open up the sequence from White forward."""
|
- **Off:** There is an item for each room of The Colorful, meaning that
|
||||||
|
random rooms in the middle of the sequence can open up without giving you
|
||||||
|
access to them.
|
||||||
|
- **On:** There are ten progressive items, which open up the sequence from
|
||||||
|
White forward.
|
||||||
|
"""
|
||||||
display_name = "Progressive Colorful"
|
display_name = "Progressive Colorful"
|
||||||
|
|
||||||
|
|
||||||
class LocationChecks(Choice):
|
class LocationChecks(Choice):
|
||||||
"""Determines what locations are available.
|
"""Determines what locations are available.
|
||||||
On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels.
|
|
||||||
On "reduced", many of the locations that are associated with opening doors are removed.
|
- **Normal:** There will be a location check for each panel set that would
|
||||||
On "insanity", every individual panel in the game is a location check."""
|
ordinarily open a door, as well as for achievement panels and a small
|
||||||
|
handful of other panels.
|
||||||
|
- **Reduced:** Many of the locations that are associated with opening doors
|
||||||
|
are removed.
|
||||||
|
- **Insanity:** Every individual panel in the game is a location check.
|
||||||
|
"""
|
||||||
display_name = "Location Checks"
|
display_name = "Location Checks"
|
||||||
option_normal = 0
|
option_normal = 0
|
||||||
option_reduced = 1
|
option_reduced = 1
|
||||||
@@ -44,16 +61,20 @@ class LocationChecks(Choice):
|
|||||||
|
|
||||||
|
|
||||||
class ShuffleColors(DefaultOnToggle):
|
class ShuffleColors(DefaultOnToggle):
|
||||||
"""
|
"""If on, an item is added to the pool for every puzzle color (besides White).
|
||||||
If on, an item is added to the pool for every puzzle color (besides White).
|
|
||||||
You will need to unlock the requisite colors in order to be able to solve puzzles of that color.
|
You will need to unlock the requisite colors in order to be able to solve
|
||||||
|
puzzles of that color.
|
||||||
"""
|
"""
|
||||||
display_name = "Shuffle Colors"
|
display_name = "Shuffle Colors"
|
||||||
|
|
||||||
|
|
||||||
class ShufflePanels(Choice):
|
class ShufflePanels(Choice):
|
||||||
"""If on, the puzzles on each panel are randomized.
|
"""If on, the puzzles on each panel are randomized.
|
||||||
On "rearrange", the puzzles are the same as the ones in the base game, but are placed in different areas."""
|
|
||||||
|
On "rearrange", the puzzles are the same as the ones in the base game, but
|
||||||
|
are placed in different areas.
|
||||||
|
"""
|
||||||
display_name = "Shuffle Panels"
|
display_name = "Shuffle Panels"
|
||||||
option_none = 0
|
option_none = 0
|
||||||
option_rearrange = 1
|
option_rearrange = 1
|
||||||
@@ -66,22 +87,26 @@ class ShufflePaintings(Toggle):
|
|||||||
|
|
||||||
class EnablePilgrimage(Toggle):
|
class EnablePilgrimage(Toggle):
|
||||||
"""Determines how the pilgrimage works.
|
"""Determines how the pilgrimage works.
|
||||||
If on, you are required to complete a pilgrimage in order to access the Pilgrim Antechamber.
|
|
||||||
If off, the pilgrimage will be deactivated, and the sun painting will be added to the pool, even if door shuffle is off."""
|
- **On:** You are required to complete a pilgrimage in order to access the
|
||||||
|
Pilgrim Antechamber.
|
||||||
|
- **Off:** The pilgrimage will be deactivated, and the sun painting will be
|
||||||
|
added to the pool, even if door shuffle is off.
|
||||||
|
"""
|
||||||
display_name = "Enable Pilgrimage"
|
display_name = "Enable Pilgrimage"
|
||||||
|
|
||||||
|
|
||||||
class PilgrimageAllowsRoofAccess(DefaultOnToggle):
|
class PilgrimageAllowsRoofAccess(DefaultOnToggle):
|
||||||
"""
|
"""If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so).
|
||||||
If on, you may use the Crossroads roof access during a pilgrimage (and you may be expected to do so).
|
|
||||||
Otherwise, pilgrimage will be deactivated when going up the stairs.
|
Otherwise, pilgrimage will be deactivated when going up the stairs.
|
||||||
"""
|
"""
|
||||||
display_name = "Allow Roof Access for Pilgrimage"
|
display_name = "Allow Roof Access for Pilgrimage"
|
||||||
|
|
||||||
|
|
||||||
class PilgrimageAllowsPaintings(DefaultOnToggle):
|
class PilgrimageAllowsPaintings(DefaultOnToggle):
|
||||||
"""
|
"""If on, you may use paintings during a pilgrimage (and you may be expected to do so).
|
||||||
If on, you may use paintings during a pilgrimage (and you may be expected to do so).
|
|
||||||
Otherwise, pilgrimage will be deactivated when going through a painting.
|
Otherwise, pilgrimage will be deactivated when going through a painting.
|
||||||
"""
|
"""
|
||||||
display_name = "Allow Paintings for Pilgrimage"
|
display_name = "Allow Paintings for Pilgrimage"
|
||||||
@@ -89,11 +114,17 @@ class PilgrimageAllowsPaintings(DefaultOnToggle):
|
|||||||
|
|
||||||
class SunwarpAccess(Choice):
|
class SunwarpAccess(Choice):
|
||||||
"""Determines how access to sunwarps works.
|
"""Determines how access to sunwarps works.
|
||||||
On "normal", all sunwarps are enabled from the start.
|
|
||||||
On "disabled", all sunwarps are disabled. Pilgrimage must be disabled when this is used.
|
- **Normal:** All sunwarps are enabled from the start.
|
||||||
On "unlock", sunwarps start off disabled, and all six activate once you receive an item.
|
- **Disabled:** All sunwarps are disabled. Pilgrimage must be disabled when
|
||||||
On "individual", sunwarps start off disabled, and each has a corresponding item that unlocks it.
|
this is used.
|
||||||
On "progressive", sunwarps start off disabled, and they unlock in order using a progressive item."""
|
- **Unlock:** Sunwarps start off disabled, and all six activate once you
|
||||||
|
receive an item.
|
||||||
|
- **Individual:** Sunwarps start off disabled, and each has a corresponding
|
||||||
|
item that unlocks it.
|
||||||
|
- **Progressive:** Sunwarps start off disabled, and they unlock in order
|
||||||
|
using a progressive item.
|
||||||
|
"""
|
||||||
display_name = "Sunwarp Access"
|
display_name = "Sunwarp Access"
|
||||||
option_normal = 0
|
option_normal = 0
|
||||||
option_disabled = 1
|
option_disabled = 1
|
||||||
@@ -109,10 +140,16 @@ class ShuffleSunwarps(Toggle):
|
|||||||
|
|
||||||
class VictoryCondition(Choice):
|
class VictoryCondition(Choice):
|
||||||
"""Change the victory condition.
|
"""Change the victory condition.
|
||||||
On "the_end", the goal is to solve THE END at the top of the tower.
|
|
||||||
On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option.
|
- **The End:** the goal is to solve THE END at the top of the tower.
|
||||||
On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option.
|
- **The Master:** The goal is to solve THE MASTER at the top of the tower,
|
||||||
On "pilgrimage", the goal is to solve PILGRIM in the Pilgrim Antechamber, typically after performing a Pilgrimage."""
|
after getting the number of achievements specified in the Mastery
|
||||||
|
Achievements option.
|
||||||
|
- **Level 2:** The goal is to solve LEVEL 2 in the second room, after
|
||||||
|
solving the number of panels specified in the Level 2 Requirement option.
|
||||||
|
- **Pilgrimage:** The goal is to solve PILGRIM in the Pilgrim Antechamber,
|
||||||
|
typically after performing a Pilgrimage.
|
||||||
|
"""
|
||||||
display_name = "Victory Condition"
|
display_name = "Victory Condition"
|
||||||
option_the_end = 0
|
option_the_end = 0
|
||||||
option_the_master = 1
|
option_the_master = 1
|
||||||
@@ -122,9 +159,12 @@ class VictoryCondition(Choice):
|
|||||||
|
|
||||||
class MasteryAchievements(Range):
|
class MasteryAchievements(Range):
|
||||||
"""The number of achievements required to unlock THE MASTER.
|
"""The number of achievements required to unlock THE MASTER.
|
||||||
In the base game, 21 achievements are needed.
|
|
||||||
If you include The Scientific and The Unchallenged, which are in the base game but are not counted for mastery, 23 would be required.
|
- In the base game, 21 achievements are needed.
|
||||||
If you include the custom achievement (The Wanderer), 24 would be required.
|
- If you include The Scientific and The Unchallenged, which are in the base
|
||||||
|
game but are not counted for mastery, 23 would be required.
|
||||||
|
- If you include the custom achievement (The Wanderer), 24 would be
|
||||||
|
required.
|
||||||
"""
|
"""
|
||||||
display_name = "Mastery Achievements"
|
display_name = "Mastery Achievements"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
@@ -134,9 +174,10 @@ class MasteryAchievements(Range):
|
|||||||
|
|
||||||
class Level2Requirement(Range):
|
class Level2Requirement(Range):
|
||||||
"""The number of panel solves required to unlock LEVEL 2.
|
"""The number of panel solves required to unlock LEVEL 2.
|
||||||
In the base game, 223 are needed.
|
|
||||||
Note that this count includes ANOTHER TRY.
|
In the base game, 223 are needed. Note that this count includes ANOTHER TRY.
|
||||||
When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free.
|
When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for
|
||||||
|
free.
|
||||||
"""
|
"""
|
||||||
display_name = "Level 2 Requirement"
|
display_name = "Level 2 Requirement"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
@@ -145,9 +186,10 @@ class Level2Requirement(Range):
|
|||||||
|
|
||||||
|
|
||||||
class EarlyColorHallways(Toggle):
|
class EarlyColorHallways(Toggle):
|
||||||
"""
|
"""When on, a painting warp to the color hallways area will appear in the starting room.
|
||||||
When on, a painting warp to the color hallways area will appear in the starting room.
|
|
||||||
This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on.
|
This lets you avoid being trapped in the starting room for long periods of
|
||||||
|
time when door shuffle is on.
|
||||||
"""
|
"""
|
||||||
display_name = "Early Color Hallways"
|
display_name = "Early Color Hallways"
|
||||||
|
|
||||||
@@ -161,8 +203,8 @@ class TrapPercentage(Range):
|
|||||||
|
|
||||||
|
|
||||||
class TrapWeights(OptionDict):
|
class TrapWeights(OptionDict):
|
||||||
"""
|
"""Specify the distribution of traps that should be placed into the pool.
|
||||||
Specify the distribution of traps that should be placed into the pool.
|
|
||||||
If you don't want a specific type of trap, set the weight to zero.
|
If you don't want a specific type of trap, set the weight to zero.
|
||||||
"""
|
"""
|
||||||
display_name = "Trap Weights"
|
display_name = "Trap Weights"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user