mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 04:13:26 -07:00
Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8971340a66 | ||
|
|
30cfd3186c | ||
|
|
1dc4e2b44b | ||
|
|
d5b4a91a13 | ||
|
|
bf5282dfa8 | ||
|
|
4eea91daab | ||
|
|
20e80d06cf | ||
|
|
59b78528a9 | ||
|
|
cd4fd18706 | ||
|
|
af44c1ba3d | ||
|
|
3ef0a56ec2 | ||
|
|
4ff282a384 | ||
|
|
f3dad894ec | ||
|
|
a5373e3672 | ||
|
|
639606e0be | ||
|
|
bb79073ce7 | ||
|
|
53b3cd029e | ||
|
|
99bd525c8e | ||
|
|
d14ab97849 | ||
|
|
f50e85b401 | ||
|
|
b64565594a | ||
|
|
ae7dad8bf9 | ||
|
|
b7c74919b7 | ||
|
|
a7f7f91aaf | ||
|
|
e62f989ce8 | ||
|
|
21c6c28755 | ||
|
|
f0403b9c9d | ||
|
|
f09f3663d6 | ||
|
|
b5bd93c420 | ||
|
|
90813c0f4b | ||
|
|
e2c4293a6d | ||
|
|
963c33c02a | ||
|
|
7d603e7d8d | ||
|
|
f2e1495d39 | ||
|
|
7927b2ee25 | ||
|
|
4f2b13a674 | ||
|
|
ffd7d5da74 | ||
|
|
67eb370200 | ||
|
|
4456e36fbb | ||
|
|
7fd9e71b3c | ||
|
|
f4a68f1c3d | ||
|
|
754a57cf69 | ||
|
|
384577e421 | ||
|
|
0ed3865c30 | ||
|
|
77b2ed54a6 | ||
|
|
0386d9f6d2 | ||
|
|
7e52b6d8bb | ||
|
|
03cf525b2c | ||
|
|
e1f46d623c | ||
|
|
5bb6ff0ce0 | ||
|
|
256f493ada | ||
|
|
3ec2d45f4f | ||
|
|
b3895750ab | ||
|
|
7591404151 | ||
|
|
d48e1e447f | ||
|
|
206f8cf5ed | ||
|
|
0c6b1827fe | ||
|
|
017f91c1b5 | ||
|
|
95b01def6b | ||
|
|
5977e401d5 | ||
|
|
21a3c74783 | ||
|
|
2fb9176511 | ||
|
|
1c69fb3c3c | ||
|
|
91502505a1 | ||
|
|
01c13ca243 | ||
|
|
c2a8b842de | ||
|
|
856efebc39 | ||
|
|
5a4203649d | ||
|
|
ddb764a9b6 | ||
|
|
9f65f22fac | ||
|
|
b7ff9b69ba | ||
|
|
012e6ba24c | ||
|
|
cd9d0bebc8 | ||
|
|
3fa6588637 | ||
|
|
e6d16c905c | ||
|
|
958829d491 | ||
|
|
9ee37b0ec5 | ||
|
|
81a239325d | ||
|
|
67bf12369a | ||
|
|
d4b793902f | ||
|
|
6671b21a86 | ||
|
|
6d13dc4944 | ||
|
|
ff9f563d4a | ||
|
|
d825576f12 | ||
|
|
5d6184f1fd | ||
|
|
e433246f0c | ||
|
|
3a190a8fb2 | ||
|
|
4b7033fce7 | ||
|
|
37499b40a1 | ||
|
|
ca2c0e6ce2 | ||
|
|
2a28a6de28 | ||
|
|
573a1a8402 | ||
|
|
060ee926e7 | ||
|
|
df55455fc0 | ||
|
|
4d7bd929bc | ||
|
|
030e41363a | ||
|
|
4bc0e84a7f | ||
|
|
070a92e76c | ||
|
|
39563cc347 | ||
|
|
54cce4c392 | ||
|
|
426a81a065 | ||
|
|
04e6a8eae8 | ||
|
|
0cfdc973f6 | ||
|
|
f3ca0a21c9 | ||
|
|
5fef41eb97 | ||
|
|
4068ba2f15 | ||
|
|
b1599c557f | ||
|
|
7fdf38b2ad | ||
|
|
2e76085cf1 | ||
|
|
c61f467218 | ||
|
|
942d689093 | ||
|
|
5e1aa52373 | ||
|
|
a95e51deda | ||
|
|
738319462d | ||
|
|
e3deb822ad | ||
|
|
d57314a407 | ||
|
|
5a8e6e61f5 | ||
|
|
17e90ce12c | ||
|
|
016157a0eb | ||
|
|
5b64c5f934 | ||
|
|
414166f6a2 | ||
|
|
e6109394ad | ||
|
|
8ca25fed63 | ||
|
|
227d59ecfb | ||
|
|
08c17c83d4 | ||
|
|
efb2ab4505 | ||
|
|
3a68ce3faa | ||
|
|
e78800d1bc | ||
|
|
96d7a3a64c | ||
|
|
30b70b2055 | ||
|
|
cd234fc04a | ||
|
|
d74c4c4c94 | ||
|
|
a4b61118cf | ||
|
|
9fa1f4e85f | ||
|
|
3a926849a0 | ||
|
|
798d823397 | ||
|
|
4ea582f14e | ||
|
|
21fb16291d | ||
|
|
805f33c39e | ||
|
|
0cf8206660 | ||
|
|
2c20b56478 | ||
|
|
1d2f7d8669 | ||
|
|
0733775f2c | ||
|
|
d6f3b27695 | ||
|
|
ce7e6bcf33 | ||
|
|
2c4658a7e0 | ||
|
|
79b8733b13 | ||
|
|
9cb9cbe47d | ||
|
|
7cad53c31a | ||
|
|
f3bdf0c5ed | ||
|
|
af7d0dbf37 | ||
|
|
0286edf20c | ||
|
|
05e36cab1c | ||
|
|
50425985c4 | ||
|
|
062d6eeace | ||
|
|
6c460bcbf7 | ||
|
|
b8659d28cc | ||
|
|
0b12d80008 | ||
|
|
5966aa5327 | ||
|
|
7c68e91d4a | ||
|
|
1d6ab13015 | ||
|
|
cb3d40624c | ||
|
|
0eb66957b1 | ||
|
|
53e2232f29 | ||
|
|
ecd2675ea8 | ||
|
|
fc2e555b4a | ||
|
|
df020bb389 | ||
|
|
7760034ff7 | ||
|
|
3e7794d5dc | ||
|
|
a3e8bb474a | ||
|
|
e4c95c940a | ||
|
|
daa1809a0f | ||
|
|
0a1261eb84 | ||
|
|
b62be6f7f4 | ||
|
|
ce2553a2b3 | ||
|
|
18c4b4b1fe | ||
|
|
a85ca9cc87 | ||
|
|
ad4846cedd | ||
|
|
b20be3ccec | ||
|
|
8af7908cd0 | ||
|
|
f078750b72 | ||
|
|
7cbeb8438b | ||
|
|
f7a0542898 | ||
|
|
cc61f16e57 | ||
|
|
9e3c2e2464 | ||
|
|
f528175d8a | ||
|
|
803d7105a1 | ||
|
|
a40f6058b5 | ||
|
|
0ff3c693d5 | ||
|
|
873a374a69 | ||
|
|
60584b7617 | ||
|
|
e24a85ca5c | ||
|
|
cc0540d3fb | ||
|
|
c360b9266c | ||
|
|
6148213e43 | ||
|
|
ff175008a1 | ||
|
|
cae1e683e2 | ||
|
|
fb1a9e9c5a | ||
|
|
555a0da46d | ||
|
|
0817305d5b | ||
|
|
995c978628 | ||
|
|
4de7ebd8b0 | ||
|
|
3cef39a387 | ||
|
|
ffff9ece55 | ||
|
|
dc2aa5f41e | ||
|
|
428344b6bc |
45
.github/workflows/build.yml
vendored
45
.github/workflows/build.yml
vendored
@@ -2,10 +2,20 @@
|
|||||||
|
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on: workflow_dispatch
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/build.yml'
|
||||||
|
- 'setup.py'
|
||||||
|
- 'requirements.txt'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/build.yml'
|
||||||
|
- 'setup.py'
|
||||||
|
- 'requirements.txt'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SNI_VERSION: v0.0.84
|
|
||||||
ENEMIZER_VERSION: 7.1
|
ENEMIZER_VERSION: 7.1
|
||||||
APPIMAGETOOL_VERSION: 13
|
APPIMAGETOOL_VERSION: 13
|
||||||
|
|
||||||
@@ -15,21 +25,18 @@ jobs:
|
|||||||
build-win-py38: # RCs will still be built and signed by hand
|
build-win-py38: # RCs will still be built and signed by hand
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
- name: Download run-time dependencies
|
- name: Download run-time dependencies
|
||||||
run: |
|
run: |
|
||||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip
|
|
||||||
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
|
|
||||||
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
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip setuptools
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
|
||||||
python setup.py build_exe --yes
|
python setup.py build_exe --yes
|
||||||
$NAME="$(ls build)".Split('.',2)[1]
|
$NAME="$(ls build)".Split('.',2)[1]
|
||||||
$ZIP_NAME="Archipelago_$NAME.7z"
|
$ZIP_NAME="Archipelago_$NAME.7z"
|
||||||
@@ -39,7 +46,7 @@ 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
|
||||||
- name: Store 7z
|
- name: Store 7z
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.ZIP_NAME }}
|
name: ${{ env.ZIP_NAME }}
|
||||||
path: dist/${{ env.ZIP_NAME }}
|
path: dist/${{ env.ZIP_NAME }}
|
||||||
@@ -49,14 +56,14 @@ jobs:
|
|||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
steps:
|
steps:
|
||||||
# - copy code below to release.yml -
|
# - copy code below to release.yml -
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install base dependencies
|
- name: Install base dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
- name: Install build-time dependencies
|
- name: Install build-time dependencies
|
||||||
@@ -69,19 +76,15 @@ jobs:
|
|||||||
chmod a+rx appimagetool
|
chmod a+rx appimagetool
|
||||||
- name: Download run-time dependencies
|
- name: Download run-time dependencies
|
||||||
run: |
|
run: |
|
||||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
|
||||||
tar xf sni-*.tar.xz
|
|
||||||
rm sni-*.tar.xz
|
|
||||||
mv sni-* SNI
|
|
||||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
# pygobject is an optional dependency for kivy that's not in requirements
|
# pygobject is an optional dependency for kivy that's not in requirements
|
||||||
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
|
# charset-normalizer was somehow incomplete in the github runner
|
||||||
"${{ env.PYTHON }}" -m venv venv
|
"${{ env.PYTHON }}" -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install -r requirements.txt
|
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
|
||||||
python setup.py build_exe --yes bdist_appimage --yes
|
python setup.py build_exe --yes bdist_appimage --yes
|
||||||
echo -e "setup.py build output:\n `ls build`"
|
echo -e "setup.py build output:\n `ls build`"
|
||||||
echo -e "setup.py dist output:\n `ls dist`"
|
echo -e "setup.py dist output:\n `ls dist`"
|
||||||
@@ -91,14 +94,18 @@ jobs:
|
|||||||
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 -
|
||||||
|
- name: Build Again
|
||||||
|
run: |
|
||||||
|
source venv/bin/activate
|
||||||
|
python setup.py build_exe --yes
|
||||||
- name: Store AppImage
|
- name: Store AppImage
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.APPIMAGE_NAME }}
|
name: ${{ env.APPIMAGE_NAME }}
|
||||||
path: dist/${{ env.APPIMAGE_NAME }}
|
path: dist/${{ env.APPIMAGE_NAME }}
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: Store .tar.gz
|
- name: Store .tar.gz
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.TAR_NAME }}
|
name: ${{ env.TAR_NAME }}
|
||||||
path: dist/${{ env.TAR_NAME }}
|
path: dist/${{ env.TAR_NAME }}
|
||||||
|
|||||||
16
.github/workflows/codeql-analysis.yml
vendored
16
.github/workflows/codeql-analysis.yml
vendored
@@ -14,9 +14,17 @@ name: "CodeQL"
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- '**.py'
|
||||||
|
- '**.js'
|
||||||
|
- '.github/workflows/codeql-analysis.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- '**.py'
|
||||||
|
- '**.js'
|
||||||
|
- '.github/workflows/codeql-analysis.yml'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '44 8 * * 1'
|
- cron: '44 8 * * 1'
|
||||||
|
|
||||||
@@ -35,11 +43,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -50,7 +58,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -64,4 +72,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
16
.github/workflows/lint.yml
vendored
16
.github/workflows/lint.yml
vendored
@@ -3,23 +3,29 @@
|
|||||||
|
|
||||||
name: lint
|
name: lint
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.py'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.py'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
flake8:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
pip install flake8 pytest pytest-subtests
|
pip install flake8
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
- '*.*.*'
|
- '*.*.*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SNI_VERSION: v0.0.84
|
|
||||||
ENEMIZER_VERSION: 7.1
|
ENEMIZER_VERSION: 7.1
|
||||||
APPIMAGETOOL_VERSION: 13
|
APPIMAGETOOL_VERSION: 13
|
||||||
|
|
||||||
@@ -36,14 +35,14 @@ jobs:
|
|||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
# - code below copied from build.yml -
|
# - code below copied from build.yml -
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Install base dependencies
|
- name: Install base dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
- name: Install build-time dependencies
|
- name: Install build-time dependencies
|
||||||
@@ -56,20 +55,16 @@ jobs:
|
|||||||
chmod a+rx appimagetool
|
chmod a+rx appimagetool
|
||||||
- name: Download run-time dependencies
|
- name: Download run-time dependencies
|
||||||
run: |
|
run: |
|
||||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
|
||||||
tar xf sni-*.tar.xz
|
|
||||||
rm sni-*.tar.xz
|
|
||||||
mv sni-* SNI
|
|
||||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
# pygobject is an optional dependency for kivy that's not in requirements
|
# pygobject is an optional dependency for kivy that's not in requirements
|
||||||
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
|
# charset-normalizer was somehow incomplete in the github runner
|
||||||
"${{ env.PYTHON }}" -m venv venv
|
"${{ env.PYTHON }}" -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install -r requirements.txt
|
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
|
||||||
python setup.py build --yes bdist_appimage --yes
|
python setup.py build_exe --yes bdist_appimage --yes
|
||||||
echo -e "setup.py build output:\n `ls build`"
|
echo -e "setup.py build output:\n `ls build`"
|
||||||
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 ..
|
||||||
|
|||||||
30
.github/workflows/unittests.yml
vendored
30
.github/workflows/unittests.yml
vendored
@@ -3,7 +3,25 @@
|
|||||||
|
|
||||||
name: unittests
|
name: unittests
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '!docs/**'
|
||||||
|
- '!setup.py'
|
||||||
|
- '!*.iss'
|
||||||
|
- '!.gitignore'
|
||||||
|
- '!.github/workflows/**'
|
||||||
|
- '.github/workflows/unittests.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '!docs/**'
|
||||||
|
- '!setup.py'
|
||||||
|
- '!*.iss'
|
||||||
|
- '!.gitignore'
|
||||||
|
- '!.github/workflows/**'
|
||||||
|
- '.github/workflows/unittests.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -23,17 +41,19 @@ jobs:
|
|||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python: {version: '3.10'} # current
|
- python: {version: '3.10'} # current
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
- python: {version: '3.10'} # current
|
||||||
|
os: macos-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python ${{ matrix.python.version }}
|
- name: Set up Python ${{ matrix.python.version }}
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python.version }}
|
python-version: ${{ matrix.python.version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip
|
||||||
pip install flake8 pytest pytest-subtests
|
pip install pytest pytest-subtests
|
||||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||||
- name: Unittests
|
- name: Unittests
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,6 +8,7 @@
|
|||||||
*.apm3
|
*.apm3
|
||||||
*.apmc
|
*.apmc
|
||||||
*.apz5
|
*.apz5
|
||||||
|
*.aptloz
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyd
|
*.pyd
|
||||||
*.sfc
|
*.sfc
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
*multisave
|
*multisave
|
||||||
*.archipelago
|
*.archipelago
|
||||||
*.apsave
|
*.apsave
|
||||||
|
*.BIN
|
||||||
|
|
||||||
build
|
build
|
||||||
bundle/components.wxs
|
bundle/components.wxs
|
||||||
@@ -50,6 +52,8 @@ Output Logs/
|
|||||||
/Archipelago.zip
|
/Archipelago.zip
|
||||||
/setup.ini
|
/setup.ini
|
||||||
/installdelete.iss
|
/installdelete.iss
|
||||||
|
/data/user.kv
|
||||||
|
/datapackage
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -137,6 +141,7 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
.code-workspace
|
.code-workspace
|
||||||
|
shell.nix
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@@ -166,6 +171,7 @@ cython_debug/
|
|||||||
jdk*/
|
jdk*/
|
||||||
minecraft*/
|
minecraft*/
|
||||||
minecraft_versions.json
|
minecraft_versions.json
|
||||||
|
!worlds/minecraft/
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|||||||
516
AdventureClient.py
Normal file
516
AdventureClient.py
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import bsdiff4
|
||||||
|
import subprocess
|
||||||
|
import zipfile
|
||||||
|
from asyncio import StreamReader, StreamWriter, CancelledError
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
from NetUtils import ClientStatus
|
||||||
|
from Utils import async_start
|
||||||
|
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||||
|
get_base_parser
|
||||||
|
from worlds.adventure import AdventureDeltaPatch
|
||||||
|
|
||||||
|
from worlds.adventure.Locations import base_location_id
|
||||||
|
from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation
|
||||||
|
from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table
|
||||||
|
from worlds.adventure.Offsets import static_item_element_size, connector_port_offset
|
||||||
|
|
||||||
|
SYSTEM_MESSAGE_ID = 0
|
||||||
|
|
||||||
|
CONNECTION_TIMING_OUT_STATUS = \
|
||||||
|
"Connection timing out. Please restart your emulator, then restart adventure_connector.lua"
|
||||||
|
CONNECTION_REFUSED_STATUS = \
|
||||||
|
"Connection Refused. Please start your emulator and make sure adventure_connector.lua is running"
|
||||||
|
CONNECTION_RESET_STATUS = \
|
||||||
|
"Connection was reset. Please restart your emulator, then restart adventure_connector.lua"
|
||||||
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||||
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||||
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||||
|
|
||||||
|
SCRIPT_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
class AdventureCommandProcessor(ClientCommandProcessor):
|
||||||
|
def __init__(self, ctx: CommonContext):
|
||||||
|
super().__init__(ctx)
|
||||||
|
|
||||||
|
def _cmd_2600(self):
|
||||||
|
"""Check 2600 Connection State"""
|
||||||
|
if isinstance(self.ctx, AdventureContext):
|
||||||
|
logger.info(f"2600 Status: {self.ctx.atari_status}")
|
||||||
|
|
||||||
|
def _cmd_aconnect(self):
|
||||||
|
"""Discard current atari 2600 connection state"""
|
||||||
|
if isinstance(self.ctx, AdventureContext):
|
||||||
|
self.ctx.atari_sync_task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
class AdventureContext(CommonContext):
|
||||||
|
command_processor = AdventureCommandProcessor
|
||||||
|
game = 'Adventure'
|
||||||
|
lua_connector_port: int = 17242
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super().__init__(server_address, password)
|
||||||
|
self.freeincarnates_used: int = -1
|
||||||
|
self.freeincarnate_pending: int = 0
|
||||||
|
self.foreign_items: [AdventureForeignItemInfo] = []
|
||||||
|
self.autocollect_items: [AdventureAutoCollectLocation] = []
|
||||||
|
self.atari_streams: (StreamReader, StreamWriter) = None
|
||||||
|
self.atari_sync_task = None
|
||||||
|
self.messages = {}
|
||||||
|
self.locations_array = None
|
||||||
|
self.atari_status = CONNECTION_INITIAL_STATUS
|
||||||
|
self.awaiting_rom = False
|
||||||
|
self.display_msgs = True
|
||||||
|
self.deathlink_pending = False
|
||||||
|
self.set_deathlink = False
|
||||||
|
self.client_compatibility_mode = 0
|
||||||
|
self.items_handling = 0b111
|
||||||
|
self.checked_locations_sent: bool = False
|
||||||
|
self.port_offset = 0
|
||||||
|
self.bat_no_touch_locations: [BatNoTouchLocation] = []
|
||||||
|
self.local_item_locations = {}
|
||||||
|
self.dragon_speed_info = {}
|
||||||
|
|
||||||
|
options = Utils.get_options()
|
||||||
|
self.display_msgs = options["adventure_options"]["display_msgs"]
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super(AdventureContext, self).server_auth(password_requested)
|
||||||
|
if not self.auth:
|
||||||
|
self.auth = self.player_name
|
||||||
|
if not self.auth:
|
||||||
|
self.awaiting_rom = True
|
||||||
|
logger.info('Awaiting connection to adventure_connector to get Player information')
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.send_connect()
|
||||||
|
|
||||||
|
def _set_message(self, msg: str, msg_id: int):
|
||||||
|
if self.display_msgs:
|
||||||
|
self.messages[(time.time(), msg_id)] = msg
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd == 'Connected':
|
||||||
|
self.locations_array = None
|
||||||
|
if Utils.get_options()["adventure_options"].get("death_link", False):
|
||||||
|
self.set_deathlink = True
|
||||||
|
async_start(self.get_freeincarnates_used())
|
||||||
|
elif cmd == "RoomInfo":
|
||||||
|
self.seed_name = args['seed_name']
|
||||||
|
elif cmd == 'Print':
|
||||||
|
msg = args['text']
|
||||||
|
if ': !' not in msg:
|
||||||
|
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||||
|
elif cmd == "ReceivedItems":
|
||||||
|
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||||
|
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||||
|
elif cmd == "Retrieved":
|
||||||
|
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
|
||||||
|
if self.freeincarnates_used is None:
|
||||||
|
self.freeincarnates_used = 0
|
||||||
|
self.freeincarnates_used += self.freeincarnate_pending
|
||||||
|
self.send_pending_freeincarnates()
|
||||||
|
elif cmd == "SetReply":
|
||||||
|
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
|
||||||
|
self.freeincarnates_used = args["value"]
|
||||||
|
if self.freeincarnates_used is None:
|
||||||
|
self.freeincarnates_used = 0
|
||||||
|
self.freeincarnates_used += self.freeincarnate_pending
|
||||||
|
self.send_pending_freeincarnates()
|
||||||
|
|
||||||
|
def on_deathlink(self, data: dict):
|
||||||
|
self.deathlink_pending = True
|
||||||
|
super().on_deathlink(data)
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
from kvui import GameManager
|
||||||
|
|
||||||
|
class AdventureManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
|
base_title = "Archipelago Adventure Client"
|
||||||
|
|
||||||
|
self.ui = AdventureManager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
async def get_freeincarnates_used(self):
|
||||||
|
if self.server and not self.server.socket.closed:
|
||||||
|
await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
|
||||||
|
await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
|
||||||
|
|
||||||
|
def send_pending_freeincarnates(self):
|
||||||
|
if self.freeincarnate_pending > 0:
|
||||||
|
async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending))
|
||||||
|
self.freeincarnate_pending = 0
|
||||||
|
|
||||||
|
async def send_pending_freeincarnates_impl(self, send_val: int) -> None:
|
||||||
|
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
|
||||||
|
"default": 0, "want_reply": False,
|
||||||
|
"operations": [{"operation": "add", "value": send_val}]}])
|
||||||
|
|
||||||
|
async def used_freeincarnate(self) -> None:
|
||||||
|
if self.server and not self.server.socket.closed:
|
||||||
|
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
|
||||||
|
"default": 0, "want_reply": True,
|
||||||
|
"operations": [{"operation": "add", "value": 1}]}])
|
||||||
|
else:
|
||||||
|
self.freeincarnate_pending = self.freeincarnate_pending + 1
|
||||||
|
|
||||||
|
|
||||||
|
def convert_item_id(ap_item_id: int):
|
||||||
|
static_item_index = ap_item_id - base_adventure_item_id
|
||||||
|
return static_item_index * static_item_element_size
|
||||||
|
|
||||||
|
|
||||||
|
def get_payload(ctx: AdventureContext):
|
||||||
|
current_time = time.time()
|
||||||
|
items = []
|
||||||
|
dragon_speed_update = {}
|
||||||
|
diff_a_locked = ctx.diff_a_mode > 0
|
||||||
|
diff_b_locked = ctx.diff_b_mode > 0
|
||||||
|
freeincarnate_count = 0
|
||||||
|
for item in ctx.items_received:
|
||||||
|
item_id_str = str(item.item)
|
||||||
|
if base_adventure_item_id < item.item <= standard_item_max:
|
||||||
|
items.append(convert_item_id(item.item))
|
||||||
|
elif item_id_str in ctx.dragon_speed_info:
|
||||||
|
if item.item in dragon_speed_update:
|
||||||
|
last_index = len(ctx.dragon_speed_info[item_id_str]) - 1
|
||||||
|
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index]
|
||||||
|
else:
|
||||||
|
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0]
|
||||||
|
elif item.item == item_table["Left Difficulty Switch"].id:
|
||||||
|
diff_a_locked = False
|
||||||
|
elif item.item == item_table["Right Difficulty Switch"].id:
|
||||||
|
diff_b_locked = False
|
||||||
|
elif item.item == item_table["Freeincarnate"].id:
|
||||||
|
freeincarnate_count = freeincarnate_count + 1
|
||||||
|
freeincarnates_available = 0
|
||||||
|
|
||||||
|
if ctx.freeincarnates_used >= 0:
|
||||||
|
freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending)
|
||||||
|
ret = json.dumps(
|
||||||
|
{
|
||||||
|
"items": items,
|
||||||
|
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||||
|
if key[0] > current_time - 10},
|
||||||
|
"deathlink": ctx.deathlink_pending,
|
||||||
|
"dragon_speeds": dragon_speed_update,
|
||||||
|
"difficulty_a_locked": diff_a_locked,
|
||||||
|
"difficulty_b_locked": diff_b_locked,
|
||||||
|
"freeincarnates_available": freeincarnates_available,
|
||||||
|
"bat_logic": ctx.bat_logic
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ctx.deathlink_pending = False
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_locations(data: List, ctx: AdventureContext):
|
||||||
|
locations = data
|
||||||
|
|
||||||
|
# for loc_name, loc_data in location_table.items():
|
||||||
|
|
||||||
|
# if flags["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||||
|
# await ctx.send_msgs([
|
||||||
|
# {"cmd": "StatusUpdate",
|
||||||
|
# "status": 30}
|
||||||
|
# ])
|
||||||
|
# ctx.finished_game = True
|
||||||
|
if locations == ctx.locations_array:
|
||||||
|
return
|
||||||
|
ctx.locations_array = locations
|
||||||
|
if locations is not None:
|
||||||
|
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
|
||||||
|
|
||||||
|
|
||||||
|
def send_ap_foreign_items(adventure_context):
|
||||||
|
foreign_item_json_list = []
|
||||||
|
autocollect_item_json_list = []
|
||||||
|
bat_no_touch_locations_json_list = []
|
||||||
|
for fi in adventure_context.foreign_items:
|
||||||
|
foreign_item_json_list.append(fi.get_dict())
|
||||||
|
for fi in adventure_context.autocollect_items:
|
||||||
|
autocollect_item_json_list.append(fi.get_dict())
|
||||||
|
for ntl in adventure_context.bat_no_touch_locations:
|
||||||
|
bat_no_touch_locations_json_list.append(ntl.get_dict())
|
||||||
|
payload = json.dumps(
|
||||||
|
{
|
||||||
|
"foreign_items": foreign_item_json_list,
|
||||||
|
"autocollect_items": autocollect_item_json_list,
|
||||||
|
"local_item_locations": adventure_context.local_item_locations,
|
||||||
|
"bat_no_touch_locations": bat_no_touch_locations_json_list
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print("sending foreign items")
|
||||||
|
msg = payload.encode()
|
||||||
|
(reader, writer) = adventure_context.atari_streams
|
||||||
|
writer.write(msg)
|
||||||
|
writer.write(b'\n')
|
||||||
|
|
||||||
|
|
||||||
|
def send_checked_locations_if_needed(adventure_context):
|
||||||
|
if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None:
|
||||||
|
if len(adventure_context.checked_locations) == 0:
|
||||||
|
return
|
||||||
|
checked_short_ids = []
|
||||||
|
for location in adventure_context.checked_locations:
|
||||||
|
checked_short_ids.append(location - base_location_id)
|
||||||
|
print("Sending checked locations")
|
||||||
|
payload = json.dumps(
|
||||||
|
{
|
||||||
|
"checked_locations": checked_short_ids,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = payload.encode()
|
||||||
|
(reader, writer) = adventure_context.atari_streams
|
||||||
|
writer.write(msg)
|
||||||
|
writer.write(b'\n')
|
||||||
|
adventure_context.checked_locations_sent = True
|
||||||
|
|
||||||
|
|
||||||
|
async def atari_sync_task(ctx: AdventureContext):
|
||||||
|
logger.info("Starting Atari 2600 connector. Use /2600 for status information")
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
try:
|
||||||
|
error_status = None
|
||||||
|
if ctx.atari_streams:
|
||||||
|
(reader, writer) = ctx.atari_streams
|
||||||
|
msg = get_payload(ctx).encode()
|
||||||
|
writer.write(msg)
|
||||||
|
writer.write(b'\n')
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
||||||
|
try:
|
||||||
|
# Data will return a dict with 1+ fields
|
||||||
|
# 1. A keepalive response of the Players Name (always)
|
||||||
|
# 2. romhash field with sha256 hash of the ROM memory region
|
||||||
|
# 3. locations, messages, and deathLink
|
||||||
|
# 4. freeincarnate, to indicate a freeincarnate was used
|
||||||
|
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||||
|
data_decoded = json.loads(data.decode())
|
||||||
|
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
|
||||||
|
msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \
|
||||||
|
"Lua and AdventureClient are from the same Archipelago installation."
|
||||||
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
|
ctx.gui_error('Error', msg)
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data:
|
||||||
|
msg = "The server is running a different multiworld than your client is. " \
|
||||||
|
"(invalid seed_name)"
|
||||||
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
|
ctx.gui_error('Error', msg)
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
if 'romhash' in data_decoded:
|
||||||
|
if ctx.rom_hash.upper() != data_decoded['romhash'].upper():
|
||||||
|
msg = "The rom hash does not match the client rom hash data"
|
||||||
|
print("got " + data_decoded['romhash'])
|
||||||
|
print("expected " + str(ctx.rom_hash))
|
||||||
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
|
ctx.gui_error('Error', msg)
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
if ctx.auth is None:
|
||||||
|
ctx.auth = ctx.player_name
|
||||||
|
if ctx.awaiting_rom:
|
||||||
|
await ctx.server_auth(False)
|
||||||
|
if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \
|
||||||
|
and not error_status and ctx.auth:
|
||||||
|
# Not just a keep alive ping, parse
|
||||||
|
async_start(parse_locations(data_decoded['locations'], ctx))
|
||||||
|
if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags:
|
||||||
|
dragon_name = "a dragon"
|
||||||
|
if data_decoded['deathLink'] == 1:
|
||||||
|
dragon_name = "Rhindle"
|
||||||
|
elif data_decoded['deathLink'] == 2:
|
||||||
|
dragon_name = "Yorgle"
|
||||||
|
elif data_decoded['deathLink'] == 3:
|
||||||
|
dragon_name = "Grundle"
|
||||||
|
print (ctx.auth + " has been eaten by " + dragon_name )
|
||||||
|
await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name)
|
||||||
|
# TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by '
|
||||||
|
if 'victory' in data_decoded and not ctx.finished_game:
|
||||||
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
|
ctx.finished_game = True
|
||||||
|
if 'freeincarnate' in data_decoded:
|
||||||
|
await ctx.used_freeincarnate()
|
||||||
|
if ctx.set_deathlink:
|
||||||
|
await ctx.update_death_link(True)
|
||||||
|
send_checked_locations_if_needed(ctx)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.debug("Read Timed Out, Reconnecting")
|
||||||
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.atari_streams = None
|
||||||
|
except ConnectionResetError as e:
|
||||||
|
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.atari_streams = None
|
||||||
|
except TimeoutError:
|
||||||
|
logger.debug("Connection Timed Out, Reconnecting")
|
||||||
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.atari_streams = None
|
||||||
|
except ConnectionResetError:
|
||||||
|
logger.debug("Connection Lost, Reconnecting")
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.atari_streams = None
|
||||||
|
except CancelledError:
|
||||||
|
logger.debug("Connection Cancelled, Reconnecting")
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.atari_streams = None
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print("unknown exception " + e)
|
||||||
|
raise
|
||||||
|
if ctx.atari_status == CONNECTION_TENTATIVE_STATUS:
|
||||||
|
if not error_status:
|
||||||
|
logger.info("Successfully Connected to 2600")
|
||||||
|
ctx.atari_status = CONNECTION_CONNECTED_STATUS
|
||||||
|
ctx.checked_locations_sent = False
|
||||||
|
send_ap_foreign_items(ctx)
|
||||||
|
send_checked_locations_if_needed(ctx)
|
||||||
|
else:
|
||||||
|
ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}"
|
||||||
|
elif error_status:
|
||||||
|
ctx.atari_status = error_status
|
||||||
|
logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
port = ctx.lua_connector_port + ctx.port_offset
|
||||||
|
logger.debug(f"Attempting to connect to 2600 on port {port}")
|
||||||
|
print(f"Attempting to connect to 2600 on port {port}")
|
||||||
|
ctx.atari_streams = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection("localhost",
|
||||||
|
port),
|
||||||
|
timeout=10)
|
||||||
|
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
|
||||||
|
except TimeoutError:
|
||||||
|
logger.debug("Connection Timed Out, Trying Again")
|
||||||
|
ctx.atari_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
continue
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
logger.debug("Connection Refused, Trying Again")
|
||||||
|
ctx.atari_status = CONNECTION_REFUSED_STATUS
|
||||||
|
continue
|
||||||
|
except CancelledError:
|
||||||
|
pass
|
||||||
|
except CancelledError:
|
||||||
|
pass
|
||||||
|
print("exiting atari sync task")
|
||||||
|
|
||||||
|
|
||||||
|
async def run_game(romfile):
|
||||||
|
auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
|
||||||
|
rom_args = Utils.get_options()["adventure_options"].get("rom_args")
|
||||||
|
if auto_start is True:
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open(romfile)
|
||||||
|
elif os.path.isfile(auto_start):
|
||||||
|
open_args = [auto_start, romfile]
|
||||||
|
if rom_args is not None:
|
||||||
|
open_args.insert(1, rom_args)
|
||||||
|
subprocess.Popen(open_args,
|
||||||
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
|
async def patch_and_run_game(patch_file, ctx):
|
||||||
|
base_name = os.path.splitext(patch_file)[0]
|
||||||
|
comp_path = base_name + '.a26'
|
||||||
|
try:
|
||||||
|
base_rom = AdventureDeltaPatch.get_source_data()
|
||||||
|
except Exception as msg:
|
||||||
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
|
ctx.gui_error('Error', msg)
|
||||||
|
|
||||||
|
with open(Utils.user_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
|
||||||
|
basepatch = bytes(file.read())
|
||||||
|
|
||||||
|
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
|
||||||
|
if not AdventureDeltaPatch.check_version(patch_archive):
|
||||||
|
logger.error("apadvn version doesn't match this client. Make sure your generator and client are the same")
|
||||||
|
raise Exception("apadvn version doesn't match this client.")
|
||||||
|
|
||||||
|
ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive)
|
||||||
|
ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive)
|
||||||
|
ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive)
|
||||||
|
ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive)
|
||||||
|
ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive)
|
||||||
|
ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive)
|
||||||
|
ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive)
|
||||||
|
ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive)
|
||||||
|
ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive)
|
||||||
|
ctx.auth = ctx.player_name
|
||||||
|
|
||||||
|
patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas)
|
||||||
|
rom_hash = hashlib.sha256()
|
||||||
|
rom_hash.update(patched_rom_data)
|
||||||
|
ctx.rom_hash = rom_hash.hexdigest()
|
||||||
|
ctx.port_offset = patched_rom_data[connector_port_offset]
|
||||||
|
|
||||||
|
with open(comp_path, "wb") as patched_rom_file:
|
||||||
|
patched_rom_file.write(patched_rom_data)
|
||||||
|
|
||||||
|
async_start(run_game(comp_path))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
Utils.init_logging("AdventureClient")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = get_base_parser()
|
||||||
|
parser.add_argument('patch_file', default="", type=str, nargs="?",
|
||||||
|
help='Path to an ADVNTURE.BIN rom file')
|
||||||
|
parser.add_argument('port', default=17242, type=int, nargs="?",
|
||||||
|
help='port for adventure_connector connection')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
ctx = AdventureContext(args.connect, args.password)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
ctx.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync")
|
||||||
|
|
||||||
|
if args.patch_file:
|
||||||
|
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
|
||||||
|
if ext == "apadvn":
|
||||||
|
logger.info("apadvn file supplied, beginning patching process...")
|
||||||
|
async_start(patch_and_run_game(args.patch_file, ctx))
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown patch file extension {ext}")
|
||||||
|
if args.port is int:
|
||||||
|
ctx.lua_connector_port = args.port
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
if ctx.atari_sync_task:
|
||||||
|
await ctx.atari_sync_task
|
||||||
|
print("finished atari_sync_task (main)")
|
||||||
|
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
colorama.deinit()
|
||||||
441
BaseClasses.py
441
BaseClasses.py
@@ -2,14 +2,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import functools
|
import functools
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import secrets
|
import secrets
|
||||||
import typing # this can go away when Python 3.8 support is dropped
|
import typing # this can go away when Python 3.8 support is dropped
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from collections import OrderedDict, Counter, deque
|
from collections import OrderedDict, Counter, deque, ChainMap
|
||||||
from enum import unique, IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
|
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
@@ -29,6 +28,20 @@ class Group(TypedDict, total=False):
|
|||||||
link_replacement: bool
|
link_replacement: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadBarrierProxy():
|
||||||
|
"""Passes through getattr while passthrough is True"""
|
||||||
|
def __init__(self, obj: Any):
|
||||||
|
self.passthrough = True
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
if self.passthrough:
|
||||||
|
return getattr(self.obj, item)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("You are in a threaded context and global random state was removed for your safety. "
|
||||||
|
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
|
||||||
|
|
||||||
|
|
||||||
class MultiWorld():
|
class MultiWorld():
|
||||||
debug_types = False
|
debug_types = False
|
||||||
player_name: Dict[int, str]
|
player_name: Dict[int, str]
|
||||||
@@ -58,9 +71,17 @@ class MultiWorld():
|
|||||||
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
||||||
indirect_connections: Dict[Region, Set[Entrance]]
|
indirect_connections: Dict[Region, Set[Entrance]]
|
||||||
exclude_locations: Dict[int, Options.ExcludeLocations]
|
exclude_locations: Dict[int, Options.ExcludeLocations]
|
||||||
|
priority_locations: Dict[int, Options.PriorityLocations]
|
||||||
|
start_inventory: Dict[int, Options.StartInventory]
|
||||||
|
start_hints: Dict[int, Options.StartHints]
|
||||||
|
start_location_hints: Dict[int, Options.StartLocationHints]
|
||||||
|
item_links: Dict[int, Options.ItemLinks]
|
||||||
|
|
||||||
game: Dict[int, str]
|
game: Dict[int, str]
|
||||||
|
|
||||||
|
random: random.Random
|
||||||
|
per_slot_randoms: Dict[int, random.Random]
|
||||||
|
|
||||||
class AttributeProxy():
|
class AttributeProxy():
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
self.rule = rule
|
self.rule = rule
|
||||||
@@ -69,7 +90,8 @@ class MultiWorld():
|
|||||||
return self.rule(player)
|
return self.rule(player)
|
||||||
|
|
||||||
def __init__(self, players: int):
|
def __init__(self, players: int):
|
||||||
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
|
# world-local random state is saved for multiple generations running concurrently
|
||||||
|
self.random = ThreadBarrierProxy(random.Random())
|
||||||
self.players = players
|
self.players = players
|
||||||
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
|
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
|
||||||
self.glitch_triforce = False
|
self.glitch_triforce = False
|
||||||
@@ -160,7 +182,7 @@ class MultiWorld():
|
|||||||
set_player_attr('completion_condition', lambda state: True)
|
set_player_attr('completion_condition', lambda state: True)
|
||||||
self.custom_data = {}
|
self.custom_data = {}
|
||||||
self.worlds = {}
|
self.worlds = {}
|
||||||
self.slot_seeds = {}
|
self.per_slot_randoms = {}
|
||||||
self.plando_options = PlandoOptions.none
|
self.plando_options = PlandoOptions.none
|
||||||
|
|
||||||
def get_all_ids(self) -> Tuple[int, ...]:
|
def get_all_ids(self) -> Tuple[int, ...]:
|
||||||
@@ -206,8 +228,8 @@ class MultiWorld():
|
|||||||
else:
|
else:
|
||||||
self.random.seed(self.seed)
|
self.random.seed(self.seed)
|
||||||
self.seed_name = name if name else str(self.seed)
|
self.seed_name = name if name else str(self.seed)
|
||||||
self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in
|
self.per_slot_randoms = {player: random.Random(self.random.getrandbits(64)) for player in
|
||||||
range(1, self.players + 1)}
|
range(1, self.players + 1)}
|
||||||
|
|
||||||
def set_options(self, args: Namespace) -> None:
|
def set_options(self, args: Namespace) -> None:
|
||||||
for option_key in Options.common_options:
|
for option_key in Options.common_options:
|
||||||
@@ -291,7 +313,7 @@ class MultiWorld():
|
|||||||
self.state = CollectionState(self)
|
self.state = CollectionState(self)
|
||||||
|
|
||||||
def secure(self):
|
def secure(self):
|
||||||
self.random = secrets.SystemRandom()
|
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
||||||
self.is_race = True
|
self.is_race = True
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
@@ -314,7 +336,7 @@ class MultiWorld():
|
|||||||
return self.player_name[player]
|
return self.player_name[player]
|
||||||
|
|
||||||
def get_file_safe_player_name(self, player: int) -> str:
|
def get_file_safe_player_name(self, player: int) -> str:
|
||||||
return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*')
|
return Utils.get_file_safe_name(self.get_player_name(player))
|
||||||
|
|
||||||
def get_out_file_name_base(self, player: int) -> str:
|
def get_out_file_name_base(self, player: int) -> str:
|
||||||
""" the base name (without file extension) for each player's output file for a seed """
|
""" the base name (without file extension) for each player's output file for a seed """
|
||||||
@@ -742,169 +764,9 @@ class CollectionState():
|
|||||||
found += self.prog_items[item_name, player]
|
found += self.prog_items[item_name, player]
|
||||||
return found
|
return found
|
||||||
|
|
||||||
def can_buy_unlimited(self, item: str, player: int) -> bool:
|
|
||||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
|
|
||||||
shop in self.multiworld.shops)
|
|
||||||
|
|
||||||
def can_buy(self, item: str, player: int) -> bool:
|
|
||||||
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for
|
|
||||||
shop in self.multiworld.shops)
|
|
||||||
|
|
||||||
def item_count(self, item: str, player: int) -> int:
|
def item_count(self, item: str, player: int) -> int:
|
||||||
return self.prog_items[item, player]
|
return self.prog_items[item, player]
|
||||||
|
|
||||||
def has_triforce_pieces(self, count: int, player: int) -> bool:
|
|
||||||
return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count
|
|
||||||
|
|
||||||
def has_crystals(self, count: int, player: int) -> bool:
|
|
||||||
found: int = 0
|
|
||||||
for crystalnumber in range(1, 8):
|
|
||||||
found += self.prog_items[f"Crystal {crystalnumber}", player]
|
|
||||||
if found >= count:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_lift_rocks(self, player: int):
|
|
||||||
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
|
|
||||||
|
|
||||||
def bottle_count(self, player: int) -> int:
|
|
||||||
return min(self.multiworld.difficulty_requirements[player].progressive_bottle_limit,
|
|
||||||
self.count_group("Bottles", player))
|
|
||||||
|
|
||||||
def has_hearts(self, player: int, count: int) -> int:
|
|
||||||
# Warning: This only considers items that are marked as advancement items
|
|
||||||
return self.heart_count(player) >= count
|
|
||||||
|
|
||||||
def heart_count(self, player: int) -> int:
|
|
||||||
# Warning: This only considers items that are marked as advancement items
|
|
||||||
diff = self.multiworld.difficulty_requirements[player]
|
|
||||||
return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
|
||||||
+ self.item_count('Sanctuary Heart Container', player) \
|
|
||||||
+ min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
|
||||||
+ 3 # starting hearts
|
|
||||||
|
|
||||||
def can_lift_heavy_rocks(self, player: int) -> bool:
|
|
||||||
return self.has('Titans Mitts', player)
|
|
||||||
|
|
||||||
def can_extend_magic(self, player: int, smallmagic: int = 16,
|
|
||||||
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
|
||||||
basemagic = 8
|
|
||||||
if self.has('Magic Upgrade (1/4)', player):
|
|
||||||
basemagic = 32
|
|
||||||
elif self.has('Magic Upgrade (1/2)', player):
|
|
||||||
basemagic = 16
|
|
||||||
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
|
|
||||||
if self.multiworld.item_functionality[player] == 'hard' and not fullrefill:
|
|
||||||
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
|
|
||||||
elif self.multiworld.item_functionality[player] == 'expert' and not fullrefill:
|
|
||||||
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
|
|
||||||
else:
|
|
||||||
basemagic = basemagic + basemagic * self.bottle_count(player)
|
|
||||||
return basemagic >= smallmagic
|
|
||||||
|
|
||||||
def can_kill_most_things(self, player: int, enemies: int = 5) -> bool:
|
|
||||||
return (self.has_melee_weapon(player)
|
|
||||||
or self.has('Cane of Somaria', player)
|
|
||||||
or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player)))
|
|
||||||
or self.can_shoot_arrows(player)
|
|
||||||
or self.has('Fire Rod', player)
|
|
||||||
or (self.has('Bombs (10)', player) and enemies < 6))
|
|
||||||
|
|
||||||
def can_shoot_arrows(self, player: int) -> bool:
|
|
||||||
if self.multiworld.retro_bow[player]:
|
|
||||||
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player)
|
|
||||||
return self.has('Bow', player) or self.has('Silver Bow', player)
|
|
||||||
|
|
||||||
def can_get_good_bee(self, player: int) -> bool:
|
|
||||||
cave = self.multiworld.get_region('Good Bee Cave', player)
|
|
||||||
return (
|
|
||||||
self.has_group("Bottles", player) and
|
|
||||||
self.has('Bug Catching Net', player) and
|
|
||||||
(self.has('Pegasus Boots', player) or (self.has_sword(player) and self.has('Quake', player))) and
|
|
||||||
cave.can_reach(self) and
|
|
||||||
self.is_not_bunny(cave, player)
|
|
||||||
)
|
|
||||||
|
|
||||||
def can_retrieve_tablet(self, player: int) -> bool:
|
|
||||||
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
|
||||||
(self.multiworld.swordless[player] and
|
|
||||||
self.has("Hammer", player)))
|
|
||||||
|
|
||||||
def has_sword(self, player: int) -> bool:
|
|
||||||
return self.has('Fighter Sword', player) \
|
|
||||||
or self.has('Master Sword', player) \
|
|
||||||
or self.has('Tempered Sword', player) \
|
|
||||||
or self.has('Golden Sword', player)
|
|
||||||
|
|
||||||
def has_beam_sword(self, player: int) -> bool:
|
|
||||||
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
|
|
||||||
player)
|
|
||||||
|
|
||||||
def has_melee_weapon(self, player: int) -> bool:
|
|
||||||
return self.has_sword(player) or self.has('Hammer', player)
|
|
||||||
|
|
||||||
def has_fire_source(self, player: int) -> bool:
|
|
||||||
return self.has('Fire Rod', player) or self.has('Lamp', player)
|
|
||||||
|
|
||||||
def can_melt_things(self, player: int) -> bool:
|
|
||||||
return self.has('Fire Rod', player) or \
|
|
||||||
(self.has('Bombos', player) and
|
|
||||||
(self.multiworld.swordless[player] or
|
|
||||||
self.has_sword(player)))
|
|
||||||
|
|
||||||
def can_avoid_lasers(self, player: int) -> bool:
|
|
||||||
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
|
|
||||||
|
|
||||||
def is_not_bunny(self, region: Region, player: int) -> bool:
|
|
||||||
if self.has('Moon Pearl', player):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return region.is_light_world if self.multiworld.mode[player] != 'inverted' else region.is_dark_world
|
|
||||||
|
|
||||||
def can_reach_light_world(self, player: int) -> bool:
|
|
||||||
if True in [i.is_light_world for i in self.reachable_regions[player]]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_reach_dark_world(self, player: int) -> bool:
|
|
||||||
if True in [i.is_dark_world for i in self.reachable_regions[player]]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def has_misery_mire_medallion(self, player: int) -> bool:
|
|
||||||
return self.has(self.multiworld.required_medallions[player][0], player)
|
|
||||||
|
|
||||||
def has_turtle_rock_medallion(self, player: int) -> bool:
|
|
||||||
return self.has(self.multiworld.required_medallions[player][1], player)
|
|
||||||
|
|
||||||
def can_boots_clip_lw(self, player: int) -> bool:
|
|
||||||
if self.multiworld.mode[player] == 'inverted':
|
|
||||||
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
|
|
||||||
return self.has('Pegasus Boots', player)
|
|
||||||
|
|
||||||
def can_boots_clip_dw(self, player: int) -> bool:
|
|
||||||
if self.multiworld.mode[player] != 'inverted':
|
|
||||||
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
|
|
||||||
return self.has('Pegasus Boots', player)
|
|
||||||
|
|
||||||
def can_get_glitched_speed_lw(self, player: int) -> bool:
|
|
||||||
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
|
||||||
if self.multiworld.mode[player] == 'inverted':
|
|
||||||
rules.append(self.has('Moon Pearl', player))
|
|
||||||
return all(rules)
|
|
||||||
|
|
||||||
def can_superbunny_mirror_with_sword(self, player: int) -> bool:
|
|
||||||
return self.has('Magic Mirror', player) and self.has_sword(player)
|
|
||||||
|
|
||||||
def can_get_glitched_speed_dw(self, player: int) -> bool:
|
|
||||||
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
|
||||||
if self.multiworld.mode[player] != 'inverted':
|
|
||||||
rules.append(self.has('Moon Pearl', player))
|
|
||||||
return all(rules)
|
|
||||||
|
|
||||||
def can_bomb_clip(self, region: Region, player: int) -> bool:
|
|
||||||
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
|
|
||||||
|
|
||||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||||
if location:
|
if location:
|
||||||
self.locations_checked.add(location)
|
self.locations_checked.add(location)
|
||||||
@@ -931,45 +793,23 @@ class CollectionState():
|
|||||||
self.stale[item.player] = True
|
self.stale[item.player] = True
|
||||||
|
|
||||||
|
|
||||||
@unique
|
|
||||||
class RegionType(IntEnum):
|
|
||||||
Generic = 0
|
|
||||||
LightWorld = 1
|
|
||||||
DarkWorld = 2
|
|
||||||
Cave = 3 # Also includes Houses
|
|
||||||
Dungeon = 4
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_indoors(self) -> bool:
|
|
||||||
"""Shorthand for checking if Cave or Dungeon"""
|
|
||||||
return self in (RegionType.Cave, RegionType.Dungeon)
|
|
||||||
|
|
||||||
|
|
||||||
class Region:
|
class Region:
|
||||||
name: str
|
name: str
|
||||||
type: RegionType
|
_hint_text: str
|
||||||
hint_text: str
|
|
||||||
player: int
|
player: int
|
||||||
multiworld: Optional[MultiWorld]
|
multiworld: Optional[MultiWorld]
|
||||||
entrances: List[Entrance]
|
entrances: List[Entrance]
|
||||||
exits: List[Entrance]
|
exits: List[Entrance]
|
||||||
locations: List[Location]
|
locations: List[Location]
|
||||||
dungeon: Optional[Dungeon] = None
|
dungeon: Optional[Dungeon] = None
|
||||||
shop: Optional = None
|
|
||||||
|
|
||||||
# LttP specific. TODO: move to a LttPRegion
|
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
||||||
# will be set after making connections.
|
|
||||||
is_light_world: bool = False
|
|
||||||
is_dark_world: bool = False
|
|
||||||
|
|
||||||
def __init__(self, name: str, type_: RegionType, hint: str, player: int, world: Optional[MultiWorld] = None):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type_
|
|
||||||
self.entrances = []
|
self.entrances = []
|
||||||
self.exits = []
|
self.exits = []
|
||||||
self.locations = []
|
self.locations = []
|
||||||
self.multiworld = world
|
self.multiworld = multiworld
|
||||||
self.hint_text = hint
|
self._hint_text = hint
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
def can_reach(self, state: CollectionState) -> bool:
|
def can_reach(self, state: CollectionState) -> bool:
|
||||||
@@ -985,6 +825,10 @@ class Region:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hint_text(self) -> str:
|
||||||
|
return self._hint_text if self._hint_text else self.name
|
||||||
|
|
||||||
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
|
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
|
||||||
for entrance in self.entrances:
|
for entrance in self.entrances:
|
||||||
if is_main_entrance(entrance):
|
if is_main_entrance(entrance):
|
||||||
@@ -1122,7 +966,7 @@ class Location:
|
|||||||
self.parent_region = parent
|
self.parent_region = parent
|
||||||
|
|
||||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||||
return (self.always_allow(state, item)
|
return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player])
|
||||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||||
and self.item_rule(item)
|
and self.item_rule(item)
|
||||||
and (not check_access or self.can_reach(state))))
|
and (not check_access or self.can_reach(state))))
|
||||||
@@ -1254,13 +1098,9 @@ class Spoiler():
|
|||||||
self.multiworld = world
|
self.multiworld = world
|
||||||
self.hashes = {}
|
self.hashes = {}
|
||||||
self.entrances = OrderedDict()
|
self.entrances = OrderedDict()
|
||||||
self.medallions = {}
|
|
||||||
self.playthrough = {}
|
self.playthrough = {}
|
||||||
self.unreachables = set()
|
self.unreachables = set()
|
||||||
self.locations = {}
|
|
||||||
self.paths = {}
|
self.paths = {}
|
||||||
self.shops = []
|
|
||||||
self.bosses = OrderedDict()
|
|
||||||
|
|
||||||
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int):
|
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int):
|
||||||
if self.multiworld.players == 1:
|
if self.multiworld.players == 1:
|
||||||
@@ -1270,117 +1110,6 @@ class Spoiler():
|
|||||||
self.entrances[(entrance, direction, player)] = OrderedDict(
|
self.entrances[(entrance, direction, player)] = OrderedDict(
|
||||||
[('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)])
|
[('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)])
|
||||||
|
|
||||||
def parse_data(self):
|
|
||||||
self.medallions = OrderedDict()
|
|
||||||
for player in self.multiworld.get_game_players("A Link to the Past"):
|
|
||||||
self.medallions[f'Misery Mire ({self.multiworld.get_player_name(player)})'] = \
|
|
||||||
self.multiworld.required_medallions[player][0]
|
|
||||||
self.medallions[f'Turtle Rock ({self.multiworld.get_player_name(player)})'] = \
|
|
||||||
self.multiworld.required_medallions[player][1]
|
|
||||||
|
|
||||||
self.locations = OrderedDict()
|
|
||||||
listed_locations = set()
|
|
||||||
|
|
||||||
lw_locations = [loc for loc in self.multiworld.get_locations() if
|
|
||||||
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
|
|
||||||
self.locations['Light World'] = OrderedDict(
|
|
||||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
||||||
lw_locations])
|
|
||||||
listed_locations.update(lw_locations)
|
|
||||||
|
|
||||||
dw_locations = [loc for loc in self.multiworld.get_locations() if
|
|
||||||
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
|
|
||||||
self.locations['Dark World'] = OrderedDict(
|
|
||||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
||||||
dw_locations])
|
|
||||||
listed_locations.update(dw_locations)
|
|
||||||
|
|
||||||
cave_locations = [loc for loc in self.multiworld.get_locations() if
|
|
||||||
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
|
|
||||||
self.locations['Caves'] = OrderedDict(
|
|
||||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
||||||
cave_locations])
|
|
||||||
listed_locations.update(cave_locations)
|
|
||||||
|
|
||||||
for dungeon in self.multiworld.dungeons.values():
|
|
||||||
dungeon_locations = [loc for loc in self.multiworld.get_locations() if
|
|
||||||
loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
|
|
||||||
self.locations[str(dungeon)] = OrderedDict(
|
|
||||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
||||||
dungeon_locations])
|
|
||||||
listed_locations.update(dungeon_locations)
|
|
||||||
|
|
||||||
other_locations = [loc for loc in self.multiworld.get_locations() if
|
|
||||||
loc not in listed_locations and loc.show_in_spoiler]
|
|
||||||
if other_locations:
|
|
||||||
self.locations['Other Locations'] = OrderedDict(
|
|
||||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
|
||||||
other_locations])
|
|
||||||
listed_locations.update(other_locations)
|
|
||||||
|
|
||||||
self.shops = []
|
|
||||||
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
|
|
||||||
for shop in self.multiworld.shops:
|
|
||||||
if not shop.custom:
|
|
||||||
continue
|
|
||||||
shopdata = {
|
|
||||||
'location': str(shop.region),
|
|
||||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
|
||||||
}
|
|
||||||
for index, item in enumerate(shop.inventory):
|
|
||||||
if item is None:
|
|
||||||
continue
|
|
||||||
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
|
|
||||||
shopdata['item_{}'.format(
|
|
||||||
index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}"
|
|
||||||
|
|
||||||
if item['player'] > 0:
|
|
||||||
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—',
|
|
||||||
'(Player {}) — '.format(
|
|
||||||
item['player']))
|
|
||||||
|
|
||||||
if item['max'] == 0:
|
|
||||||
continue
|
|
||||||
shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
|
|
||||||
|
|
||||||
if item['replacement'] is None:
|
|
||||||
continue
|
|
||||||
shopdata['item_{}'.format(
|
|
||||||
index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
|
|
||||||
self.shops.append(shopdata)
|
|
||||||
|
|
||||||
for player in self.multiworld.get_game_players("A Link to the Past"):
|
|
||||||
self.bosses[str(player)] = OrderedDict()
|
|
||||||
self.bosses[str(player)]["Eastern Palace"] = self.multiworld.get_dungeon("Eastern Palace", player).boss.name
|
|
||||||
self.bosses[str(player)]["Desert Palace"] = self.multiworld.get_dungeon("Desert Palace", player).boss.name
|
|
||||||
self.bosses[str(player)]["Tower Of Hera"] = self.multiworld.get_dungeon("Tower of Hera", player).boss.name
|
|
||||||
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
|
|
||||||
self.bosses[str(player)]["Palace Of Darkness"] = self.multiworld.get_dungeon("Palace of Darkness",
|
|
||||||
player).boss.name
|
|
||||||
self.bosses[str(player)]["Swamp Palace"] = self.multiworld.get_dungeon("Swamp Palace", player).boss.name
|
|
||||||
self.bosses[str(player)]["Skull Woods"] = self.multiworld.get_dungeon("Skull Woods", player).boss.name
|
|
||||||
self.bosses[str(player)]["Thieves Town"] = self.multiworld.get_dungeon("Thieves Town", player).boss.name
|
|
||||||
self.bosses[str(player)]["Ice Palace"] = self.multiworld.get_dungeon("Ice Palace", player).boss.name
|
|
||||||
self.bosses[str(player)]["Misery Mire"] = self.multiworld.get_dungeon("Misery Mire", player).boss.name
|
|
||||||
self.bosses[str(player)]["Turtle Rock"] = self.multiworld.get_dungeon("Turtle Rock", player).boss.name
|
|
||||||
if self.multiworld.mode[player] != 'inverted':
|
|
||||||
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
|
||||||
self.multiworld.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
|
||||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[
|
|
||||||
'middle'].name
|
|
||||||
self.bosses[str(player)]["Ganons Tower Top"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[
|
|
||||||
'top'].name
|
|
||||||
else:
|
|
||||||
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
|
||||||
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
|
|
||||||
self.bosses[str(player)]["Ganons Tower Middle"] = \
|
|
||||||
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
|
|
||||||
self.bosses[str(player)]["Ganons Tower Top"] = \
|
|
||||||
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
|
|
||||||
|
|
||||||
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
|
||||||
self.bosses[str(player)]["Ganon"] = "Ganon"
|
|
||||||
|
|
||||||
def create_playthrough(self, create_paths: bool = True):
|
def create_playthrough(self, create_paths: bool = True):
|
||||||
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
@@ -1479,7 +1208,7 @@ class Spoiler():
|
|||||||
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
||||||
|
|
||||||
# we can finally output our playthrough
|
# we can finally output our playthrough
|
||||||
self.playthrough = {"0": sorted([str(item) for item in
|
self.playthrough = {"0": sorted([self.multiworld.get_name_string_for_object(item) for item in
|
||||||
chain.from_iterable(multiworld.precollected_items.values())
|
chain.from_iterable(multiworld.precollected_items.values())
|
||||||
if item.advancement])}
|
if item.advancement])}
|
||||||
|
|
||||||
@@ -1532,35 +1261,12 @@ class Spoiler():
|
|||||||
self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \
|
self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \
|
||||||
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
|
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
|
||||||
|
|
||||||
def to_json(self):
|
|
||||||
self.parse_data()
|
|
||||||
out = OrderedDict()
|
|
||||||
out['Entrances'] = list(self.entrances.values())
|
|
||||||
out.update(self.locations)
|
|
||||||
out['Special'] = self.medallions
|
|
||||||
if self.hashes:
|
|
||||||
out['Hashes'] = self.hashes
|
|
||||||
if self.shops:
|
|
||||||
out['Shops'] = self.shops
|
|
||||||
out['playthrough'] = self.playthrough
|
|
||||||
out['paths'] = self.paths
|
|
||||||
out['Bosses'] = self.bosses
|
|
||||||
|
|
||||||
return json.dumps(out)
|
|
||||||
|
|
||||||
def to_file(self, filename: str):
|
def to_file(self, filename: str):
|
||||||
self.parse_data()
|
|
||||||
|
|
||||||
def bool_to_text(variable: Union[bool, str]) -> str:
|
|
||||||
if type(variable) == str:
|
|
||||||
return variable
|
|
||||||
return 'Yes' if variable else 'No'
|
|
||||||
|
|
||||||
def write_option(option_key: str, option_obj: type(Options.Option)):
|
def write_option(option_key: str, option_obj: type(Options.Option)):
|
||||||
res = getattr(self.multiworld, option_key)[player]
|
res = getattr(self.multiworld, option_key)[player]
|
||||||
display_name = getattr(option_obj, "display_name", option_key)
|
display_name = getattr(option_obj, "display_name", option_key)
|
||||||
try:
|
try:
|
||||||
outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n')
|
outfile.write(f'{display_name + ":":33}{res.current_option_name}\n')
|
||||||
except:
|
except:
|
||||||
raise Exception
|
raise Exception
|
||||||
|
|
||||||
@@ -1577,46 +1283,13 @@ class Spoiler():
|
|||||||
if self.multiworld.players > 1:
|
if self.multiworld.players > 1:
|
||||||
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
|
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
|
||||||
outfile.write('Game: %s\n' % self.multiworld.game[player])
|
outfile.write('Game: %s\n' % self.multiworld.game[player])
|
||||||
for f_option, option in Options.per_game_common_options.items():
|
|
||||||
|
options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions)
|
||||||
|
for f_option, option in options.items():
|
||||||
write_option(f_option, option)
|
write_option(f_option, option)
|
||||||
options = self.multiworld.worlds[player].option_definitions
|
|
||||||
if options:
|
|
||||||
for f_option, option in options.items():
|
|
||||||
write_option(f_option, option)
|
|
||||||
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)
|
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)
|
||||||
|
|
||||||
if player in self.multiworld.get_game_players("A Link to the Past"):
|
|
||||||
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
|
|
||||||
|
|
||||||
outfile.write('Logic: %s\n' % self.multiworld.logic[player])
|
|
||||||
outfile.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[player])
|
|
||||||
outfile.write('Mode: %s\n' % self.multiworld.mode[player])
|
|
||||||
outfile.write('Goal: %s\n' % self.multiworld.goal[player])
|
|
||||||
if "triforce" in self.multiworld.goal[player]: # triforce hunt
|
|
||||||
outfile.write("Pieces available for Triforce: %s\n" %
|
|
||||||
self.multiworld.triforce_pieces_available[player])
|
|
||||||
outfile.write("Pieces required for Triforce: %s\n" %
|
|
||||||
self.multiworld.triforce_pieces_required[player])
|
|
||||||
outfile.write('Difficulty: %s\n' % self.multiworld.difficulty[player])
|
|
||||||
outfile.write('Item Functionality: %s\n' % self.multiworld.item_functionality[player])
|
|
||||||
outfile.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[player])
|
|
||||||
if self.multiworld.shuffle[player] != "vanilla":
|
|
||||||
outfile.write('Entrance Shuffle Seed %s\n' % self.multiworld.worlds[player].er_seed)
|
|
||||||
outfile.write('Shop inventory shuffle: %s\n' %
|
|
||||||
bool_to_text("i" in self.multiworld.shop_shuffle[player]))
|
|
||||||
outfile.write('Shop price shuffle: %s\n' %
|
|
||||||
bool_to_text("p" in self.multiworld.shop_shuffle[player]))
|
|
||||||
outfile.write('Shop upgrade shuffle: %s\n' %
|
|
||||||
bool_to_text("u" in self.multiworld.shop_shuffle[player]))
|
|
||||||
outfile.write('New Shop inventory: %s\n' %
|
|
||||||
bool_to_text("g" in self.multiworld.shop_shuffle[player] or
|
|
||||||
"f" in self.multiworld.shop_shuffle[player]))
|
|
||||||
outfile.write('Custom Potion Shop: %s\n' %
|
|
||||||
bool_to_text("w" in self.multiworld.shop_shuffle[player]))
|
|
||||||
outfile.write('Enemy health: %s\n' % self.multiworld.enemy_health[player])
|
|
||||||
outfile.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[player])
|
|
||||||
outfile.write('Prize shuffle %s\n' %
|
|
||||||
self.multiworld.shuffle_prizes[player])
|
|
||||||
if self.entrances:
|
if self.entrances:
|
||||||
outfile.write('\n\nEntrances:\n\n')
|
outfile.write('\n\nEntrances:\n\n')
|
||||||
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: '
|
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: '
|
||||||
@@ -1625,30 +1298,14 @@ class Spoiler():
|
|||||||
'<=' if entry['direction'] == 'exit' else '=>',
|
'<=' if entry['direction'] == 'exit' else '=>',
|
||||||
entry['exit']) for entry in self.entrances.values()]))
|
entry['exit']) for entry in self.entrances.values()]))
|
||||||
|
|
||||||
if self.medallions:
|
|
||||||
outfile.write('\n\nMedallions:\n')
|
|
||||||
for dungeon, medallion in self.medallions.items():
|
|
||||||
outfile.write(f'\n{dungeon}: {medallion}')
|
|
||||||
|
|
||||||
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
|
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
|
||||||
|
|
||||||
|
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
|
||||||
|
for location in self.multiworld.get_locations() if location.show_in_spoiler]
|
||||||
outfile.write('\n\nLocations:\n\n')
|
outfile.write('\n\nLocations:\n\n')
|
||||||
outfile.write('\n'.join(
|
outfile.write('\n'.join(
|
||||||
['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in
|
['%s: %s' % (location, item) for location, item in locations]))
|
||||||
grouping.items()]))
|
|
||||||
|
|
||||||
if self.shops:
|
|
||||||
outfile.write('\n\nShops:\n\n')
|
|
||||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(
|
|
||||||
item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if
|
|
||||||
item)) for shop in self.shops))
|
|
||||||
|
|
||||||
for player in self.multiworld.get_game_players("A Link to the Past"):
|
|
||||||
if self.multiworld.boss_shuffle[player] != 'none':
|
|
||||||
bossmap = self.bosses[str(player)] if self.multiworld.players > 1 else self.bosses
|
|
||||||
outfile.write(
|
|
||||||
f'\n\nBosses{(f" ({self.multiworld.get_player_name(player)})" if self.multiworld.players > 1 else "")}:\n')
|
|
||||||
outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
|
||||||
outfile.write('\n\nPlaythrough:\n\n')
|
outfile.write('\n\nPlaythrough:\n\n')
|
||||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
|
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
|
||||||
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [
|
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
|
|
||||||
def _cmd_received(self) -> bool:
|
def _cmd_received(self) -> bool:
|
||||||
"""List all received items"""
|
"""List all received items"""
|
||||||
logger.info(f'{len(self.ctx.items_received)} received items:')
|
self.output(f'{len(self.ctx.items_received)} received items:')
|
||||||
for index, item in enumerate(self.ctx.items_received, 1):
|
for index, item in enumerate(self.ctx.items_received, 1):
|
||||||
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
|
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
|
||||||
return True
|
return True
|
||||||
@@ -136,7 +136,7 @@ class CommonContext:
|
|||||||
items_handling: typing.Optional[int] = None
|
items_handling: typing.Optional[int] = None
|
||||||
want_slot_data: bool = True # should slot_data be retrieved via Connect
|
want_slot_data: bool = True # should slot_data be retrieved via Connect
|
||||||
|
|
||||||
# datapackage
|
# data package
|
||||||
# Contents in flux until connection to server is made, to download correct data for this multiworld.
|
# Contents in flux until connection to server is made, to download correct data for this multiworld.
|
||||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||||
@@ -223,7 +223,7 @@ class CommonContext:
|
|||||||
self.watcher_event = asyncio.Event()
|
self.watcher_event = asyncio.Event()
|
||||||
|
|
||||||
self.jsontotextparser = JSONtoTextParser(self)
|
self.jsontotextparser = JSONtoTextParser(self)
|
||||||
self.update_datapackage(network_data_package)
|
self.update_data_package(network_data_package)
|
||||||
|
|
||||||
# execution
|
# execution
|
||||||
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||||
@@ -341,6 +341,11 @@ class CommonContext:
|
|||||||
return self.slot in self.slot_info[slot].group_members
|
return self.slot in self.slot_info[slot].group_members
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def is_echoed_chat(self, print_json_packet: dict) -> bool:
|
||||||
|
return print_json_packet.get("type", "") == "Chat" \
|
||||||
|
and print_json_packet.get("team", None) == self.team \
|
||||||
|
and print_json_packet.get("slot", None) == self.slot
|
||||||
|
|
||||||
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
|
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
|
||||||
"""Helper function for filtering out ItemSend prints that do not concern the local player."""
|
"""Helper function for filtering out ItemSend prints that do not concern the local player."""
|
||||||
return print_json_packet.get("type", "") == "ItemSend" \
|
return print_json_packet.get("type", "") == "ItemSend" \
|
||||||
@@ -394,32 +399,40 @@ class CommonContext:
|
|||||||
self.input_task.cancel()
|
self.input_task.cancel()
|
||||||
|
|
||||||
# DataPackage
|
# DataPackage
|
||||||
async def prepare_datapackage(self, relevant_games: typing.Set[str],
|
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
||||||
remote_datepackage_versions: typing.Dict[str, int]):
|
remote_date_package_versions: typing.Dict[str, int],
|
||||||
|
remote_data_package_checksums: typing.Dict[str, str]):
|
||||||
"""Validate that all data is present for the current multiworld.
|
"""Validate that all data is present for the current multiworld.
|
||||||
Download, assimilate and cache missing data from the server."""
|
Download, assimilate and cache missing data from the server."""
|
||||||
# by documentation any game can use Archipelago locations/items -> always relevant
|
# by documentation any game can use Archipelago locations/items -> always relevant
|
||||||
relevant_games.add("Archipelago")
|
relevant_games.add("Archipelago")
|
||||||
|
|
||||||
cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
|
||||||
needed_updates: typing.Set[str] = set()
|
needed_updates: typing.Set[str] = set()
|
||||||
for game in relevant_games:
|
for game in relevant_games:
|
||||||
if game not in remote_datepackage_versions:
|
if game not in remote_date_package_versions and game not in remote_data_package_checksums:
|
||||||
continue
|
continue
|
||||||
remote_version: int = remote_datepackage_versions[game]
|
|
||||||
|
|
||||||
if remote_version == 0: # custom datapackage for this game
|
remote_version: int = remote_date_package_versions.get(game, 0)
|
||||||
|
remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game)
|
||||||
|
|
||||||
|
if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game
|
||||||
needed_updates.add(game)
|
needed_updates.add(game)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
|
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
|
||||||
|
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
||||||
# no action required if local version is new enough
|
# no action required if local version is new enough
|
||||||
if remote_version > local_version:
|
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
|
||||||
cache_version: int = cache_package.get(game, {}).get("version", 0)
|
or remote_checksum != local_checksum:
|
||||||
|
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
|
||||||
|
cache_version: int = cached_game.get("version", 0)
|
||||||
|
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
|
||||||
# download remote version if cache is not new enough
|
# download remote version if cache is not new enough
|
||||||
if remote_version > cache_version:
|
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
|
||||||
|
or remote_checksum != cache_checksum:
|
||||||
needed_updates.add(game)
|
needed_updates.add(game)
|
||||||
else:
|
else:
|
||||||
self.update_game(cache_package[game])
|
self.update_game(cached_game)
|
||||||
if needed_updates:
|
if needed_updates:
|
||||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
|
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
|
||||||
|
|
||||||
@@ -429,15 +442,17 @@ class CommonContext:
|
|||||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||||
self.location_names[location_id] = location_name
|
self.location_names[location_id] = location_name
|
||||||
|
|
||||||
def update_datapackage(self, data_package: dict):
|
def update_data_package(self, data_package: dict):
|
||||||
for game, gamedata in data_package["games"].items():
|
for game, game_data in data_package["games"].items():
|
||||||
self.update_game(gamedata)
|
self.update_game(game_data)
|
||||||
|
|
||||||
def consume_network_datapackage(self, data_package: dict):
|
def consume_network_data_package(self, data_package: dict):
|
||||||
self.update_datapackage(data_package)
|
self.update_data_package(data_package)
|
||||||
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||||
current_cache.update(data_package["games"])
|
current_cache.update(data_package["games"])
|
||||||
Utils.persistent_store("datapackage", "games", current_cache)
|
Utils.persistent_store("datapackage", "games", current_cache)
|
||||||
|
for game, game_data in data_package["games"].items():
|
||||||
|
Utils.store_data_package_for_checksum(game, game_data)
|
||||||
|
|
||||||
# DeathLink hooks
|
# DeathLink hooks
|
||||||
|
|
||||||
@@ -656,14 +671,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
current_team = network_player.team
|
current_team = network_player.team
|
||||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||||
|
|
||||||
# update datapackage
|
# update data package
|
||||||
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
|
data_package_versions = args.get("datapackage_versions", {})
|
||||||
|
data_package_checksums = args.get("datapackage_checksums", {})
|
||||||
|
await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums)
|
||||||
|
|
||||||
await ctx.server_auth(args['password'])
|
await ctx.server_auth(args['password'])
|
||||||
|
|
||||||
elif cmd == 'DataPackage':
|
elif cmd == 'DataPackage':
|
||||||
logger.info("Got new ID/Name DataPackage")
|
logger.info("Got new ID/Name DataPackage")
|
||||||
ctx.consume_network_datapackage(args['data'])
|
ctx.consume_network_data_package(args['data'])
|
||||||
|
|
||||||
elif cmd == 'ConnectionRefused':
|
elif cmd == 'ConnectionRefused':
|
||||||
errors = args["errors"]
|
errors = args["errors"]
|
||||||
|
|||||||
@@ -109,9 +109,10 @@ class FactorioContext(CommonContext):
|
|||||||
|
|
||||||
def on_print_json(self, args: dict):
|
def on_print_json(self, args: dict):
|
||||||
if self.rcon_client:
|
if self.rcon_client:
|
||||||
if not self.filter_item_sends or not self.is_uninteresting_item_send(args):
|
if (not self.filter_item_sends or not self.is_uninteresting_item_send(args)) \
|
||||||
|
and not self.is_echoed_chat(args):
|
||||||
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
|
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
|
||||||
if not text.startswith(self.player_names[self.slot] + ":"):
|
if not text.startswith(self.player_names[self.slot] + ":"): # TODO: Remove string heuristic in the future.
|
||||||
self.print_to_game(text)
|
self.print_to_game(text)
|
||||||
super(FactorioContext, self).on_print_json(args)
|
super(FactorioContext, self).on_print_json(args)
|
||||||
|
|
||||||
|
|||||||
44
Fill.py
44
Fill.py
@@ -23,15 +23,27 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
|||||||
|
|
||||||
|
|
||||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||||
itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||||
allow_partial: bool = False) -> None:
|
allow_partial: bool = False, allow_excluded: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
:param world: Multiworld to be filled.
|
||||||
|
:param base_state: State assumed before fill.
|
||||||
|
:param locations: Locations to be filled with item_pool
|
||||||
|
:param item_pool: Items to fill into the locations
|
||||||
|
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
|
||||||
|
:param lock: locations are set to locked as they are filled
|
||||||
|
:param swap: if true, swaps of already place items are done in the event of a dead end
|
||||||
|
:param on_place: callback that is called when a placement happens
|
||||||
|
:param allow_partial: only place what is possible. Remaining items will be in the item_pool list.
|
||||||
|
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
|
||||||
|
"""
|
||||||
unplaced_items: typing.List[Item] = []
|
unplaced_items: typing.List[Item] = []
|
||||||
placements: typing.List[Location] = []
|
placements: typing.List[Location] = []
|
||||||
|
|
||||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||||
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
|
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
|
||||||
for item in itempool:
|
for item in item_pool:
|
||||||
reachable_items.setdefault(item.player, deque()).append(item)
|
reachable_items.setdefault(item.player, deque()).append(item)
|
||||||
|
|
||||||
while any(reachable_items.values()) and locations:
|
while any(reachable_items.values()) and locations:
|
||||||
@@ -39,9 +51,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
items_to_place = [items.pop()
|
items_to_place = [items.pop()
|
||||||
for items in reachable_items.values() if items]
|
for items in reachable_items.values() if items]
|
||||||
for item in items_to_place:
|
for item in items_to_place:
|
||||||
itempool.remove(item)
|
item_pool.remove(item)
|
||||||
maximum_exploration_state = sweep_from_pool(
|
maximum_exploration_state = sweep_from_pool(
|
||||||
base_state, itempool + unplaced_items)
|
base_state, item_pool + unplaced_items)
|
||||||
|
|
||||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||||
|
|
||||||
@@ -111,7 +123,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
|
|
||||||
reachable_items[placed_item.player].appendleft(
|
reachable_items[placed_item.player].appendleft(
|
||||||
placed_item)
|
placed_item)
|
||||||
itempool.append(placed_item)
|
item_pool.append(placed_item)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -133,6 +145,21 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
if on_place:
|
if on_place:
|
||||||
on_place(spot_to_fill)
|
on_place(spot_to_fill)
|
||||||
|
|
||||||
|
if allow_excluded:
|
||||||
|
# check if partial fill is the result of excluded locations, in which case retry
|
||||||
|
excluded_locations = [
|
||||||
|
location for location in locations
|
||||||
|
if location.progress_type == location.progress_type.EXCLUDED and not location.item
|
||||||
|
]
|
||||||
|
if excluded_locations:
|
||||||
|
for location in excluded_locations:
|
||||||
|
location.progress_type = location.progress_type.DEFAULT
|
||||||
|
fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
|
||||||
|
swap, on_place, allow_partial, False)
|
||||||
|
for location in excluded_locations:
|
||||||
|
if not location.item:
|
||||||
|
location.progress_type = location.progress_type.EXCLUDED
|
||||||
|
|
||||||
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
|
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
|
||||||
# There are leftover unplaceable items and locations that won't accept them
|
# There are leftover unplaceable items and locations that won't accept them
|
||||||
if world.can_beat_game():
|
if world.can_beat_game():
|
||||||
@@ -142,7 +169,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||||
|
|
||||||
itempool.extend(unplaced_items)
|
item_pool.extend(unplaced_items)
|
||||||
|
|
||||||
|
|
||||||
def remaining_fill(world: MultiWorld,
|
def remaining_fill(world: MultiWorld,
|
||||||
@@ -840,8 +867,7 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
maxcount = placement['count']['target']
|
maxcount = placement['count']['target']
|
||||||
from_pool = placement['from_pool']
|
from_pool = placement['from_pool']
|
||||||
|
|
||||||
candidates = list(location for location in world.get_unfilled_locations_for_players(locations,
|
candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||||
worlds))
|
|
||||||
world.random.shuffle(candidates)
|
world.random.shuffle(candidates)
|
||||||
world.random.shuffle(items)
|
world.random.shuffle(items)
|
||||||
count = 0
|
count = 0
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ def main(args=None, callback=ERmain):
|
|||||||
player_files = {}
|
player_files = {}
|
||||||
for file in os.scandir(args.player_files_path):
|
for file in os.scandir(args.player_files_path):
|
||||||
fname = file.name
|
fname = file.name
|
||||||
if file.is_file() and not file.name.startswith(".") and \
|
if file.is_file() and not fname.startswith(".") and \
|
||||||
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
||||||
path = os.path.join(args.player_files_path, fname)
|
path = os.path.join(args.player_files_path, fname)
|
||||||
try:
|
try:
|
||||||
|
|||||||
906
KH2Client.py
Normal file
906
KH2Client.py
Normal file
@@ -0,0 +1,906 @@
|
|||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import ModuleUpdate
|
||||||
|
import json
|
||||||
|
import Utils
|
||||||
|
from pymem import pymem
|
||||||
|
from worlds.kh2.Items import exclusionItem_table, CheckDupingItems
|
||||||
|
from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table
|
||||||
|
|
||||||
|
from worlds.kh2.WorldLocations import *
|
||||||
|
|
||||||
|
from worlds import network_data_package
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
Utils.init_logging("KH2Client", exception_logger="Client")
|
||||||
|
|
||||||
|
from NetUtils import ClientStatus
|
||||||
|
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
|
||||||
|
CommonContext, server_loop
|
||||||
|
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"]
|
||||||
|
|
||||||
|
|
||||||
|
# class KH2CommandProcessor(ClientCommandProcessor):
|
||||||
|
|
||||||
|
|
||||||
|
class KH2Context(CommonContext):
|
||||||
|
# command_processor: int = KH2CommandProcessor
|
||||||
|
game = "Kingdom Hearts 2"
|
||||||
|
items_handling = 0b101 # Indicates you get items sent from other worlds.
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super(KH2Context, self).__init__(server_address, password)
|
||||||
|
self.kh2LocalItems = None
|
||||||
|
self.ability = None
|
||||||
|
self.growthlevel = None
|
||||||
|
self.KH2_sync_task = None
|
||||||
|
self.syncing = False
|
||||||
|
self.kh2connected = False
|
||||||
|
self.serverconneced = False
|
||||||
|
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||||
|
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||||
|
self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in
|
||||||
|
item_dictionary_table.items() if data.code}
|
||||||
|
self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in
|
||||||
|
all_locations.items() if data.code}
|
||||||
|
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
|
||||||
|
|
||||||
|
self.location_table = {}
|
||||||
|
self.collectible_table = {}
|
||||||
|
self.collectible_override_flags_address = 0
|
||||||
|
self.collectible_offsets = {}
|
||||||
|
self.sending = []
|
||||||
|
# flag for if the player has gotten their starting inventory from the server
|
||||||
|
self.hasStartingInvo = False
|
||||||
|
# list used to keep track of locations+items player has. Used for disoneccting
|
||||||
|
self.kh2seedsave = {"checked_locations": {"0": []},
|
||||||
|
"starting_inventory": self.hasStartingInvo,
|
||||||
|
|
||||||
|
# Character: [back of invo, front of invo]
|
||||||
|
"SoraInvo": [0x25CC, 0x2546],
|
||||||
|
"DonaldInvo": [0x2678, 0x2658],
|
||||||
|
"GoofyInvo": [0x278E, 0x276C],
|
||||||
|
"AmountInvo": {
|
||||||
|
"ServerItems": {
|
||||||
|
"Ability": {},
|
||||||
|
"Amount": {},
|
||||||
|
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, "Aerial Dodge": 0,
|
||||||
|
"Glide": 0},
|
||||||
|
"Bitmask": [],
|
||||||
|
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
|
||||||
|
"Equipment": [],
|
||||||
|
"Magic": {},
|
||||||
|
"StatIncrease": {},
|
||||||
|
"Boost": {},
|
||||||
|
},
|
||||||
|
"LocalItems": {
|
||||||
|
"Ability": {},
|
||||||
|
"Amount": {},
|
||||||
|
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
|
||||||
|
"Aerial Dodge": 0, "Glide": 0},
|
||||||
|
"Bitmask": [],
|
||||||
|
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
|
||||||
|
"Equipment": [],
|
||||||
|
"Magic": {},
|
||||||
|
"StatIncrease": {},
|
||||||
|
"Boost": {},
|
||||||
|
}},
|
||||||
|
# 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked
|
||||||
|
"worldIdChecks": {
|
||||||
|
"1": [], # world of darkness (story cutscenes)
|
||||||
|
"2": [],
|
||||||
|
"3": [], # destiny island doesn't have checks to ima put tt checks here
|
||||||
|
"4": [],
|
||||||
|
"5": [],
|
||||||
|
"6": [],
|
||||||
|
"7": [],
|
||||||
|
"8": [],
|
||||||
|
"9": [],
|
||||||
|
"10": [],
|
||||||
|
"11": [],
|
||||||
|
# atlantica isn't a supported world. if you go in atlantica it will check dc
|
||||||
|
"12": [],
|
||||||
|
"13": [],
|
||||||
|
"14": [],
|
||||||
|
"15": [],
|
||||||
|
# world map, but you only go to the world map while on the way to goa so checking hb
|
||||||
|
"16": [],
|
||||||
|
"17": [],
|
||||||
|
"18": [],
|
||||||
|
"255": [], # starting screen
|
||||||
|
},
|
||||||
|
"Levels": {
|
||||||
|
"SoraLevel": 0,
|
||||||
|
"ValorLevel": 0,
|
||||||
|
"WisdomLevel": 0,
|
||||||
|
"LimitLevel": 0,
|
||||||
|
"MasterLevel": 0,
|
||||||
|
"FinalLevel": 0,
|
||||||
|
},
|
||||||
|
"SoldEquipment": [],
|
||||||
|
"SoldBoosts": {"Power Boost": 0,
|
||||||
|
"Magic Boost": 0,
|
||||||
|
"Defense Boost": 0,
|
||||||
|
"AP Boost": 0}
|
||||||
|
}
|
||||||
|
self.slotDataProgressionNames = {}
|
||||||
|
self.kh2seedname = None
|
||||||
|
self.kh2slotdata = None
|
||||||
|
self.itemamount = {}
|
||||||
|
# sora equipped, valor equipped, master equipped, final equipped
|
||||||
|
self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4)
|
||||||
|
if "localappdata" in os.environ:
|
||||||
|
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
|
||||||
|
self.amountOfPieces = 0
|
||||||
|
# hooked object
|
||||||
|
self.kh2 = None
|
||||||
|
self.ItemIsSafe = False
|
||||||
|
self.game_connected = False
|
||||||
|
self.finalxemnas = False
|
||||||
|
self.worldid = {
|
||||||
|
# 1: {}, # world of darkness (story cutscenes)
|
||||||
|
2: TT_Checks,
|
||||||
|
# 3: {}, # destiny island doesn't have checks to ima put tt checks here
|
||||||
|
4: HB_Checks,
|
||||||
|
5: BC_Checks,
|
||||||
|
6: Oc_Checks,
|
||||||
|
7: AG_Checks,
|
||||||
|
8: LoD_Checks,
|
||||||
|
9: HundredAcreChecks,
|
||||||
|
10: PL_Checks,
|
||||||
|
11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc
|
||||||
|
12: DC_Checks,
|
||||||
|
13: TR_Checks,
|
||||||
|
14: HT_Checks,
|
||||||
|
15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
|
||||||
|
16: PR_Checks,
|
||||||
|
17: SP_Checks,
|
||||||
|
18: TWTNW_Checks,
|
||||||
|
# 255: {}, # starting screen
|
||||||
|
}
|
||||||
|
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
|
||||||
|
self.sveroom = 0x2A09C00 + 0x41
|
||||||
|
# 0 not in battle 1 in yellow battle 2 red battle #short
|
||||||
|
self.inBattle = 0x2A0EAC4 + 0x40
|
||||||
|
self.onDeath = 0xAB9078
|
||||||
|
# PC Address anchors
|
||||||
|
self.Now = 0x0714DB8
|
||||||
|
self.Save = 0x09A70B0
|
||||||
|
self.Sys3 = 0x2A59DF0
|
||||||
|
self.Bt10 = 0x2A74880
|
||||||
|
self.BtlEnd = 0x2A0D3E0
|
||||||
|
self.Slot1 = 0x2A20C98
|
||||||
|
|
||||||
|
self.chest_set = set(exclusion_table["Chests"])
|
||||||
|
|
||||||
|
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
|
||||||
|
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
|
||||||
|
self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
|
||||||
|
|
||||||
|
self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
|
||||||
|
|
||||||
|
self.equipment_categories = CheckDupingItems["Equipment"]
|
||||||
|
self.armor_set = set(self.equipment_categories["Armor"])
|
||||||
|
self.accessories_set = set(self.equipment_categories["Accessories"])
|
||||||
|
self.all_equipment = self.armor_set.union(self.accessories_set)
|
||||||
|
|
||||||
|
self.Equipment_Anchor_Dict = {
|
||||||
|
"Armor": [0x2504, 0x2506, 0x2508, 0x250A],
|
||||||
|
"Accessories": [0x2514, 0x2516, 0x2518, 0x251A]}
|
||||||
|
|
||||||
|
self.AbilityQuantityDict = {}
|
||||||
|
self.ability_categories = CheckDupingItems["Abilities"]
|
||||||
|
|
||||||
|
self.sora_ability_set = set(self.ability_categories["Sora"])
|
||||||
|
self.donald_ability_set = set(self.ability_categories["Donald"])
|
||||||
|
self.goofy_ability_set = set(self.ability_categories["Goofy"])
|
||||||
|
|
||||||
|
self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
|
||||||
|
|
||||||
|
self.boost_set = set(CheckDupingItems["Boosts"])
|
||||||
|
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
|
||||||
|
|
||||||
|
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
|
||||||
|
# Growth:[level 1,level 4,slot]
|
||||||
|
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE],
|
||||||
|
"Quick Run": [0x62, 0x65, 0x25D0],
|
||||||
|
"Dodge Roll": [0x234, 0x237, 0x25D2],
|
||||||
|
"Aerial Dodge": [0x066, 0x069, 0x25D4],
|
||||||
|
"Glide": [0x6A, 0x6D, 0x25D6]}
|
||||||
|
self.boost_to_anchor_dict = {
|
||||||
|
"Power Boost": 0x24F9,
|
||||||
|
"Magic Boost": 0x24FA,
|
||||||
|
"Defense Boost": 0x24FB,
|
||||||
|
"AP Boost": 0x24F8}
|
||||||
|
|
||||||
|
self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]]
|
||||||
|
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
|
||||||
|
|
||||||
|
self.bitmask_item_code = [
|
||||||
|
0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007
|
||||||
|
, 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C
|
||||||
|
, 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023
|
||||||
|
, 0x13002A, 0x13002B, 0x13002C, 0x13002D]
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super(KH2Context, self).server_auth(password_requested)
|
||||||
|
await self.get_username()
|
||||||
|
await self.send_connect()
|
||||||
|
|
||||||
|
async def connection_closed(self):
|
||||||
|
self.kh2connected = False
|
||||||
|
self.serverconneced = False
|
||||||
|
if self.kh2seedname is not None and self.auth is not None:
|
||||||
|
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||||
|
'w') as f:
|
||||||
|
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||||
|
await super(KH2Context, self).connection_closed()
|
||||||
|
|
||||||
|
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||||
|
self.kh2connected = False
|
||||||
|
self.serverconneced = False
|
||||||
|
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||||
|
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||||
|
'w') as f:
|
||||||
|
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||||
|
await super(KH2Context, self).disconnect()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def endpoints(self):
|
||||||
|
if self.server:
|
||||||
|
return [self.server]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||||
|
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||||
|
'w') as f:
|
||||||
|
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||||
|
await super(KH2Context, self).shutdown()
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd in {"RoomInfo"}:
|
||||||
|
self.kh2seedname = args['seed_name']
|
||||||
|
if not os.path.exists(self.game_communication_path):
|
||||||
|
os.makedirs(self.game_communication_path)
|
||||||
|
if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
||||||
|
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||||
|
'wt') as f:
|
||||||
|
pass
|
||||||
|
elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
||||||
|
with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f:
|
||||||
|
self.kh2seedsave = json.load(f)
|
||||||
|
|
||||||
|
if cmd in {"Connected"}:
|
||||||
|
for player in args['players']:
|
||||||
|
if str(player.slot) not in self.kh2seedsave["checked_locations"]:
|
||||||
|
self.kh2seedsave["checked_locations"].update({str(player.slot): []})
|
||||||
|
self.kh2slotdata = args['slot_data']
|
||||||
|
self.serverconneced = True
|
||||||
|
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
|
||||||
|
try:
|
||||||
|
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||||
|
logger.info("You are now auto-tracking")
|
||||||
|
self.kh2connected = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Line 247")
|
||||||
|
if self.kh2connected:
|
||||||
|
logger.info("Connection Lost")
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
if cmd in {"ReceivedItems"}:
|
||||||
|
start_index = args["index"]
|
||||||
|
if start_index != len(self.items_received):
|
||||||
|
for item in args['items']:
|
||||||
|
# starting invo from server
|
||||||
|
if item.location in {-2}:
|
||||||
|
if not self.kh2seedsave["starting_inventory"]:
|
||||||
|
asyncio.create_task(self.give_item(item.item))
|
||||||
|
# if location is not already given or is !getitem
|
||||||
|
elif item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
|
||||||
|
or item.location in {-1}:
|
||||||
|
asyncio.create_task(self.give_item(item.item))
|
||||||
|
if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
|
||||||
|
and item.location not in {-1, -2}:
|
||||||
|
self.kh2seedsave["checked_locations"][str(item.player)].append(item.location)
|
||||||
|
if not self.kh2seedsave["starting_inventory"]:
|
||||||
|
self.kh2seedsave["starting_inventory"] = True
|
||||||
|
|
||||||
|
if cmd in {"RoomUpdate"}:
|
||||||
|
if "checked_locations" in args:
|
||||||
|
new_locations = set(args["checked_locations"])
|
||||||
|
# TODO: make this take locations from other players on the same slot so proper coop happens
|
||||||
|
# items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if
|
||||||
|
# location_id in self.kh2LocalItems.keys()]
|
||||||
|
self.checked_locations |= new_locations
|
||||||
|
|
||||||
|
async def checkWorldLocations(self):
|
||||||
|
try:
|
||||||
|
currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")
|
||||||
|
if currentworldint in self.worldid:
|
||||||
|
curworldid = self.worldid[currentworldint]
|
||||||
|
for location, data in curworldid.items():
|
||||||
|
if location not in self.locations_checked \
|
||||||
|
and (int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||||
|
"big") & 0x1 << data.bitIndex) > 0:
|
||||||
|
self.locations_checked.add(location)
|
||||||
|
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Line 285")
|
||||||
|
if self.kh2connected:
|
||||||
|
logger.info("Connection Lost.")
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
async def checkLevels(self):
|
||||||
|
try:
|
||||||
|
for location, data in SoraLevels.items():
|
||||||
|
currentLevel = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
|
||||||
|
if location not in self.locations_checked \
|
||||||
|
and currentLevel >= data.bitIndex:
|
||||||
|
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
|
||||||
|
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
|
||||||
|
self.locations_checked.add(location)
|
||||||
|
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||||
|
formDict = {
|
||||||
|
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
|
||||||
|
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
|
||||||
|
for i in range(5):
|
||||||
|
for location, data in formDict[i][1].items():
|
||||||
|
formlevel = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
|
||||||
|
if location not in self.locations_checked \
|
||||||
|
and formlevel >= data.bitIndex:
|
||||||
|
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
|
||||||
|
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
|
||||||
|
self.locations_checked.add(location)
|
||||||
|
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Line 312")
|
||||||
|
if self.kh2connected:
|
||||||
|
logger.info("Connection Lost.")
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
async def checkSlots(self):
|
||||||
|
try:
|
||||||
|
for location, data in weaponSlots.items():
|
||||||
|
if location not in self.locations_checked:
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||||
|
"big") > 0:
|
||||||
|
self.locations_checked.add(location)
|
||||||
|
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||||
|
|
||||||
|
for location, data in formSlots.items():
|
||||||
|
if location not in self.locations_checked:
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||||
|
"big") & 0x1 << data.bitIndex > 0:
|
||||||
|
self.locations_checked.add(location)
|
||||||
|
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||||
|
except Exception as e:
|
||||||
|
if self.kh2connected:
|
||||||
|
logger.info("Line 333")
|
||||||
|
logger.info("Connection Lost.")
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
async def verifyChests(self):
|
||||||
|
try:
|
||||||
|
currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big"))
|
||||||
|
for location in self.kh2seedsave["worldIdChecks"][currentworld]:
|
||||||
|
locationName = self.lookup_id_to_Location[location]
|
||||||
|
if locationName in self.chest_set:
|
||||||
|
if locationName in self.location_name_to_worlddata.keys():
|
||||||
|
locationData = self.location_name_to_worlddata[locationName]
|
||||||
|
if int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
|
||||||
|
"big") & 0x1 << locationData.bitIndex == 0:
|
||||||
|
roomData = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
|
||||||
|
1), "big")
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
|
||||||
|
(roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if self.kh2connected:
|
||||||
|
logger.info("Line 350")
|
||||||
|
logger.info("Connection Lost.")
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
async def verifyLevel(self):
|
||||||
|
for leveltype, anchor in {"SoraLevel": 0x24FF,
|
||||||
|
"ValorLevel": 0x32F6,
|
||||||
|
"WisdomLevel": 0x332E,
|
||||||
|
"LimitLevel": 0x3366,
|
||||||
|
"MasterLevel": 0x339E,
|
||||||
|
"FinalLevel": 0x33D6}.items():
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \
|
||||||
|
self.kh2seedsave["Levels"][leveltype]:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
|
||||||
|
(self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
def verifyLocation(self, location):
|
||||||
|
locationData = self.location_name_to_worlddata[location]
|
||||||
|
locationName = self.lookup_id_to_Location[location]
|
||||||
|
isChecked = True
|
||||||
|
|
||||||
|
if locationName not in levels_locations:
|
||||||
|
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
|
||||||
|
"big") & 0x1 << locationData.bitIndex) == 0:
|
||||||
|
isChecked = False
|
||||||
|
elif locationName in SoraLevels:
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1),
|
||||||
|
"big") < locationData.bitIndex:
|
||||||
|
isChecked = False
|
||||||
|
elif int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
|
||||||
|
"big") < locationData.bitIndex:
|
||||||
|
isChecked = False
|
||||||
|
return isChecked
|
||||||
|
|
||||||
|
async def give_item(self, item, ItemType="ServerItems"):
|
||||||
|
try:
|
||||||
|
itemname = self.lookup_id_to_item[item]
|
||||||
|
itemcode = self.item_name_to_data[itemname]
|
||||||
|
if itemcode.ability:
|
||||||
|
abilityInvoType = 0
|
||||||
|
TwilightZone = 2
|
||||||
|
if ItemType == "LocalItems":
|
||||||
|
abilityInvoType = 1
|
||||||
|
TwilightZone = -2
|
||||||
|
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = []
|
||||||
|
# appending the slot that the ability should be in
|
||||||
|
|
||||||
|
if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \
|
||||||
|
self.AbilityQuantityDict[itemname]:
|
||||||
|
if itemname in self.sora_ability_set:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||||
|
self.kh2seedsave["SoraInvo"][abilityInvoType])
|
||||||
|
self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone
|
||||||
|
elif itemname in self.donald_ability_set:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||||
|
self.kh2seedsave["DonaldInvo"][abilityInvoType])
|
||||||
|
self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone
|
||||||
|
else:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||||
|
self.kh2seedsave["GoofyInvo"][abilityInvoType])
|
||||||
|
self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone
|
||||||
|
|
||||||
|
elif itemcode.code in self.bitmask_item_code:
|
||||||
|
|
||||||
|
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname)
|
||||||
|
|
||||||
|
elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
|
||||||
|
|
||||||
|
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1
|
||||||
|
else:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1
|
||||||
|
elif itemname in self.all_equipment:
|
||||||
|
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname)
|
||||||
|
|
||||||
|
elif itemname in self.all_weapons:
|
||||||
|
if itemname in self.keyblade_set:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname)
|
||||||
|
elif itemname in self.staff_set:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname)
|
||||||
|
else:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname)
|
||||||
|
|
||||||
|
elif itemname in self.boost_set:
|
||||||
|
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1
|
||||||
|
else:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1
|
||||||
|
|
||||||
|
elif itemname in self.stat_increase_set:
|
||||||
|
|
||||||
|
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1
|
||||||
|
else:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1
|
||||||
|
else:
|
||||||
|
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if self.kh2connected:
|
||||||
|
logger.info("Line 398")
|
||||||
|
logger.info("Connection Lost.")
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
"""Import kivy UI system and start running it as self.ui_task."""
|
||||||
|
from kvui import GameManager
|
||||||
|
|
||||||
|
class KH2Manager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
|
base_title = "Archipelago KH2 Client"
|
||||||
|
|
||||||
|
self.ui = KH2Manager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
async def IsInShop(self, sellable, master_boost):
|
||||||
|
# journal = 0x741230 shop = 0x741320
|
||||||
|
# if journal=-1 and shop = 5 then in shop
|
||||||
|
# if journam !=-1 and shop = 10 then journal
|
||||||
|
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
|
||||||
|
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
|
||||||
|
if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||||
|
# print("your in the shop")
|
||||||
|
sellable_dict = {}
|
||||||
|
for itemName in sellable:
|
||||||
|
itemdata = self.item_name_to_data[itemName]
|
||||||
|
amount = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
|
||||||
|
sellable_dict[itemName] = amount
|
||||||
|
while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||||
|
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
|
||||||
|
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
for item, amount in sellable_dict.items():
|
||||||
|
itemdata = self.item_name_to_data[item]
|
||||||
|
afterShop = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
|
||||||
|
if afterShop < amount:
|
||||||
|
if item in master_boost:
|
||||||
|
self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop)
|
||||||
|
else:
|
||||||
|
self.kh2seedsave["SoldEquipment"].append(item)
|
||||||
|
|
||||||
|
async def verifyItems(self):
|
||||||
|
try:
|
||||||
|
local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys())
|
||||||
|
server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys())
|
||||||
|
master_amount = local_amount | server_amount
|
||||||
|
|
||||||
|
local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys())
|
||||||
|
server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys())
|
||||||
|
master_ability = local_ability | server_ability
|
||||||
|
|
||||||
|
local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"])
|
||||||
|
server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"])
|
||||||
|
master_bitmask = local_bitmask | server_bitmask
|
||||||
|
|
||||||
|
local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"])
|
||||||
|
local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"])
|
||||||
|
local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"])
|
||||||
|
|
||||||
|
server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"])
|
||||||
|
server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"])
|
||||||
|
server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"])
|
||||||
|
|
||||||
|
master_keyblade = local_keyblade | server_keyblade
|
||||||
|
master_staff = local_staff | server_staff
|
||||||
|
master_shield = local_shield | server_shield
|
||||||
|
|
||||||
|
local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"])
|
||||||
|
server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"])
|
||||||
|
master_equipment = local_equipment | server_equipment
|
||||||
|
|
||||||
|
local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys())
|
||||||
|
server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys())
|
||||||
|
master_magic = local_magic | server_magic
|
||||||
|
|
||||||
|
local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys())
|
||||||
|
server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys())
|
||||||
|
master_stat = local_stat | server_stat
|
||||||
|
|
||||||
|
local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys())
|
||||||
|
server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys())
|
||||||
|
master_boost = local_boost | server_boost
|
||||||
|
|
||||||
|
master_sell = master_equipment | master_staff | master_shield | master_boost
|
||||||
|
await asyncio.create_task(self.IsInShop(master_sell, master_boost))
|
||||||
|
for itemName in master_amount:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
amountOfItems = 0
|
||||||
|
if itemName in local_amount:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName]
|
||||||
|
if itemName in server_amount:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName]
|
||||||
|
|
||||||
|
if itemName == "Torn Page":
|
||||||
|
# Torn Pages are handled differently because they can be consumed.
|
||||||
|
# Will check the progression in 100 acre and - the amount of visits
|
||||||
|
# amountofitems-amount of visits done
|
||||||
|
for location, data in tornPageLocks.items():
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||||
|
"big") & 0x1 << data.bitIndex > 0:
|
||||||
|
amountOfItems -= 1
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != amountOfItems and amountOfItems >= 0:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
amountOfItems.to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
for itemName in master_keyblade:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
# if the inventory slot for that keyblade is less than the amount they should have
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1),
|
||||||
|
"big") != 13:
|
||||||
|
# Checking form anchors for the keyblade
|
||||||
|
if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \
|
||||||
|
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \
|
||||||
|
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \
|
||||||
|
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(0).to_bytes(1, 'big'), 1)
|
||||||
|
else:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(1).to_bytes(1, 'big'), 1)
|
||||||
|
for itemName in master_staff:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != 1 \
|
||||||
|
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \
|
||||||
|
and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(1).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
for itemName in master_shield:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != 1 \
|
||||||
|
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \
|
||||||
|
and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(1).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
for itemName in master_ability:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
ability_slot = []
|
||||||
|
if itemName in local_ability:
|
||||||
|
ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName]
|
||||||
|
if itemName in server_ability:
|
||||||
|
ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName]
|
||||||
|
for slot in ability_slot:
|
||||||
|
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
||||||
|
ability = current & 0x0FFF
|
||||||
|
if ability | 0x8000 != (0x8000 + itemData.memaddr):
|
||||||
|
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
|
||||||
|
|
||||||
|
for itemName in self.master_growth:
|
||||||
|
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
|
||||||
|
+ self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName]
|
||||||
|
if growthLevel > 0:
|
||||||
|
slot = self.growth_values_dict[itemName][2]
|
||||||
|
min_growth = self.growth_values_dict[itemName][0]
|
||||||
|
max_growth = self.growth_values_dict[itemName][1]
|
||||||
|
if growthLevel > 4:
|
||||||
|
growthLevel = 4
|
||||||
|
current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
||||||
|
ability = current_growth_level & 0x0FFF
|
||||||
|
# if the player should be getting a growth ability
|
||||||
|
if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel:
|
||||||
|
# if it should be level one of that growth
|
||||||
|
if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth:
|
||||||
|
self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth)
|
||||||
|
# if it is already in the inventory
|
||||||
|
elif ability | 0x8000 < (0x8000 + max_growth):
|
||||||
|
self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1)
|
||||||
|
|
||||||
|
for itemName in master_bitmask:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
itemMemory = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big")
|
||||||
|
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") & 0x1 << itemData.bitmask) == 0:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
for itemName in master_equipment:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
isThere = False
|
||||||
|
if itemName in self.accessories_set:
|
||||||
|
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"]
|
||||||
|
else:
|
||||||
|
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"]
|
||||||
|
# Checking form anchors for the equipment
|
||||||
|
for slot in Equipment_Anchor_List:
|
||||||
|
if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id:
|
||||||
|
isThere = True
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != 0:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(0).to_bytes(1, 'big'), 1)
|
||||||
|
break
|
||||||
|
if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != 1:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(1).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
for itemName in master_magic:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
amountOfItems = 0
|
||||||
|
if itemName in local_magic:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName]
|
||||||
|
if itemName in server_magic:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName]
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != amountOfItems \
|
||||||
|
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
amountOfItems.to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
for itemName in master_stat:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
amountOfItems = 0
|
||||||
|
if itemName in local_stat:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName]
|
||||||
|
if itemName in server_stat:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
|
||||||
|
|
||||||
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big") != amountOfItems \
|
||||||
|
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
|
||||||
|
"big") >= 5:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
amountOfItems.to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
for itemName in master_boost:
|
||||||
|
itemData = self.item_name_to_data[itemName]
|
||||||
|
amountOfItems = 0
|
||||||
|
if itemName in local_boost:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName]
|
||||||
|
if itemName in server_boost:
|
||||||
|
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName]
|
||||||
|
amountOfBoostsInInvo = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
|
"big")
|
||||||
|
amountOfUsedBoosts = int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1),
|
||||||
|
"big")
|
||||||
|
# Ap Boots start at +50 for some reason
|
||||||
|
if itemName == "AP Boost":
|
||||||
|
amountOfUsedBoosts -= 50
|
||||||
|
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
|
||||||
|
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][itemName] and amountOfBoostsInInvo < 255:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
|
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Line 573")
|
||||||
|
if self.kh2connected:
|
||||||
|
logger.info("Connection Lost.")
|
||||||
|
self.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
|
||||||
|
def finishedGame(ctx: KH2Context, message):
|
||||||
|
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||||
|
if 0x1301ED in message[0]["locations"]:
|
||||||
|
ctx.finalxemnas = True
|
||||||
|
# three proofs
|
||||||
|
if ctx.kh2slotdata['Goal'] == 0:
|
||||||
|
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \
|
||||||
|
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \
|
||||||
|
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0:
|
||||||
|
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||||
|
if ctx.finalxemnas:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
elif ctx.kh2slotdata['Goal'] == 1:
|
||||||
|
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \
|
||||||
|
ctx.kh2slotdata['LuckyEmblemsRequired']:
|
||||||
|
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
|
||||||
|
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
|
||||||
|
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
|
||||||
|
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||||
|
if ctx.finalxemnas:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
elif ctx.kh2slotdata['Goal'] == 2:
|
||||||
|
for boss in ctx.kh2slotdata["hitlist"]:
|
||||||
|
if boss in message[0]["locations"]:
|
||||||
|
ctx.amountOfPieces += 1
|
||||||
|
if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]:
|
||||||
|
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
|
||||||
|
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
|
||||||
|
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
|
||||||
|
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||||
|
if ctx.finalxemnas:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def kh2_watcher(ctx: KH2Context):
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
try:
|
||||||
|
if ctx.kh2connected and ctx.serverconneced:
|
||||||
|
ctx.sending = []
|
||||||
|
await asyncio.create_task(ctx.checkWorldLocations())
|
||||||
|
await asyncio.create_task(ctx.checkLevels())
|
||||||
|
await asyncio.create_task(ctx.checkSlots())
|
||||||
|
await asyncio.create_task(ctx.verifyChests())
|
||||||
|
await asyncio.create_task(ctx.verifyItems())
|
||||||
|
await asyncio.create_task(ctx.verifyLevel())
|
||||||
|
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||||
|
if finishedGame(ctx, message):
|
||||||
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
|
ctx.finished_game = True
|
||||||
|
location_ids = []
|
||||||
|
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
|
||||||
|
for location in location_ids:
|
||||||
|
currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big")
|
||||||
|
if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]:
|
||||||
|
ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location)
|
||||||
|
if location in ctx.kh2LocalItems:
|
||||||
|
item = ctx.kh2slotdata["LocalItems"][str(location)]
|
||||||
|
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
elif not ctx.kh2connected and ctx.serverconneced:
|
||||||
|
logger.info("Game is not open. Disconnecting from Server.")
|
||||||
|
await ctx.disconnect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Line 661")
|
||||||
|
if ctx.kh2connected:
|
||||||
|
logger.info("Connection Lost.")
|
||||||
|
ctx.kh2connected = False
|
||||||
|
logger.info(e)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
async def main(args):
|
||||||
|
ctx = KH2Context(args.connect, args.password)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
progression_watcher = asyncio.create_task(
|
||||||
|
kh2_watcher(ctx), name="KH2ProgressionWatcher")
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
|
||||||
|
await progression_watcher
|
||||||
|
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
parser = get_base_parser(description="KH2 Client, for text interfacing.")
|
||||||
|
|
||||||
|
args, rest = parser.parse_known_args()
|
||||||
|
colorama.init()
|
||||||
|
asyncio.run(main(args))
|
||||||
|
colorama.deinit()
|
||||||
96
Launcher.py
96
Launcher.py
@@ -14,10 +14,11 @@ import itertools
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from enum import Enum, auto
|
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Iterable, Sequence, Callable, Union, Optional
|
from typing import Sequence, Union, Optional
|
||||||
|
|
||||||
|
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
@@ -70,99 +71,12 @@ def browse_files():
|
|||||||
webbrowser.open(file)
|
webbrowser.open(file)
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyArgumentList
|
components.extend([
|
||||||
class Type(Enum):
|
|
||||||
TOOL = auto()
|
|
||||||
FUNC = auto() # not a real component
|
|
||||||
CLIENT = auto()
|
|
||||||
ADJUSTER = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class SuffixIdentifier:
|
|
||||||
suffixes: Iterable[str]
|
|
||||||
|
|
||||||
def __init__(self, *args: str):
|
|
||||||
self.suffixes = args
|
|
||||||
|
|
||||||
def __call__(self, path: str):
|
|
||||||
if isinstance(path, str):
|
|
||||||
for suffix in self.suffixes:
|
|
||||||
if path.endswith(suffix):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Component:
|
|
||||||
display_name: str
|
|
||||||
type: Optional[Type]
|
|
||||||
script_name: Optional[str]
|
|
||||||
frozen_name: Optional[str]
|
|
||||||
icon: str # just the name, no suffix
|
|
||||||
cli: bool
|
|
||||||
func: Optional[Callable]
|
|
||||||
file_identifier: Optional[Callable[[str], bool]]
|
|
||||||
|
|
||||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
|
||||||
cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None,
|
|
||||||
file_identifier: Optional[Callable[[str], bool]] = None):
|
|
||||||
self.display_name = display_name
|
|
||||||
self.script_name = script_name
|
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
|
||||||
self.icon = icon
|
|
||||||
self.cli = cli
|
|
||||||
self.type = component_type or \
|
|
||||||
None if not display_name else \
|
|
||||||
Type.FUNC if func else \
|
|
||||||
Type.CLIENT if 'Client' in display_name else \
|
|
||||||
Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL
|
|
||||||
self.func = func
|
|
||||||
self.file_identifier = file_identifier
|
|
||||||
|
|
||||||
def handles_file(self, path: str):
|
|
||||||
return self.file_identifier(path) if self.file_identifier else False
|
|
||||||
|
|
||||||
|
|
||||||
components: Iterable[Component] = (
|
|
||||||
# Launcher
|
|
||||||
Component('', 'Launcher'),
|
|
||||||
# Core
|
|
||||||
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
|
||||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
|
||||||
Component('Generate', 'Generate', cli=True),
|
|
||||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
|
|
||||||
# SNI
|
|
||||||
Component('SNI Client', 'SNIClient',
|
|
||||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')),
|
|
||||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
|
||||||
# Factorio
|
|
||||||
Component('Factorio Client', 'FactorioClient'),
|
|
||||||
# Minecraft
|
|
||||||
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
|
||||||
file_identifier=SuffixIdentifier('.apmc')),
|
|
||||||
# Ocarina of Time
|
|
||||||
Component('OoT Client', 'OoTClient',
|
|
||||||
file_identifier=SuffixIdentifier('.apz5')),
|
|
||||||
Component('OoT Adjuster', 'OoTAdjuster'),
|
|
||||||
# FF1
|
|
||||||
Component('FF1 Client', 'FF1Client'),
|
|
||||||
# Pokémon
|
|
||||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
|
||||||
# ChecksFinder
|
|
||||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
|
||||||
# Starcraft 2
|
|
||||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
|
||||||
# Zillion
|
|
||||||
Component('Zillion Client', 'ZillionClient',
|
|
||||||
file_identifier=SuffixIdentifier('.apzl')),
|
|
||||||
# Functions
|
# Functions
|
||||||
Component('Open host.yaml', func=open_host_yaml),
|
Component('Open host.yaml', func=open_host_yaml),
|
||||||
Component('Open Patch', func=open_patch),
|
Component('Open Patch', func=open_patch),
|
||||||
Component('Browse Files', func=browse_files),
|
Component('Browse Files', func=browse_files),
|
||||||
)
|
])
|
||||||
icon_paths = {
|
|
||||||
'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
|
|
||||||
'mcicon': local_path('data', 'mcicon.ico')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]):
|
def identify(path: Union[None, str]):
|
||||||
|
|||||||
609
LinksAwakeningClient.py
Normal file
609
LinksAwakeningClient.py
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
import ModuleUpdate
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
import typing
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
|
||||||
|
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||||
|
server_loop)
|
||||||
|
from NetUtils import ClientStatus
|
||||||
|
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||||
|
from worlds.ladx.GpsTracker import GpsTracker
|
||||||
|
from worlds.ladx.ItemTracker import ItemTracker
|
||||||
|
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
||||||
|
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||||
|
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
||||||
|
|
||||||
|
class GameboyException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RetroArchDisconnectError(GameboyException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidEmulatorStateError(GameboyException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadRetroArchResponse(GameboyException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def magpie_logo():
|
||||||
|
from kivy.uix.image import CoreImage
|
||||||
|
binary_data = """
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
|
||||||
|
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
|
||||||
|
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
|
||||||
|
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
|
||||||
|
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
|
||||||
|
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
|
||||||
|
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
|
||||||
|
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
|
||||||
|
binary_data = base64.b64decode(binary_data)
|
||||||
|
data = io.BytesIO(binary_data)
|
||||||
|
return CoreImage(data, ext="png").texture
|
||||||
|
|
||||||
|
|
||||||
|
class LAClientConstants:
|
||||||
|
# Connector version
|
||||||
|
VERSION = 0x01
|
||||||
|
#
|
||||||
|
# Memory locations of LADXR
|
||||||
|
ROMGameID = 0x0051 # 4 bytes
|
||||||
|
SlotName = 0x0134
|
||||||
|
# Unused
|
||||||
|
# ROMWorldID = 0x0055
|
||||||
|
# ROMConnectorVersion = 0x0056
|
||||||
|
# RO: We should only act if this is higher then 6, as it indicates that the game is running normally
|
||||||
|
wGameplayType = 0xDB95
|
||||||
|
# RO: Starts at 0, increases every time an item is received from the server and processed
|
||||||
|
wLinkSyncSequenceNumber = 0xDDF6
|
||||||
|
wLinkStatusBits = 0xDDF7 # RW:
|
||||||
|
# Bit0: wLinkGive* contains valid data, set from script cleared from ROM.
|
||||||
|
wLinkHealth = 0xDB5A
|
||||||
|
wLinkGiveItem = 0xDDF8 # RW
|
||||||
|
wLinkGiveItemFrom = 0xDDF9 # RW
|
||||||
|
# All of these six bytes are unused, we can repurpose
|
||||||
|
# wLinkSendItemRoomHigh = 0xDDFA # RO
|
||||||
|
# wLinkSendItemRoomLow = 0xDDFB # RO
|
||||||
|
# wLinkSendItemTarget = 0xDDFC # RO
|
||||||
|
# wLinkSendItemItem = 0xDDFD # RO
|
||||||
|
# wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items)
|
||||||
|
# RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0
|
||||||
|
# wLinkSendShopTarget = 0xDDFF
|
||||||
|
|
||||||
|
|
||||||
|
wRecvIndex = 0xDDFE # 0xDB58
|
||||||
|
wCheckAddress = 0xC0FF - 0x4
|
||||||
|
WRamCheckSize = 0x4
|
||||||
|
WRamSafetyValue = bytearray([0]*WRamCheckSize)
|
||||||
|
|
||||||
|
MinGameplayValue = 0x06
|
||||||
|
MaxGameplayValue = 0x1A
|
||||||
|
VictoryGameplayAndSub = 0x0102
|
||||||
|
|
||||||
|
|
||||||
|
class RAGameboy():
|
||||||
|
cache = []
|
||||||
|
cache_start = 0
|
||||||
|
cache_size = 0
|
||||||
|
last_cache_read = None
|
||||||
|
socket = None
|
||||||
|
|
||||||
|
def __init__(self, address, port) -> None:
|
||||||
|
self.address = address
|
||||||
|
self.port = port
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
assert (self.socket)
|
||||||
|
self.socket.setblocking(False)
|
||||||
|
|
||||||
|
def get_retroarch_version(self):
|
||||||
|
self.send(b'VERSION\n')
|
||||||
|
select.select([self.socket], [], [])
|
||||||
|
response_str, addr = self.socket.recvfrom(16)
|
||||||
|
return response_str.rstrip()
|
||||||
|
|
||||||
|
def get_retroarch_status(self, timeout):
|
||||||
|
self.send(b'GET_STATUS\n')
|
||||||
|
select.select([self.socket], [], [], timeout)
|
||||||
|
response_str, addr = self.socket.recvfrom(1000, )
|
||||||
|
return response_str.rstrip()
|
||||||
|
|
||||||
|
def set_cache_limits(self, cache_start, cache_size):
|
||||||
|
self.cache_start = cache_start
|
||||||
|
self.cache_size = cache_size
|
||||||
|
|
||||||
|
def send(self, b):
|
||||||
|
if type(b) is str:
|
||||||
|
b = b.encode('ascii')
|
||||||
|
self.socket.sendto(b, (self.address, self.port))
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
select.select([self.socket], [], [])
|
||||||
|
response, _ = self.socket.recvfrom(4096)
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def async_recv(self):
|
||||||
|
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def check_safe_gameplay(self, throw=True):
|
||||||
|
async def check_wram():
|
||||||
|
check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize)
|
||||||
|
|
||||||
|
if check_values != LAClientConstants.WRamSafetyValue:
|
||||||
|
if throw:
|
||||||
|
raise InvalidEmulatorStateError()
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not await check_wram():
|
||||||
|
if throw:
|
||||||
|
raise InvalidEmulatorStateError()
|
||||||
|
return False
|
||||||
|
|
||||||
|
gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType)
|
||||||
|
gameplay_value = gameplay_value[0]
|
||||||
|
# In gameplay or credits
|
||||||
|
if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1:
|
||||||
|
if throw:
|
||||||
|
logger.info("invalid emu state")
|
||||||
|
raise InvalidEmulatorStateError()
|
||||||
|
return False
|
||||||
|
if not await check_wram():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# We're sadly unable to update the whole cache at once
|
||||||
|
# as RetroArch only gives back some number of bytes at a time
|
||||||
|
# So instead read as big as chunks at a time as we can manage
|
||||||
|
async def update_cache(self):
|
||||||
|
# First read the safety address - if it's invalid, bail
|
||||||
|
self.cache = []
|
||||||
|
|
||||||
|
if not await self.check_safe_gameplay():
|
||||||
|
return
|
||||||
|
|
||||||
|
cache = []
|
||||||
|
remaining_size = self.cache_size
|
||||||
|
while remaining_size:
|
||||||
|
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
|
||||||
|
remaining_size -= len(block)
|
||||||
|
cache += block
|
||||||
|
|
||||||
|
if not await self.check_safe_gameplay():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cache = cache
|
||||||
|
self.last_cache_read = time.time()
|
||||||
|
|
||||||
|
async def read_memory_cache(self, addresses):
|
||||||
|
# TODO: can we just update once per frame?
|
||||||
|
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
|
||||||
|
await self.update_cache()
|
||||||
|
if not self.cache:
|
||||||
|
return None
|
||||||
|
assert (len(self.cache) == self.cache_size)
|
||||||
|
for address in addresses:
|
||||||
|
assert self.cache_start <= address <= self.cache_start + self.cache_size
|
||||||
|
r = {address: self.cache[address - self.cache_start]
|
||||||
|
for address in addresses}
|
||||||
|
return r
|
||||||
|
|
||||||
|
async def async_read_memory_safe(self, address, size=1):
|
||||||
|
# whenever we do a read for a check, we need to make sure that we aren't reading
|
||||||
|
# garbage memory values - we also need to protect against reading a value, then the emulator resetting
|
||||||
|
#
|
||||||
|
# ...actually, we probably _only_ need the post check
|
||||||
|
|
||||||
|
# Check before read
|
||||||
|
if not await self.check_safe_gameplay():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Do read
|
||||||
|
r = await self.async_read_memory(address, size)
|
||||||
|
|
||||||
|
# Check after read
|
||||||
|
if not await self.check_safe_gameplay():
|
||||||
|
return None
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
def read_memory(self, address, size=1):
|
||||||
|
command = "READ_CORE_MEMORY"
|
||||||
|
|
||||||
|
self.send(f'{command} {hex(address)} {size}\n')
|
||||||
|
response = self.recv()
|
||||||
|
|
||||||
|
splits = response.decode().split(" ", 2)
|
||||||
|
|
||||||
|
assert (splits[0] == command)
|
||||||
|
# Ignore the address for now
|
||||||
|
|
||||||
|
# TODO: transform to bytes
|
||||||
|
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
|
||||||
|
raise BadRetroArchResponse()
|
||||||
|
return bytearray.fromhex(splits[2])
|
||||||
|
|
||||||
|
async def async_read_memory(self, address, size=1):
|
||||||
|
command = "READ_CORE_MEMORY"
|
||||||
|
|
||||||
|
self.send(f'{command} {hex(address)} {size}\n')
|
||||||
|
response = await self.async_recv()
|
||||||
|
response = response[:-1]
|
||||||
|
splits = response.decode().split(" ", 2)
|
||||||
|
|
||||||
|
assert (splits[0] == command)
|
||||||
|
# Ignore the address for now
|
||||||
|
|
||||||
|
# TODO: transform to bytes
|
||||||
|
return bytearray.fromhex(splits[2])
|
||||||
|
|
||||||
|
def write_memory(self, address, bytes):
|
||||||
|
command = "WRITE_CORE_MEMORY"
|
||||||
|
|
||||||
|
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
|
||||||
|
select.select([self.socket], [], [])
|
||||||
|
response, _ = self.socket.recvfrom(4096)
|
||||||
|
|
||||||
|
splits = response.decode().split(" ", 3)
|
||||||
|
|
||||||
|
assert (splits[0] == command)
|
||||||
|
|
||||||
|
if splits[2] == "-1":
|
||||||
|
logger.info(splits[3])
|
||||||
|
|
||||||
|
|
||||||
|
class LinksAwakeningClient():
|
||||||
|
socket = None
|
||||||
|
gameboy = None
|
||||||
|
tracker = None
|
||||||
|
auth = None
|
||||||
|
game_crc = None
|
||||||
|
pending_deathlink = False
|
||||||
|
deathlink_debounce = True
|
||||||
|
recvd_checks = {}
|
||||||
|
|
||||||
|
def msg(self, m):
|
||||||
|
logger.info(m)
|
||||||
|
s = f"SHOW_MSG {m}\n"
|
||||||
|
self.gameboy.send(s)
|
||||||
|
|
||||||
|
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
|
||||||
|
self.gameboy = RAGameboy(retroarch_address, retroarch_port)
|
||||||
|
|
||||||
|
async def wait_for_retroarch_connection(self):
|
||||||
|
logger.info("Waiting on connection to Retroarch...")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
version = self.gameboy.get_retroarch_version()
|
||||||
|
NO_CONTENT = b"GET_STATUS CONTENTLESS"
|
||||||
|
status = NO_CONTENT
|
||||||
|
core_type = None
|
||||||
|
GAME_BOY = b"game_boy"
|
||||||
|
while status == NO_CONTENT or core_type != GAME_BOY:
|
||||||
|
try:
|
||||||
|
status = self.gameboy.get_retroarch_status(0.1)
|
||||||
|
if status.count(b" ") < 2:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
||||||
|
if status.count(b",") < 2:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
continue
|
||||||
|
core_type, rom_name, self.game_crc = info.split(b",", 2)
|
||||||
|
if core_type != GAME_BOY:
|
||||||
|
logger.info(
|
||||||
|
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
continue
|
||||||
|
except (BlockingIOError, TimeoutError) as e:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
pass
|
||||||
|
logger.info(f"Connected to Retroarch {version} {info}")
|
||||||
|
self.gameboy.read_memory(0x1000)
|
||||||
|
return
|
||||||
|
except ConnectionResetError:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset_auth(self):
|
||||||
|
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
|
||||||
|
|
||||||
|
if self.auth:
|
||||||
|
assert (auth == self.auth)
|
||||||
|
|
||||||
|
self.auth = auth
|
||||||
|
|
||||||
|
async def wait_and_init_tracker(self):
|
||||||
|
await self.wait_for_game_ready()
|
||||||
|
self.tracker = LocationTracker(self.gameboy)
|
||||||
|
self.item_tracker = ItemTracker(self.gameboy)
|
||||||
|
self.gps_tracker = GpsTracker(self.gameboy)
|
||||||
|
|
||||||
|
async def recved_item_from_ap(self, item_id, from_player, next_index):
|
||||||
|
# Don't allow getting an item until you've got your first check
|
||||||
|
if not self.tracker.has_start_item():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Spin until we either:
|
||||||
|
# get an exception from a bad read (emu shut down or reset)
|
||||||
|
# beat the game
|
||||||
|
# the client handles the last pending item
|
||||||
|
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
||||||
|
while not (await self.is_victory()) and status & 1 == 1:
|
||||||
|
time.sleep(0.1)
|
||||||
|
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
||||||
|
|
||||||
|
item_id -= LABaseID
|
||||||
|
# The player name table only goes up to 100, so don't go past that
|
||||||
|
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
|
||||||
|
if from_player > 100:
|
||||||
|
from_player = 100
|
||||||
|
|
||||||
|
next_index += 1
|
||||||
|
self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [
|
||||||
|
item_id, from_player])
|
||||||
|
status |= 1
|
||||||
|
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
||||||
|
self.gameboy.write_memory(LAClientConstants.wRecvIndex, [next_index])
|
||||||
|
|
||||||
|
async def wait_for_game_ready(self):
|
||||||
|
logger.info("Waiting on game to be in valid state...")
|
||||||
|
while not await self.gameboy.check_safe_gameplay(throw=False):
|
||||||
|
pass
|
||||||
|
logger.info("Ready!")
|
||||||
|
last_index = 0
|
||||||
|
|
||||||
|
async def is_victory(self):
|
||||||
|
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
||||||
|
|
||||||
|
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
|
||||||
|
await self.tracker.readChecks(item_get_cb)
|
||||||
|
await self.item_tracker.readItems()
|
||||||
|
await self.gps_tracker.read_location()
|
||||||
|
|
||||||
|
next_index = self.gameboy.read_memory(LAClientConstants.wRecvIndex)[0]
|
||||||
|
if next_index != self.last_index:
|
||||||
|
self.last_index = next_index
|
||||||
|
# logger.info(f"Got new index {next_index}")
|
||||||
|
|
||||||
|
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
|
||||||
|
if self.deathlink_debounce and current_health != 0:
|
||||||
|
self.deathlink_debounce = False
|
||||||
|
elif not self.deathlink_debounce and current_health == 0:
|
||||||
|
# logger.info("YOU DIED.")
|
||||||
|
await deathlink_cb()
|
||||||
|
self.deathlink_debounce = True
|
||||||
|
|
||||||
|
if self.pending_deathlink:
|
||||||
|
logger.info("Got a deathlink")
|
||||||
|
self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0])
|
||||||
|
self.pending_deathlink = False
|
||||||
|
self.deathlink_debounce = True
|
||||||
|
|
||||||
|
if await self.is_victory():
|
||||||
|
await win_cb()
|
||||||
|
|
||||||
|
recv_index = (await self.gameboy.async_read_memory_safe(LAClientConstants.wRecvIndex))[0]
|
||||||
|
|
||||||
|
# Play back one at a time
|
||||||
|
if recv_index in self.recvd_checks:
|
||||||
|
item = self.recvd_checks[recv_index]
|
||||||
|
await self.recved_item_from_ap(item.item, item.player, recv_index)
|
||||||
|
|
||||||
|
|
||||||
|
all_tasks = set()
|
||||||
|
|
||||||
|
def create_task_log_exception(awaitable) -> asyncio.Task:
|
||||||
|
async def _log_exception(awaitable):
|
||||||
|
try:
|
||||||
|
return await awaitable
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
all_tasks.remove(task)
|
||||||
|
task = asyncio.create_task(_log_exception(awaitable))
|
||||||
|
all_tasks.add(task)
|
||||||
|
|
||||||
|
|
||||||
|
class LinksAwakeningContext(CommonContext):
|
||||||
|
tags = {"AP"}
|
||||||
|
game = "Links Awakening DX"
|
||||||
|
items_handling = 0b101
|
||||||
|
want_slot_data = True
|
||||||
|
la_task = None
|
||||||
|
client = None
|
||||||
|
# TODO: does this need to re-read on reset?
|
||||||
|
found_checks = []
|
||||||
|
last_resend = time.time()
|
||||||
|
|
||||||
|
magpie = MagpieBridge()
|
||||||
|
magpie_task = None
|
||||||
|
won = False
|
||||||
|
|
||||||
|
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
|
||||||
|
self.client = LinksAwakeningClient()
|
||||||
|
super().__init__(server_address, password)
|
||||||
|
|
||||||
|
def run_gui(self) -> None:
|
||||||
|
import webbrowser
|
||||||
|
import kvui
|
||||||
|
from kvui import Button, GameManager
|
||||||
|
from kivy.uix.image import Image
|
||||||
|
|
||||||
|
class LADXManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago"),
|
||||||
|
("Tracker", "Tracker"),
|
||||||
|
]
|
||||||
|
base_title = "Archipelago Links Awakening DX Client"
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
b = super().build()
|
||||||
|
|
||||||
|
button = Button(text="", size=(30, 30), size_hint_x=None,
|
||||||
|
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||||
|
image = Image(size=(16, 16), texture=magpie_logo())
|
||||||
|
button.add_widget(image)
|
||||||
|
|
||||||
|
def set_center(_, center):
|
||||||
|
image.center = center
|
||||||
|
button.bind(center=set_center)
|
||||||
|
|
||||||
|
self.connect_layout.add_widget(button)
|
||||||
|
return b
|
||||||
|
|
||||||
|
self.ui = LADXManager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
async def send_checks(self):
|
||||||
|
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
||||||
|
await self.send_msgs(message)
|
||||||
|
|
||||||
|
ENABLE_DEATHLINK = False
|
||||||
|
async def send_deathlink(self):
|
||||||
|
if self.ENABLE_DEATHLINK:
|
||||||
|
message = [{"cmd": 'Deathlink',
|
||||||
|
'time': time.time(),
|
||||||
|
'cause': 'Had a nightmare',
|
||||||
|
# 'source': self.slot_info[self.slot].name,
|
||||||
|
}]
|
||||||
|
await self.send_msgs(message)
|
||||||
|
|
||||||
|
async def send_victory(self):
|
||||||
|
if not self.won:
|
||||||
|
message = [{"cmd": "StatusUpdate",
|
||||||
|
"status": ClientStatus.CLIENT_GOAL}]
|
||||||
|
logger.info("victory!")
|
||||||
|
await self.send_msgs(message)
|
||||||
|
self.won = True
|
||||||
|
|
||||||
|
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||||
|
if self.ENABLE_DEATHLINK:
|
||||||
|
self.client.pending_deathlink = True
|
||||||
|
|
||||||
|
def new_checks(self, item_ids, ladxr_ids):
|
||||||
|
self.found_checks += item_ids
|
||||||
|
create_task_log_exception(self.send_checks())
|
||||||
|
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super(LinksAwakeningContext, self).server_auth(password_requested)
|
||||||
|
self.auth = self.client.auth
|
||||||
|
await self.get_username()
|
||||||
|
await self.send_connect()
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd == "Connected":
|
||||||
|
self.game = self.slot_info[self.slot].game
|
||||||
|
# TODO - use watcher_event
|
||||||
|
if cmd == "ReceivedItems":
|
||||||
|
for index, item in enumerate(args["items"], args["index"]):
|
||||||
|
self.client.recvd_checks[index] = item
|
||||||
|
|
||||||
|
item_id_lookup = get_locations_to_id()
|
||||||
|
|
||||||
|
async def run_game_loop(self):
|
||||||
|
def on_item_get(ladxr_checks):
|
||||||
|
checks = [self.item_id_lookup[meta_to_name(
|
||||||
|
checkMetadataTable[check.id])] for check in ladxr_checks]
|
||||||
|
self.new_checks(checks, [check.id for check in ladxr_checks])
|
||||||
|
|
||||||
|
async def victory():
|
||||||
|
await self.send_victory()
|
||||||
|
|
||||||
|
async def deathlink():
|
||||||
|
await self.send_deathlink()
|
||||||
|
|
||||||
|
self.magpie_task = asyncio.create_task(self.magpie.serve())
|
||||||
|
|
||||||
|
# yield to allow UI to start
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# TODO: cancel all client tasks
|
||||||
|
logger.info("(Re)Starting game loop")
|
||||||
|
self.found_checks.clear()
|
||||||
|
await self.client.wait_for_retroarch_connection()
|
||||||
|
self.client.reset_auth()
|
||||||
|
await self.client.wait_and_init_tracker()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
await self.client.main_tick(on_item_get, victory, deathlink)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
now = time.time()
|
||||||
|
if self.last_resend + 5.0 < now:
|
||||||
|
self.last_resend = now
|
||||||
|
await self.send_checks()
|
||||||
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
|
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||||
|
await self.magpie.send_gps(self.client.gps_tracker)
|
||||||
|
|
||||||
|
except GameboyException:
|
||||||
|
time.sleep(1.0)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = get_base_parser(description="Link's Awakening Client.")
|
||||||
|
parser.add_argument("--url", help="Archipelago connection url")
|
||||||
|
|
||||||
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
|
help='Path to a .apladx Archipelago Binary Patch file')
|
||||||
|
args = parser.parse_args()
|
||||||
|
logger.info(args)
|
||||||
|
|
||||||
|
if args.diff_file:
|
||||||
|
import Patch
|
||||||
|
logger.info("patch file was supplied - creating rom...")
|
||||||
|
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||||
|
if "server" in meta:
|
||||||
|
args.url = meta["server"]
|
||||||
|
logger.info(f"wrote rom file to {rom_file}")
|
||||||
|
|
||||||
|
if args.url:
|
||||||
|
url = urllib.parse.urlparse(args.url)
|
||||||
|
args.connect = url.netloc
|
||||||
|
if url.password:
|
||||||
|
args.password = urllib.parse.unquote(url.password)
|
||||||
|
|
||||||
|
ctx = LinksAwakeningContext(args.connect, args.password)
|
||||||
|
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
|
|
||||||
|
# TODO: nothing about the lambda about has to be in a lambda
|
||||||
|
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
colorama.init()
|
||||||
|
asyncio.run(main())
|
||||||
|
colorama.deinit()
|
||||||
@@ -35,7 +35,7 @@ class AdjusterWorld(object):
|
|||||||
def __init__(self, sprite_pool):
|
def __init__(self, sprite_pool):
|
||||||
import random
|
import random
|
||||||
self.sprite_pool = {1: sprite_pool}
|
self.sprite_pool = {1: sprite_pool}
|
||||||
self.slot_seeds = {1: random}
|
self.per_slot_randoms = {1: random}
|
||||||
|
|
||||||
|
|
||||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||||
|
|||||||
40
Main.py
40
Main.py
@@ -9,11 +9,12 @@ import tempfile
|
|||||||
import zipfile
|
import zipfile
|
||||||
from typing import Dict, List, Tuple, Optional, Set
|
from typing import Dict, List, Tuple, Optional, Set
|
||||||
|
|
||||||
from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
|
from BaseClasses import Item, MultiWorld, CollectionState, Region, LocationProgressType, Location
|
||||||
import worlds
|
import worlds
|
||||||
|
from worlds.alttp.SubClasses import LTTPRegionType
|
||||||
from worlds.alttp.Regions import is_main_entrance
|
from worlds.alttp.Regions import is_main_entrance
|
||||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||||
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
from worlds.alttp.Shops import FillDisabledShopSlots
|
||||||
from Utils import output_path, get_options, __version__, version_tuple
|
from Utils import output_path, get_options, __version__, version_tuple
|
||||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
from worlds.generic.Rules import locality_rules, exclusion_rules
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
@@ -37,7 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
world = MultiWorld(args.multi)
|
world = MultiWorld(args.multi)
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
|
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||||
world.plando_options = args.plando_options
|
world.plando_options = args.plando_options
|
||||||
|
|
||||||
world.shuffle = args.shuffle.copy()
|
world.shuffle = args.shuffle.copy()
|
||||||
@@ -52,7 +53,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
world.enemy_damage = args.enemy_damage.copy()
|
world.enemy_damage = args.enemy_damage.copy()
|
||||||
world.beemizer_total_chance = args.beemizer_total_chance.copy()
|
world.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||||
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||||
world.timer = args.timer.copy()
|
|
||||||
world.countdown_start_time = args.countdown_start_time.copy()
|
world.countdown_start_time = args.countdown_start_time.copy()
|
||||||
world.red_clock_time = args.red_clock_time.copy()
|
world.red_clock_time = args.red_clock_time.copy()
|
||||||
world.blue_clock_time = args.blue_clock_time.copy()
|
world.blue_clock_time = args.blue_clock_time.copy()
|
||||||
@@ -78,7 +78,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
world.state = CollectionState(world)
|
world.state = CollectionState(world)
|
||||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||||
|
|
||||||
logger.info("Found World Types:")
|
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
|
||||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||||
|
|
||||||
max_item = 0
|
max_item = 0
|
||||||
@@ -191,7 +191,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
new_item.classification |= classifications[item_name]
|
new_item.classification |= classifications[item_name]
|
||||||
new_itempool.append(new_item)
|
new_itempool.append(new_item)
|
||||||
|
|
||||||
region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world)
|
region = Region("Menu", group_id, world, "ItemLink")
|
||||||
world.regions.append(region)
|
world.regions.append(region)
|
||||||
locations = region.locations = []
|
locations = region.locations = []
|
||||||
for item in world.itempool:
|
for item in world.itempool:
|
||||||
@@ -251,6 +251,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
balance_multiworld_progression(world)
|
balance_multiworld_progression(world)
|
||||||
|
|
||||||
logger.info(f'Beginning output...')
|
logger.info(f'Beginning output...')
|
||||||
|
|
||||||
|
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
||||||
|
world.random.passthrough = False
|
||||||
|
|
||||||
outfilebase = 'AP_' + world.seed_name
|
outfilebase = 'AP_' + world.seed_name
|
||||||
|
|
||||||
output = tempfile.TemporaryDirectory()
|
output = tempfile.TemporaryDirectory()
|
||||||
@@ -286,13 +290,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||||
checks_in_area[location.player][dungeonname].append(location.address)
|
checks_in_area[location.player][dungeonname].append(location.address)
|
||||||
elif location.parent_region.type == RegionType.LightWorld:
|
elif location.parent_region.type == LTTPRegionType.LightWorld:
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
checks_in_area[location.player]["Light World"].append(location.address)
|
||||||
elif location.parent_region.type == RegionType.DarkWorld:
|
elif location.parent_region.type == LTTPRegionType.DarkWorld:
|
||||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
checks_in_area[location.player]["Light World"].append(location.address)
|
||||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
|
||||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||||
checks_in_area[location.player]["Total"] += 1
|
checks_in_area[location.player]["Total"] += 1
|
||||||
|
|
||||||
@@ -351,18 +355,16 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
||||||
precollect_hint(location)
|
precollect_hint(location)
|
||||||
|
|
||||||
# custom datapackage
|
# embedded data package
|
||||||
datapackage = {}
|
data_package = {
|
||||||
for game_world in world.worlds.values():
|
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||||
if game_world.data_version == 0 and game_world.game not in datapackage:
|
for game_world in world.worlds.values()
|
||||||
datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game]
|
}
|
||||||
datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups
|
|
||||||
|
|
||||||
multidata = {
|
multidata = {
|
||||||
"slot_data": slot_data,
|
"slot_data": slot_data,
|
||||||
"slot_info": slot_info,
|
"slot_info": slot_info,
|
||||||
"names": names, # TODO: remove around 0.2.5 in favor of slot_info
|
"names": names, # TODO: remove after 0.3.9
|
||||||
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
|
|
||||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||||
"locations": locations_data,
|
"locations": locations_data,
|
||||||
"checks_in_area": checks_in_area,
|
"checks_in_area": checks_in_area,
|
||||||
@@ -374,7 +376,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
"tags": ["AP"],
|
"tags": ["AP"],
|
||||||
"minimum_versions": minimum_versions,
|
"minimum_versions": minimum_versions,
|
||||||
"seed_name": world.seed_name,
|
"seed_name": world.seed_name,
|
||||||
"datapackage": datapackage,
|
"datapackage": data_package,
|
||||||
}
|
}
|
||||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||||
|
|
||||||
|
|||||||
@@ -77,49 +77,34 @@ def read_apmc_file(apmc_file):
|
|||||||
return json.loads(b64decode(f.read()))
|
return json.loads(b64decode(f.read()))
|
||||||
|
|
||||||
|
|
||||||
def update_mod(forge_dir, minecraft_version: str, get_prereleases=False):
|
def update_mod(forge_dir, url: str):
|
||||||
"""Check mod version, download new mod from GitHub releases page if needed. """
|
"""Check mod version, download new mod from GitHub releases page if needed. """
|
||||||
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
||||||
|
os.path.basename(url)
|
||||||
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
|
if ap_randomizer is not None:
|
||||||
resp = requests.get(client_releases_endpoint)
|
logging.info(f"Your current mod is {ap_randomizer}.")
|
||||||
if resp.status_code == 200: # OK
|
|
||||||
try:
|
|
||||||
latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and
|
|
||||||
(minecraft_version in release['assets'][0]['name']),
|
|
||||||
resp.json()))
|
|
||||||
if ap_randomizer != latest_release['assets'][0]['name']:
|
|
||||||
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
|
||||||
f"{latest_release['assets'][0]['name']}")
|
|
||||||
if ap_randomizer is not None:
|
|
||||||
logging.info(f"Your current mod is {ap_randomizer}.")
|
|
||||||
else:
|
|
||||||
logging.info(f"You do not have the AP randomizer mod installed.")
|
|
||||||
if prompt_yes_no("Would you like to update?"):
|
|
||||||
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
|
||||||
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
|
|
||||||
logging.info("Downloading AP randomizer mod. This may take a moment...")
|
|
||||||
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
|
|
||||||
if apmod_resp.status_code == 200:
|
|
||||||
with open(new_ap_mod, 'wb') as f:
|
|
||||||
f.write(apmod_resp.content)
|
|
||||||
logging.info(f"Wrote new mod file to {new_ap_mod}")
|
|
||||||
if old_ap_mod is not None:
|
|
||||||
os.remove(old_ap_mod)
|
|
||||||
logging.info(f"Removed old mod file from {old_ap_mod}")
|
|
||||||
else:
|
|
||||||
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
|
||||||
logging.error(f"Please report this issue on the Archipelago Discord server.")
|
|
||||||
sys.exit(1)
|
|
||||||
except StopIteration:
|
|
||||||
logging.warning(f"No compatible mod version found for {minecraft_version}.")
|
|
||||||
if not prompt_yes_no("Run server anyway?"):
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
else:
|
||||||
logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
|
logging.info(f"You do not have the AP randomizer mod installed.")
|
||||||
logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.")
|
|
||||||
if not prompt_yes_no("Continue anyways?"):
|
if ap_randomizer != os.path.basename(url):
|
||||||
sys.exit(0)
|
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
||||||
|
f"{os.path.basename(url)}")
|
||||||
|
if prompt_yes_no("Would you like to update?"):
|
||||||
|
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
||||||
|
new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url))
|
||||||
|
logging.info("Downloading AP randomizer mod. This may take a moment...")
|
||||||
|
apmod_resp = requests.get(url)
|
||||||
|
if apmod_resp.status_code == 200:
|
||||||
|
with open(new_ap_mod, 'wb') as f:
|
||||||
|
f.write(apmod_resp.content)
|
||||||
|
logging.info(f"Wrote new mod file to {new_ap_mod}")
|
||||||
|
if old_ap_mod is not None:
|
||||||
|
os.remove(old_ap_mod)
|
||||||
|
logging.info(f"Removed old mod file from {old_ap_mod}")
|
||||||
|
else:
|
||||||
|
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||||
|
logging.error(f"Please report this issue on the Archipelago Discord server.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def check_eula(forge_dir):
|
def check_eula(forge_dir):
|
||||||
@@ -264,8 +249,13 @@ def get_minecraft_versions(version, release_channel="release"):
|
|||||||
return next(filter(lambda entry: entry["version"] == version, data[release_channel]))
|
return next(filter(lambda entry: entry["version"] == version, data[release_channel]))
|
||||||
else:
|
else:
|
||||||
return resp.json()[release_channel][0]
|
return resp.json()[release_channel][0]
|
||||||
except StopIteration:
|
except (StopIteration, KeyError):
|
||||||
logging.error(f"No compatible mod version found for client version {version}.")
|
logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.")
|
||||||
|
if release_channel != "release":
|
||||||
|
logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file")
|
||||||
|
else:
|
||||||
|
logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
def is_correct_forge(forge_dir) -> bool:
|
def is_correct_forge(forge_dir) -> bool:
|
||||||
@@ -286,6 +276,8 @@ if __name__ == '__main__':
|
|||||||
help="specify java version.")
|
help="specify java version.")
|
||||||
parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store',
|
parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store',
|
||||||
help="specify forge version. (Minecraft Version-Forge Version)")
|
help="specify forge version. (Minecraft Version-Forge Version)")
|
||||||
|
parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store',
|
||||||
|
help="specify Mod data version to download.")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
||||||
@@ -296,12 +288,12 @@ if __name__ == '__main__':
|
|||||||
options = Utils.get_options()
|
options = Utils.get_options()
|
||||||
channel = args.channel or options["minecraft_options"]["release_channel"]
|
channel = args.channel or options["minecraft_options"]["release_channel"]
|
||||||
apmc_data = None
|
apmc_data = None
|
||||||
data_version = None
|
data_version = args.data_version or None
|
||||||
|
|
||||||
if apmc_file is None and not args.install:
|
if apmc_file is None and not args.install:
|
||||||
apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),))
|
apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),))
|
||||||
|
|
||||||
if apmc_file is not None:
|
if apmc_file is not None and data_version is None:
|
||||||
apmc_data = read_apmc_file(apmc_file)
|
apmc_data = read_apmc_file(apmc_file)
|
||||||
data_version = apmc_data.get('client_version', '')
|
data_version = apmc_data.get('client_version', '')
|
||||||
|
|
||||||
@@ -311,6 +303,7 @@ if __name__ == '__main__':
|
|||||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||||
forge_version = args.forge or versions["forge"]
|
forge_version = args.forge or versions["forge"]
|
||||||
java_version = args.java or versions["java"]
|
java_version = args.java or versions["java"]
|
||||||
|
mod_url = versions["url"]
|
||||||
java_dir = find_jdk_dir(java_version)
|
java_dir = find_jdk_dir(java_version)
|
||||||
|
|
||||||
if args.install:
|
if args.install:
|
||||||
@@ -344,7 +337,7 @@ if __name__ == '__main__':
|
|||||||
if not max_heap_re.match(max_heap):
|
if not max_heap_re.match(max_heap):
|
||||||
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
|
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
|
||||||
|
|
||||||
update_mod(forge_dir, f"MC{forge_version.split('-')[0]}", channel != "release")
|
update_mod(forge_dir, mod_url)
|
||||||
replace_apmc_files(forge_dir, apmc_file)
|
replace_apmc_files(forge_dir, apmc_file)
|
||||||
check_eula(forge_dir)
|
check_eula(forge_dir)
|
||||||
server_process = run_forge_server(forge_dir, java_version, max_heap)
|
server_process = run_forge_server(forge_dir, java_version, max_heap)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import pkg_resources
|
import warnings
|
||||||
|
|
||||||
local_dir = os.path.dirname(__file__)
|
local_dir = os.path.dirname(__file__)
|
||||||
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
|
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
|
||||||
@@ -21,24 +21,58 @@ if not update_ran:
|
|||||||
requirements_files.add(req_file)
|
requirements_files.add(req_file)
|
||||||
|
|
||||||
|
|
||||||
|
def check_pip():
|
||||||
|
# detect if pip is available
|
||||||
|
try:
|
||||||
|
import pip # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
raise RuntimeError("pip not available. Please install pip.")
|
||||||
|
|
||||||
|
|
||||||
|
def confirm(msg: str):
|
||||||
|
try:
|
||||||
|
input(f"\n{msg}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nAborting")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def update_command():
|
def update_command():
|
||||||
|
check_pip()
|
||||||
for file in requirements_files:
|
for file in requirements_files:
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
|
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"])
|
||||||
|
|
||||||
|
|
||||||
|
def install_pkg_resources(yes=False):
|
||||||
|
try:
|
||||||
|
import pkg_resources # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
check_pip()
|
||||||
|
if not yes:
|
||||||
|
confirm("pkg_resources not found, press enter to install it")
|
||||||
|
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
|
||||||
|
|
||||||
|
|
||||||
def update(yes=False, force=False):
|
def update(yes=False, force=False):
|
||||||
global update_ran
|
global update_ran
|
||||||
if not update_ran:
|
if not update_ran:
|
||||||
update_ran = True
|
update_ran = True
|
||||||
|
|
||||||
if force:
|
if force:
|
||||||
update_command()
|
update_command()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
install_pkg_resources(yes=yes)
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
for req_file in requirements_files:
|
for req_file in requirements_files:
|
||||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
path = os.path.join(os.path.dirname(__file__), req_file)
|
path = os.path.join(os.path.dirname(__file__), req_file)
|
||||||
with open(path) as requirementsfile:
|
with open(path) as requirementsfile:
|
||||||
for line in requirementsfile:
|
for line in requirementsfile:
|
||||||
|
if not line or line[0] == "#":
|
||||||
|
continue # ignore comments
|
||||||
if line.startswith(("https://", "git+https://")):
|
if line.startswith(("https://", "git+https://")):
|
||||||
# extract name and version for url
|
# extract name and version for url
|
||||||
rest = line.split('/')[-1]
|
rest = line.split('/')[-1]
|
||||||
@@ -46,8 +80,10 @@ def update(yes=False, force=False):
|
|||||||
if "#egg=" in rest:
|
if "#egg=" in rest:
|
||||||
# from egg info
|
# from egg info
|
||||||
rest, egg = rest.split("#egg=", 1)
|
rest, egg = rest.split("#egg=", 1)
|
||||||
egg = egg.split(";", 1)[0]
|
egg = egg.split(";", 1)[0].rstrip()
|
||||||
if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")):
|
if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")):
|
||||||
|
warnings.warn(f"Specifying version as #egg={egg} will become unavailable in pip 25.0. "
|
||||||
|
"Use name @ url#version instead.", DeprecationWarning)
|
||||||
line = egg
|
line = egg
|
||||||
else:
|
else:
|
||||||
egg = ""
|
egg = ""
|
||||||
@@ -58,16 +94,23 @@ def update(yes=False, force=False):
|
|||||||
rest = rest.replace(".zip", "-").replace(".tar.gz", "-")
|
rest = rest.replace(".zip", "-").replace(".tar.gz", "-")
|
||||||
name, version, _ = rest.split("-", 2)
|
name, version, _ = rest.split("-", 2)
|
||||||
line = f'{egg or name}=={version}'
|
line = f'{egg or name}=={version}'
|
||||||
|
elif "@" in line and "#" in line:
|
||||||
|
# PEP 508 does not allow us to specify a version, so we use custom syntax
|
||||||
|
# name @ url#version ; marker
|
||||||
|
name, rest = line.split("@", 1)
|
||||||
|
version = rest.split("#", 1)[1].split(";", 1)[0].rstrip()
|
||||||
|
line = f"{name.rstrip()}=={version}"
|
||||||
|
if ";" in rest: # keep marker
|
||||||
|
line += rest[rest.find(";"):]
|
||||||
requirements = pkg_resources.parse_requirements(line)
|
requirements = pkg_resources.parse_requirements(line)
|
||||||
for requirement in requirements:
|
for requirement in map(str, requirements):
|
||||||
requirement = str(requirement)
|
|
||||||
try:
|
try:
|
||||||
pkg_resources.require(requirement)
|
pkg_resources.require(requirement)
|
||||||
except pkg_resources.ResolutionError:
|
except pkg_resources.ResolutionError:
|
||||||
if not yes:
|
if not yes:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
confirm(f"Requirement {requirement} is not satisfied, press enter to install it")
|
||||||
update_command()
|
update_command()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
244
MultiServer.py
244
MultiServer.py
@@ -7,17 +7,20 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
import zlib
|
import zlib
|
||||||
import collections
|
import collections
|
||||||
import typing
|
|
||||||
import inspect
|
|
||||||
import weakref
|
|
||||||
import datetime
|
import datetime
|
||||||
import threading
|
import functools
|
||||||
import random
|
|
||||||
import pickle
|
|
||||||
import itertools
|
|
||||||
import time
|
|
||||||
import operator
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import inspect
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import operator
|
||||||
|
import pickle
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import typing
|
||||||
|
import weakref
|
||||||
|
import zlib
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
@@ -41,7 +44,6 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
|
|||||||
SlotType
|
SlotType
|
||||||
|
|
||||||
min_client_version = Version(0, 1, 6)
|
min_client_version = Version(0, 1, 6)
|
||||||
print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
|
|
||||||
@@ -159,11 +161,15 @@ class Context:
|
|||||||
stored_data: typing.Dict[str, object]
|
stored_data: typing.Dict[str, object]
|
||||||
read_data: typing.Dict[str, object]
|
read_data: typing.Dict[str, object]
|
||||||
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
|
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
|
||||||
|
slot_info: typing.Dict[int, NetworkSlot]
|
||||||
|
|
||||||
|
checksums: typing.Dict[str, str]
|
||||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||||
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||||
|
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||||
|
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||||
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
||||||
|
|
||||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||||
@@ -171,7 +177,7 @@ class Context:
|
|||||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||||
log_network: bool = False):
|
log_network: bool = False):
|
||||||
super(Context, self).__init__()
|
super(Context, self).__init__()
|
||||||
self.slot_info: typing.Dict[int, NetworkSlot] = {}
|
self.slot_info = {}
|
||||||
self.log_network = log_network
|
self.log_network = log_network
|
||||||
self.endpoints = []
|
self.endpoints = []
|
||||||
self.clients = {}
|
self.clients = {}
|
||||||
@@ -231,30 +237,38 @@ class Context:
|
|||||||
|
|
||||||
# init empty to satisfy linter, I suppose
|
# init empty to satisfy linter, I suppose
|
||||||
self.gamespackage = {}
|
self.gamespackage = {}
|
||||||
|
self.checksums = {}
|
||||||
self.item_name_groups = {}
|
self.item_name_groups = {}
|
||||||
|
self.location_name_groups = {}
|
||||||
self.all_item_and_group_names = {}
|
self.all_item_and_group_names = {}
|
||||||
|
self.all_location_and_group_names = {}
|
||||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||||
|
|
||||||
self._load_game_data()
|
self._load_game_data()
|
||||||
|
|
||||||
# Datapackage retrieval
|
# Data package retrieval
|
||||||
def _load_game_data(self):
|
def _load_game_data(self):
|
||||||
import worlds
|
import worlds
|
||||||
self.gamespackage = worlds.network_data_package["games"]
|
self.gamespackage = worlds.network_data_package["games"]
|
||||||
|
|
||||||
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
||||||
worlds.AutoWorldRegister.world_types.items()}
|
worlds.AutoWorldRegister.world_types.items()}
|
||||||
|
self.location_name_groups = {world_name: world.location_name_groups for world_name, world in
|
||||||
|
worlds.AutoWorldRegister.world_types.items()}
|
||||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||||
self.non_hintable_names[world_name] = world.hint_blacklist
|
self.non_hintable_names[world_name] = world.hint_blacklist
|
||||||
|
|
||||||
def _init_game_data(self):
|
def _init_game_data(self):
|
||||||
for game_name, game_package in self.gamespackage.items():
|
for game_name, game_package in self.gamespackage.items():
|
||||||
|
self.checksums[game_name] = game_package["checksum"]
|
||||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||||
self.item_names[item_id] = item_name
|
self.item_names[item_id] = item_name
|
||||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||||
self.location_names[location_id] = location_name
|
self.location_names[location_id] = location_name
|
||||||
self.all_item_and_group_names[game_name] = \
|
self.all_item_and_group_names[game_name] = \
|
||||||
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||||
|
self.all_location_and_group_names[game_name] = \
|
||||||
|
set(game_package["location_name_to_id"]) | set(self.location_name_groups[game_name])
|
||||||
|
|
||||||
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||||
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
|
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
|
||||||
@@ -309,6 +323,10 @@ class Context:
|
|||||||
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
|
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
|
||||||
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
|
||||||
|
|
||||||
|
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
|
||||||
|
logging.info("Notice (all): %s" % text)
|
||||||
|
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||||
|
|
||||||
def broadcast_team(self, team: int, msgs: typing.List[dict]):
|
def broadcast_team(self, team: int, msgs: typing.List[dict]):
|
||||||
msgs = self.dumper(msgs)
|
msgs = self.dumper(msgs)
|
||||||
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
|
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
|
||||||
@@ -325,29 +343,19 @@ class Context:
|
|||||||
self.clients[endpoint.team][endpoint.slot].remove(endpoint)
|
self.clients[endpoint.team][endpoint.slot].remove(endpoint)
|
||||||
await on_client_disconnected(self, endpoint)
|
await on_client_disconnected(self, endpoint)
|
||||||
|
|
||||||
# text
|
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
|
||||||
|
|
||||||
def notify_all(self, text: str):
|
|
||||||
logging.info("Notice (all): %s" % text)
|
|
||||||
broadcast_text_all(self, text)
|
|
||||||
|
|
||||||
def notify_client(self, client: Client, text: str):
|
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||||
if client.version >= print_command_compatability_threshold:
|
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
|
||||||
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]}]))
|
|
||||||
else:
|
|
||||||
async_start(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
|
||||||
|
|
||||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
if client.version >= print_command_compatability_threshold:
|
async_start(self.send_msgs(client,
|
||||||
async_start(self.send_msgs(client,
|
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
|
||||||
[{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts]))
|
for text in texts]))
|
||||||
else:
|
|
||||||
async_start(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
|
||||||
|
|
||||||
# loading
|
# loading
|
||||||
|
|
||||||
@@ -365,7 +373,7 @@ class Context:
|
|||||||
with open(multidatapath, 'rb') as f:
|
with open(multidatapath, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
||||||
self._load(self.decompress(data), use_embedded_server_options)
|
self._load(self.decompress(data), {}, use_embedded_server_options)
|
||||||
self.data_filename = multidatapath
|
self.data_filename = multidatapath
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -375,7 +383,8 @@ class Context:
|
|||||||
raise Utils.VersionException("Incompatible multidata.")
|
raise Utils.VersionException("Incompatible multidata.")
|
||||||
return restricted_loads(zlib.decompress(data[1:]))
|
return restricted_loads(zlib.decompress(data[1:]))
|
||||||
|
|
||||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any],
|
||||||
|
use_embedded_server_options: bool):
|
||||||
self.read_data = {}
|
self.read_data = {}
|
||||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||||
if mdata_ver > Utils.version_tuple:
|
if mdata_ver > Utils.version_tuple:
|
||||||
@@ -386,15 +395,23 @@ class Context:
|
|||||||
for player, version in clients_ver.items():
|
for player, version in clients_ver.items():
|
||||||
self.minimum_client_versions[player] = max(Utils.Version(*version), min_client_version)
|
self.minimum_client_versions[player] = max(Utils.Version(*version), min_client_version)
|
||||||
|
|
||||||
self.clients = {}
|
self.slot_info = decoded_obj["slot_info"]
|
||||||
for team, names in enumerate(decoded_obj['names']):
|
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||||
self.clients[team] = {}
|
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
|
||||||
for player, name in enumerate(names, 1):
|
if slot_info.type == SlotType.group}
|
||||||
self.clients[team][player] = []
|
|
||||||
self.player_names[team, player] = name
|
self.clients = {0: {}}
|
||||||
self.player_name_lookup[name] = team, player
|
slot_info: NetworkSlot
|
||||||
self.read_data[f"hints_{team}_{player}"] = lambda local_team=team, local_player=player: \
|
slot_id: int
|
||||||
list(self.get_rechecked_hints(local_team, local_player))
|
|
||||||
|
team_0 = self.clients[0]
|
||||||
|
for slot_id, slot_info in self.slot_info.items():
|
||||||
|
team_0[slot_id] = []
|
||||||
|
self.player_names[0, slot_id] = slot_info.name
|
||||||
|
self.player_name_lookup[slot_info.name] = 0, slot_id
|
||||||
|
self.read_data[f"hints_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \
|
||||||
|
list(self.get_rechecked_hints(local_team, local_player))
|
||||||
|
|
||||||
self.seed_name = decoded_obj["seed_name"]
|
self.seed_name = decoded_obj["seed_name"]
|
||||||
self.random.seed(self.seed_name)
|
self.random.seed(self.seed_name)
|
||||||
self.connect_names = decoded_obj['connect_names']
|
self.connect_names = decoded_obj['connect_names']
|
||||||
@@ -409,29 +426,9 @@ class Context:
|
|||||||
for slot, item_codes in decoded_obj["precollected_items"].items():
|
for slot, item_codes in decoded_obj["precollected_items"].items():
|
||||||
self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
|
||||||
|
|
||||||
for team in range(len(decoded_obj['names'])):
|
for slot, hints in decoded_obj["precollected_hints"].items():
|
||||||
for slot, hints in decoded_obj["precollected_hints"].items():
|
self.hints[0, slot].update(hints)
|
||||||
self.hints[team, slot].update(hints)
|
|
||||||
if "slot_info" in decoded_obj:
|
|
||||||
self.slot_info = decoded_obj["slot_info"]
|
|
||||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
|
||||||
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
|
|
||||||
if slot_info.type == SlotType.group}
|
|
||||||
else:
|
|
||||||
self.games = decoded_obj["games"]
|
|
||||||
self.groups = {}
|
|
||||||
self.slot_info = {
|
|
||||||
slot: NetworkSlot(
|
|
||||||
self.player_names[0, slot],
|
|
||||||
self.games[slot],
|
|
||||||
SlotType(int(bool(locations))))
|
|
||||||
for slot, locations in self.locations.items()
|
|
||||||
}
|
|
||||||
# locations may need converting
|
|
||||||
for slot, locations in self.locations.items():
|
|
||||||
for location, item_data in locations.items():
|
|
||||||
if len(item_data) < 3:
|
|
||||||
locations[location] = (*item_data, 0)
|
|
||||||
# declare slots that aren't players as done
|
# declare slots that aren't players as done
|
||||||
for slot, slot_info in self.slot_info.items():
|
for slot, slot_info in self.slot_info.items():
|
||||||
if slot_info.type.always_goal:
|
if slot_info.type.always_goal:
|
||||||
@@ -442,15 +439,21 @@ class Context:
|
|||||||
server_options = decoded_obj.get("server_options", {})
|
server_options = decoded_obj.get("server_options", {})
|
||||||
self._set_options(server_options)
|
self._set_options(server_options)
|
||||||
|
|
||||||
# custom datapackage
|
# embedded data package
|
||||||
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
||||||
logging.info(f"Loading custom datapackage for game {game_name}")
|
if game_name in game_data_packages:
|
||||||
|
data = game_data_packages[game_name]
|
||||||
|
logging.info(f"Loading embedded data package for game {game_name}")
|
||||||
self.gamespackage[game_name] = data
|
self.gamespackage[game_name] = data
|
||||||
self.item_name_groups[game_name] = data["item_name_groups"]
|
self.item_name_groups[game_name] = data["item_name_groups"]
|
||||||
del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups
|
self.location_name_groups[game_name] = data["location_name_groups"]
|
||||||
|
del data["item_name_groups"] # remove from data package, but keep in self.item_name_groups
|
||||||
|
del data["location_name_groups"]
|
||||||
self._init_game_data()
|
self._init_game_data()
|
||||||
for game_name, data in self.item_name_groups.items():
|
for game_name, data in self.item_name_groups.items():
|
||||||
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
|
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
|
||||||
|
for game_name, data in self.location_name_groups.items():
|
||||||
|
self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame]
|
||||||
|
|
||||||
# saving
|
# saving
|
||||||
|
|
||||||
@@ -594,7 +597,7 @@ class Context:
|
|||||||
|
|
||||||
def get_hint_cost(self, slot):
|
def get_hint_cost(self, slot):
|
||||||
if self.hint_cost:
|
if self.hint_cost:
|
||||||
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
|
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
|
||||||
@@ -685,7 +688,7 @@ class Context:
|
|||||||
def on_goal_achieved(self, client: Client):
|
def on_goal_achieved(self, client: Client):
|
||||||
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
||||||
f' has completed their goal.'
|
f' has completed their goal.'
|
||||||
self.notify_all(finished_msg)
|
self.broadcast_text_all(finished_msg, {"type": "Goal", "team": client.team, "slot": client.slot})
|
||||||
if "auto" in self.collect_mode:
|
if "auto" in self.collect_mode:
|
||||||
collect_player(self, client.team, client.slot)
|
collect_player(self, client.team, client.slot)
|
||||||
if "auto" in self.release_mode:
|
if "auto" in self.release_mode:
|
||||||
@@ -742,10 +745,12 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
NetworkPlayer(team, slot,
|
NetworkPlayer(team, slot,
|
||||||
ctx.name_aliases.get((team, slot), name), name)
|
ctx.name_aliases.get((team, slot), name), name)
|
||||||
)
|
)
|
||||||
|
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
||||||
|
games.add("Archipelago")
|
||||||
await ctx.send_msgs(client, [{
|
await ctx.send_msgs(client, [{
|
||||||
'cmd': 'RoomInfo',
|
'cmd': 'RoomInfo',
|
||||||
'password': bool(ctx.password),
|
'password': bool(ctx.password),
|
||||||
'games': {ctx.games[x] for x in range(1, len(ctx.games) + 1)},
|
'games': games,
|
||||||
# tags are for additional features in the communication.
|
# tags are for additional features in the communication.
|
||||||
# Name them by feature or fork, as you feel is appropriate.
|
# Name them by feature or fork, as you feel is appropriate.
|
||||||
'tags': ctx.tags,
|
'tags': ctx.tags,
|
||||||
@@ -754,7 +759,9 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
'location_check_points': ctx.location_check_points,
|
'location_check_points': ctx.location_check_points,
|
||||||
'datapackage_versions': {game: game_data["version"] for game, game_data
|
'datapackage_versions': {game: game_data["version"] for game, game_data
|
||||||
in ctx.gamespackage.items()},
|
in ctx.gamespackage.items() if game in games},
|
||||||
|
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
|
||||||
|
in ctx.gamespackage.items() if game in games},
|
||||||
'seed_name': ctx.seed_name,
|
'seed_name': ctx.seed_name,
|
||||||
'time': time.time(),
|
'time': time.time(),
|
||||||
}])
|
}])
|
||||||
@@ -775,58 +782,47 @@ async def on_client_disconnected(ctx: Context, client: Client):
|
|||||||
|
|
||||||
|
|
||||||
async def on_client_joined(ctx: Context, client: Client):
|
async def on_client_joined(ctx: Context, client: Client):
|
||||||
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN:
|
||||||
|
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
||||||
version_str = '.'.join(str(x) for x in client.version)
|
version_str = '.'.join(str(x) for x in client.version)
|
||||||
verb = "tracking" if "Tracker" in client.tags else "playing"
|
verb = "tracking" if "Tracker" in client.tags else "playing"
|
||||||
ctx.notify_all(
|
ctx.broadcast_text_all(
|
||||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||||
f"{verb} {ctx.games[client.slot]} has joined. "
|
f"{verb} {ctx.games[client.slot]} has joined. "
|
||||||
f"Client({version_str}), {client.tags}).")
|
f"Client({version_str}), {client.tags}).",
|
||||||
|
{"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags})
|
||||||
ctx.notify_client(client, "Now that you are connected, "
|
ctx.notify_client(client, "Now that you are connected, "
|
||||||
"you can use !help to list commands to run via the server. "
|
"you can use !help to list commands to run via the server. "
|
||||||
"If your client supports it, "
|
"If your client supports it, "
|
||||||
"you may have additional local commands you can list with /help.")
|
"you may have additional local commands you can list with /help.",
|
||||||
|
{"type": "Tutorial"})
|
||||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
async def on_client_left(ctx: Context, client: Client):
|
async def on_client_left(ctx: Context, client: Client):
|
||||||
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
if len(ctx.clients[client.team][client.slot]) < 1:
|
||||||
ctx.notify_all(
|
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
||||||
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
|
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
ctx.broadcast_text_all(
|
||||||
|
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
|
||||||
|
{"type": "Part", "team": client.team, "slot": client.slot})
|
||||||
|
|
||||||
|
|
||||||
async def countdown(ctx: Context, timer: int):
|
async def countdown(ctx: Context, timer: int):
|
||||||
broadcast_countdown(ctx, timer, f"[Server]: Starting countdown of {timer}s")
|
ctx.broadcast_text_all(f"[Server]: Starting countdown of {timer}s", {"type": "Countdown", "countdown": timer})
|
||||||
if ctx.countdown_timer:
|
if ctx.countdown_timer:
|
||||||
ctx.countdown_timer = timer # timer is already running, set it to a different time
|
ctx.countdown_timer = timer # timer is already running, set it to a different time
|
||||||
else:
|
else:
|
||||||
ctx.countdown_timer = timer
|
ctx.countdown_timer = timer
|
||||||
while ctx.countdown_timer > 0:
|
while ctx.countdown_timer > 0:
|
||||||
broadcast_countdown(ctx, ctx.countdown_timer, f"[Server]: {ctx.countdown_timer}")
|
ctx.broadcast_text_all(f"[Server]: {ctx.countdown_timer}",
|
||||||
|
{"type": "Countdown", "countdown": ctx.countdown_timer})
|
||||||
ctx.countdown_timer -= 1
|
ctx.countdown_timer -= 1
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
broadcast_countdown(ctx, 0, f"[Server]: GO")
|
ctx.broadcast_text_all(f"[Server]: GO", {"type": "Countdown", "countdown": 0})
|
||||||
ctx.countdown_timer = 0
|
ctx.countdown_timer = 0
|
||||||
|
|
||||||
|
|
||||||
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
|
|
||||||
old_clients, new_clients = [], []
|
|
||||||
|
|
||||||
for teams in ctx.clients.values():
|
|
||||||
for clients in teams.values():
|
|
||||||
for client in clients:
|
|
||||||
new_clients.append(client) if client.version >= print_command_compatability_threshold \
|
|
||||||
else old_clients.append(client)
|
|
||||||
|
|
||||||
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
|
|
||||||
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
|
||||||
|
|
||||||
|
|
||||||
def broadcast_countdown(ctx: Context, timer: int, message: str):
|
|
||||||
broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer})
|
|
||||||
|
|
||||||
|
|
||||||
def get_players_string(ctx: Context):
|
def get_players_string(ctx: Context):
|
||||||
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
||||||
|
|
||||||
@@ -894,7 +890,9 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
|
|||||||
def release_player(ctx: Context, team: int, slot: int):
|
def release_player(ctx: Context, team: int, slot: int):
|
||||||
"""register any locations that are in the multidata"""
|
"""register any locations that are in the multidata"""
|
||||||
all_locations = set(ctx.locations[slot])
|
all_locations = set(ctx.locations[slot])
|
||||||
ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1))
|
ctx.broadcast_text_all("%s (Team #%d) has released all remaining items from their world."
|
||||||
|
% (ctx.player_names[(team, slot)], team + 1),
|
||||||
|
{"type": "Release", "team": team, "slot": slot})
|
||||||
register_location_checks(ctx, team, slot, all_locations)
|
register_location_checks(ctx, team, slot, all_locations)
|
||||||
update_checked_locations(ctx, team, slot)
|
update_checked_locations(ctx, team, slot)
|
||||||
|
|
||||||
@@ -907,7 +905,9 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
|||||||
if values[1] == slot:
|
if values[1] == slot:
|
||||||
all_locations[source_slot].add(location_id)
|
all_locations[source_slot].add(location_id)
|
||||||
|
|
||||||
ctx.notify_all("%s (Team #%d) has collected their items from other worlds." % (ctx.player_names[(team, slot)], team + 1))
|
ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds."
|
||||||
|
% (ctx.player_names[(team, slot)], team + 1),
|
||||||
|
{"type": "Collect", "team": team, "slot": slot})
|
||||||
for source_player, location_ids in all_locations.items():
|
for source_player, location_ids in all_locations.items():
|
||||||
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
|
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
|
||||||
update_checked_locations(ctx, team, source_player)
|
update_checked_locations(ctx, team, source_player)
|
||||||
@@ -1177,11 +1177,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
def __call__(self, raw: str) -> typing.Optional[bool]:
|
def __call__(self, raw: str) -> typing.Optional[bool]:
|
||||||
if not raw.startswith("!admin"):
|
if not raw.startswith("!admin"):
|
||||||
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw)
|
self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw,
|
||||||
|
{"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": raw})
|
||||||
return super(ClientMessageProcessor, self).__call__(raw)
|
return super(ClientMessageProcessor, self).__call__(raw)
|
||||||
|
|
||||||
def output(self, text):
|
def output(self, text: str):
|
||||||
self.ctx.notify_client(self.client, text)
|
self.ctx.notify_client(self.client, text, {"type": "CommandResult"})
|
||||||
|
|
||||||
|
def output_multiple(self, texts: typing.List[str]):
|
||||||
|
self.ctx.notify_client_multiple(self.client, texts, {"type": "CommandResult"})
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
pass # default is client sending just text
|
pass # default is client sending just text
|
||||||
@@ -1204,9 +1208,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
# disallow others from knowing what the new remote administration password is.
|
# disallow others from knowing what the new remote administration password is.
|
||||||
"!admin /option server_password"):
|
"!admin /option server_password"):
|
||||||
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
|
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
|
||||||
# Otherwise notify the others what is happening.
|
self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output,
|
||||||
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team,
|
{"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": output})
|
||||||
self.client.slot) + ': ' + output)
|
|
||||||
|
|
||||||
if not self.ctx.server_password:
|
if not self.ctx.server_password:
|
||||||
self.output("Sorry, Remote administration is disabled")
|
self.output("Sorry, Remote administration is disabled")
|
||||||
@@ -1243,7 +1246,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
def _cmd_players(self) -> bool:
|
def _cmd_players(self) -> bool:
|
||||||
"""Get information about connected and missing players."""
|
"""Get information about connected and missing players."""
|
||||||
if len(self.ctx.player_names) < 10:
|
if len(self.ctx.player_names) < 10:
|
||||||
self.ctx.notify_all(get_players_string(self.ctx))
|
self.ctx.broadcast_text_all(get_players_string(self.ctx), {"type": "CommandResult"})
|
||||||
else:
|
else:
|
||||||
self.output(get_players_string(self.ctx))
|
self.output(get_players_string(self.ctx))
|
||||||
return True
|
return True
|
||||||
@@ -1332,7 +1335,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if locations:
|
if locations:
|
||||||
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
|
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
|
||||||
texts.append(f"Found {len(locations)} missing location checks")
|
texts.append(f"Found {len(locations)} missing location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.output_multiple(texts)
|
||||||
else:
|
else:
|
||||||
self.output("No missing location checks found.")
|
self.output("No missing location checks found.")
|
||||||
return True
|
return True
|
||||||
@@ -1345,7 +1348,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if locations:
|
if locations:
|
||||||
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
|
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
|
||||||
texts.append(f"Found {len(locations)} done location checks")
|
texts.append(f"Found {len(locations)} done location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.output_multiple(texts)
|
||||||
else:
|
else:
|
||||||
self.output("No done location checks found.")
|
self.output("No done location checks found.")
|
||||||
return True
|
return True
|
||||||
@@ -1381,9 +1384,10 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
new_item = NetworkItem(names[item_name], -1, self.client.slot)
|
new_item = NetworkItem(names[item_name], -1, self.client.slot)
|
||||||
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
|
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
|
||||||
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
||||||
self.ctx.notify_all(
|
self.ctx.broadcast_text_all(
|
||||||
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
|
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
|
||||||
self.client.slot))
|
self.client.slot),
|
||||||
|
{"type": "ItemCheat", "team": self.client.team, "receiving": self.client.slot, "item": new_item})
|
||||||
send_new_items(self.ctx)
|
send_new_items(self.ctx)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -1427,7 +1431,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if game not in self.ctx.all_item_and_group_names:
|
if game not in self.ctx.all_item_and_group_names:
|
||||||
self.output("Can't look up item/location for unknown game. Hint for ID instead.")
|
self.output("Can't look up item/location for unknown game. Hint for ID instead.")
|
||||||
return False
|
return False
|
||||||
names = self.ctx.location_names_for_game(game) \
|
names = self.ctx.all_location_and_group_names[game] \
|
||||||
if for_location else \
|
if for_location else \
|
||||||
self.ctx.all_item_and_group_names[game]
|
self.ctx.all_item_and_group_names[game]
|
||||||
hint_name, usable, response = get_intended_text(input_text, names)
|
hint_name, usable, response = get_intended_text(input_text, names)
|
||||||
@@ -1443,6 +1447,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
||||||
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||||
|
elif hint_name in self.ctx.location_name_groups[game]: # location group name
|
||||||
|
hints = []
|
||||||
|
for loc_name in self.ctx.location_name_groups[game][hint_name]:
|
||||||
|
if loc_name in self.ctx.location_names_for_game(game):
|
||||||
|
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name))
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||||
|
|
||||||
@@ -1682,9 +1691,10 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
client.tags = args["tags"]
|
client.tags = args["tags"]
|
||||||
if set(old_tags) != set(client.tags):
|
if set(old_tags) != set(client.tags):
|
||||||
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
||||||
ctx.notify_all(
|
ctx.broadcast_text_all(
|
||||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
|
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
|
||||||
f"from {old_tags} to {client.tags}.")
|
f"from {old_tags} to {client.tags}.",
|
||||||
|
{"type": "TagsChanged", "team": client.team, "slot": client.slot, "tags": client.tags})
|
||||||
|
|
||||||
elif cmd == 'Sync':
|
elif cmd == 'Sync':
|
||||||
start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory)
|
start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory)
|
||||||
@@ -1802,11 +1812,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
def output(self, text: str):
|
def output(self, text: str):
|
||||||
if self.client:
|
if self.client:
|
||||||
self.ctx.notify_client(self.client, text)
|
self.ctx.notify_client(self.client, text, {"type": "AdminCommandResult"})
|
||||||
super(ServerCommandProcessor, self).output(text)
|
super(ServerCommandProcessor, self).output(text)
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
self.ctx.notify_all('[Server]: ' + raw)
|
self.ctx.broadcast_text_all('[Server]: ' + raw, {"type": "ServerChat", "message": raw})
|
||||||
|
|
||||||
def _cmd_save(self) -> bool:
|
def _cmd_save(self) -> bool:
|
||||||
"""Save current state to multidata"""
|
"""Save current state to multidata"""
|
||||||
@@ -1947,7 +1957,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
send_items_to(self.ctx, team, slot, *new_items)
|
send_items_to(self.ctx, team, slot, *new_items)
|
||||||
|
|
||||||
send_new_items(self.ctx)
|
send_new_items(self.ctx)
|
||||||
self.ctx.notify_all(
|
self.ctx.broadcast_text_all(
|
||||||
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
|
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
|
||||||
f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
|
f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class SlotType(enum.IntFlag):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def always_goal(self) -> bool:
|
def always_goal(self) -> bool:
|
||||||
"""Mark this slot has having reached its goal instantly."""
|
"""Mark this slot as having reached its goal instantly."""
|
||||||
return self.value != 0b01
|
return self.value != 0b01
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ def set_icon(window):
|
|||||||
def adjust(args):
|
def adjust(args):
|
||||||
# Create a fake world and OOTWorld to use as a base
|
# Create a fake world and OOTWorld to use as a base
|
||||||
world = MultiWorld(1)
|
world = MultiWorld(1)
|
||||||
world.slot_seeds = {1: random}
|
world.per_slot_randoms = {1: random}
|
||||||
ootworld = OOTWorld(world, 1)
|
ootworld = OOTWorld(world, 1)
|
||||||
# Set options in the fake OOTWorld
|
# Set options in the fake OOTWorld
|
||||||
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
||||||
|
|||||||
@@ -289,7 +289,10 @@ async def patch_and_run_game(apz5_file):
|
|||||||
decomp_path = base_name + '-decomp.z64'
|
decomp_path = base_name + '-decomp.z64'
|
||||||
comp_path = base_name + '.z64'
|
comp_path = base_name + '.z64'
|
||||||
# Load vanilla ROM, patch file, compress ROM
|
# Load vanilla ROM, patch file, compress ROM
|
||||||
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
|
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
|
||||||
|
if not os.path.exists(rom_file_name):
|
||||||
|
rom_file_name = Utils.user_path(rom_file_name)
|
||||||
|
rom = Rom(rom_file_name)
|
||||||
apply_patch_file(rom, apz5_file,
|
apply_patch_file(rom, apz5_file,
|
||||||
sub_file=(os.path.basename(base_name) + '.zpf'
|
sub_file=(os.path.basename(base_name) + '.zpf'
|
||||||
if zipfile.is_zipfile(apz5_file)
|
if zipfile.is_zipfile(apz5_file)
|
||||||
|
|||||||
79
Options.py
79
Options.py
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import abc
|
import abc
|
||||||
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import math
|
import math
|
||||||
import numbers
|
import numbers
|
||||||
@@ -9,6 +10,10 @@ import random
|
|||||||
from schema import Schema, And, Or, Optional
|
from schema import Schema, And, Or, Optional
|
||||||
from Utils import get_fuzzy_results
|
from Utils import get_fuzzy_results
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from BaseClasses import PlandoOptions
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
|
||||||
|
|
||||||
class AssembleOptions(abc.ABCMeta):
|
class AssembleOptions(abc.ABCMeta):
|
||||||
def __new__(mcs, name, bases, attrs):
|
def __new__(mcs, name, bases, attrs):
|
||||||
@@ -79,9 +84,6 @@ class AssembleOptions(abc.ABCMeta):
|
|||||||
|
|
||||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
@abc.abstractclassmethod
|
|
||||||
def from_any(cls, value: typing.Any) -> "Option[typing.Any]": ...
|
|
||||||
|
|
||||||
|
|
||||||
T = typing.TypeVar('T')
|
T = typing.TypeVar('T')
|
||||||
|
|
||||||
@@ -98,11 +100,11 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
|||||||
supports_weighting = True
|
supports_weighting = True
|
||||||
|
|
||||||
# filled by AssembleOptions:
|
# filled by AssembleOptions:
|
||||||
name_lookup: typing.Dict[int, str]
|
name_lookup: typing.Dict[T, str]
|
||||||
options: typing.Dict[str, int]
|
options: typing.Dict[str, int]
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.__class__.__name__}({self.get_current_option_name()})"
|
return f"{self.__class__.__name__}({self.current_option_name})"
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(self.value)
|
return hash(self.value)
|
||||||
@@ -112,7 +114,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
|||||||
return self.name_lookup[self.value]
|
return self.name_lookup[self.value]
|
||||||
|
|
||||||
def get_current_option_name(self) -> str:
|
def get_current_option_name(self) -> str:
|
||||||
"""For display purposes."""
|
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
|
||||||
|
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
|
||||||
|
f" use current_option_name instead. Worlds should use {self}.current_key"))
|
||||||
|
return self.current_option_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option_name(self) -> str:
|
||||||
|
"""For display purposes. Worlds should be using current_key."""
|
||||||
return self.get_option_name(self.value)
|
return self.get_option_name(self.value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -129,21 +138,19 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
|||||||
return bool(self.value)
|
return bool(self.value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@abc.abstractmethod
|
||||||
def from_any(cls, data: typing.Any) -> Option[T]:
|
def from_any(cls, data: typing.Any) -> Option[T]:
|
||||||
raise NotImplementedError
|
...
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from Generate import PlandoOptions
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||||
from worlds.AutoWorld import World
|
|
||||||
|
|
||||||
def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None:
|
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
def verify(self, *args, **kwargs) -> None:
|
def verify(self, *args, **kwargs) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FreeText(Option):
|
class FreeText(Option[str]):
|
||||||
"""Text option that allows users to enter strings.
|
"""Text option that allows users to enter strings.
|
||||||
Needs to be validated by the world or option definition."""
|
Needs to be validated by the world or option definition."""
|
||||||
|
|
||||||
@@ -164,11 +171,11 @@ class FreeText(Option):
|
|||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_option_name(cls, value: T) -> str:
|
def get_option_name(cls, value: str) -> str:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class NumericOption(Option[int], numbers.Integral):
|
class NumericOption(Option[int], numbers.Integral, abc.ABC):
|
||||||
default = 0
|
default = 0
|
||||||
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
|
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
|
||||||
# `int` is not a `numbers.Integral` according to the official typestubs
|
# `int` is not a `numbers.Integral` according to the official typestubs
|
||||||
@@ -426,6 +433,7 @@ class Choice(NumericOption):
|
|||||||
|
|
||||||
class TextChoice(Choice):
|
class TextChoice(Choice):
|
||||||
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
||||||
|
value: typing.Union[str, int]
|
||||||
|
|
||||||
def __init__(self, value: typing.Union[str, int]):
|
def __init__(self, value: typing.Union[str, int]):
|
||||||
assert isinstance(value, str) or isinstance(value, int), \
|
assert isinstance(value, str) or isinstance(value, int), \
|
||||||
@@ -436,8 +444,7 @@ class TextChoice(Choice):
|
|||||||
def current_key(self) -> str:
|
def current_key(self) -> str:
|
||||||
if isinstance(self.value, str):
|
if isinstance(self.value, str):
|
||||||
return self.value
|
return self.value
|
||||||
else:
|
return super().current_key
|
||||||
return self.name_lookup[self.value]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_text(cls, text: str) -> TextChoice:
|
def from_text(cls, text: str) -> TextChoice:
|
||||||
@@ -452,7 +459,7 @@ class TextChoice(Choice):
|
|||||||
def get_option_name(cls, value: T) -> str:
|
def get_option_name(cls, value: T) -> str:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
return cls.name_lookup[value]
|
return super().get_option_name(value)
|
||||||
|
|
||||||
def __eq__(self, other: typing.Any):
|
def __eq__(self, other: typing.Any):
|
||||||
if isinstance(other, self.__class__):
|
if isinstance(other, self.__class__):
|
||||||
@@ -575,12 +582,11 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
|
|||||||
def valid_location_name(cls, value: str) -> bool:
|
def valid_location_name(cls, value: str) -> bool:
|
||||||
return value in cls.locations
|
return value in cls.locations
|
||||||
|
|
||||||
def verify(self, world, player_name: str, plando_options) -> None:
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||||
if isinstance(self.value, int):
|
if isinstance(self.value, int):
|
||||||
return
|
return
|
||||||
from Generate import PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
if not(PlandoOptions.bosses & plando_options):
|
if not(PlandoOptions.bosses & plando_options):
|
||||||
import logging
|
|
||||||
# plando is disabled but plando options were given so pull the option and change it to an int
|
# plando is disabled but plando options were given so pull the option and change it to an int
|
||||||
option = self.value.split(";")[-1]
|
option = self.value.split(";")[-1]
|
||||||
self.value = self.options[option]
|
self.value = self.options[option]
|
||||||
@@ -718,7 +724,7 @@ class VerifyKeys:
|
|||||||
value: typing.Any
|
value: typing.Any
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def verify_keys(cls, data):
|
def verify_keys(cls, data: typing.List[str]):
|
||||||
if cls.valid_keys:
|
if cls.valid_keys:
|
||||||
data = set(data)
|
data = set(data)
|
||||||
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
||||||
@@ -727,12 +733,17 @@ class VerifyKeys:
|
|||||||
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
||||||
f"Allowed keys: {cls.valid_keys}.")
|
f"Allowed keys: {cls.valid_keys}.")
|
||||||
|
|
||||||
def verify(self, world, player_name: str, plando_options) -> None:
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||||
if self.convert_name_groups and self.verify_item_name:
|
if self.convert_name_groups and self.verify_item_name:
|
||||||
new_value = type(self.value)() # empty container of whatever value is
|
new_value = type(self.value)() # empty container of whatever value is
|
||||||
for item_name in self.value:
|
for item_name in self.value:
|
||||||
new_value |= world.item_name_groups.get(item_name, {item_name})
|
new_value |= world.item_name_groups.get(item_name, {item_name})
|
||||||
self.value = new_value
|
self.value = new_value
|
||||||
|
elif self.convert_name_groups and self.verify_location_name:
|
||||||
|
new_value = type(self.value)()
|
||||||
|
for loc_name in self.value:
|
||||||
|
new_value |= world.location_name_groups.get(loc_name, {loc_name})
|
||||||
|
self.value = new_value
|
||||||
if self.verify_item_name:
|
if self.verify_item_name:
|
||||||
for item_name in self.value:
|
for item_name in self.value:
|
||||||
if item_name not in world.item_names:
|
if item_name not in world.item_names:
|
||||||
@@ -832,7 +843,9 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
|||||||
return item in self.value
|
return item in self.value
|
||||||
|
|
||||||
|
|
||||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
class ItemSet(OptionSet):
|
||||||
|
verify_item_name = True
|
||||||
|
convert_name_groups = True
|
||||||
|
|
||||||
|
|
||||||
class Accessibility(Choice):
|
class Accessibility(Choice):
|
||||||
@@ -868,11 +881,6 @@ common_options = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ItemSet(OptionSet):
|
|
||||||
verify_item_name = True
|
|
||||||
convert_name_groups = True
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
@@ -894,22 +902,23 @@ class StartHints(ItemSet):
|
|||||||
display_name = "Start Hints"
|
display_name = "Start Hints"
|
||||||
|
|
||||||
|
|
||||||
class StartLocationHints(OptionSet):
|
class LocationSet(OptionSet):
|
||||||
|
verify_location_name = True
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
verify_location_name = True
|
|
||||||
|
|
||||||
|
|
||||||
class ExcludeLocations(OptionSet):
|
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"
|
||||||
verify_location_name = True
|
|
||||||
|
|
||||||
|
|
||||||
class PriorityLocations(OptionSet):
|
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"
|
||||||
verify_location_name = True
|
|
||||||
|
|
||||||
|
|
||||||
class DeathLink(Toggle):
|
class DeathLink(Toggle):
|
||||||
@@ -950,7 +959,7 @@ class ItemLinks(OptionList):
|
|||||||
pool |= {item_name}
|
pool |= {item_name}
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
def verify(self, world, player_name: str, plando_options) -> None:
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||||
link: dict
|
link: dict
|
||||||
super(ItemLinks, self).verify(world, player_name, plando_options)
|
super(ItemLinks, self).verify(world, player_name, plando_options)
|
||||||
existing_links = set()
|
existing_links = set()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP
|
|||||||
from worlds.pokemon_rb.locations import location_data
|
from worlds.pokemon_rb.locations import location_data
|
||||||
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
|
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
|
||||||
|
|
||||||
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}}
|
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
|
||||||
location_bytes_bits = {}
|
location_bytes_bits = {}
|
||||||
for location in location_data:
|
for location in location_data:
|
||||||
if location.ram_address is not None:
|
if location.ram_address is not None:
|
||||||
@@ -40,7 +40,7 @@ CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
|||||||
|
|
||||||
DISPLAY_MSGS = True
|
DISPLAY_MSGS = True
|
||||||
|
|
||||||
SCRIPT_VERSION = 1
|
SCRIPT_VERSION = 3
|
||||||
|
|
||||||
|
|
||||||
class GBCommandProcessor(ClientCommandProcessor):
|
class GBCommandProcessor(ClientCommandProcessor):
|
||||||
@@ -70,6 +70,8 @@ class GBContext(CommonContext):
|
|||||||
self.set_deathlink = False
|
self.set_deathlink = False
|
||||||
self.client_compatibility_mode = 0
|
self.client_compatibility_mode = 0
|
||||||
self.items_handling = 0b001
|
self.items_handling = 0b001
|
||||||
|
self.sent_release = False
|
||||||
|
self.sent_collect = False
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool = False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
@@ -124,7 +126,8 @@ def get_payload(ctx: GBContext):
|
|||||||
"items": [item.item for item in ctx.items_received],
|
"items": [item.item for item in ctx.items_received],
|
||||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||||
if key[0] > current_time - 10},
|
if key[0] > current_time - 10},
|
||||||
"deathlink": ctx.deathlink_pending
|
"deathlink": ctx.deathlink_pending,
|
||||||
|
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ctx.deathlink_pending = False
|
ctx.deathlink_pending = False
|
||||||
@@ -134,10 +137,13 @@ def get_payload(ctx: GBContext):
|
|||||||
async def parse_locations(data: List, ctx: GBContext):
|
async def parse_locations(data: List, ctx: GBContext):
|
||||||
locations = []
|
locations = []
|
||||||
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
|
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
|
||||||
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]}
|
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
|
||||||
|
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
|
||||||
|
|
||||||
if len(flags['Rod']) > 1:
|
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
|
||||||
return
|
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
|
||||||
|
else:
|
||||||
|
flags["DexSanityFlag"] = [0] * 19
|
||||||
|
|
||||||
for flag_type, loc_map in location_map.items():
|
for flag_type, loc_map in location_map.items():
|
||||||
for flag, loc_id in loc_map.items():
|
for flag, loc_id in loc_map.items():
|
||||||
@@ -207,6 +213,16 @@ async def gb_sync_task(ctx: GBContext):
|
|||||||
async_start(parse_locations(data_decoded['locations'], ctx))
|
async_start(parse_locations(data_decoded['locations'], ctx))
|
||||||
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
|
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
|
||||||
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
|
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
|
||||||
|
if 'options' in data_decoded:
|
||||||
|
msgs = []
|
||||||
|
if data_decoded['options'] & 4 and not ctx.sent_release:
|
||||||
|
ctx.sent_release = True
|
||||||
|
msgs.append({"cmd": "Say", "text": "!release"})
|
||||||
|
if data_decoded['options'] & 8 and not ctx.sent_collect:
|
||||||
|
ctx.sent_collect = True
|
||||||
|
msgs.append({"cmd": "Say", "text": "!collect"})
|
||||||
|
if msgs:
|
||||||
|
await ctx.send_msgs(msgs)
|
||||||
if ctx.set_deathlink:
|
if ctx.set_deathlink:
|
||||||
await ctx.update_death_link(True)
|
await ctx.update_death_link(True)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ Currently, the following games are supported:
|
|||||||
* Overcooked! 2
|
* Overcooked! 2
|
||||||
* Zillion
|
* Zillion
|
||||||
* Lufia II Ancient Cave
|
* Lufia II Ancient Cave
|
||||||
|
* Blasphemous
|
||||||
|
* Wargroove
|
||||||
|
* Stardew Valley
|
||||||
|
* The Legend of Zelda
|
||||||
|
* The Messenger
|
||||||
|
* Kingdom Hearts 2
|
||||||
|
* The Legend of Zelda: Link's Awakening DX
|
||||||
|
* Clique
|
||||||
|
* Adventure
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|||||||
34
SNIClient.py
34
SNIClient.py
@@ -56,7 +56,9 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
|||||||
"""Connect to a snes. Optionally include network address of a snes to connect to,
|
"""Connect to a snes. Optionally include network address of a snes to connect to,
|
||||||
otherwise show available devices; and a SNES device number if more than one SNES is detected.
|
otherwise show available devices; and a SNES device number if more than one SNES is detected.
|
||||||
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
|
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
|
||||||
|
if self.ctx.snes_state in {SNESState.SNES_ATTACHED, SNESState.SNES_CONNECTED, SNESState.SNES_CONNECTING}:
|
||||||
|
self.output("Already connected to SNES. Disconnecting first.")
|
||||||
|
self._cmd_snes_close()
|
||||||
return self.connect_to_snes(snes_options)
|
return self.connect_to_snes(snes_options)
|
||||||
|
|
||||||
def connect_to_snes(self, snes_options: str = "") -> bool:
|
def connect_to_snes(self, snes_options: str = "") -> bool:
|
||||||
@@ -84,7 +86,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
|||||||
"""Close connection to a currently connected snes"""
|
"""Close connection to a currently connected snes"""
|
||||||
self.ctx.snes_reconnect_address = None
|
self.ctx.snes_reconnect_address = None
|
||||||
self.ctx.cancel_snes_autoreconnect()
|
self.ctx.cancel_snes_autoreconnect()
|
||||||
if self.ctx.snes_socket is not None and not self.ctx.snes_socket.closed:
|
if self.ctx.snes_socket and not self.ctx.snes_socket.closed:
|
||||||
async_start(self.ctx.snes_socket.close())
|
async_start(self.ctx.snes_socket.close())
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -113,8 +115,8 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
|||||||
|
|
||||||
class SNIContext(CommonContext):
|
class SNIContext(CommonContext):
|
||||||
command_processor: typing.Type[SNIClientCommandProcessor] = SNIClientCommandProcessor
|
command_processor: typing.Type[SNIClientCommandProcessor] = SNIClientCommandProcessor
|
||||||
game = None # set in validate_rom
|
game: typing.Optional[str] = None # set in validate_rom
|
||||||
items_handling = None # set in game_watcher
|
items_handling: typing.Optional[int] = None # set in game_watcher
|
||||||
snes_connect_task: "typing.Optional[asyncio.Task[None]]" = None
|
snes_connect_task: "typing.Optional[asyncio.Task[None]]" = None
|
||||||
snes_autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None
|
snes_autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None
|
||||||
|
|
||||||
@@ -442,7 +444,8 @@ async def snes_connect(ctx: SNIContext, address: str, deviceIndex: int = -1) ->
|
|||||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if recv_task is not None:
|
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
||||||
|
if task_alive(recv_task):
|
||||||
if not ctx.snes_socket.closed:
|
if not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
else:
|
else:
|
||||||
@@ -450,15 +453,9 @@ async def snes_connect(ctx: SNIContext, address: str, deviceIndex: int = -1) ->
|
|||||||
if not ctx.snes_socket.closed:
|
if not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
ctx.snes_socket = None
|
ctx.snes_socket = None
|
||||||
ctx.snes_state = SNESState.SNES_DISCONNECTED
|
snes_logger.error(f"Error connecting to snes ({e}), retrying in {_global_snes_reconnect_delay} seconds")
|
||||||
if not ctx.snes_reconnect_address:
|
ctx.snes_autoreconnect_task = asyncio.create_task(snes_autoreconnect(ctx), name="snes auto-reconnect")
|
||||||
snes_logger.error("Error connecting to snes (%s)" % e)
|
|
||||||
else:
|
|
||||||
snes_logger.error(f"Error connecting to snes, retrying in {_global_snes_reconnect_delay} seconds")
|
|
||||||
assert ctx.snes_autoreconnect_task is None
|
|
||||||
ctx.snes_autoreconnect_task = asyncio.create_task(snes_autoreconnect(ctx), name="snes auto-reconnect")
|
|
||||||
_global_snes_reconnect_delay *= 2
|
_global_snes_reconnect_delay *= 2
|
||||||
|
|
||||||
else:
|
else:
|
||||||
_global_snes_reconnect_delay = ctx.starting_reconnect_delay
|
_global_snes_reconnect_delay = ctx.starting_reconnect_delay
|
||||||
snes_logger.info(f"Attached to {device}")
|
snes_logger.info(f"Attached to {device}")
|
||||||
@@ -471,10 +468,17 @@ async def snes_disconnect(ctx: SNIContext) -> None:
|
|||||||
ctx.snes_socket = None
|
ctx.snes_socket = None
|
||||||
|
|
||||||
|
|
||||||
|
def task_alive(task: typing.Optional[asyncio.Task]) -> bool:
|
||||||
|
if task:
|
||||||
|
return not task.done()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def snes_autoreconnect(ctx: SNIContext) -> None:
|
async def snes_autoreconnect(ctx: SNIContext) -> None:
|
||||||
await asyncio.sleep(_global_snes_reconnect_delay)
|
await asyncio.sleep(_global_snes_reconnect_delay)
|
||||||
if ctx.snes_reconnect_address and not ctx.snes_socket and not ctx.snes_connect_task:
|
if not ctx.snes_socket and not task_alive(ctx.snes_connect_task):
|
||||||
ctx.snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_reconnect_address), name="SNES Connect")
|
address = ctx.snes_reconnect_address if ctx.snes_reconnect_address else ctx.snes_address
|
||||||
|
ctx.snes_connect_task = asyncio.create_task(snes_connect(ctx, address), name="SNES Connect")
|
||||||
|
|
||||||
|
|
||||||
async def snes_recv_loop(ctx: SNIContext) -> None:
|
async def snes_recv_loop(ctx: SNIContext) -> None:
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
|||||||
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
|
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
|
||||||
options = difficulty.split()
|
options = difficulty.split()
|
||||||
num_options = len(options)
|
num_options = len(options)
|
||||||
difficulty_choice = options[0].lower()
|
|
||||||
|
|
||||||
if num_options > 0:
|
if num_options > 0:
|
||||||
|
difficulty_choice = options[0].lower()
|
||||||
if difficulty_choice == "casual":
|
if difficulty_choice == "casual":
|
||||||
self.ctx.difficulty_override = 0
|
self.ctx.difficulty_override = 0
|
||||||
elif difficulty_choice == "normal":
|
elif difficulty_choice == "normal":
|
||||||
@@ -71,7 +71,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.output("Difficulty needs to be specified in the command.")
|
if self.ctx.difficulty == -1:
|
||||||
|
self.output("Please connect to a seed before checking difficulty.")
|
||||||
|
else:
|
||||||
|
self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty])
|
||||||
|
self.output("To change the difficulty, add the name of the difficulty after the command.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cmd_disable_mission_check(self) -> bool:
|
def _cmd_disable_mission_check(self) -> bool:
|
||||||
|
|||||||
88
Utils.py
88
Utils.py
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import typing
|
import typing
|
||||||
import builtins
|
import builtins
|
||||||
import os
|
import os
|
||||||
@@ -12,7 +13,7 @@ import io
|
|||||||
import collections
|
import collections
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
from typing import BinaryIO, ClassVar, Coroutine, Optional, Set
|
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||||
|
|
||||||
from yaml import load, load_all, dump, SafeLoader
|
from yaml import load, load_all, dump, SafeLoader
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ class Version(typing.NamedTuple):
|
|||||||
build: int
|
build: int
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.3.8"
|
__version__ = "0.4.0"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -87,7 +88,10 @@ def is_frozen() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def local_path(*path: str) -> str:
|
def local_path(*path: str) -> str:
|
||||||
"""Returns path to a file in the local Archipelago installation or source."""
|
"""
|
||||||
|
Returns path to a file in the local Archipelago installation or source.
|
||||||
|
This might be read-only and user_path should be used instead for ROMs, configuration, etc.
|
||||||
|
"""
|
||||||
if hasattr(local_path, 'cached_path'):
|
if hasattr(local_path, 'cached_path'):
|
||||||
pass
|
pass
|
||||||
elif is_frozen():
|
elif is_frozen():
|
||||||
@@ -142,6 +146,17 @@ def user_path(*path: str) -> str:
|
|||||||
return os.path.join(user_path.cached_path, *path)
|
return os.path.join(user_path.cached_path, *path)
|
||||||
|
|
||||||
|
|
||||||
|
def cache_path(*path: str) -> str:
|
||||||
|
"""Returns path to a file in the user's Archipelago cache directory."""
|
||||||
|
if hasattr(cache_path, "cached_path"):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
import platformdirs
|
||||||
|
cache_path.cached_path = platformdirs.user_cache_dir("Archipelago", False)
|
||||||
|
|
||||||
|
return os.path.join(cache_path.cached_path, *path)
|
||||||
|
|
||||||
|
|
||||||
def output_path(*path: str) -> str:
|
def output_path(*path: str) -> str:
|
||||||
if hasattr(output_path, 'cached_path'):
|
if hasattr(output_path, 'cached_path'):
|
||||||
return os.path.join(output_path.cached_path, *path)
|
return os.path.join(output_path.cached_path, *path)
|
||||||
@@ -195,11 +210,11 @@ def get_public_ipv4() -> str:
|
|||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip()
|
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip()
|
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
pass # we could be offline, in a local game, so no point in erroring out
|
pass # we could be offline, in a local game, so no point in erroring out
|
||||||
@@ -213,7 +228,7 @@ def get_public_ipv6() -> str:
|
|||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip()
|
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||||
@@ -248,6 +263,9 @@ def get_default_options() -> OptionsType:
|
|||||||
"lttp_options": {
|
"lttp_options": {
|
||||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||||
},
|
},
|
||||||
|
"ladx_options": {
|
||||||
|
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
|
||||||
|
},
|
||||||
"server_options": {
|
"server_options": {
|
||||||
"host": None,
|
"host": None,
|
||||||
"port": 38281,
|
"port": 38281,
|
||||||
@@ -310,6 +328,20 @@ def get_default_options() -> OptionsType:
|
|||||||
"lufia2ac_options": {
|
"lufia2ac_options": {
|
||||||
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
||||||
},
|
},
|
||||||
|
"tloz_options": {
|
||||||
|
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
|
||||||
|
"rom_start": True,
|
||||||
|
"display_msgs": True,
|
||||||
|
},
|
||||||
|
"wargroove_options": {
|
||||||
|
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||||
|
},
|
||||||
|
"adventure_options": {
|
||||||
|
"rom_file": "ADVNTURE.BIN",
|
||||||
|
"display_msgs": True,
|
||||||
|
"rom_start": True,
|
||||||
|
"rom_args": ""
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
|
|
||||||
@@ -377,6 +409,45 @@ def persistent_load() -> typing.Dict[str, dict]:
|
|||||||
return storage
|
return storage
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_safe_name(name: str) -> str:
|
||||||
|
return "".join(c for c in name if c not in '<>:"/\\|?*')
|
||||||
|
|
||||||
|
|
||||||
|
def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) -> Dict[str, Any]:
|
||||||
|
if checksum and game:
|
||||||
|
if checksum != get_file_safe_name(checksum):
|
||||||
|
raise ValueError(f"Bad symbols in checksum: {checksum}")
|
||||||
|
path = cache_path("datapackage", get_file_safe_name(game), f"{checksum}.json")
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8-sig") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Could not load data package: {e}")
|
||||||
|
|
||||||
|
# fall back to old cache
|
||||||
|
cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
|
||||||
|
if cache.get("checksum") == checksum:
|
||||||
|
return cache
|
||||||
|
|
||||||
|
# cache does not match
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> None:
|
||||||
|
checksum = data.get("checksum")
|
||||||
|
if checksum and game:
|
||||||
|
if checksum != get_file_safe_name(checksum):
|
||||||
|
raise ValueError(f"Bad symbols in checksum: {checksum}")
|
||||||
|
game_folder = cache_path("datapackage", get_file_safe_name(game))
|
||||||
|
os.makedirs(game_folder, exist_ok=True)
|
||||||
|
try:
|
||||||
|
with open(os.path.join(game_folder, f"{checksum}.json"), "w", encoding="utf-8-sig") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Could not store data package: {e}")
|
||||||
|
|
||||||
|
|
||||||
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
|
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
|
||||||
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
|
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
|
||||||
return adjuster_settings
|
return adjuster_settings
|
||||||
@@ -662,7 +733,10 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
|||||||
|
|
||||||
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
|
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
|
||||||
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||||
def sorter(element: str) -> str:
|
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
||||||
|
if (not isinstance(element, str)):
|
||||||
|
element = element["title"]
|
||||||
|
|
||||||
parts = element.split(maxsplit=1)
|
parts = element.split(maxsplit=1)
|
||||||
if parts[0].lower() in ignore:
|
if parts[0].lower() in ignore:
|
||||||
return parts[1].lower()
|
return parts[1].lower()
|
||||||
|
|||||||
445
WargrooveClient.py
Normal file
445
WargrooveClient.py
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
from typing import Tuple, List, Iterable, Dict
|
||||||
|
|
||||||
|
from worlds.wargroove import WargrooveWorld
|
||||||
|
from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData
|
||||||
|
|
||||||
|
import ModuleUpdate
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
Utils.init_logging("WargrooveClient", exception_logger="Client")
|
||||||
|
|
||||||
|
from NetUtils import NetworkItem, ClientStatus
|
||||||
|
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
|
||||||
|
CommonContext, server_loop
|
||||||
|
|
||||||
|
wg_logger = logging.getLogger("WG")
|
||||||
|
|
||||||
|
|
||||||
|
class WargrooveClientCommandProcessor(ClientCommandProcessor):
|
||||||
|
def _cmd_resync(self):
|
||||||
|
"""Manually trigger a resync."""
|
||||||
|
self.output(f"Syncing items.")
|
||||||
|
self.ctx.syncing = True
|
||||||
|
|
||||||
|
def _cmd_commander(self, *commander_name: Iterable[str]):
|
||||||
|
"""Set the current commander to the given commander."""
|
||||||
|
if commander_name:
|
||||||
|
self.ctx.set_commander(' '.join(commander_name))
|
||||||
|
else:
|
||||||
|
if self.ctx.can_choose_commander:
|
||||||
|
commanders = self.ctx.get_commanders()
|
||||||
|
wg_logger.info('Unlocked commanders: ' +
|
||||||
|
', '.join((commander.name for commander, unlocked in commanders if unlocked)))
|
||||||
|
wg_logger.info('Locked commanders: ' +
|
||||||
|
', '.join((commander.name for commander, unlocked in commanders if not unlocked)))
|
||||||
|
else:
|
||||||
|
wg_logger.error('Cannot set commanders in this game mode.')
|
||||||
|
|
||||||
|
|
||||||
|
class WargrooveContext(CommonContext):
|
||||||
|
command_processor: int = WargrooveClientCommandProcessor
|
||||||
|
game = "Wargroove"
|
||||||
|
items_handling = 0b111 # full remote
|
||||||
|
current_commander: CommanderData = faction_table["Starter"][0]
|
||||||
|
can_choose_commander: bool = False
|
||||||
|
commander_defense_boost_multiplier: int = 0
|
||||||
|
income_boost_multiplier: int = 0
|
||||||
|
starting_groove_multiplier: float
|
||||||
|
faction_item_ids = {
|
||||||
|
'Starter': 0,
|
||||||
|
'Cherrystone': 52025,
|
||||||
|
'Felheim': 52026,
|
||||||
|
'Floran': 52027,
|
||||||
|
'Heavensong': 52028,
|
||||||
|
'Requiem': 52029,
|
||||||
|
'Outlaw': 52030
|
||||||
|
}
|
||||||
|
buff_item_ids = {
|
||||||
|
'Income Boost': 52023,
|
||||||
|
'Commander Defense Boost': 52024,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super(WargrooveContext, self).__init__(server_address, password)
|
||||||
|
self.send_index: int = 0
|
||||||
|
self.syncing = False
|
||||||
|
self.awaiting_bridge = False
|
||||||
|
# self.game_communication_path: files go in this path to pass data between us and the actual game
|
||||||
|
if "appdata" in os.environ:
|
||||||
|
options = Utils.get_options()
|
||||||
|
root_directory = os.path.join(options["wargroove_options"]["root_directory"])
|
||||||
|
data_directory = os.path.join("lib", "worlds", "wargroove", "data")
|
||||||
|
dev_data_directory = os.path.join("worlds", "wargroove", "data")
|
||||||
|
appdata_wargroove = os.path.expandvars(os.path.join("%APPDATA%", "Chucklefish", "Wargroove"))
|
||||||
|
if not os.path.isfile(os.path.join(root_directory, "win64_bin", "wargroove64.exe")):
|
||||||
|
print_error_and_close("WargrooveClient couldn't find wargroove64.exe. "
|
||||||
|
"Unable to infer required game_communication_path")
|
||||||
|
self.game_communication_path = os.path.join(root_directory, "AP")
|
||||||
|
if not os.path.exists(self.game_communication_path):
|
||||||
|
os.makedirs(self.game_communication_path)
|
||||||
|
self.remove_communication_files()
|
||||||
|
atexit.register(self.remove_communication_files)
|
||||||
|
if not os.path.isdir(appdata_wargroove):
|
||||||
|
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
|
||||||
|
"Boot Wargroove and then close it to attempt to fix this error")
|
||||||
|
if not os.path.isdir(data_directory):
|
||||||
|
data_directory = dev_data_directory
|
||||||
|
if not os.path.isdir(data_directory):
|
||||||
|
print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!")
|
||||||
|
shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True)
|
||||||
|
else:
|
||||||
|
print_error_and_close("WargrooveClient couldn't detect system type. "
|
||||||
|
"Unable to infer required game_communication_path")
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super(WargrooveContext, self).server_auth(password_requested)
|
||||||
|
await self.get_username()
|
||||||
|
await self.send_connect()
|
||||||
|
|
||||||
|
async def connection_closed(self):
|
||||||
|
await super(WargrooveContext, self).connection_closed()
|
||||||
|
self.remove_communication_files()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def endpoints(self):
|
||||||
|
if self.server:
|
||||||
|
return [self.server]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
await super(WargrooveContext, self).shutdown()
|
||||||
|
self.remove_communication_files()
|
||||||
|
|
||||||
|
def remove_communication_files(self):
|
||||||
|
for root, dirs, files in os.walk(self.game_communication_path):
|
||||||
|
for file in files:
|
||||||
|
os.remove(root + "/" + file)
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd in {"Connected"}:
|
||||||
|
filename = f"AP_settings.json"
|
||||||
|
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||||
|
slot_data = args["slot_data"]
|
||||||
|
json.dump(args["slot_data"], f)
|
||||||
|
self.can_choose_commander = slot_data["can_choose_commander"]
|
||||||
|
print('can choose commander:', self.can_choose_commander)
|
||||||
|
self.starting_groove_multiplier = slot_data["starting_groove_multiplier"]
|
||||||
|
self.income_boost_multiplier = slot_data["income_boost"]
|
||||||
|
self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"]
|
||||||
|
f.close()
|
||||||
|
for ss in self.checked_locations:
|
||||||
|
filename = f"send{ss}"
|
||||||
|
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||||
|
f.close()
|
||||||
|
self.update_commander_data()
|
||||||
|
self.ui.update_tracker()
|
||||||
|
|
||||||
|
random.seed(self.seed_name + str(self.slot))
|
||||||
|
# Our indexes start at 1 and we have 24 levels
|
||||||
|
for i in range(1, 25):
|
||||||
|
filename = f"seed{i}"
|
||||||
|
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||||
|
f.write(str(random.randint(0, 4294967295)))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
if cmd in {"RoomInfo"}:
|
||||||
|
self.seed_name = args["seed_name"]
|
||||||
|
|
||||||
|
if cmd in {"ReceivedItems"}:
|
||||||
|
received_ids = [item.item for item in self.items_received]
|
||||||
|
for network_item in self.items_received:
|
||||||
|
filename = f"AP_{str(network_item.item)}.item"
|
||||||
|
path = os.path.join(self.game_communication_path, filename)
|
||||||
|
|
||||||
|
# Newly-obtained items
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
open(path, 'w').close()
|
||||||
|
# Announcing commander unlocks
|
||||||
|
item_name = self.item_names[network_item.item]
|
||||||
|
if item_name in faction_table.keys():
|
||||||
|
for commander in faction_table[item_name]:
|
||||||
|
logger.info(f"{commander.name} has been unlocked!")
|
||||||
|
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
item_count = received_ids.count(network_item.item)
|
||||||
|
if self.buff_item_ids["Income Boost"] == network_item.item:
|
||||||
|
f.write(f"{item_count * self.income_boost_multiplier}")
|
||||||
|
elif self.buff_item_ids["Commander Defense Boost"] == network_item.item:
|
||||||
|
f.write(f"{item_count * self.commander_defense_boost_multiplier}")
|
||||||
|
else:
|
||||||
|
f.write(f"{item_count}")
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
print_filename = f"AP_{str(network_item.item)}.item.print"
|
||||||
|
print_path = os.path.join(self.game_communication_path, print_filename)
|
||||||
|
if not os.path.isfile(print_path):
|
||||||
|
open(print_path, 'w').close()
|
||||||
|
with open(print_path, 'w') as f:
|
||||||
|
f.write("Received " +
|
||||||
|
self.item_names[network_item.item] +
|
||||||
|
" from " +
|
||||||
|
self.player_names[network_item.player])
|
||||||
|
f.close()
|
||||||
|
self.update_commander_data()
|
||||||
|
self.ui.update_tracker()
|
||||||
|
|
||||||
|
if cmd in {"RoomUpdate"}:
|
||||||
|
if "checked_locations" in args:
|
||||||
|
for ss in self.checked_locations:
|
||||||
|
filename = f"send{ss}"
|
||||||
|
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
"""Import kivy UI system and start running it as self.ui_task."""
|
||||||
|
from kvui import GameManager, HoverBehavior, ServerToolTip
|
||||||
|
from kivy.uix.tabbedpanel import TabbedPanelItem
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.uix.button import Button
|
||||||
|
from kivy.uix.togglebutton import ToggleButton
|
||||||
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
|
from kivy.uix.gridlayout import GridLayout
|
||||||
|
from kivy.uix.image import AsyncImage, Image
|
||||||
|
from kivy.uix.stacklayout import StackLayout
|
||||||
|
from kivy.uix.label import Label
|
||||||
|
from kivy.properties import ColorProperty
|
||||||
|
from kivy.uix.image import Image
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
class TrackerLayout(BoxLayout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CommanderSelect(BoxLayout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CommanderButton(ToggleButton):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FactionBox(BoxLayout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CommanderGroup(BoxLayout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ItemTracker(BoxLayout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ItemLabel(Label):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WargrooveManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago"),
|
||||||
|
("WG", "WG Console"),
|
||||||
|
]
|
||||||
|
base_title = "Archipelago Wargroove Client"
|
||||||
|
ctx: WargrooveContext
|
||||||
|
unit_tracker: ItemTracker
|
||||||
|
trigger_tracker: BoxLayout
|
||||||
|
boost_tracker: BoxLayout
|
||||||
|
commander_buttons: Dict[int, List[CommanderButton]]
|
||||||
|
tracker_items = {
|
||||||
|
"Swordsman": ItemData(None, "Unit", False),
|
||||||
|
"Dog": ItemData(None, "Unit", False),
|
||||||
|
**item_table
|
||||||
|
}
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
container = super().build()
|
||||||
|
panel = TabbedPanelItem(text="Wargroove")
|
||||||
|
panel.content = self.build_tracker()
|
||||||
|
self.tabs.add_widget(panel)
|
||||||
|
return container
|
||||||
|
|
||||||
|
def build_tracker(self) -> TrackerLayout:
|
||||||
|
try:
|
||||||
|
tracker = TrackerLayout(orientation="horizontal")
|
||||||
|
commander_select = CommanderSelect(orientation="vertical")
|
||||||
|
self.commander_buttons = {}
|
||||||
|
|
||||||
|
for faction, commanders in faction_table.items():
|
||||||
|
faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70)
|
||||||
|
commander_group = CommanderGroup()
|
||||||
|
commander_buttons = []
|
||||||
|
for commander in commanders:
|
||||||
|
commander_button = CommanderButton(text=commander.name, group="commanders")
|
||||||
|
if faction == "Starter":
|
||||||
|
commander_button.disabled = False
|
||||||
|
commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text))
|
||||||
|
commander_buttons.append(commander_button)
|
||||||
|
commander_group.add_widget(commander_button)
|
||||||
|
self.commander_buttons[faction] = commander_buttons
|
||||||
|
faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10))
|
||||||
|
faction_box.add_widget(commander_group)
|
||||||
|
commander_select.add_widget(faction_box)
|
||||||
|
item_tracker = ItemTracker(padding=[0,20])
|
||||||
|
self.unit_tracker = BoxLayout(orientation="vertical")
|
||||||
|
other_tracker = BoxLayout(orientation="vertical")
|
||||||
|
self.trigger_tracker = BoxLayout(orientation="vertical")
|
||||||
|
self.boost_tracker = BoxLayout(orientation="vertical")
|
||||||
|
other_tracker.add_widget(self.trigger_tracker)
|
||||||
|
other_tracker.add_widget(self.boost_tracker)
|
||||||
|
item_tracker.add_widget(self.unit_tracker)
|
||||||
|
item_tracker.add_widget(other_tracker)
|
||||||
|
tracker.add_widget(commander_select)
|
||||||
|
tracker.add_widget(item_tracker)
|
||||||
|
self.update_tracker()
|
||||||
|
return tracker
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
def update_tracker(self):
|
||||||
|
received_ids = [item.item for item in self.ctx.items_received]
|
||||||
|
for faction, item_id in self.ctx.faction_item_ids.items():
|
||||||
|
for commander_button in self.commander_buttons[faction]:
|
||||||
|
commander_button.disabled = not (faction == "Starter" or item_id in received_ids)
|
||||||
|
self.unit_tracker.clear_widgets()
|
||||||
|
self.trigger_tracker.clear_widgets()
|
||||||
|
for name, item in self.tracker_items.items():
|
||||||
|
if item.type in ("Unit", "Trigger"):
|
||||||
|
status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1)
|
||||||
|
label = ItemLabel(text=name, color=status_color)
|
||||||
|
if item.type == "Unit":
|
||||||
|
self.unit_tracker.add_widget(label)
|
||||||
|
else:
|
||||||
|
self.trigger_tracker.add_widget(label)
|
||||||
|
self.boost_tracker.clear_widgets()
|
||||||
|
extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier
|
||||||
|
extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier
|
||||||
|
income_boost = ItemLabel(text="Extra Income: " + str(extra_income))
|
||||||
|
defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense))
|
||||||
|
self.boost_tracker.add_widget(income_boost)
|
||||||
|
self.boost_tracker.add_widget(defense_boost)
|
||||||
|
|
||||||
|
self.ui = WargrooveManager(self)
|
||||||
|
data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode()
|
||||||
|
Builder.load_string(data)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
def update_commander_data(self):
|
||||||
|
if self.can_choose_commander:
|
||||||
|
faction_items = 0
|
||||||
|
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
|
||||||
|
for network_item in self.items_received:
|
||||||
|
if self.item_names[network_item.item] in faction_item_names:
|
||||||
|
faction_items += 1
|
||||||
|
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
|
||||||
|
# Must be an integer larger than 0
|
||||||
|
starting_groove = int(max(starting_groove, 0))
|
||||||
|
data = {
|
||||||
|
"commander": self.current_commander.internal_name,
|
||||||
|
"starting_groove": starting_groove
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"commander": "seed",
|
||||||
|
"starting_groove": 0
|
||||||
|
}
|
||||||
|
filename = 'commander.json'
|
||||||
|
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
if self.ui:
|
||||||
|
self.ui.update_tracker()
|
||||||
|
|
||||||
|
def set_commander(self, commander_name: str) -> bool:
|
||||||
|
"""Sets the current commander to the given one, if possible"""
|
||||||
|
if not self.can_choose_commander:
|
||||||
|
wg_logger.error("Cannot set commanders in this game mode.")
|
||||||
|
return
|
||||||
|
match_name = commander_name.lower()
|
||||||
|
for commander, unlocked in self.get_commanders():
|
||||||
|
if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name:
|
||||||
|
if unlocked:
|
||||||
|
self.current_commander = commander
|
||||||
|
self.syncing = True
|
||||||
|
wg_logger.info(f"Commander set to {commander.name}.")
|
||||||
|
self.update_commander_data()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
wg_logger.error(f"Commander {commander.name} has not been unlocked.")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.")
|
||||||
|
|
||||||
|
def get_commanders(self) -> List[Tuple[CommanderData, bool]]:
|
||||||
|
"""Gets a list of commanders with their unlocked status"""
|
||||||
|
commanders = []
|
||||||
|
received_ids = [item.item for item in self.items_received]
|
||||||
|
for faction in faction_table.keys():
|
||||||
|
unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids
|
||||||
|
commanders += [(commander, unlocked) for commander in faction_table[faction]]
|
||||||
|
return commanders
|
||||||
|
|
||||||
|
|
||||||
|
async def game_watcher(ctx: WargrooveContext):
|
||||||
|
from worlds.wargroove.Locations import location_table
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
if ctx.syncing == True:
|
||||||
|
sync_msg = [{'cmd': 'Sync'}]
|
||||||
|
if ctx.locations_checked:
|
||||||
|
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||||
|
await ctx.send_msgs(sync_msg)
|
||||||
|
ctx.syncing = False
|
||||||
|
sending = []
|
||||||
|
victory = False
|
||||||
|
for root, dirs, files in os.walk(ctx.game_communication_path):
|
||||||
|
for file in files:
|
||||||
|
if file.find("send") > -1:
|
||||||
|
st = file.split("send", -1)[1]
|
||||||
|
sending = sending+[(int(st))]
|
||||||
|
if file.find("victory") > -1:
|
||||||
|
victory = True
|
||||||
|
ctx.locations_checked = sending
|
||||||
|
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
if not ctx.finished_game and victory:
|
||||||
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
|
ctx.finished_game = True
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def print_error_and_close(msg):
|
||||||
|
logger.error("Error: " + msg)
|
||||||
|
Utils.messagebox("Error", msg, error=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
async def main(args):
|
||||||
|
ctx = WargrooveContext(args.connect, args.password)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
progression_watcher = asyncio.create_task(
|
||||||
|
game_watcher(ctx), name="WargrooveProgressionWatcher")
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
|
||||||
|
await progression_watcher
|
||||||
|
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
||||||
|
|
||||||
|
args, rest = parser.parse_known_args()
|
||||||
|
colorama.init()
|
||||||
|
asyncio.run(main(args))
|
||||||
|
colorama.deinit()
|
||||||
@@ -33,6 +33,11 @@ def get_app():
|
|||||||
import yaml
|
import yaml
|
||||||
app.config.from_file(configpath, yaml.safe_load)
|
app.config.from_file(configpath, yaml.safe_load)
|
||||||
logging.info(f"Updated config from {configpath}")
|
logging.info(f"Updated config from {configpath}")
|
||||||
|
if not app.config["HOST_ADDRESS"]:
|
||||||
|
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
|
||||||
|
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||||
|
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
||||||
|
|
||||||
db.bind(**app.config["PONY"])
|
db.bind(**app.config["PONY"])
|
||||||
db.generate_mapping(create_tables=True)
|
db.generate_mapping(create_tables=True)
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ app.config["PONY"] = {
|
|||||||
app.config["MAX_ROLL"] = 20
|
app.config["MAX_ROLL"] = 20
|
||||||
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
|
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
|
||||||
app.config["JSON_AS_ASCII"] = False
|
app.config["JSON_AS_ASCII"] = False
|
||||||
app.config["PATCH_TARGET"] = "archipelago.gg"
|
app.config["HOST_ADDRESS"] = ""
|
||||||
|
|
||||||
cache = Cache(app)
|
cache = Cache(app)
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
|||||||
@@ -39,12 +39,21 @@ def get_datapackage():
|
|||||||
|
|
||||||
@api_endpoints.route('/datapackage_version')
|
@api_endpoints.route('/datapackage_version')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
|
|
||||||
def get_datapackage_versions():
|
def get_datapackage_versions():
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import AutoWorldRegister
|
||||||
|
|
||||||
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
||||||
return version_package
|
return version_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
|
from . import generate, user # trigger registration
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ class MultiworldInstance():
|
|||||||
self.ponyconfig = config["PONY"]
|
self.ponyconfig = config["PONY"]
|
||||||
self.cert = config["SELFLAUNCHCERT"]
|
self.cert = config["SELFLAUNCHCERT"]
|
||||||
self.key = config["SELFLAUNCHKEY"]
|
self.key = config["SELFLAUNCHKEY"]
|
||||||
|
self.host = config["HOST_ADDRESS"]
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.process and self.process.is_alive():
|
if self.process and self.process.is_alive():
|
||||||
@@ -187,7 +188,7 @@ class MultiworldInstance():
|
|||||||
logging.info(f"Spinning up {self.room_id}")
|
logging.info(f"Spinning up {self.room_id}")
|
||||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||||
args=(self.room_id, self.ponyconfig, get_static_server_data(),
|
args=(self.room_id, self.ponyconfig, get_static_server_data(),
|
||||||
self.cert, self.key),
|
self.cert, self.key, self.host),
|
||||||
name="MultiHost")
|
name="MultiHost")
|
||||||
process.start()
|
process.start()
|
||||||
# bind after start to prevent thread sync issues with guardian.
|
# bind after start to prevent thread sync issues with guardian.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import Utils
|
|||||||
|
|
||||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
||||||
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
|
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
|
||||||
from .models import Room, Command, db
|
from .models import Command, GameDataPackage, Room, db
|
||||||
|
|
||||||
|
|
||||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||||
@@ -92,7 +92,20 @@ class WebHostContext(Context):
|
|||||||
else:
|
else:
|
||||||
self.port = get_random_port()
|
self.port = get_random_port()
|
||||||
|
|
||||||
return self._load(self.decompress(room.seed.multidata), True)
|
multidata = self.decompress(room.seed.multidata)
|
||||||
|
game_data_packages = {}
|
||||||
|
for game in list(multidata["datapackage"]):
|
||||||
|
game_data = multidata["datapackage"][game]
|
||||||
|
if "checksum" in game_data:
|
||||||
|
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||||
|
# non-custom. remove from multidata
|
||||||
|
# games package could be dropped from static data once all rooms embed data package
|
||||||
|
del multidata["datapackage"][game]
|
||||||
|
else:
|
||||||
|
data = Utils.restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
|
||||||
|
game_data_packages[game] = data
|
||||||
|
|
||||||
|
return self._load(multidata, game_data_packages, True)
|
||||||
|
|
||||||
@db_session
|
@db_session
|
||||||
def init_save(self, enabled: bool = True):
|
def init_save(self, enabled: bool = True):
|
||||||
@@ -131,6 +144,8 @@ def get_static_server_data() -> dict:
|
|||||||
"gamespackage": worlds.network_data_package["games"],
|
"gamespackage": worlds.network_data_package["games"],
|
||||||
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
|
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
|
||||||
worlds.AutoWorldRegister.world_types.items()},
|
worlds.AutoWorldRegister.world_types.items()},
|
||||||
|
"location_name_groups": {world_name: world.location_name_groups for world_name, world in
|
||||||
|
worlds.AutoWorldRegister.world_types.items()},
|
||||||
}
|
}
|
||||||
|
|
||||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||||
@@ -140,7 +155,8 @@ def get_static_server_data() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]):
|
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||||
|
host: str):
|
||||||
# establish DB connection for multidata and multisave
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
@@ -165,17 +181,18 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
for wssocket in ctx.server.ws_server.sockets:
|
for wssocket in ctx.server.ws_server.sockets:
|
||||||
socketname = wssocket.getsockname()
|
socketname = wssocket.getsockname()
|
||||||
if wssocket.family == socket.AF_INET6:
|
if wssocket.family == socket.AF_INET6:
|
||||||
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
|
|
||||||
# Prefer IPv4, as most users seem to not have working ipv6 support
|
# Prefer IPv4, as most users seem to not have working ipv6 support
|
||||||
if not port:
|
if not port:
|
||||||
port = socketname[1]
|
port = socketname[1]
|
||||||
elif wssocket.family == socket.AF_INET:
|
elif wssocket.family == socket.AF_INET:
|
||||||
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
|
|
||||||
port = socketname[1]
|
port = socketname[1]
|
||||||
if port:
|
if port:
|
||||||
|
logging.info(f'Hosting game at {host}:{port}')
|
||||||
with db_session:
|
with db_session:
|
||||||
room = Room.get(id=ctx.room_id)
|
room = Room.get(id=ctx.room_id)
|
||||||
room.last_port = port
|
room.last_port = port
|
||||||
|
else:
|
||||||
|
logging.exception("Could not determine port. Likely hosting failure.")
|
||||||
with db_session:
|
with db_session:
|
||||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||||
@@ -186,6 +203,11 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
with Locker(room_id):
|
with Locker(room_id):
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
with db_session:
|
||||||
|
room = Room.get(id=room_id)
|
||||||
|
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||||
|
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||||
except:
|
except:
|
||||||
with db_session:
|
with db_session:
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def download_patch(room_id, patch_id):
|
|||||||
with zipfile.ZipFile(filelike, "a") as zf:
|
with zipfile.ZipFile(filelike, "a") as zf:
|
||||||
with zf.open("archipelago.json", "r") as f:
|
with zf.open("archipelago.json", "r") as f:
|
||||||
manifest = json.load(f)
|
manifest = json.load(f)
|
||||||
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None
|
manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None
|
||||||
with zipfile.ZipFile(new_file, "w") as new_zip:
|
with zipfile.ZipFile(new_file, "w") as new_zip:
|
||||||
for file in zf.infolist():
|
for file in zf.infolist():
|
||||||
if file.filename == "archipelago.json":
|
if file.filename == "archipelago.json":
|
||||||
@@ -64,7 +64,7 @@ def download_slot_file(room_id, player_id: int):
|
|||||||
if slot_data.game == "Minecraft":
|
if slot_data.game == "Minecraft":
|
||||||
from worlds.minecraft import mc_update_output
|
from worlds.minecraft import mc_update_output
|
||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||||
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
|
data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
|
||||||
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
|
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
|
||||||
elif slot_data.game == "Factorio":
|
elif slot_data.game == "Factorio":
|
||||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||||
@@ -88,6 +88,8 @@ def download_slot_file(room_id, player_id: int):
|
|||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex"
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex"
|
||||||
elif slot_data.game == "Dark Souls III":
|
elif slot_data.game == "Dark Souls III":
|
||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
||||||
|
elif slot_data.game == "Kingdom Hearts 2":
|
||||||
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
|
||||||
else:
|
else:
|
||||||
return "Game download not supported."
|
return "Game download not supported."
|
||||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
|
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
from typing import List, Dict, 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
|
||||||
@@ -163,8 +164,9 @@ def get_datapackage():
|
|||||||
@app.route('/index')
|
@app.route('/index')
|
||||||
@app.route('/sitemap')
|
@app.route('/sitemap')
|
||||||
def get_sitemap():
|
def get_sitemap():
|
||||||
available_games = []
|
available_games: List[Dict[str, Union[str, bool]]] = []
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
if not world.hidden:
|
if not world.hidden:
|
||||||
available_games.append(game)
|
has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page
|
||||||
|
available_games.append({ 'title': game, 'has_settings': has_settings })
|
||||||
return render_template("siteMap.html", games=available_games)
|
return render_template("siteMap.html", games=available_games)
|
||||||
|
|||||||
@@ -56,3 +56,8 @@ class Generation(db.Entity):
|
|||||||
options = Required(buffer, lazy=True)
|
options = Required(buffer, lazy=True)
|
||||||
meta = Required(LongStr, default=lambda: "{\"race\": false}")
|
meta = Required(LongStr, default=lambda: "{\"race\": false}")
|
||||||
state = Required(int, default=0, index=True)
|
state = Required(int, default=0, index=True)
|
||||||
|
|
||||||
|
|
||||||
|
class GameDataPackage(db.Entity):
|
||||||
|
checksum = PrimaryKey(str)
|
||||||
|
data = Required(bytes)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from Utils import __version__, local_path
|
|||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||||
"exclude_locations"}
|
"exclude_locations", "priority_locations"}
|
||||||
|
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
@@ -88,7 +88,7 @@ def create():
|
|||||||
if option_name in handled_in_js:
|
if option_name in handled_in_js:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif option.options:
|
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
|
||||||
game_options[option_name] = this_option = {
|
game_options[option_name] = this_option = {
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
@@ -98,15 +98,15 @@ def create():
|
|||||||
}
|
}
|
||||||
|
|
||||||
for sub_option_id, sub_option_name in option.name_lookup.items():
|
for sub_option_id, sub_option_name in option.name_lookup.items():
|
||||||
this_option["options"].append({
|
if sub_option_name != "random":
|
||||||
"name": option.get_option_name(sub_option_id),
|
this_option["options"].append({
|
||||||
"value": sub_option_name,
|
"name": option.get_option_name(sub_option_id),
|
||||||
})
|
"value": sub_option_name,
|
||||||
|
})
|
||||||
if sub_option_id == option.default:
|
if sub_option_id == option.default:
|
||||||
this_option["defaultValue"] = sub_option_name
|
this_option["defaultValue"] = sub_option_name
|
||||||
|
|
||||||
if option.default == "random":
|
if not this_option["defaultValue"]:
|
||||||
this_option["defaultValue"] = "random"
|
this_option["defaultValue"] = "random"
|
||||||
|
|
||||||
elif issubclass(option, Options.Range):
|
elif issubclass(option, Options.Range):
|
||||||
@@ -126,21 +126,21 @@ def create():
|
|||||||
for key, val in option.special_range_names.items():
|
for key, val in option.special_range_names.items():
|
||||||
game_options[option_name]["value_names"][key] = val
|
game_options[option_name]["value_names"][key] = val
|
||||||
|
|
||||||
elif getattr(option, "verify_item_name", False):
|
elif issubclass(option, Options.ItemSet):
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
"type": "items-list",
|
"type": "items-list",
|
||||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
"description": get_html_doc(option),
|
"description": get_html_doc(option),
|
||||||
}
|
}
|
||||||
|
|
||||||
elif getattr(option, "verify_location_name", False):
|
elif issubclass(option, Options.LocationSet):
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
"type": "locations-list",
|
"type": "locations-list",
|
||||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
"description": get_html_doc(option),
|
"description": get_html_doc(option),
|
||||||
}
|
}
|
||||||
|
|
||||||
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
|
elif issubclass(option, Options.VerifyKeys):
|
||||||
if option.valid_keys:
|
if option.valid_keys:
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
"type": "custom-list",
|
"type": "custom-list",
|
||||||
@@ -160,6 +160,14 @@ def create():
|
|||||||
json.dump(player_settings, f, indent=2, separators=(',', ': '))
|
json.dump(player_settings, f, indent=2, separators=(',', ': '))
|
||||||
|
|
||||||
if not world.hidden and world.web.settings_page is True:
|
if not world.hidden and world.web.settings_page is True:
|
||||||
|
# Add the random option to Choice, TextChoice, and Toggle settings
|
||||||
|
for option in game_options.values():
|
||||||
|
if option["type"] == "select":
|
||||||
|
option["options"].append({"name": "Random", "value": "random"})
|
||||||
|
|
||||||
|
if not option["defaultValue"]:
|
||||||
|
option["defaultValue"] = "random"
|
||||||
|
|
||||||
weighted_settings["baseOptions"]["game"][game_name] = 0
|
weighted_settings["baseOptions"]["game"][game_name] = 0
|
||||||
weighted_settings["games"][game_name] = {}
|
weighted_settings["games"][game_name] = {}
|
||||||
weighted_settings["games"][game_name]["gameSettings"] = game_options
|
weighted_settings["games"][game_name]["gameSettings"] = game_options
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
flask>=2.2.2
|
flask>=2.2.3
|
||||||
pony>=0.7.16
|
pony>=0.7.16
|
||||||
waitress>=2.1.2
|
waitress>=2.1.2
|
||||||
Flask-Caching>=2.0.1
|
Flask-Caching>=2.0.2
|
||||||
Flask-Compress>=1.13
|
Flask-Compress>=1.13
|
||||||
Flask-Limiter>=2.8.1
|
Flask-Limiter>=3.3.0
|
||||||
bokeh>=3.0.2
|
bokeh>=3.1.0
|
||||||
|
|||||||
40
WebHostLib/static/assets/baseHeader.js
Normal file
40
WebHostLib/static/assets/baseHeader.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Mobile menu handling
|
||||||
|
const menuButton = document.getElementById('base-header-mobile-menu-button');
|
||||||
|
const mobileMenu = document.getElementById('base-header-mobile-menu');
|
||||||
|
|
||||||
|
menuButton.addEventListener('click', (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
if (!mobileMenu.style.display || mobileMenu.style.display === 'none') {
|
||||||
|
return mobileMenu.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileMenu.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
mobileMenu.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Popover handling
|
||||||
|
const popoverText = document.getElementById('base-header-popover-text');
|
||||||
|
const popoverMenu = document.getElementById('base-header-popover-menu');
|
||||||
|
|
||||||
|
popoverText.addEventListener('click', (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
if (!popoverMenu.style.display || popoverMenu.style.display === 'none') {
|
||||||
|
return popoverMenu.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
popoverMenu.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('click', () => {
|
||||||
|
mobileMenu.style.display = 'none';
|
||||||
|
popoverMenu.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
49
WebHostLib/static/assets/checksfinderTracker.js
Normal file
49
WebHostLib/static/assets/checksfinderTracker.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Reload tracker every 60 seconds
|
||||||
|
const url = window.location;
|
||||||
|
setInterval(() => {
|
||||||
|
const ajax = new XMLHttpRequest();
|
||||||
|
ajax.onreadystatechange = () => {
|
||||||
|
if (ajax.readyState !== 4) { return; }
|
||||||
|
|
||||||
|
// Create a fake DOM using the returned HTML
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||||
|
|
||||||
|
// Update item tracker
|
||||||
|
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||||
|
// Update only counters in the location-table
|
||||||
|
let counters = document.getElementsByClassName('counter');
|
||||||
|
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||||
|
for (let i = 0; i < counters.length; i++) {
|
||||||
|
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ajax.open('GET', url);
|
||||||
|
ajax.send();
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
|
// Collapsible advancement sections
|
||||||
|
const categories = document.getElementsByClassName("location-category");
|
||||||
|
for (let i = 0; i < categories.length; i++) {
|
||||||
|
let hide_id = categories[i].id.split('-')[0];
|
||||||
|
if (hide_id == 'Total') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
categories[i].addEventListener('click', function() {
|
||||||
|
// Toggle the advancement list
|
||||||
|
document.getElementById(hide_id).classList.toggle("hide");
|
||||||
|
// Change text of the header
|
||||||
|
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||||
|
const orig_text = tab_header.innerHTML;
|
||||||
|
let new_text;
|
||||||
|
if (orig_text.includes("▼")) {
|
||||||
|
new_text = orig_text.replace("▼", "▲");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new_text = orig_text.replace("▲", "▼");
|
||||||
|
}
|
||||||
|
tab_header.innerHTML = new_text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
6
WebHostLib/static/assets/lttpMultiTracker.js
Normal file
6
WebHostLib/static/assets/lttpMultiTracker.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
$(".table-wrapper").scrollsync({
|
||||||
|
y_sync: true,
|
||||||
|
x_sync: true
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
const adjustTableHeight = () => {
|
const adjustTableHeight = () => {
|
||||||
const tablesContainer = document.getElementById('tables-container');
|
const tablesContainer = document.getElementById('tables-container');
|
||||||
|
if (!tablesContainer)
|
||||||
|
return;
|
||||||
const upperDistance = tablesContainer.getBoundingClientRect().top;
|
const upperDistance = tablesContainer.getBoundingClientRect().top;
|
||||||
|
|
||||||
const containerHeight = window.innerHeight - upperDistance;
|
const containerHeight = window.innerHeight - upperDistance;
|
||||||
@@ -18,7 +20,8 @@ window.addEventListener('load', () => {
|
|||||||
info: false,
|
info: false,
|
||||||
dom: "t",
|
dom: "t",
|
||||||
stateSave: true,
|
stateSave: true,
|
||||||
stateSaveCallback: function(settings,data) {
|
stateSaveCallback: function(settings, data) {
|
||||||
|
delete data.search;
|
||||||
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
|
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
|
||||||
},
|
},
|
||||||
stateLoadCallback: function(settings) {
|
stateLoadCallback: function(settings) {
|
||||||
@@ -70,10 +73,30 @@ window.addEventListener('load', () => {
|
|||||||
// the tbody and render two separate tables.
|
// the tbody and render two separate tables.
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('search').addEventListener('keyup', (event) => {
|
const searchBox = document.getElementById("search");
|
||||||
tables.search(event.target.value);
|
searchBox.value = tables.search();
|
||||||
console.info(tables.search());
|
searchBox.focus();
|
||||||
|
searchBox.select();
|
||||||
|
const doSearch = () => {
|
||||||
|
tables.search(searchBox.value);
|
||||||
tables.draw();
|
tables.draw();
|
||||||
|
};
|
||||||
|
searchBox.addEventListener("keyup", doSearch);
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
if (!event.ctrlKey && !event.altKey && event.key.length === 1 && document.activeElement !== searchBox) {
|
||||||
|
searchBox.focus();
|
||||||
|
searchBox.select();
|
||||||
|
}
|
||||||
|
if (!event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "Escape") {
|
||||||
|
if (searchBox.value !== "") {
|
||||||
|
searchBox.value = "";
|
||||||
|
doSearch();
|
||||||
|
}
|
||||||
|
searchBox.blur();
|
||||||
|
if (!document.getElementById("tables-container"))
|
||||||
|
window.scroll(0, 0);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
|
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
|
||||||
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
|
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
|
||||||
@@ -87,7 +110,7 @@ window.addEventListener('load', () => {
|
|||||||
const update = () => {
|
const update = () => {
|
||||||
const target = $("<div></div>");
|
const target = $("<div></div>");
|
||||||
console.log("Updating Tracker...");
|
console.log("Updating Tracker...");
|
||||||
target.load("/tracker/" + tracker, function (response, status) {
|
target.load(location.href, function (response, status) {
|
||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
target.find(".table").each(function (i, new_table) {
|
target.find(".table").each(function (i, new_table) {
|
||||||
const new_trs = $(new_table).find("tbody>tr");
|
const new_trs = $(new_table).find("tbody>tr");
|
||||||
@@ -114,10 +137,5 @@ window.addEventListener('load', () => {
|
|||||||
tables.draw();
|
tables.draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".table-wrapper").scrollsync({
|
|
||||||
y_sync: true,
|
|
||||||
x_sync: true
|
|
||||||
});
|
|
||||||
|
|
||||||
adjustTableHeight();
|
adjustTableHeight();
|
||||||
});
|
});
|
||||||
@@ -78,8 +78,6 @@ const createDefaultSettings = (settingData) => {
|
|||||||
break;
|
break;
|
||||||
case 'range':
|
case 'range':
|
||||||
case 'special_range':
|
case 'special_range':
|
||||||
newSettings[game][gameSetting][setting.min] = 0;
|
|
||||||
newSettings[game][gameSetting][setting.max] = 0;
|
|
||||||
newSettings[game][gameSetting]['random'] = 0;
|
newSettings[game][gameSetting]['random'] = 0;
|
||||||
newSettings[game][gameSetting]['random-low'] = 0;
|
newSettings[game][gameSetting]['random-low'] = 0;
|
||||||
newSettings[game][gameSetting]['random-high'] = 0;
|
newSettings[game][gameSetting]['random-high'] = 0;
|
||||||
@@ -103,6 +101,7 @@ const createDefaultSettings = (settingData) => {
|
|||||||
|
|
||||||
newSettings[game].start_inventory = {};
|
newSettings[game].start_inventory = {};
|
||||||
newSettings[game].exclude_locations = [];
|
newSettings[game].exclude_locations = [];
|
||||||
|
newSettings[game].priority_locations = [];
|
||||||
newSettings[game].local_items = [];
|
newSettings[game].local_items = [];
|
||||||
newSettings[game].non_local_items = [];
|
newSettings[game].non_local_items = [];
|
||||||
newSettings[game].start_hints = [];
|
newSettings[game].start_hints = [];
|
||||||
@@ -138,21 +137,28 @@ const buildUI = (settingData) => {
|
|||||||
expandButton.classList.add('invisible');
|
expandButton.classList.add('invisible');
|
||||||
gameDiv.appendChild(expandButton);
|
gameDiv.appendChild(expandButton);
|
||||||
|
|
||||||
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings);
|
settingData.games[game].gameItems.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
|
||||||
|
settingData.games[game].gameLocations.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
|
||||||
|
|
||||||
|
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings,
|
||||||
|
settingData.games[game].gameItems, settingData.games[game].gameLocations);
|
||||||
gameDiv.appendChild(weightedSettingsDiv);
|
gameDiv.appendChild(weightedSettingsDiv);
|
||||||
|
|
||||||
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
||||||
gameDiv.appendChild(itemsDiv);
|
gameDiv.appendChild(itemPoolDiv);
|
||||||
|
|
||||||
const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
|
const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
|
||||||
gameDiv.appendChild(hintsDiv);
|
gameDiv.appendChild(hintsDiv);
|
||||||
|
|
||||||
|
const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations);
|
||||||
|
gameDiv.appendChild(locationsDiv);
|
||||||
|
|
||||||
gamesWrapper.appendChild(gameDiv);
|
gamesWrapper.appendChild(gameDiv);
|
||||||
|
|
||||||
collapseButton.addEventListener('click', () => {
|
collapseButton.addEventListener('click', () => {
|
||||||
collapseButton.classList.add('invisible');
|
collapseButton.classList.add('invisible');
|
||||||
weightedSettingsDiv.classList.add('invisible');
|
weightedSettingsDiv.classList.add('invisible');
|
||||||
itemsDiv.classList.add('invisible');
|
itemPoolDiv.classList.add('invisible');
|
||||||
hintsDiv.classList.add('invisible');
|
hintsDiv.classList.add('invisible');
|
||||||
expandButton.classList.remove('invisible');
|
expandButton.classList.remove('invisible');
|
||||||
});
|
});
|
||||||
@@ -160,7 +166,7 @@ const buildUI = (settingData) => {
|
|||||||
expandButton.addEventListener('click', () => {
|
expandButton.addEventListener('click', () => {
|
||||||
collapseButton.classList.remove('invisible');
|
collapseButton.classList.remove('invisible');
|
||||||
weightedSettingsDiv.classList.remove('invisible');
|
weightedSettingsDiv.classList.remove('invisible');
|
||||||
itemsDiv.classList.remove('invisible');
|
itemPoolDiv.classList.remove('invisible');
|
||||||
hintsDiv.classList.remove('invisible');
|
hintsDiv.classList.remove('invisible');
|
||||||
expandButton.classList.add('invisible');
|
expandButton.classList.add('invisible');
|
||||||
});
|
});
|
||||||
@@ -228,7 +234,7 @@ const buildGameChoice = (games) => {
|
|||||||
gameChoiceDiv.appendChild(table);
|
gameChoiceDiv.appendChild(table);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildWeightedSettingsDiv = (game, settings) => {
|
const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
const settingsWrapper = document.createElement('div');
|
const settingsWrapper = document.createElement('div');
|
||||||
settingsWrapper.classList.add('settings-wrapper');
|
settingsWrapper.classList.add('settings-wrapper');
|
||||||
@@ -270,7 +276,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
|||||||
range.setAttribute('data-type', setting.type);
|
range.setAttribute('data-type', setting.type);
|
||||||
range.setAttribute('min', 0);
|
range.setAttribute('min', 0);
|
||||||
range.setAttribute('max', 50);
|
range.setAttribute('max', 50);
|
||||||
range.addEventListener('change', updateGameSetting);
|
range.addEventListener('change', updateRangeSetting);
|
||||||
range.value = currentSettings[game][settingName][option.value];
|
range.value = currentSettings[game][settingName][option.value];
|
||||||
tdMiddle.appendChild(range);
|
tdMiddle.appendChild(range);
|
||||||
tr.appendChild(tdMiddle);
|
tr.appendChild(tdMiddle);
|
||||||
@@ -296,33 +302,33 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
|||||||
if (((setting.max - setting.min) + 1) < 11) {
|
if (((setting.max - setting.min) + 1) < 11) {
|
||||||
for (let i=setting.min; i <= setting.max; ++i) {
|
for (let i=setting.min; i <= setting.max; ++i) {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
const tdLeft = document.createElement('td');
|
const tdLeft = document.createElement('td');
|
||||||
tdLeft.classList.add('td-left');
|
tdLeft.classList.add('td-left');
|
||||||
tdLeft.innerText = i;
|
tdLeft.innerText = i;
|
||||||
tr.appendChild(tdLeft);
|
tr.appendChild(tdLeft);
|
||||||
|
|
||||||
const tdMiddle = document.createElement('td');
|
const tdMiddle = document.createElement('td');
|
||||||
tdMiddle.classList.add('td-middle');
|
tdMiddle.classList.add('td-middle');
|
||||||
const range = document.createElement('input');
|
const range = document.createElement('input');
|
||||||
range.setAttribute('type', 'range');
|
range.setAttribute('type', 'range');
|
||||||
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
|
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
|
||||||
range.setAttribute('data-game', game);
|
range.setAttribute('data-game', game);
|
||||||
range.setAttribute('data-setting', settingName);
|
range.setAttribute('data-setting', settingName);
|
||||||
range.setAttribute('data-option', i);
|
range.setAttribute('data-option', i);
|
||||||
range.setAttribute('min', 0);
|
range.setAttribute('min', 0);
|
||||||
range.setAttribute('max', 50);
|
range.setAttribute('max', 50);
|
||||||
range.addEventListener('change', updateGameSetting);
|
range.addEventListener('change', updateRangeSetting);
|
||||||
range.value = currentSettings[game][settingName][i];
|
range.value = currentSettings[game][settingName][i] || 0;
|
||||||
tdMiddle.appendChild(range);
|
tdMiddle.appendChild(range);
|
||||||
tr.appendChild(tdMiddle);
|
tr.appendChild(tdMiddle);
|
||||||
|
|
||||||
const tdRight = document.createElement('td');
|
const tdRight = document.createElement('td');
|
||||||
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
|
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
|
||||||
tdRight.classList.add('td-right');
|
tdRight.classList.add('td-right');
|
||||||
tdRight.innerText = range.value;
|
tdRight.innerText = range.value;
|
||||||
tr.appendChild(tdRight);
|
tr.appendChild(tdRight);
|
||||||
|
|
||||||
rangeTbody.appendChild(tr);
|
rangeTbody.appendChild(tr);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const hintText = document.createElement('p');
|
const hintText = document.createElement('p');
|
||||||
@@ -379,7 +385,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
|||||||
range.setAttribute('data-option', option);
|
range.setAttribute('data-option', option);
|
||||||
range.setAttribute('min', 0);
|
range.setAttribute('min', 0);
|
||||||
range.setAttribute('max', 50);
|
range.setAttribute('max', 50);
|
||||||
range.addEventListener('change', updateGameSetting);
|
range.addEventListener('change', updateRangeSetting);
|
||||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||||
tdMiddle.appendChild(range);
|
tdMiddle.appendChild(range);
|
||||||
tr.appendChild(tdMiddle);
|
tr.appendChild(tdMiddle);
|
||||||
@@ -430,7 +436,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
|||||||
range.setAttribute('data-option', option);
|
range.setAttribute('data-option', option);
|
||||||
range.setAttribute('min', 0);
|
range.setAttribute('min', 0);
|
||||||
range.setAttribute('max', 50);
|
range.setAttribute('max', 50);
|
||||||
range.addEventListener('change', updateGameSetting);
|
range.addEventListener('change', updateRangeSetting);
|
||||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||||
tdMiddle.appendChild(range);
|
tdMiddle.appendChild(range);
|
||||||
tr.appendChild(tdMiddle);
|
tr.appendChild(tdMiddle);
|
||||||
@@ -464,7 +470,17 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
|||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
const tdLeft = document.createElement('td');
|
const tdLeft = document.createElement('td');
|
||||||
tdLeft.classList.add('td-left');
|
tdLeft.classList.add('td-left');
|
||||||
tdLeft.innerText = option;
|
switch(option){
|
||||||
|
case 'random':
|
||||||
|
tdLeft.innerText = 'Random';
|
||||||
|
break;
|
||||||
|
case 'random-low':
|
||||||
|
tdLeft.innerText = "Random (Low)";
|
||||||
|
break;
|
||||||
|
case 'random-high':
|
||||||
|
tdLeft.innerText = "Random (High)";
|
||||||
|
break;
|
||||||
|
}
|
||||||
tr.appendChild(tdLeft);
|
tr.appendChild(tdLeft);
|
||||||
|
|
||||||
const tdMiddle = document.createElement('td');
|
const tdMiddle = document.createElement('td');
|
||||||
@@ -477,7 +493,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
|||||||
range.setAttribute('data-option', option);
|
range.setAttribute('data-option', option);
|
||||||
range.setAttribute('min', 0);
|
range.setAttribute('min', 0);
|
||||||
range.setAttribute('max', 50);
|
range.setAttribute('max', 50);
|
||||||
range.addEventListener('change', updateGameSetting);
|
range.addEventListener('change', updateRangeSetting);
|
||||||
range.value = currentSettings[game][settingName][option];
|
range.value = currentSettings[game][settingName][option];
|
||||||
tdMiddle.appendChild(range);
|
tdMiddle.appendChild(range);
|
||||||
tr.appendChild(tdMiddle);
|
tr.appendChild(tdMiddle);
|
||||||
@@ -495,15 +511,108 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'items-list':
|
case 'items-list':
|
||||||
// TODO
|
const itemsList = document.createElement('div');
|
||||||
|
itemsList.classList.add('simple-list');
|
||||||
|
|
||||||
|
Object.values(gameItems).forEach((item) => {
|
||||||
|
const itemRow = document.createElement('div');
|
||||||
|
itemRow.classList.add('list-row');
|
||||||
|
|
||||||
|
const itemLabel = document.createElement('label');
|
||||||
|
itemLabel.setAttribute('for', `${game}-${settingName}-${item}`)
|
||||||
|
|
||||||
|
const itemCheckbox = document.createElement('input');
|
||||||
|
itemCheckbox.setAttribute('id', `${game}-${settingName}-${item}`);
|
||||||
|
itemCheckbox.setAttribute('type', 'checkbox');
|
||||||
|
itemCheckbox.setAttribute('data-game', game);
|
||||||
|
itemCheckbox.setAttribute('data-setting', settingName);
|
||||||
|
itemCheckbox.setAttribute('data-option', item.toString());
|
||||||
|
itemCheckbox.addEventListener('change', updateListSetting);
|
||||||
|
if (currentSettings[game][settingName].includes(item)) {
|
||||||
|
itemCheckbox.setAttribute('checked', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemName = document.createElement('span');
|
||||||
|
itemName.innerText = item.toString();
|
||||||
|
|
||||||
|
itemLabel.appendChild(itemCheckbox);
|
||||||
|
itemLabel.appendChild(itemName);
|
||||||
|
|
||||||
|
itemRow.appendChild(itemLabel);
|
||||||
|
itemsList.appendChild((itemRow));
|
||||||
|
});
|
||||||
|
|
||||||
|
settingWrapper.appendChild(itemsList);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'locations-list':
|
case 'locations-list':
|
||||||
// TODO
|
const locationsList = document.createElement('div');
|
||||||
|
locationsList.classList.add('simple-list');
|
||||||
|
|
||||||
|
Object.values(gameLocations).forEach((location) => {
|
||||||
|
const locationRow = document.createElement('div');
|
||||||
|
locationRow.classList.add('list-row');
|
||||||
|
|
||||||
|
const locationLabel = document.createElement('label');
|
||||||
|
locationLabel.setAttribute('for', `${game}-${settingName}-${location}`)
|
||||||
|
|
||||||
|
const locationCheckbox = document.createElement('input');
|
||||||
|
locationCheckbox.setAttribute('id', `${game}-${settingName}-${location}`);
|
||||||
|
locationCheckbox.setAttribute('type', 'checkbox');
|
||||||
|
locationCheckbox.setAttribute('data-game', game);
|
||||||
|
locationCheckbox.setAttribute('data-setting', settingName);
|
||||||
|
locationCheckbox.setAttribute('data-option', location.toString());
|
||||||
|
locationCheckbox.addEventListener('change', updateListSetting);
|
||||||
|
if (currentSettings[game][settingName].includes(location)) {
|
||||||
|
locationCheckbox.setAttribute('checked', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationName = document.createElement('span');
|
||||||
|
locationName.innerText = location.toString();
|
||||||
|
|
||||||
|
locationLabel.appendChild(locationCheckbox);
|
||||||
|
locationLabel.appendChild(locationName);
|
||||||
|
|
||||||
|
locationRow.appendChild(locationLabel);
|
||||||
|
locationsList.appendChild((locationRow));
|
||||||
|
});
|
||||||
|
|
||||||
|
settingWrapper.appendChild(locationsList);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'custom-list':
|
case 'custom-list':
|
||||||
// TODO
|
const customList = document.createElement('div');
|
||||||
|
customList.classList.add('simple-list');
|
||||||
|
|
||||||
|
Object.values(settings[settingName].options).forEach((listItem) => {
|
||||||
|
const customListRow = document.createElement('div');
|
||||||
|
customListRow.classList.add('list-row');
|
||||||
|
|
||||||
|
const customItemLabel = document.createElement('label');
|
||||||
|
customItemLabel.setAttribute('for', `${game}-${settingName}-${listItem}`)
|
||||||
|
|
||||||
|
const customItemCheckbox = document.createElement('input');
|
||||||
|
customItemCheckbox.setAttribute('id', `${game}-${settingName}-${listItem}`);
|
||||||
|
customItemCheckbox.setAttribute('type', 'checkbox');
|
||||||
|
customItemCheckbox.setAttribute('data-game', game);
|
||||||
|
customItemCheckbox.setAttribute('data-setting', settingName);
|
||||||
|
customItemCheckbox.setAttribute('data-option', listItem.toString());
|
||||||
|
customItemCheckbox.addEventListener('change', updateListSetting);
|
||||||
|
if (currentSettings[game][settingName].includes(listItem)) {
|
||||||
|
customItemCheckbox.setAttribute('checked', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customItemName = document.createElement('span');
|
||||||
|
customItemName.innerText = listItem.toString();
|
||||||
|
|
||||||
|
customItemLabel.appendChild(customItemCheckbox);
|
||||||
|
customItemLabel.appendChild(customItemName);
|
||||||
|
|
||||||
|
customListRow.appendChild(customItemLabel);
|
||||||
|
customList.appendChild((customListRow));
|
||||||
|
});
|
||||||
|
|
||||||
|
settingWrapper.appendChild(customList);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -729,21 +838,22 @@ const buildHintsDiv = (game, items, locations) => {
|
|||||||
const hintsDescription = document.createElement('p');
|
const hintsDescription = document.createElement('p');
|
||||||
hintsDescription.classList.add('setting-description');
|
hintsDescription.classList.add('setting-description');
|
||||||
hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' +
|
hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' +
|
||||||
' items are, or what those locations contain. Excluded locations will not contain progression items.';
|
' items are, or what those locations contain.';
|
||||||
hintsDiv.appendChild(hintsDescription);
|
hintsDiv.appendChild(hintsDescription);
|
||||||
|
|
||||||
const itemHintsContainer = document.createElement('div');
|
const itemHintsContainer = document.createElement('div');
|
||||||
itemHintsContainer.classList.add('hints-container');
|
itemHintsContainer.classList.add('hints-container');
|
||||||
|
|
||||||
|
// Item Hints
|
||||||
const itemHintsWrapper = document.createElement('div');
|
const itemHintsWrapper = document.createElement('div');
|
||||||
itemHintsWrapper.classList.add('hints-wrapper');
|
itemHintsWrapper.classList.add('hints-wrapper');
|
||||||
itemHintsWrapper.innerText = 'Starting Item Hints';
|
itemHintsWrapper.innerText = 'Starting Item Hints';
|
||||||
|
|
||||||
const itemHintsDiv = document.createElement('div');
|
const itemHintsDiv = document.createElement('div');
|
||||||
itemHintsDiv.classList.add('item-container');
|
itemHintsDiv.classList.add('simple-list');
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
const itemDiv = document.createElement('div');
|
const itemRow = document.createElement('div');
|
||||||
itemDiv.classList.add('hint-div');
|
itemRow.classList.add('list-row');
|
||||||
|
|
||||||
const itemLabel = document.createElement('label');
|
const itemLabel = document.createElement('label');
|
||||||
itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
|
itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
|
||||||
@@ -757,29 +867,30 @@ const buildHintsDiv = (game, items, locations) => {
|
|||||||
if (currentSettings[game].start_hints.includes(item)) {
|
if (currentSettings[game].start_hints.includes(item)) {
|
||||||
itemCheckbox.setAttribute('checked', 'true');
|
itemCheckbox.setAttribute('checked', 'true');
|
||||||
}
|
}
|
||||||
itemCheckbox.addEventListener('change', hintChangeHandler);
|
itemCheckbox.addEventListener('change', updateListSetting);
|
||||||
itemLabel.appendChild(itemCheckbox);
|
itemLabel.appendChild(itemCheckbox);
|
||||||
|
|
||||||
const itemName = document.createElement('span');
|
const itemName = document.createElement('span');
|
||||||
itemName.innerText = item;
|
itemName.innerText = item;
|
||||||
itemLabel.appendChild(itemName);
|
itemLabel.appendChild(itemName);
|
||||||
|
|
||||||
itemDiv.appendChild(itemLabel);
|
itemRow.appendChild(itemLabel);
|
||||||
itemHintsDiv.appendChild(itemDiv);
|
itemHintsDiv.appendChild(itemRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
itemHintsWrapper.appendChild(itemHintsDiv);
|
itemHintsWrapper.appendChild(itemHintsDiv);
|
||||||
itemHintsContainer.appendChild(itemHintsWrapper);
|
itemHintsContainer.appendChild(itemHintsWrapper);
|
||||||
|
|
||||||
|
// Starting Location Hints
|
||||||
const locationHintsWrapper = document.createElement('div');
|
const locationHintsWrapper = document.createElement('div');
|
||||||
locationHintsWrapper.classList.add('hints-wrapper');
|
locationHintsWrapper.classList.add('hints-wrapper');
|
||||||
locationHintsWrapper.innerText = 'Starting Location Hints';
|
locationHintsWrapper.innerText = 'Starting Location Hints';
|
||||||
|
|
||||||
const locationHintsDiv = document.createElement('div');
|
const locationHintsDiv = document.createElement('div');
|
||||||
locationHintsDiv.classList.add('item-container');
|
locationHintsDiv.classList.add('simple-list');
|
||||||
locations.forEach((location) => {
|
locations.forEach((location) => {
|
||||||
const locationDiv = document.createElement('div');
|
const locationRow = document.createElement('div');
|
||||||
locationDiv.classList.add('hint-div');
|
locationRow.classList.add('list-row');
|
||||||
|
|
||||||
const locationLabel = document.createElement('label');
|
const locationLabel = document.createElement('label');
|
||||||
locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
|
locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
|
||||||
@@ -793,29 +904,89 @@ const buildHintsDiv = (game, items, locations) => {
|
|||||||
if (currentSettings[game].start_location_hints.includes(location)) {
|
if (currentSettings[game].start_location_hints.includes(location)) {
|
||||||
locationCheckbox.setAttribute('checked', '1');
|
locationCheckbox.setAttribute('checked', '1');
|
||||||
}
|
}
|
||||||
locationCheckbox.addEventListener('change', hintChangeHandler);
|
locationCheckbox.addEventListener('change', updateListSetting);
|
||||||
locationLabel.appendChild(locationCheckbox);
|
locationLabel.appendChild(locationCheckbox);
|
||||||
|
|
||||||
const locationName = document.createElement('span');
|
const locationName = document.createElement('span');
|
||||||
locationName.innerText = location;
|
locationName.innerText = location;
|
||||||
locationLabel.appendChild(locationName);
|
locationLabel.appendChild(locationName);
|
||||||
|
|
||||||
locationDiv.appendChild(locationLabel);
|
locationRow.appendChild(locationLabel);
|
||||||
locationHintsDiv.appendChild(locationDiv);
|
locationHintsDiv.appendChild(locationRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
locationHintsWrapper.appendChild(locationHintsDiv);
|
locationHintsWrapper.appendChild(locationHintsDiv);
|
||||||
itemHintsContainer.appendChild(locationHintsWrapper);
|
itemHintsContainer.appendChild(locationHintsWrapper);
|
||||||
|
|
||||||
|
hintsDiv.appendChild(itemHintsContainer);
|
||||||
|
return hintsDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLocationsDiv = (game, locations) => {
|
||||||
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
|
locations.sort(); // Sort alphabetical, in-place
|
||||||
|
|
||||||
|
const locationsDiv = document.createElement('div');
|
||||||
|
locationsDiv.classList.add('locations-div');
|
||||||
|
const locationsHeader = document.createElement('h3');
|
||||||
|
locationsHeader.innerText = 'Priority & Exclusion Locations';
|
||||||
|
locationsDiv.appendChild(locationsHeader);
|
||||||
|
const locationsDescription = document.createElement('p');
|
||||||
|
locationsDescription.classList.add('setting-description');
|
||||||
|
locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' +
|
||||||
|
'excluded locations will not contain progression or useful items.';
|
||||||
|
locationsDiv.appendChild(locationsDescription);
|
||||||
|
|
||||||
|
const locationsContainer = document.createElement('div');
|
||||||
|
locationsContainer.classList.add('locations-container');
|
||||||
|
|
||||||
|
// Priority Locations
|
||||||
|
const priorityLocationsWrapper = document.createElement('div');
|
||||||
|
priorityLocationsWrapper.classList.add('locations-wrapper');
|
||||||
|
priorityLocationsWrapper.innerText = 'Priority Locations';
|
||||||
|
|
||||||
|
const priorityLocationsDiv = document.createElement('div');
|
||||||
|
priorityLocationsDiv.classList.add('simple-list');
|
||||||
|
locations.forEach((location) => {
|
||||||
|
const locationRow = document.createElement('div');
|
||||||
|
locationRow.classList.add('list-row');
|
||||||
|
|
||||||
|
const locationLabel = document.createElement('label');
|
||||||
|
locationLabel.setAttribute('for', `${game}-priority_locations-${location}`);
|
||||||
|
|
||||||
|
const locationCheckbox = document.createElement('input');
|
||||||
|
locationCheckbox.setAttribute('type', 'checkbox');
|
||||||
|
locationCheckbox.setAttribute('id', `${game}-priority_locations-${location}`);
|
||||||
|
locationCheckbox.setAttribute('data-game', game);
|
||||||
|
locationCheckbox.setAttribute('data-setting', 'priority_locations');
|
||||||
|
locationCheckbox.setAttribute('data-option', location);
|
||||||
|
if (currentSettings[game].priority_locations.includes(location)) {
|
||||||
|
locationCheckbox.setAttribute('checked', '1');
|
||||||
|
}
|
||||||
|
locationCheckbox.addEventListener('change', updateListSetting);
|
||||||
|
locationLabel.appendChild(locationCheckbox);
|
||||||
|
|
||||||
|
const locationName = document.createElement('span');
|
||||||
|
locationName.innerText = location;
|
||||||
|
locationLabel.appendChild(locationName);
|
||||||
|
|
||||||
|
locationRow.appendChild(locationLabel);
|
||||||
|
priorityLocationsDiv.appendChild(locationRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
priorityLocationsWrapper.appendChild(priorityLocationsDiv);
|
||||||
|
locationsContainer.appendChild(priorityLocationsWrapper);
|
||||||
|
|
||||||
|
// Exclude Locations
|
||||||
const excludeLocationsWrapper = document.createElement('div');
|
const excludeLocationsWrapper = document.createElement('div');
|
||||||
excludeLocationsWrapper.classList.add('hints-wrapper');
|
excludeLocationsWrapper.classList.add('locations-wrapper');
|
||||||
excludeLocationsWrapper.innerText = 'Exclude Locations';
|
excludeLocationsWrapper.innerText = 'Exclude Locations';
|
||||||
|
|
||||||
const excludeLocationsDiv = document.createElement('div');
|
const excludeLocationsDiv = document.createElement('div');
|
||||||
excludeLocationsDiv.classList.add('item-container');
|
excludeLocationsDiv.classList.add('simple-list');
|
||||||
locations.forEach((location) => {
|
locations.forEach((location) => {
|
||||||
const locationDiv = document.createElement('div');
|
const locationRow = document.createElement('div');
|
||||||
locationDiv.classList.add('hint-div');
|
locationRow.classList.add('list-row');
|
||||||
|
|
||||||
const locationLabel = document.createElement('label');
|
const locationLabel = document.createElement('label');
|
||||||
locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
|
locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
|
||||||
@@ -829,40 +1000,22 @@ const buildHintsDiv = (game, items, locations) => {
|
|||||||
if (currentSettings[game].exclude_locations.includes(location)) {
|
if (currentSettings[game].exclude_locations.includes(location)) {
|
||||||
locationCheckbox.setAttribute('checked', '1');
|
locationCheckbox.setAttribute('checked', '1');
|
||||||
}
|
}
|
||||||
locationCheckbox.addEventListener('change', hintChangeHandler);
|
locationCheckbox.addEventListener('change', updateListSetting);
|
||||||
locationLabel.appendChild(locationCheckbox);
|
locationLabel.appendChild(locationCheckbox);
|
||||||
|
|
||||||
const locationName = document.createElement('span');
|
const locationName = document.createElement('span');
|
||||||
locationName.innerText = location;
|
locationName.innerText = location;
|
||||||
locationLabel.appendChild(locationName);
|
locationLabel.appendChild(locationName);
|
||||||
|
|
||||||
locationDiv.appendChild(locationLabel);
|
locationRow.appendChild(locationLabel);
|
||||||
excludeLocationsDiv.appendChild(locationDiv);
|
excludeLocationsDiv.appendChild(locationRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
|
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
|
||||||
itemHintsContainer.appendChild(excludeLocationsWrapper);
|
locationsContainer.appendChild(excludeLocationsWrapper);
|
||||||
|
|
||||||
hintsDiv.appendChild(itemHintsContainer);
|
locationsDiv.appendChild(locationsContainer);
|
||||||
return hintsDiv;
|
return locationsDiv;
|
||||||
};
|
|
||||||
|
|
||||||
const hintChangeHandler = (evt) => {
|
|
||||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
|
||||||
const game = evt.target.getAttribute('data-game');
|
|
||||||
const setting = evt.target.getAttribute('data-setting');
|
|
||||||
const option = evt.target.getAttribute('data-option');
|
|
||||||
|
|
||||||
if (evt.target.checked) {
|
|
||||||
if (!currentSettings[game][setting].includes(option)) {
|
|
||||||
currentSettings[game][setting].push(option);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (currentSettings[game][setting].includes(option)) {
|
|
||||||
currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVisibleGames = () => {
|
const updateVisibleGames = () => {
|
||||||
@@ -908,13 +1061,12 @@ const updateBaseSetting = (event) => {
|
|||||||
localStorage.setItem('weighted-settings', JSON.stringify(settings));
|
localStorage.setItem('weighted-settings', JSON.stringify(settings));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateGameSetting = (evt) => {
|
const updateRangeSetting = (evt) => {
|
||||||
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
const game = evt.target.getAttribute('data-game');
|
const game = evt.target.getAttribute('data-game');
|
||||||
const setting = evt.target.getAttribute('data-setting');
|
const setting = evt.target.getAttribute('data-setting');
|
||||||
const option = evt.target.getAttribute('data-option');
|
const option = evt.target.getAttribute('data-option');
|
||||||
document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value;
|
document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value;
|
||||||
console.log(event);
|
|
||||||
if (evt.action && evt.action === 'rangeDelete') {
|
if (evt.action && evt.action === 'rangeDelete') {
|
||||||
delete options[game][setting][option];
|
delete options[game][setting][option];
|
||||||
} else {
|
} else {
|
||||||
@@ -923,6 +1075,26 @@ const updateGameSetting = (evt) => {
|
|||||||
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateListSetting = (evt) => {
|
||||||
|
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
|
const game = evt.target.getAttribute('data-game');
|
||||||
|
const setting = evt.target.getAttribute('data-setting');
|
||||||
|
const option = evt.target.getAttribute('data-option');
|
||||||
|
|
||||||
|
if (evt.target.checked) {
|
||||||
|
// If the option is to be enabled and it is already enabled, do nothing
|
||||||
|
if (options[game][setting].includes(option)) { return; }
|
||||||
|
|
||||||
|
options[game][setting].push(option);
|
||||||
|
} else {
|
||||||
|
// If the option is to be disabled and it is already disabled, do nothing
|
||||||
|
if (!options[game][setting].includes(option)) { return; }
|
||||||
|
|
||||||
|
options[game][setting].splice(options[game][setting].indexOf(option), 1);
|
||||||
|
}
|
||||||
|
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
||||||
|
};
|
||||||
|
|
||||||
const updateItemSetting = (evt) => {
|
const updateItemSetting = (evt) => {
|
||||||
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
const game = evt.target.getAttribute('data-game');
|
const game = evt.target.getAttribute('data-game');
|
||||||
|
|||||||
BIN
WebHostLib/static/static/button-images/hamburger-menu-icon.png
Normal file
BIN
WebHostLib/static/static/button-images/hamburger-menu-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
WebHostLib/static/static/button-images/popover.png
Normal file
BIN
WebHostLib/static/static/button-images/popover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
30
WebHostLib/static/styles/checksfinderTracker.css
Normal file
30
WebHostLib/static/styles/checksfinderTracker.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#player-tracker-wrapper{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table{
|
||||||
|
padding: 8px 10px 2px 6px;
|
||||||
|
background-color: #42b149;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table tr.column-headers td {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0 5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table td{
|
||||||
|
padding: 0 0.5rem 0.5rem;
|
||||||
|
font-family: LexendDeca-Light, monospace;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table td img{
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -15,3 +15,33 @@
|
|||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
color: #dfedc6;
|
color: #dfedc6;
|
||||||
}
|
}
|
||||||
|
@media all and (max-width: 900px) {
|
||||||
|
#island-footer{
|
||||||
|
font-size: 17px;
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (max-width: 768px) {
|
||||||
|
#island-footer{
|
||||||
|
font-size: 15px;
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (max-width: 650px) {
|
||||||
|
#island-footer{
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (max-width: 580px) {
|
||||||
|
#island-footer{
|
||||||
|
font-size: 11px;
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (max-width: 512px) {
|
||||||
|
#island-footer{
|
||||||
|
font-size: 9px;
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ html{
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing-header h4{
|
#landing-header h4{
|
||||||
@@ -223,7 +222,7 @@ html{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#landing{
|
#landing{
|
||||||
width: 700px;
|
max-width: 700px;
|
||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ html{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#base-header-right{
|
#base-header-right{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ html{
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#base-header a{
|
#base-header a, #base-header-mobile-menu a, #base-header-popover-text{
|
||||||
color: #2f6b83;
|
color: #2f6b83;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -51,3 +53,126 @@ html{
|
|||||||
font-family: LondrinaSolid-Light, sans-serif;
|
font-family: LondrinaSolid-Light, sans-serif;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#base-header-right-mobile{
|
||||||
|
display: none;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-mobile-menu{
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 10000;
|
||||||
|
width: 100vw;
|
||||||
|
border-bottom-left-radius: 20px;
|
||||||
|
border-bottom-right-radius: 20px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 7rem;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-mobile-menu a{
|
||||||
|
padding: 3rem 1.5rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 5rem;
|
||||||
|
color: #699ca8;
|
||||||
|
border-top: 1px solid #d3d3d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-mobile-menu :first-child, #base-header-popover-menu :first-child{
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-right-mobile img{
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-popover-menu{
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #fff;
|
||||||
|
margin-left: -108px;
|
||||||
|
margin-top: 2.25rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 2px solid #d0ebe6;
|
||||||
|
border-bottom: 2px solid #d0ebe6;
|
||||||
|
border-right: 1px solid #d0ebe6;
|
||||||
|
filter: drop-shadow(-6px 6px 2px #2e3e83);
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-popover-menu a{
|
||||||
|
color: #699ca8;
|
||||||
|
border-top: 1px solid #d3d3d3;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
margin-right: 2px;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-popover-icon {
|
||||||
|
width: 14px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 960px), only screen and (max-device-width: 768px) {
|
||||||
|
#base-header-right{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-right-mobile{
|
||||||
|
display: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 960px){
|
||||||
|
#base-header-right-mobile{
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-right-mobile img{
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-mobile-menu{
|
||||||
|
top: 3.3rem;
|
||||||
|
width: unset;
|
||||||
|
border-left: 2px solid #d0ebe6;
|
||||||
|
border-bottom: 2px solid #d0ebe6;
|
||||||
|
filter: drop-shadow(-6px 6px 2px #2e3e83);
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header-mobile-menu a{
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-device-width: 768px){
|
||||||
|
html{
|
||||||
|
padding-top: 260px;
|
||||||
|
scroll-padding-top: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header{
|
||||||
|
height: 200px;
|
||||||
|
background-size: auto 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#base-header #site-title img{
|
||||||
|
height: calc(38px * 2);
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,19 +9,54 @@
|
|||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
padding: 3px 3px 10px;
|
padding: 3px 3px 10px;
|
||||||
width: 384px;
|
width: 374px;
|
||||||
background-color: #8d60a7;
|
background-color: #8d60a7;
|
||||||
}
|
|
||||||
|
|
||||||
#inventory-table td{
|
display: grid;
|
||||||
width: 40px;
|
grid-template-rows: repeat(5, 48px);
|
||||||
height: 40px;
|
}
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
#inventory-table img{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.table-row{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.C1{
|
||||||
|
grid-column: 1;
|
||||||
|
place-content: center;
|
||||||
|
place-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#inventory-table div.C2{
|
||||||
|
grid-column: 2;
|
||||||
|
place-content: center;
|
||||||
|
place-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#inventory-table div.C3{
|
||||||
|
grid-column: 3;
|
||||||
|
place-content: center;
|
||||||
|
place-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#inventory-table div.C4{
|
||||||
|
grid-column: 4;
|
||||||
|
place-content: center;
|
||||||
|
place-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#inventory-table div.C5{
|
||||||
|
grid-column: 5;
|
||||||
|
place-content: center;
|
||||||
|
place-items: center;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table img{
|
#inventory-table img{
|
||||||
height: 100%;
|
|
||||||
max-width: 40px;
|
max-width: 40px;
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||||
@@ -31,11 +66,70 @@
|
|||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table div.counted-item {
|
#inventory-table img.acquired.purple{ /*00FFFF*/
|
||||||
|
filter: hue-rotate(270deg) saturate(6) brightness(0.8);
|
||||||
|
}
|
||||||
|
#inventory-table img.acquired.cyan{ /*FF00FF*/
|
||||||
|
filter: hue-rotate(138deg) saturate(10) brightness(0.8);
|
||||||
|
}
|
||||||
|
#inventory-table img.acquired.green{ /*32CD32*/
|
||||||
|
filter: hue-rotate(84deg) saturate(10) brightness(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack{
|
||||||
|
display: grid;
|
||||||
|
position: relative;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack div.stack-back{
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack div.stack-front{
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px 20px;
|
||||||
|
grid-template-rows: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack div.stack-top-left{
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack div.stack-top-right{
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack div.stack-bottum-left{
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack div.stack-bottum-right{
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.image-stack div.stack-front img{
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.counted-item{
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table div.item-count {
|
#inventory-table div.item-count{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: white;
|
color: white;
|
||||||
font-family: "Minecraftia", monospace;
|
font-family: "Minecraftia", monospace;
|
||||||
@@ -69,16 +163,16 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#location-table td.counter {
|
#location-table td.counter{
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#location-table td.toggle-arrow {
|
#location-table td.toggle-arrow{
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#location-table tr#Total-header {
|
#location-table tr#Total-header{
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +182,14 @@
|
|||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#location-table tbody.locations {
|
#location-table tbody.locations{
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#location-table td.location-name {
|
#location-table td.location-name{
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide {
|
.hide{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,33 @@ img.alttp-sprite {
|
|||||||
background-color: #d3c97d;
|
background-color: #d3c97d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tracker-navigation {
|
||||||
|
display: inline-flex;
|
||||||
|
background-color: #b0a77d;
|
||||||
|
margin: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracker-navigation-button {
|
||||||
|
display: block;
|
||||||
|
margin: 4px;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracker-navigation-button:hover {
|
||||||
|
background-color: #e2eabb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracker-navigation-button.selected {
|
||||||
|
background-color: rgb(220, 226, 189);
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 1700px) {
|
@media all and (max-width: 1700px) {
|
||||||
table.dataTable thead th.upper-row{
|
table.dataTable thead th.upper-row{
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
|
|||||||
@@ -157,41 +157,29 @@ html{
|
|||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings .hints-div{
|
#weighted-settings .hints-div, #weighted-settings .locations-div{
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings .hints-div h3{
|
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings .hints-div .hints-container{
|
#weighted-settings .hints-container, #weighted-settings .locations-container{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
|
||||||
|
width: calc(50% - 0.5rem);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings .hints-div .hints-wrapper{
|
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
|
||||||
width: 32.5%;
|
margin-top: 0.25rem;
|
||||||
}
|
height: 300px;
|
||||||
|
font-weight: normal;
|
||||||
#weighted-settings .hints-div .hints-wrapper .hint-div{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#weighted-settings .hints-div .hints-wrapper .hint-div:hover{
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#weighted-settings .hints-div .hints-wrapper .hint-div label{
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 0.125rem 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings #weighted-settings-button-row{
|
#weighted-settings #weighted-settings-button-row{
|
||||||
@@ -280,6 +268,30 @@ html{
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list .list-row label{
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 0.5rem);
|
||||||
|
padding: 0.0625rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list .list-row label:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .simple-list .list-row label input[type=checkbox]{
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
#weighted-settings .invisible{
|
#weighted-settings .invisible{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
35
WebHostLib/templates/checksfinderTracker.html
Normal file
35
WebHostLib/templates/checksfinderTracker.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ player_name }}'s Tracker</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/checksfinderTracker.css') }}" />
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/checksfinderTracker.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||||
|
<table id="inventory-table">
|
||||||
|
<tr class="column-headers">
|
||||||
|
<td colspan="2">Checks Available:</td>
|
||||||
|
<td colspan="2">Map Bombs:</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img alt="Checks Available" src="{{ icons['Checks Available'] }}" /></td>
|
||||||
|
<td>{{ checks_available }}</td>
|
||||||
|
<td><img alt="Bombs Remaining" src="{{ icons['Map Bombs'] }}" /></td>
|
||||||
|
<td>{{ bombs_display }}/20</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="column-headers">
|
||||||
|
<td colspan="2">Map Width:</td>
|
||||||
|
<td colspan="2">Map Height:</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img alt="Map Width" src="{{ icons['Map Width'] }}" /></td>
|
||||||
|
<td>{{ width_display }}/10</td>
|
||||||
|
<td><img alt="Map Height" src="{{ icons['Map Height'] }}" /></td>
|
||||||
|
<td>{{ height_display }}/10</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<title>{{ player_name }}'s Tracker</title>
|
<title>{{ player_name }}'s Tracker</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/themes/base.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/themes/base.css") }}" />
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/baseHeader.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
@@ -10,11 +11,33 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="base-header-right">
|
<div id="base-header-right">
|
||||||
<a href="/games">supported games</a>
|
<div id="base-header-popover-text">
|
||||||
<a href="/tutorial">setup guides</a>
|
<img id="base-header-popover-icon" src="/static/static/button-images/popover.png" alt="Popover Menu" />
|
||||||
<a href="/start-playing">start playing</a>
|
get started
|
||||||
<a href="/faq/en">f.a.q.</a>
|
</div>
|
||||||
|
<div id="base-header-popover-menu">
|
||||||
|
<a href="/games">supported games</a>
|
||||||
|
<a href="/tutorial">setup guides</a>
|
||||||
|
<a href="/generate">generate game</a>
|
||||||
|
<a href="/uploads">host game</a>
|
||||||
|
<a href="/user-content">user content</a>
|
||||||
|
</div>
|
||||||
|
<a href="/faq/en">f.a.q</a>
|
||||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="base-header-right-mobile">
|
||||||
|
<a id="base-header-mobile-menu-button" href="#">
|
||||||
|
<img src="/static/static/button-images/hamburger-menu-icon.png" alt="Menu" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div id="base-header-mobile-menu">
|
||||||
|
<a href="/games">supported games</a>
|
||||||
|
<a href="/tutorial">setup guides</a>
|
||||||
|
<a href="/faq/en">f.a.q.</a>
|
||||||
|
<a href="/generate">generate game</a>
|
||||||
|
<a href="/uploads">host game</a>
|
||||||
|
<a href="/user-content">user content</a>
|
||||||
|
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<br />
|
<br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if room.tracker %}
|
{% if room.tracker %}
|
||||||
This room has a <a href="{{ url_for("getTracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
|
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
|
||||||
<br />
|
<br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
The most likely failure reason is that the multiworld is too old to be loaded now.
|
The most likely failure reason is that the multiworld is too old to be loaded now.
|
||||||
{% elif room.last_port %}
|
{% elif room.last_port %}
|
||||||
You can connect to this room by using <span class="interactive"
|
You can connect to this room by using <span class="interactive"
|
||||||
data-tooltip="This means address/ip is {{ config['PATCH_TARGET'] }} and port is {{ room.last_port }}.">
|
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
|
||||||
'/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}'
|
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
|
||||||
</span>
|
</span>
|
||||||
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
|
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
{% extends 'tablepage.html' %}
|
{% extends 'tablepage.html' %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<title>Multiworld Tracker</title>
|
<title>ALttP Multiworld Tracker</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttpMultiTracker.js") }}"></script>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/dirtHeader.html' %}
|
{% include 'header/dirtHeader.html' %}
|
||||||
|
{% include 'multiTrackerNavigation.html' %}
|
||||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||||
<div id="tracker-header-bar">
|
<div id="tracker-header-bar">
|
||||||
<input placeholder="Search" id="search"/>
|
<input placeholder="Search" id="search"/>
|
||||||
@@ -98,6 +100,7 @@
|
|||||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
<th rowspan="2" class="center-column">%</th>
|
||||||
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -140,6 +143,7 @@
|
|||||||
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
|
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||||
{%- if activity_timers[(team, player)] -%}
|
{%- if activity_timers[(team, player)] -%}
|
||||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ patch.player_id }}</td>
|
<td>{{ patch.player_id }}</td>
|
||||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['PATCH_TARGET'] }}:{{ room.last_port }}">{{ patch.player_name }}<a/></td>
|
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||||
<td>{{ patch.game }}</td>
|
<td>{{ patch.game }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if patch.game == "Minecraft" %}
|
{% if patch.game == "Minecraft" %}
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
{% elif patch.game == "Factorio" %}
|
{% elif patch.game == "Factorio" %}
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
Download Factorio Mod...</a>
|
Download Factorio Mod...</a>
|
||||||
|
{% elif patch.game == "Kingdom Hearts 2" %}
|
||||||
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
|
Download Kingdom Hearts 2 Mod...</a>
|
||||||
{% elif patch.game == "Ocarina of Time" %}
|
{% elif patch.game == "Ocarina of Time" %}
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
Download APZ5 File...</a>
|
Download APZ5 File...</a>
|
||||||
|
|||||||
44
WebHostLib/templates/multiFactorioTracker.html
Normal file
44
WebHostLib/templates/multiFactorioTracker.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% extends "multiTracker.html" %}
|
||||||
|
{% block custom_table_headers %}
|
||||||
|
<th class="center-column">
|
||||||
|
<img src="https://wiki.factorio.com/images/thumb/Logistic_science_pack.png/32px-Logistic_science_pack.png"
|
||||||
|
alt="Logistic Science Pack">
|
||||||
|
</th>
|
||||||
|
<th class="center-column">
|
||||||
|
<img src="https://wiki.factorio.com/images/thumb/Military_science_pack.png/32px-Military_science_pack.png"
|
||||||
|
alt="Military Science Pack">
|
||||||
|
</th>
|
||||||
|
<th class="center-column">
|
||||||
|
<img src="https://wiki.factorio.com/images/thumb/Chemical_science_pack.png/32px-Chemical_science_pack.png"
|
||||||
|
alt="Chemical Science Pack">
|
||||||
|
</th>
|
||||||
|
<th class="center-column">
|
||||||
|
<img src="https://wiki.factorio.com/images/thumb/Production_science_pack.png/32px-Production_science_pack.png"
|
||||||
|
alt="Production Science Pack">
|
||||||
|
</th>
|
||||||
|
<th class="center-column">
|
||||||
|
<img src="https://wiki.factorio.com/images/thumb/Utility_science_pack.png/32px-Utility_science_pack.png"
|
||||||
|
alt="Utility Science Pack">
|
||||||
|
</th>
|
||||||
|
<th class="center-column">
|
||||||
|
<img src="https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"
|
||||||
|
alt="Space Science Pack">
|
||||||
|
</th>
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_table_row scoped %}
|
||||||
|
{% if games[player] == "Factorio" %}
|
||||||
|
<td class="center-column">{% if inventory[team][player][131161] or inventory[team][player][131281] %}✔{% endif %}</td>
|
||||||
|
<td class="center-column">{% if inventory[team][player][131172] or inventory[team][player][131281] > 1%}✔{% endif %}</td>
|
||||||
|
<td class="center-column">{% if inventory[team][player][131195] or inventory[team][player][131281] > 2%}✔{% endif %}</td>
|
||||||
|
<td class="center-column">{% if inventory[team][player][131240] or inventory[team][player][131281] > 3%}✔{% endif %}</td>
|
||||||
|
<td class="center-column">{% if inventory[team][player][131240] or inventory[team][player][131281] > 4%}✔{% endif %}</td>
|
||||||
|
<td class="center-column">{% if inventory[team][player][131220] or inventory[team][player][131281] > 5%}✔{% endif %}</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="center-column">❌</td>
|
||||||
|
<td class="center-column">❌</td>
|
||||||
|
<td class="center-column">❌</td>
|
||||||
|
<td class="center-column">❌</td>
|
||||||
|
<td class="center-column">❌</td>
|
||||||
|
<td class="center-column">❌</td>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock%}
|
||||||
98
WebHostLib/templates/multiTracker.html
Normal file
98
WebHostLib/templates/multiTracker.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{% extends 'tablepage.html' %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<title>Multiworld Tracker</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% include 'header/dirtHeader.html' %}
|
||||||
|
{% include 'multiTrackerNavigation.html' %}
|
||||||
|
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||||
|
<div id="tracker-header-bar">
|
||||||
|
<input placeholder="Search" id="search"/>
|
||||||
|
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
|
||||||
|
<a target="_blank" href="https://multistream.me/
|
||||||
|
{%- for platform, link in video.values()|unique(False, 1)-%}
|
||||||
|
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||||
|
{%- endfor -%}">
|
||||||
|
Multistream
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
|
||||||
|
</div>
|
||||||
|
<div id="tables-container">
|
||||||
|
{% for team, players in checks_done.items() %}
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="checks-table" class="table non-unique-item-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Game</th>
|
||||||
|
{% block custom_table_headers %}
|
||||||
|
{# implement this block in game-specific multi trackers #}
|
||||||
|
{% endblock %}
|
||||||
|
<th class="center-column">Checks</th>
|
||||||
|
<th class="center-column">%</th>
|
||||||
|
<th class="center-column">Status</th>
|
||||||
|
<th class="center-column hours">Last<br>Activity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- for player, checks in players.items() -%}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||||
|
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||||
|
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||||
|
<td>{{ games[player] }}</td>
|
||||||
|
{% block custom_table_row scoped %}
|
||||||
|
{# implement this block in game-specific multi trackers #}
|
||||||
|
{% endblock %}
|
||||||
|
<td class="center-column">{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}</td>
|
||||||
|
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||||
|
<td>{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing",
|
||||||
|
30: "Goal Completed"}.get(states[team, player], "Unknown State") }}</td>
|
||||||
|
{%- if activity_timers[team, player] -%}
|
||||||
|
<td class="center-column">{{ activity_timers[team, player].total_seconds() }}</td>
|
||||||
|
{%- else -%}
|
||||||
|
<td class="center-column">None</td>
|
||||||
|
{%- endif -%}
|
||||||
|
</tr>
|
||||||
|
{%- endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% for team, hints in hints.items() %}
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Finder</th>
|
||||||
|
<th>Receiver</th>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Entrance</th>
|
||||||
|
<th>Found</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- for hint in hints -%}
|
||||||
|
<tr>
|
||||||
|
<td>{{ long_player_names[team, hint.finding_player] }}</td>
|
||||||
|
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
|
||||||
|
<td>{{ hint.item|item_name }}</td>
|
||||||
|
<td>{{ hint.location|location_name }}</td>
|
||||||
|
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||||
|
<td>{% if hint.found %}✔{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
9
WebHostLib/templates/multiTrackerNavigation.html
Normal file
9
WebHostLib/templates/multiTrackerNavigation.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{%- if enabled_multiworld_trackers|length > 1 -%}
|
||||||
|
<div id="tracker-navigation">
|
||||||
|
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||||
|
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
||||||
|
<a class="tracker-navigation-button{%- if enabled_tracker.current -%} selected{% endif %}"
|
||||||
|
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{%- endif -%}
|
||||||
@@ -29,17 +29,30 @@
|
|||||||
<li><a href="/glossary/en">Glossary</a></li>
|
<li><a href="/glossary/en">Glossary</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h2>Tutorials</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/tutorial/Archipelago/setup/en">Multiworld Setup Tutorial</a></li>
|
||||||
|
<li><a href="/tutorial/Archipelago/using_website/en">Website User Guide</a></li>
|
||||||
|
<li><a href="/tutorial/Archipelago/mac/en">Setup Guide for Mac</a></li>
|
||||||
|
<li><a href="/tutorial/Archipelago/commands/en">Server and Client Commands</a></li>
|
||||||
|
<li><a href="/tutorial/Archipelago/advanced_settings/en">Advanced YAML Guide</a></li>
|
||||||
|
<li><a href="/tutorial/Archipelago/triggers/en">Triggers Guide</a></li>
|
||||||
|
<li><a href="/tutorial/Archipelago/plando/en">Plando Guide</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h2>Game Info Pages</h2>
|
<h2>Game Info Pages</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for game in games | title_sorted %}
|
{% for game in games | title_sorted %}
|
||||||
<li><a href="{{ url_for('game_info', game=game, lang='en') }}">{{ game }}</a></li>
|
<li><a href="{{ url_for('game_info', game=game['title'], lang='en') }}">{{ game['title'] }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Game Settings Pages</h2>
|
<h2>Game Settings Pages</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for game in games | title_sorted %}
|
{% for game in games | title_sorted %}
|
||||||
<li><a href="{{ url_for('player_settings', game=game) }}">{{ game }}</a></li>
|
{% if game['has_settings'] %}
|
||||||
|
<li><a href="{{ url_for('player_settings', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,79 +8,94 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||||
<table id="inventory-table">
|
<div id="inventory-table">
|
||||||
<tr>
|
<div class="table-row">
|
||||||
<td><img src="{{ icons['Timespinner Wheel'] }}" class="{{ 'acquired' if 'Timespinner Wheel' in acquired_items }}" title="Timespinner Wheel" /></td>
|
<div class="C1"><img src="{{ icons['Timespinner Wheel'] }}" class="{{ 'acquired' if 'Timespinner Wheel' in acquired_items }}" title="Timespinner Wheel" /></div>
|
||||||
<td><img src="{{ icons['Timespinner Spindle'] }}" class="{{ 'acquired' if 'Timespinner Spindle' in acquired_items }}" title="Timespinner Spindle" /></td>
|
<div class="C2"><img src="{{ icons['Timespinner Spindle'] }}" class="{{ 'acquired' if 'Timespinner Spindle' in acquired_items }}" title="Timespinner Spindle" /></div>
|
||||||
<td><img src="{{ icons['Timespinner Gear 1'] }}" class="{{ 'acquired' if 'Timespinner Gear 1' in acquired_items }}" title="Timespinner Gear 1" /></td>
|
<div class="C3"><img src="{{ icons['Timespinner Gear 1'] }}" class="{{ 'acquired' if 'Timespinner Gear 1' in acquired_items }}" title="Timespinner Gear 1" /></div>
|
||||||
<td><img src="{{ icons['Timespinner Gear 2'] }}" class="{{ 'acquired' if 'Timespinner Gear 2' in acquired_items }}" title="Timespinner Gear 2" /></td>
|
<div class="C4"><img src="{{ icons['Timespinner Gear 2'] }}" class="{{ 'acquired' if 'Timespinner Gear 2' in acquired_items }}" title="Timespinner Gear 2" /></div>
|
||||||
<td><img src="{{ icons['Timespinner Gear 3'] }}" class="{{ 'acquired' if 'Timespinner Gear 3' in acquired_items }}" title="Timespinner Gear 3" /></td>
|
<div class="C5"><img src="{{ icons['Timespinner Gear 3'] }}" class="{{ 'acquired' if 'Timespinner Gear 3' in acquired_items }}" title="Timespinner Gear 3" /></div>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
<div class="table-row">
|
||||||
<td><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items or 'QuickSeed' in options }}" title="Talaria Attachment" /></td>
|
<div class="C1"><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items or 'QuickSeed' in options }}" title="Talaria Attachment" /></div>
|
||||||
<td><img src="{{ icons['Succubus Hairpin'] }}" class="{{ 'acquired' if 'Succubus Hairpin' in acquired_items }}" title="Succubus Hairpin" /></td>
|
<div class="C2"><img src="{{ icons['Succubus Hairpin'] }}" class="{{ 'acquired' if 'Succubus Hairpin' in acquired_items }}" title="Succubus Hairpin" /></div>
|
||||||
<td><img src="{{ icons['Lightwall'] }}" class="{{ 'acquired' if 'Lightwall' in acquired_items }}" title="Lightwall" /></td>
|
<div class="C3"><img src="{{ icons['Lightwall'] }}" class="{{ 'acquired' if 'Lightwall' in acquired_items }}" title="Lightwall" /></div>
|
||||||
<td><img src="{{ icons['Celestial Sash'] }}" class="{{ 'acquired' if 'Celestial Sash' in acquired_items }}" title="Celestial Sash" /></td>
|
<div class="C4"><img src="{{ icons['Celestial Sash'] }}" class="{{ 'acquired' if 'Celestial Sash' in acquired_items }}" title="Celestial Sash" /></div>
|
||||||
<td><img src="{{ icons['Twin Pyramid Key'] }}" class="{{ 'acquired' if 'Twin Pyramid Key' in acquired_items }}" title="Twin Pyramid Key" /></td>
|
<div class="C5">
|
||||||
</tr>
|
<div class="image-stack">
|
||||||
<tr>
|
<div class="stack-back">
|
||||||
<td><img src="{{ icons['Security Keycard A'] }}" class="{{ 'acquired' if 'Security Keycard A' in acquired_items }}" title="Security Keycard A" /></td>
|
<img src="{{ icons['Twin Pyramid Key'] }}" class="{{ 'acquired' if 'Twin Pyramid Key' in acquired_items or 'UnchainedKeys' in options }}" title="Twin Pyramid Key" />
|
||||||
<td><img src="{{ icons['Security Keycard B'] }}" class="{{ 'acquired' if 'Security Keycard B' in acquired_items }}" title="Security Keycard B" /></td>
|
</div>
|
||||||
<td><img src="{{ icons['Security Keycard C'] }}" class="{{ 'acquired' if 'Security Keycard C' in acquired_items }}" title="Security Keycard C" /></td>
|
<div class="stack-front">
|
||||||
<td><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></td>
|
{% if 'UnchainedKeys' in options %}
|
||||||
{% if 'DownloadableItems' in options %}
|
{% if 'EnterSandman' in options %}
|
||||||
<td><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></td>
|
<div class="stack-top-right">
|
||||||
{% else %}
|
<img src="{{ icons['Twin Pyramid Key'] }}" class="green {{ 'acquired' if 'Mysterious Warp Beacon' in acquired_items }}" title="Mysterious Warp Beacon" />
|
||||||
<td></td>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
<div class="stack-bottum-left">
|
||||||
<tr>
|
<img src="{{ icons['Twin Pyramid Key'] }}" class="cyan {{ 'acquired' if 'Timeworn Warp Beacon' in acquired_items }}" title="Timeworn Warp Beacon" />
|
||||||
{% if 'DownloadableItems' in options %}
|
</div>
|
||||||
<td><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></td>
|
<div class="stack-bottum-right">
|
||||||
{% else %}
|
<img src="{{ icons['Twin Pyramid Key'] }}" class="purple {{ 'acquired' if 'Modern Warp Beacon' in acquired_items }}" title="Modern Warp Beacon" />
|
||||||
<td></td>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></td>
|
</div>
|
||||||
{% if 'EyeSpy' in options %}
|
</div>
|
||||||
<td><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></td>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<td></td>
|
<div class="table-row">
|
||||||
{% endif %}
|
<div class="C1"><img src="{{ icons['Security Keycard A'] }}" class="{{ 'acquired' if 'Security Keycard A' in acquired_items }}" title="Security Keycard A" /></div>
|
||||||
<td><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></td>
|
<div class="C2"><img src="{{ icons['Security Keycard B'] }}" class="{{ 'acquired' if 'Security Keycard B' in acquired_items }}" title="Security Keycard B" /></div>
|
||||||
<td><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></td>
|
<div class="C3"><img src="{{ icons['Security Keycard C'] }}" class="{{ 'acquired' if 'Security Keycard C' in acquired_items }}" title="Security Keycard C" /></div>
|
||||||
</tr>
|
<div class="C4"><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></div>
|
||||||
<tr>
|
{% if 'DownloadableItems' in options %}
|
||||||
{% if 'GyreArchives' in options %}
|
<div class="C5"><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></div>
|
||||||
<td><img src="{{ icons['Kobo'] }}" class="{{ 'acquired' if 'Kobo' in acquired_items }}" title="Kobo" /></td>
|
{% endif %}
|
||||||
<td><img src="{{ icons['Merchant Crow'] }}" class="{{ 'acquired' if 'Merchant Crow' in acquired_items }}" title="Merchant Crow" /></td>
|
</div>
|
||||||
{% else %}
|
<div class="table-row">
|
||||||
<td></td>
|
{% if 'DownloadableItems' in options %}
|
||||||
<td></td>
|
<div class="C1"><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="C2"><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></div>
|
||||||
|
{% if 'EyeSpy' in options %}
|
||||||
|
<div class="C3"><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="C4"><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></div>
|
||||||
|
<div class="C5"><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="table-row">
|
||||||
|
{% if 'GyreArchives' in options %}
|
||||||
|
<div class="C1"><img src="{{ icons['Kobo'] }}" class="{{ 'acquired' if 'Kobo' in acquired_items }}" title="Kobo" /></div>
|
||||||
|
<div class="C2"><img src="{{ icons['Merchant Crow'] }}" class="{{ 'acquired' if 'Merchant Crow' in acquired_items }}" title="Merchant Crow" /></div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="C3">
|
||||||
{% if 'Djinn Inferno' in acquired_items %}
|
{% if 'Djinn Inferno' in acquired_items %}
|
||||||
<td><img src="{{ icons['Djinn Inferno'] }}" class="acquired" title="Djinn Inferno" /></td>
|
<img src="{{ icons['Djinn Inferno'] }}" class="acquired" title="Djinn Inferno" />
|
||||||
{% elif 'Pyro Ring' in acquired_items %}
|
{% elif 'Pyro Ring' in acquired_items %}
|
||||||
<td><img src="{{ icons['Pyro Ring'] }}" class="acquired" title="Pyro Ring" /></td>
|
<img src="{{ icons['Pyro Ring'] }}" class="acquired" title="Pyro Ring" />
|
||||||
{% elif 'Fire Orb' in acquired_items %}
|
{% elif 'Fire Orb' in acquired_items %}
|
||||||
<td><img src="{{ icons['Fire Orb'] }}" class="acquired" title="Fire Orb" /></td>
|
<img src="{{ icons['Fire Orb'] }}" class="acquired" title="Fire Orb" />
|
||||||
{% elif 'Infernal Flames' in acquired_items %}
|
{% elif 'Infernal Flames' in acquired_items %}
|
||||||
<td><img src="{{ icons['Infernal Flames'] }}" class="acquired" title="Infernal Flames" /></td>
|
<img src="{{ icons['Infernal Flames'] }}" class="acquired" title="Infernal Flames" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><img src="{{ icons['Djinn Inferno'] }}" title="Djinn Inferno" /></td>
|
<img src="{{ icons['Djinn Inferno'] }}" title="Djinn Inferno" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="C4">
|
||||||
{% if 'Royal Ring' in acquired_items %}
|
{% if 'Royal Ring' in acquired_items %}
|
||||||
<td><img src="{{ icons['Royal Ring'] }}" class="acquired" title="Royal Ring" /></td>
|
<img src="{{ icons['Royal Ring'] }}" class="acquired" title="Royal Ring" />
|
||||||
{% elif 'Plasma Geyser' in acquired_items %}
|
{% elif 'Plasma Geyser' in acquired_items %}
|
||||||
<td><img src="{{ icons['Plasma Geyser'] }}" class="acquired" title="Plasma Geyser" /></td>
|
<img src="{{ icons['Plasma Geyser'] }}" class="acquired" title="Plasma Geyser" />
|
||||||
{% elif 'Plasma Orb' in acquired_items %}
|
{% elif 'Plasma Orb' in acquired_items %}
|
||||||
<td><img src="{{ icons['Plasma Orb'] }}" class="acquired" title="Plasma Orb" /></td>
|
<img src="{{ icons['Plasma Orb'] }}" class="acquired" title="Plasma Orb" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><img src="{{ icons['Royal Ring'] }}" title="Royal Ring" /></td>
|
<img src="{{ icons['Royal Ring'] }}" title="Royal Ring" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table id="location-table">
|
<table id="location-table">
|
||||||
{% for area in checks_done %}
|
{% for area in checks_done %}
|
||||||
<tr class="location-category" id="{{area}}-header">
|
<tr class="location-category" id="{{area}}-header">
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ from typing import Counter, Optional, Dict, Any, Tuple
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
from jinja2 import pass_context, runtime
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
from MultiServer import Context, get_saving_second
|
from MultiServer import Context, get_saving_second
|
||||||
from NetUtils import SlotType
|
from NetUtils import SlotType
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package
|
||||||
from worlds.alttp import Items
|
from worlds.alttp import Items
|
||||||
from . import app, cache
|
from . import app, cache
|
||||||
from .models import Room
|
from .models import GameDataPackage, Room
|
||||||
|
|
||||||
alttp_icons = {
|
alttp_icons = {
|
||||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||||
@@ -83,9 +84,6 @@ def get_alttp_id(item_name):
|
|||||||
return Items.item_table[item_name][2]
|
return Items.item_table[item_name][2]
|
||||||
|
|
||||||
|
|
||||||
app.jinja_env.filters["location_name"] = lambda location: lookup_any_location_id_to_name.get(location, location)
|
|
||||||
app.jinja_env.filters['item_name'] = lambda id: lookup_any_item_id_to_name.get(id, id)
|
|
||||||
|
|
||||||
links = {"Bow": "Progressive Bow",
|
links = {"Bow": "Progressive Bow",
|
||||||
"Silver Arrows": "Progressive Bow",
|
"Silver Arrows": "Progressive Bow",
|
||||||
"Silver Bow": "Progressive Bow",
|
"Silver Bow": "Progressive Bow",
|
||||||
@@ -212,14 +210,6 @@ del data
|
|||||||
del item
|
del item
|
||||||
|
|
||||||
|
|
||||||
def attribute_item(inventory, team, recipient, item):
|
|
||||||
target_item = links.get(item, item)
|
|
||||||
if item in levels: # non-progressive
|
|
||||||
inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
|
|
||||||
else:
|
|
||||||
inventory[team][recipient][target_item] += 1
|
|
||||||
|
|
||||||
|
|
||||||
def attribute_item_solo(inventory, item):
|
def attribute_item_solo(inventory, item):
|
||||||
"""Adds item to inventory counter, converts everything to progressive."""
|
"""Adds item to inventory counter, converts everything to progressive."""
|
||||||
target_item = links.get(item, item)
|
target_item = links.get(item, item)
|
||||||
@@ -237,6 +227,23 @@ def render_timedelta(delta: datetime.timedelta):
|
|||||||
return f"{hours}:{minutes}"
|
return f"{hours}:{minutes}"
|
||||||
|
|
||||||
|
|
||||||
|
@pass_context
|
||||||
|
def get_location_name(context: runtime.Context, loc: int) -> str:
|
||||||
|
# once all rooms embed data package, the chain lookup can be dropped
|
||||||
|
context_locations = context.get("custom_locations", {})
|
||||||
|
return collections.ChainMap(context_locations, lookup_any_location_id_to_name).get(loc, loc)
|
||||||
|
|
||||||
|
|
||||||
|
@pass_context
|
||||||
|
def get_item_name(context: runtime.Context, item: int) -> str:
|
||||||
|
context_items = context.get("custom_items", {})
|
||||||
|
return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item)
|
||||||
|
|
||||||
|
|
||||||
|
app.jinja_env.filters["location_name"] = get_location_name
|
||||||
|
app.jinja_env.filters["item_name"] = get_item_name
|
||||||
|
|
||||||
|
|
||||||
_multidata_cache = {}
|
_multidata_cache = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -258,10 +265,33 @@ def get_static_room_data(room: Room):
|
|||||||
# in > 100 players this can take a bit of time and is the main reason for the cache
|
# in > 100 players this can take a bit of time and is the main reason for the cache
|
||||||
locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
|
locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
|
||||||
names: Dict[int, Dict[int, str]] = multidata["names"]
|
names: Dict[int, Dict[int, str]] = multidata["names"]
|
||||||
|
games = {}
|
||||||
groups = {}
|
groups = {}
|
||||||
|
custom_locations = {}
|
||||||
|
custom_items = {}
|
||||||
if "slot_info" in multidata:
|
if "slot_info" in multidata:
|
||||||
|
games = {slot: slot_info.game for slot, slot_info in multidata["slot_info"].items()}
|
||||||
groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items()
|
groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items()
|
||||||
if slot_info.type == SlotType.group}
|
if slot_info.type == SlotType.group}
|
||||||
|
|
||||||
|
for game in games.values():
|
||||||
|
if game not in multidata["datapackage"]:
|
||||||
|
continue
|
||||||
|
game_data = multidata["datapackage"][game]
|
||||||
|
if "checksum" in game_data:
|
||||||
|
if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]:
|
||||||
|
# non-custom. remove from multidata
|
||||||
|
# network_data_package import could be skipped once all rooms embed data package
|
||||||
|
del multidata["datapackage"][game]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
|
||||||
|
custom_locations.update(
|
||||||
|
{id_: name for name, id_ in game_data["location_name_to_id"].items()})
|
||||||
|
custom_items.update(
|
||||||
|
{id_: name for name, id_ in game_data["item_name_to_id"].items()})
|
||||||
|
elif "games" in multidata:
|
||||||
|
games = multidata["games"]
|
||||||
seed_checks_in_area = checks_in_area.copy()
|
seed_checks_in_area = checks_in_area.copy()
|
||||||
|
|
||||||
use_door_tracker = False
|
use_door_tracker = False
|
||||||
@@ -282,7 +312,8 @@ def get_static_room_data(room: Room):
|
|||||||
if playernumber not in groups}
|
if playernumber not in groups}
|
||||||
saving_second = get_saving_second(multidata["seed_name"])
|
saving_second = get_saving_second(multidata["seed_name"])
|
||||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
||||||
multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups, saving_second
|
multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \
|
||||||
|
custom_locations, custom_items
|
||||||
_multidata_cache[room.seed.id] = result
|
_multidata_cache[room.seed.id] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -309,7 +340,8 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
|
|||||||
|
|
||||||
# Collect seed information and pare it down to a single player
|
# Collect seed information and pare it down to a single player
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||||
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
|
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||||
|
get_static_room_data(room)
|
||||||
player_name = names[tracked_team][tracked_player - 1]
|
player_name = names[tracked_team][tracked_player - 1]
|
||||||
location_to_area = player_location_to_area[tracked_player]
|
location_to_area = player_location_to_area[tracked_player]
|
||||||
inventory = collections.Counter()
|
inventory = collections.Counter()
|
||||||
@@ -351,7 +383,7 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
|
|||||||
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
|
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
|
||||||
else:
|
else:
|
||||||
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||||
seed_checks_in_area, checks_done, saving_second)
|
seed_checks_in_area, checks_done, saving_second, custom_locations, custom_items)
|
||||||
|
|
||||||
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
|
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
|
||||||
|
|
||||||
@@ -457,7 +489,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
|
|||||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||||
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
||||||
"Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
|
"Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
|
||||||
"Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
|
"Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
|
||||||
"Saddle": "https://i.imgur.com/2QtDyR0.png",
|
"Saddle": "https://i.imgur.com/2QtDyR0.png",
|
||||||
"Channeling Book": "https://i.imgur.com/J3WsYZw.png",
|
"Channeling Book": "https://i.imgur.com/J3WsYZw.png",
|
||||||
"Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
|
"Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
|
||||||
@@ -465,7 +497,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
|
|||||||
}
|
}
|
||||||
|
|
||||||
minecraft_location_ids = {
|
minecraft_location_ids = {
|
||||||
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
|
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
|
||||||
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
|
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
|
||||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
|
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
|
||||||
@@ -627,7 +659,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
|||||||
|
|
||||||
if base_name == "hookshot":
|
if base_name == "hookshot":
|
||||||
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
|
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
|
||||||
if base_name == "wallet":
|
if base_name == "wallet":
|
||||||
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
|
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
|
||||||
|
|
||||||
# Determine display for bottles. Show letter if it's obtained, determine bottle count
|
# Determine display for bottles. Show letter if it's obtained, determine bottle count
|
||||||
@@ -645,7 +677,6 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
|||||||
}
|
}
|
||||||
for item_name, item_id in multi_items.items():
|
for item_name, item_id in multi_items.items():
|
||||||
base_name = item_name.split()[-1].lower()
|
base_name = item_name.split()[-1].lower()
|
||||||
count = inventory[item_id]
|
|
||||||
display_data[base_name+"_count"] = inventory[item_id]
|
display_data[base_name+"_count"] = inventory[item_id]
|
||||||
|
|
||||||
# Gather dungeon locations
|
# Gather dungeon locations
|
||||||
@@ -775,7 +806,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
|||||||
}
|
}
|
||||||
|
|
||||||
timespinner_location_ids = {
|
timespinner_location_ids = {
|
||||||
"Present": [
|
"Present": [
|
||||||
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
|
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
|
||||||
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
|
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
|
||||||
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
|
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
|
||||||
@@ -796,20 +827,20 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
|||||||
1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
|
1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
|
||||||
1337171, 1337172, 1337173, 1337174, 1337175],
|
1337171, 1337172, 1337173, 1337174, 1337175],
|
||||||
"Ancient Pyramid": [
|
"Ancient Pyramid": [
|
||||||
1337236,
|
1337236,
|
||||||
1337246, 1337247, 1337248, 1337249]
|
1337246, 1337247, 1337248, 1337249]
|
||||||
}
|
}
|
||||||
|
|
||||||
if(slot_data["DownloadableItems"]):
|
if(slot_data["DownloadableItems"]):
|
||||||
timespinner_location_ids["Present"] += [
|
timespinner_location_ids["Present"] += [
|
||||||
1337156, 1337157, 1337159,
|
1337156, 1337157, 1337159,
|
||||||
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
|
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
|
||||||
1337170]
|
1337170]
|
||||||
if(slot_data["Cantoran"]):
|
if(slot_data["Cantoran"]):
|
||||||
timespinner_location_ids["Past"].append(1337176)
|
timespinner_location_ids["Past"].append(1337176)
|
||||||
if(slot_data["LoreChecks"]):
|
if(slot_data["LoreChecks"]):
|
||||||
timespinner_location_ids["Present"] += [
|
timespinner_location_ids["Present"] += [
|
||||||
1337177, 1337178, 1337179,
|
1337177, 1337178, 1337179,
|
||||||
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
|
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
|
||||||
timespinner_location_ids["Past"] += [
|
timespinner_location_ids["Past"] += [
|
||||||
1337188, 1337189,
|
1337188, 1337189,
|
||||||
@@ -1190,11 +1221,89 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||||
**display_data)
|
**display_data)
|
||||||
|
|
||||||
|
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||||
|
inventory: Counter, team: int, player: int, playerName: str,
|
||||||
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str:
|
||||||
|
|
||||||
|
icons = {
|
||||||
|
"Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png",
|
||||||
|
"Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png",
|
||||||
|
"Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png",
|
||||||
|
"Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png",
|
||||||
|
|
||||||
|
"Nothing": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
checksfinder_location_ids = {
|
||||||
|
"Tile 1": 81000,
|
||||||
|
"Tile 2": 81001,
|
||||||
|
"Tile 3": 81002,
|
||||||
|
"Tile 4": 81003,
|
||||||
|
"Tile 5": 81004,
|
||||||
|
"Tile 6": 81005,
|
||||||
|
"Tile 7": 81006,
|
||||||
|
"Tile 8": 81007,
|
||||||
|
"Tile 9": 81008,
|
||||||
|
"Tile 10": 81009,
|
||||||
|
"Tile 11": 81010,
|
||||||
|
"Tile 12": 81011,
|
||||||
|
"Tile 13": 81012,
|
||||||
|
"Tile 14": 81013,
|
||||||
|
"Tile 15": 81014,
|
||||||
|
"Tile 16": 81015,
|
||||||
|
"Tile 17": 81016,
|
||||||
|
"Tile 18": 81017,
|
||||||
|
"Tile 19": 81018,
|
||||||
|
"Tile 20": 81019,
|
||||||
|
"Tile 21": 81020,
|
||||||
|
"Tile 22": 81021,
|
||||||
|
"Tile 23": 81022,
|
||||||
|
"Tile 24": 81023,
|
||||||
|
"Tile 25": 81024,
|
||||||
|
}
|
||||||
|
|
||||||
|
display_data = {}
|
||||||
|
|
||||||
|
# Multi-items
|
||||||
|
multi_items = {
|
||||||
|
"Map Width": 80000,
|
||||||
|
"Map Height": 80001,
|
||||||
|
"Map Bombs": 80002
|
||||||
|
}
|
||||||
|
for item_name, item_id in multi_items.items():
|
||||||
|
base_name = item_name.split()[-1].lower()
|
||||||
|
count = inventory[item_id]
|
||||||
|
display_data[base_name + "_count"] = count
|
||||||
|
display_data[base_name + "_display"] = count + 5
|
||||||
|
|
||||||
|
# Get location info
|
||||||
|
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||||
|
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||||
|
location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])}
|
||||||
|
checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])}
|
||||||
|
checks_done['Total'] = len(checked_locations)
|
||||||
|
checks_in_area = checks_done
|
||||||
|
|
||||||
|
# Calculate checks available
|
||||||
|
display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25)
|
||||||
|
display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0)
|
||||||
|
|
||||||
|
# Victory condition
|
||||||
|
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||||
|
display_data['game_finished'] = game_state == 30
|
||||||
|
|
||||||
|
return render_template("checksfinderTracker.html",
|
||||||
|
inventory=inventory, icons=icons,
|
||||||
|
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||||
|
id in lookup_any_item_id_to_name},
|
||||||
|
player=player, team=team, room=room, player_name=playerName,
|
||||||
|
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||||
|
**display_data)
|
||||||
|
|
||||||
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||||
inventory: Counter, team: int, player: int, playerName: str,
|
inventory: Counter, team: int, player: int, playerName: str,
|
||||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
|
||||||
saving_second: int) -> str:
|
saving_second: int, custom_locations: Dict[int, str], custom_items: Dict[int, str]) -> str:
|
||||||
|
|
||||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||||
player_received_items = {}
|
player_received_items = {}
|
||||||
@@ -1212,26 +1321,45 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
|
|||||||
player=player, team=team, room=room, player_name=playerName,
|
player=player, team=team, room=room, player_name=playerName,
|
||||||
checked_locations=checked_locations,
|
checked_locations=checked_locations,
|
||||||
not_checked_locations=set(locations[player]) - checked_locations,
|
not_checked_locations=set(locations[player]) - checked_locations,
|
||||||
received_items=player_received_items,
|
received_items=player_received_items, saving_second=saving_second,
|
||||||
saving_second=saving_second)
|
custom_items=custom_items, custom_locations=custom_locations)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tracker/<suuid:tracker>')
|
def get_enabled_multiworld_trackers(room: Room, current: str):
|
||||||
@cache.memoize(timeout=1) # multisave is currently created at most every minute
|
enabled = [
|
||||||
def getTracker(tracker: UUID):
|
{
|
||||||
|
"name": "Generic",
|
||||||
|
"endpoint": "get_multiworld_tracker",
|
||||||
|
"current": current == "Generic"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for game_name, endpoint in multi_trackers.items():
|
||||||
|
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
|
||||||
|
enabled.append({
|
||||||
|
"name": game_name,
|
||||||
|
"endpoint": endpoint.__name__,
|
||||||
|
"current": current == game_name}
|
||||||
|
)
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
|
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||||
room: Room = Room.get(tracker=tracker)
|
room: Room = Room.get(tracker=tracker)
|
||||||
if not room:
|
if not room:
|
||||||
abort(404)
|
return None
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
|
||||||
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
|
|
||||||
|
|
||||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
|
||||||
for teamnumber, team in enumerate(names)}
|
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||||
|
get_static_room_data(room)
|
||||||
|
|
||||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||||
for teamnumber, team in enumerate(names)}
|
for teamnumber, team in enumerate(names)}
|
||||||
|
|
||||||
|
percent_total_checks_done = {teamnumber: {playernumber: 0
|
||||||
|
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||||
|
for teamnumber, team in enumerate(names)}
|
||||||
|
|
||||||
hints = {team: set() for team in range(len(names))}
|
hints = {team: set() for team in range(len(names))}
|
||||||
if room.multisave:
|
if room.multisave:
|
||||||
multisave = restricted_loads(room.multisave)
|
multisave = restricted_loads(room.multisave)
|
||||||
@@ -1241,6 +1369,128 @@ def getTracker(tracker: UUID):
|
|||||||
for (team, slot), slot_hints in multisave["hints"].items():
|
for (team, slot), slot_hints in multisave["hints"].items():
|
||||||
hints[team] |= set(slot_hints)
|
hints[team] |= set(slot_hints)
|
||||||
|
|
||||||
|
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||||
|
if player in groups:
|
||||||
|
continue
|
||||||
|
player_locations = locations[player]
|
||||||
|
checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations)
|
||||||
|
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
|
||||||
|
checks_in_area[player]["Total"] * 100) \
|
||||||
|
if checks_in_area[player]["Total"] else 100
|
||||||
|
|
||||||
|
activity_timers = {}
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
||||||
|
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||||
|
|
||||||
|
player_names = {}
|
||||||
|
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||||
|
for team, names in enumerate(names):
|
||||||
|
for player, name in enumerate(names, 1):
|
||||||
|
player_names[team, player] = name
|
||||||
|
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||||
|
long_player_names = player_names.copy()
|
||||||
|
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||||
|
player_names[team, player] = alias
|
||||||
|
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
|
||||||
|
|
||||||
|
video = {}
|
||||||
|
for (team, player), data in multisave.get("video", []):
|
||||||
|
video[team, player] = data
|
||||||
|
|
||||||
|
return dict(player_names=player_names, room=room, checks_done=checks_done,
|
||||||
|
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
|
||||||
|
activity_timers=activity_timers, video=video, hints=hints,
|
||||||
|
long_player_names=long_player_names,
|
||||||
|
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
||||||
|
locations=locations, games=games, states=states)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
||||||
|
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
|
||||||
|
for teamnumber, team_data in data["checks_done"].items()}
|
||||||
|
|
||||||
|
groups = data["groups"]
|
||||||
|
|
||||||
|
for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items():
|
||||||
|
if player in data["groups"]:
|
||||||
|
continue
|
||||||
|
player_locations = data["locations"][player]
|
||||||
|
precollected = data["precollected_items"][player]
|
||||||
|
for item_id in precollected:
|
||||||
|
inventory[team][player][item_id] += 1
|
||||||
|
for location in locations_checked:
|
||||||
|
item_id, recipient, flags = player_locations[location]
|
||||||
|
recipients = groups.get(recipient, [recipient])
|
||||||
|
for recipient in recipients:
|
||||||
|
inventory[team][recipient][item_id] += 1
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tracker/<suuid:tracker>')
|
||||||
|
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||||
|
def get_multiworld_tracker(tracker: UUID):
|
||||||
|
data = _get_multiworld_tracker_data(tracker)
|
||||||
|
if not data:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
|
||||||
|
|
||||||
|
return render_template("multiTracker.html", **data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tracker/<suuid:tracker>/Factorio')
|
||||||
|
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||||
|
def get_Factorio_multiworld_tracker(tracker: UUID):
|
||||||
|
data = _get_multiworld_tracker_data(tracker)
|
||||||
|
if not data:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
data["inventory"] = _get_inventory_data(data)
|
||||||
|
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
|
||||||
|
|
||||||
|
return render_template("multiFactorioTracker.html", **data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tracker/<suuid:tracker>/A Link to the Past')
|
||||||
|
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||||
|
def get_LttP_multiworld_tracker(tracker: UUID):
|
||||||
|
room: Room = Room.get(tracker=tracker)
|
||||||
|
if not room:
|
||||||
|
abort(404)
|
||||||
|
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||||
|
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||||
|
get_static_room_data(room)
|
||||||
|
|
||||||
|
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if
|
||||||
|
playernumber not in groups}
|
||||||
|
for teamnumber, team in enumerate(names)}
|
||||||
|
|
||||||
|
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||||
|
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||||
|
for teamnumber, team in enumerate(names)}
|
||||||
|
|
||||||
|
percent_total_checks_done = {teamnumber: {playernumber: 0
|
||||||
|
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||||
|
for teamnumber, team in enumerate(names)}
|
||||||
|
|
||||||
|
hints = {team: set() for team in range(len(names))}
|
||||||
|
if room.multisave:
|
||||||
|
multisave = restricted_loads(room.multisave)
|
||||||
|
else:
|
||||||
|
multisave = {}
|
||||||
|
if "hints" in multisave:
|
||||||
|
for (team, slot), slot_hints in multisave["hints"].items():
|
||||||
|
hints[team] |= set(slot_hints)
|
||||||
|
|
||||||
|
def attribute_item(team: int, recipient: int, item: int):
|
||||||
|
nonlocal inventory
|
||||||
|
target_item = links.get(item, item)
|
||||||
|
if item in levels: # non-progressive
|
||||||
|
inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
|
||||||
|
else:
|
||||||
|
inventory[team][recipient][target_item] += 1
|
||||||
|
|
||||||
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||||
if player in groups:
|
if player in groups:
|
||||||
continue
|
continue
|
||||||
@@ -1248,17 +1498,19 @@ def getTracker(tracker: UUID):
|
|||||||
if precollected_items:
|
if precollected_items:
|
||||||
precollected = precollected_items[player]
|
precollected = precollected_items[player]
|
||||||
for item_id in precollected:
|
for item_id in precollected:
|
||||||
attribute_item(inventory, team, player, item_id)
|
attribute_item(team, player, item_id)
|
||||||
for location in locations_checked:
|
for location in locations_checked:
|
||||||
if location not in player_locations or location not in player_location_to_area[player]:
|
if location not in player_locations or location not in player_location_to_area[player]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item, recipient, flags = player_locations[location]
|
item, recipient, flags = player_locations[location]
|
||||||
|
recipients = groups.get(recipient, [recipient])
|
||||||
if recipient in names:
|
for recipient in recipients:
|
||||||
attribute_item(inventory, team, recipient, item)
|
attribute_item(team, recipient, item)
|
||||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||||
checks_done[team][player]["Total"] += 1
|
checks_done[team][player]["Total"] += 1
|
||||||
|
percent_total_checks_done[team][player] = int(
|
||||||
|
checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if \
|
||||||
|
seed_checks_in_area[player]["Total"] else 100
|
||||||
|
|
||||||
for (team, player), game_state in multisave.get("client_game_state", {}).items():
|
for (team, player), game_state in multisave.get("client_game_state", {}).items():
|
||||||
if player in groups:
|
if player in groups:
|
||||||
@@ -1300,14 +1552,19 @@ def getTracker(tracker: UUID):
|
|||||||
for (team, player), data in multisave.get("video", []):
|
for (team, player), data in multisave.get("video", []):
|
||||||
video[(team, player)] = data
|
video[(team, player)] = data
|
||||||
|
|
||||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past")
|
||||||
|
|
||||||
|
return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
||||||
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
||||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
||||||
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
multi_items=multi_items, checks_done=checks_done,
|
||||||
checks_in_area=seed_checks_in_area, activity_timers=activity_timers,
|
percent_total_checks_done=percent_total_checks_done,
|
||||||
|
ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area,
|
||||||
|
activity_timers=activity_timers,
|
||||||
key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
|
key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
|
||||||
video=video, big_key_locations=group_big_key_locations,
|
video=video, big_key_locations=group_big_key_locations,
|
||||||
hints=hints, long_player_names=long_player_names)
|
hints=hints, long_player_names=long_player_names,
|
||||||
|
enabled_multiworld_trackers=enabled_multiworld_trackers)
|
||||||
|
|
||||||
|
|
||||||
game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
||||||
@@ -1315,6 +1572,12 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
|||||||
"Ocarina of Time": __renderOoTTracker,
|
"Ocarina of Time": __renderOoTTracker,
|
||||||
"Timespinner": __renderTimespinnerTracker,
|
"Timespinner": __renderTimespinnerTracker,
|
||||||
"A Link to the Past": __renderAlttpTracker,
|
"A Link to the Past": __renderAlttpTracker,
|
||||||
|
"ChecksFinder": __renderChecksfinder,
|
||||||
"Super Metroid": __renderSuperMetroidTracker,
|
"Super Metroid": __renderSuperMetroidTracker,
|
||||||
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
|
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
multi_trackers: typing.Dict[str, typing.Callable] = {
|
||||||
|
"A Link to the Past": get_LttP_multiworld_tracker,
|
||||||
|
"Factorio": get_Factorio_multiworld_tracker,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import pickle
|
||||||
import typing
|
import typing
|
||||||
import uuid
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
from io import BytesIO
|
import zlib
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
from flask import request, flash, redirect, url_for, session, render_template, Markup
|
from flask import request, flash, redirect, url_for, session, render_template, Markup
|
||||||
from pony.orm import flush, select
|
from pony.orm import commit, flush, select, rollback
|
||||||
|
from pony.orm.core import TransactionIntegrityError
|
||||||
|
|
||||||
import MultiServer
|
import MultiServer
|
||||||
from NetUtils import NetworkSlot, SlotType
|
from NetUtils import NetworkSlot, SlotType
|
||||||
from Utils import VersionException, __version__
|
from Utils import VersionException, __version__
|
||||||
from worlds.Files import AutoPatchRegister
|
from worlds.Files import AutoPatchRegister
|
||||||
from . import app
|
from . import app
|
||||||
from .models import Seed, Room, Slot
|
from .models import Seed, Room, Slot, GameDataPackage
|
||||||
|
|
||||||
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||||
|
|
||||||
@@ -78,6 +81,27 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||||||
# Load multi data.
|
# Load multi data.
|
||||||
if multidata:
|
if multidata:
|
||||||
decompressed_multidata = MultiServer.Context.decompress(multidata)
|
decompressed_multidata = MultiServer.Context.decompress(multidata)
|
||||||
|
recompress = False
|
||||||
|
|
||||||
|
if "datapackage" in decompressed_multidata:
|
||||||
|
# strip datapackage from multidata, leaving only the checksums
|
||||||
|
game_data_packages: typing.List[GameDataPackage] = []
|
||||||
|
for game, game_data in decompressed_multidata["datapackage"].items():
|
||||||
|
if game_data.get("checksum"):
|
||||||
|
game_data_package = GameDataPackage(checksum=game_data["checksum"],
|
||||||
|
data=pickle.dumps(game_data))
|
||||||
|
decompressed_multidata["datapackage"][game] = {
|
||||||
|
"version": game_data.get("version", 0),
|
||||||
|
"checksum": game_data["checksum"]
|
||||||
|
}
|
||||||
|
recompress = True
|
||||||
|
try:
|
||||||
|
commit() # commit game data package
|
||||||
|
game_data_packages.append(game_data_package)
|
||||||
|
except TransactionIntegrityError:
|
||||||
|
del game_data_package
|
||||||
|
rollback()
|
||||||
|
|
||||||
if "slot_info" in decompressed_multidata:
|
if "slot_info" in decompressed_multidata:
|
||||||
for slot, slot_info in decompressed_multidata["slot_info"].items():
|
for slot, slot_info in decompressed_multidata["slot_info"].items():
|
||||||
# Ignore Player Groups (e.g. item links)
|
# Ignore Player Groups (e.g. item links)
|
||||||
@@ -90,6 +114,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||||||
|
|
||||||
flush() # commit slots
|
flush() # commit slots
|
||||||
|
|
||||||
|
if recompress:
|
||||||
|
multidata = multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
|
||||||
|
|
||||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
||||||
id=sid if sid else uuid.uuid4())
|
id=sid if sid else uuid.uuid4())
|
||||||
flush() # create seed
|
flush() # create seed
|
||||||
|
|||||||
393
Zelda1Client.py
Normal file
393
Zelda1Client.py
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
# Based (read: copied almost wholesale and edited) off the FF1 Client.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import typing
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
from Utils import async_start
|
||||||
|
from worlds import lookup_any_location_id_to_name
|
||||||
|
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
|
||||||
|
get_base_parser
|
||||||
|
|
||||||
|
from worlds.tloz.Items import item_game_ids
|
||||||
|
from worlds.tloz.Locations import location_ids
|
||||||
|
from worlds.tloz import Items, Locations, Rom
|
||||||
|
|
||||||
|
SYSTEM_MESSAGE_ID = 0
|
||||||
|
|
||||||
|
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua"
|
||||||
|
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running"
|
||||||
|
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua"
|
||||||
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||||
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||||
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||||
|
|
||||||
|
DISPLAY_MSGS = True
|
||||||
|
|
||||||
|
item_ids = item_game_ids
|
||||||
|
location_ids = location_ids
|
||||||
|
items_by_id = {id: item for item, id in item_ids.items()}
|
||||||
|
locations_by_id = {id: location for location, id in location_ids.items()}
|
||||||
|
|
||||||
|
|
||||||
|
class ZeldaCommandProcessor(ClientCommandProcessor):
|
||||||
|
|
||||||
|
def _cmd_nes(self):
|
||||||
|
"""Check NES Connection State"""
|
||||||
|
if isinstance(self.ctx, ZeldaContext):
|
||||||
|
logger.info(f"NES Status: {self.ctx.nes_status}")
|
||||||
|
|
||||||
|
def _cmd_toggle_msgs(self):
|
||||||
|
"""Toggle displaying messages in bizhawk"""
|
||||||
|
global DISPLAY_MSGS
|
||||||
|
DISPLAY_MSGS = not DISPLAY_MSGS
|
||||||
|
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
||||||
|
|
||||||
|
|
||||||
|
class ZeldaContext(CommonContext):
|
||||||
|
command_processor = ZeldaCommandProcessor
|
||||||
|
items_handling = 0b101 # get sent remote and starting items
|
||||||
|
# Infinite Hyrule compatibility
|
||||||
|
overworld_item = 0x5F
|
||||||
|
armos_item = 0x24
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super().__init__(server_address, password)
|
||||||
|
self.bonus_items = []
|
||||||
|
self.nes_streams: (StreamReader, StreamWriter) = None
|
||||||
|
self.nes_sync_task = None
|
||||||
|
self.messages = {}
|
||||||
|
self.locations_array = None
|
||||||
|
self.nes_status = CONNECTION_INITIAL_STATUS
|
||||||
|
self.game = 'The Legend of Zelda'
|
||||||
|
self.awaiting_rom = False
|
||||||
|
self.shop_slots_left = 0
|
||||||
|
self.shop_slots_middle = 0
|
||||||
|
self.shop_slots_right = 0
|
||||||
|
self.shop_slots = [self.shop_slots_left, self.shop_slots_middle, self.shop_slots_right]
|
||||||
|
self.slot_data = dict()
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super(ZeldaContext, self).server_auth(password_requested)
|
||||||
|
if not self.auth:
|
||||||
|
self.awaiting_rom = True
|
||||||
|
logger.info('Awaiting connection to NES to get Player information')
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.send_connect()
|
||||||
|
|
||||||
|
def _set_message(self, msg: str, msg_id: int):
|
||||||
|
if DISPLAY_MSGS:
|
||||||
|
self.messages[(time.time(), msg_id)] = msg
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd == 'Connected':
|
||||||
|
self.slot_data = args.get("slot_data", {})
|
||||||
|
asyncio.create_task(parse_locations(self.locations_array, self, True))
|
||||||
|
elif cmd == 'Print':
|
||||||
|
msg = args['text']
|
||||||
|
if ': !' not in msg:
|
||||||
|
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||||
|
|
||||||
|
def on_print_json(self, args: dict):
|
||||||
|
if self.ui:
|
||||||
|
self.ui.print_json(copy.deepcopy(args["data"]))
|
||||||
|
else:
|
||||||
|
text = self.jsontotextparser(copy.deepcopy(args["data"]))
|
||||||
|
logger.info(text)
|
||||||
|
relevant = args.get("type", None) in {"Hint", "ItemSend"}
|
||||||
|
if relevant:
|
||||||
|
item = args["item"]
|
||||||
|
# goes to this world
|
||||||
|
if self.slot_concerns_self(args["receiving"]):
|
||||||
|
relevant = True
|
||||||
|
# found in this world
|
||||||
|
elif self.slot_concerns_self(item.player):
|
||||||
|
relevant = True
|
||||||
|
# not related
|
||||||
|
else:
|
||||||
|
relevant = False
|
||||||
|
if relevant:
|
||||||
|
item = args["item"]
|
||||||
|
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
|
||||||
|
self._set_message(msg, item.item)
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
from kvui import GameManager
|
||||||
|
|
||||||
|
class ZeldaManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
|
base_title = "Archipelago Zelda 1 Client"
|
||||||
|
|
||||||
|
self.ui = ZeldaManager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
|
||||||
|
def get_payload(ctx: ZeldaContext):
|
||||||
|
current_time = time.time()
|
||||||
|
bonus_items = [item for item in ctx.bonus_items]
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"items": [item.item for item in ctx.items_received],
|
||||||
|
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||||
|
if key[0] > current_time - 10},
|
||||||
|
"shops": {
|
||||||
|
"left": ctx.shop_slots_left,
|
||||||
|
"middle": ctx.shop_slots_middle,
|
||||||
|
"right": ctx.shop_slots_right
|
||||||
|
},
|
||||||
|
"bonusItems": bonus_items
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_shops(ctx: ZeldaContext):
|
||||||
|
checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
|
||||||
|
shops = [location for location in checked_location_names if "Shop" in location]
|
||||||
|
left_slots = [shop for shop in shops if "Left" in shop]
|
||||||
|
middle_slots = [shop for shop in shops if "Middle" in shop]
|
||||||
|
right_slots = [shop for shop in shops if "Right" in shop]
|
||||||
|
for shop in left_slots:
|
||||||
|
ctx.shop_slots_left |= get_shop_bit_from_name(shop)
|
||||||
|
for shop in middle_slots:
|
||||||
|
ctx.shop_slots_middle |= get_shop_bit_from_name(shop)
|
||||||
|
for shop in right_slots:
|
||||||
|
ctx.shop_slots_right |= get_shop_bit_from_name(shop)
|
||||||
|
|
||||||
|
|
||||||
|
def get_shop_bit_from_name(location_name):
|
||||||
|
if "Potion" in location_name:
|
||||||
|
return Rom.potion_shop
|
||||||
|
elif "Arrow" in location_name:
|
||||||
|
return Rom.arrow_shop
|
||||||
|
elif "Shield" in location_name:
|
||||||
|
return Rom.shield_shop
|
||||||
|
elif "Ring" in location_name:
|
||||||
|
return Rom.ring_shop
|
||||||
|
elif "Candle" in location_name:
|
||||||
|
return Rom.candle_shop
|
||||||
|
elif "Take" in location_name:
|
||||||
|
return Rom.take_any
|
||||||
|
return 0 # this should never be hit
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone="None"):
|
||||||
|
if locations_array == ctx.locations_array and not force:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# print("New values")
|
||||||
|
ctx.locations_array = locations_array
|
||||||
|
locations_checked = []
|
||||||
|
location = None
|
||||||
|
for location in ctx.missing_locations:
|
||||||
|
location_name = lookup_any_location_id_to_name[location]
|
||||||
|
|
||||||
|
if location_name in Locations.overworld_locations and zone == "overworld":
|
||||||
|
status = locations_array[Locations.major_location_offsets[location_name]]
|
||||||
|
if location_name == "Ocean Heart Container":
|
||||||
|
status = locations_array[ctx.overworld_item]
|
||||||
|
if location_name == "Armos Knights":
|
||||||
|
status = locations_array[ctx.armos_item]
|
||||||
|
if status & 0x10:
|
||||||
|
ctx.locations_checked.add(location)
|
||||||
|
locations_checked.append(location)
|
||||||
|
elif location_name in Locations.underworld1_locations and zone == "underworld1":
|
||||||
|
status = locations_array[Locations.floor_location_game_offsets_early[location_name]]
|
||||||
|
if status & 0x10:
|
||||||
|
ctx.locations_checked.add(location)
|
||||||
|
locations_checked.append(location)
|
||||||
|
elif location_name in Locations.underworld2_locations and zone == "underworld2":
|
||||||
|
status = locations_array[Locations.floor_location_game_offsets_late[location_name]]
|
||||||
|
if status & 0x10:
|
||||||
|
ctx.locations_checked.add(location)
|
||||||
|
locations_checked.append(location)
|
||||||
|
elif (location_name in Locations.shop_locations or "Take" in location_name) and zone == "caves":
|
||||||
|
shop_bit = get_shop_bit_from_name(location_name)
|
||||||
|
slot = 0
|
||||||
|
context_slot = 0
|
||||||
|
if "Left" in location_name:
|
||||||
|
slot = "slot1"
|
||||||
|
context_slot = 0
|
||||||
|
elif "Middle" in location_name:
|
||||||
|
slot = "slot2"
|
||||||
|
context_slot = 1
|
||||||
|
elif "Right" in location_name:
|
||||||
|
slot = "slot3"
|
||||||
|
context_slot = 2
|
||||||
|
if locations_array[slot] & shop_bit > 0:
|
||||||
|
locations_checked.append(location)
|
||||||
|
ctx.shop_slots[context_slot] |= shop_bit
|
||||||
|
if locations_array["takeAnys"] and locations_array["takeAnys"] >= 4:
|
||||||
|
if "Take Any" in location_name:
|
||||||
|
short_name = None
|
||||||
|
if "Left" in location_name:
|
||||||
|
short_name = "TakeAnyLeft"
|
||||||
|
elif "Middle" in location_name:
|
||||||
|
short_name = "TakeAnyMiddle"
|
||||||
|
elif "Right" in location_name:
|
||||||
|
short_name = "TakeAnyRight"
|
||||||
|
if short_name is not None:
|
||||||
|
item_code = ctx.slot_data[short_name]
|
||||||
|
if item_code > 0:
|
||||||
|
ctx.bonus_items.append(item_code)
|
||||||
|
locations_checked.append(location)
|
||||||
|
if locations_checked:
|
||||||
|
await ctx.send_msgs([
|
||||||
|
{"cmd": "LocationChecks",
|
||||||
|
"locations": locations_checked}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
async def nes_sync_task(ctx: ZeldaContext):
|
||||||
|
logger.info("Starting nes connector. Use /nes for status information")
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
error_status = None
|
||||||
|
if ctx.nes_streams:
|
||||||
|
(reader, writer) = ctx.nes_streams
|
||||||
|
msg = get_payload(ctx).encode()
|
||||||
|
writer.write(msg)
|
||||||
|
writer.write(b'\n')
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
||||||
|
try:
|
||||||
|
# Data will return a dict with up to two fields:
|
||||||
|
# 1. A keepalive response of the Players Name (always)
|
||||||
|
# 2. An array representing the memory values of the locations area (if in game)
|
||||||
|
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||||
|
data_decoded = json.loads(data.decode())
|
||||||
|
if data_decoded["overworldHC"] is not None:
|
||||||
|
ctx.overworld_item = data_decoded["overworldHC"]
|
||||||
|
if data_decoded["overworldPB"] is not None:
|
||||||
|
ctx.armos_item = data_decoded["overworldPB"]
|
||||||
|
if data_decoded['gameMode'] == 19 and ctx.finished_game == False:
|
||||||
|
await ctx.send_msgs([
|
||||||
|
{"cmd": "StatusUpdate",
|
||||||
|
"status": 30}
|
||||||
|
])
|
||||||
|
ctx.finished_game = True
|
||||||
|
if ctx.game is not None and 'overworld' in data_decoded:
|
||||||
|
# Not just a keep alive ping, parse
|
||||||
|
asyncio.create_task(parse_locations(data_decoded['overworld'], ctx, False, "overworld"))
|
||||||
|
if ctx.game is not None and 'underworld1' in data_decoded:
|
||||||
|
asyncio.create_task(parse_locations(data_decoded['underworld1'], ctx, False, "underworld1"))
|
||||||
|
if ctx.game is not None and 'underworld2' in data_decoded:
|
||||||
|
asyncio.create_task(parse_locations(data_decoded['underworld2'], ctx, False, "underworld2"))
|
||||||
|
if ctx.game is not None and 'caves' in data_decoded:
|
||||||
|
asyncio.create_task(parse_locations(data_decoded['caves'], ctx, False, "caves"))
|
||||||
|
if not ctx.auth:
|
||||||
|
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||||
|
if ctx.auth == '':
|
||||||
|
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
|
||||||
|
"the ROM using the same link but adding your slot name")
|
||||||
|
if ctx.awaiting_rom:
|
||||||
|
await ctx.server_auth(False)
|
||||||
|
reconcile_shops(ctx)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.debug("Read Timed Out, Reconnecting")
|
||||||
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.nes_streams = None
|
||||||
|
except ConnectionResetError as e:
|
||||||
|
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.nes_streams = None
|
||||||
|
except TimeoutError:
|
||||||
|
logger.debug("Connection Timed Out, Reconnecting")
|
||||||
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.nes_streams = None
|
||||||
|
except ConnectionResetError:
|
||||||
|
logger.debug("Connection Lost, Reconnecting")
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.nes_streams = None
|
||||||
|
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
|
||||||
|
if not error_status:
|
||||||
|
logger.info("Successfully Connected to NES")
|
||||||
|
ctx.nes_status = CONNECTION_CONNECTED_STATUS
|
||||||
|
else:
|
||||||
|
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
|
||||||
|
elif error_status:
|
||||||
|
ctx.nes_status = error_status
|
||||||
|
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
logger.debug("Attempting to connect to NES")
|
||||||
|
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
|
||||||
|
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
|
||||||
|
except TimeoutError:
|
||||||
|
logger.debug("Connection Timed Out, Trying Again")
|
||||||
|
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
continue
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
logger.debug("Connection Refused, Trying Again")
|
||||||
|
ctx.nes_status = CONNECTION_REFUSED_STATUS
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Text Mode to use !hint and such with games that have no text entry
|
||||||
|
Utils.init_logging("ZeldaClient")
|
||||||
|
|
||||||
|
options = Utils.get_options()
|
||||||
|
DISPLAY_MSGS = options["tloz_options"]["display_msgs"]
|
||||||
|
|
||||||
|
|
||||||
|
async def run_game(romfile: str) -> None:
|
||||||
|
auto_start = typing.cast(typing.Union[bool, str],
|
||||||
|
Utils.get_options()["tloz_options"].get("rom_start", True))
|
||||||
|
if auto_start is True:
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open(romfile)
|
||||||
|
elif isinstance(auto_start, str) and os.path.isfile(auto_start):
|
||||||
|
subprocess.Popen([auto_start, romfile],
|
||||||
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
|
async def main(args):
|
||||||
|
if args.diff_file:
|
||||||
|
import Patch
|
||||||
|
logging.info("Patch file was supplied. Creating nes rom..")
|
||||||
|
meta, romfile = Patch.create_rom_file(args.diff_file)
|
||||||
|
if "server" in meta:
|
||||||
|
args.connect = meta["server"]
|
||||||
|
logging.info(f"Wrote rom file to {romfile}")
|
||||||
|
async_start(run_game(romfile))
|
||||||
|
ctx = ZeldaContext(args.connect, args.password)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
if ctx.nes_sync_task:
|
||||||
|
await ctx.nes_sync_task
|
||||||
|
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
parser = get_base_parser()
|
||||||
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
|
help='Path to a Archipelago Binary Patch file')
|
||||||
|
args = parser.parse_args()
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
asyncio.run(main(args))
|
||||||
|
colorama.deinit()
|
||||||
BIN
data/adventure_basepatch.bsdiff4
Normal file
BIN
data/adventure_basepatch.bsdiff4
Normal file
Binary file not shown.
@@ -1,4 +1,21 @@
|
|||||||
<TabbedPanel>
|
<TextColors>:
|
||||||
|
# Hex-format RGB colors used in clients. Resets after an update/install.
|
||||||
|
# To avoid, you can copy the TextColors section into a new "user.kv" next to this file
|
||||||
|
# and it will read from there instead.
|
||||||
|
black: "000000"
|
||||||
|
red: "EE0000"
|
||||||
|
green: "00FF7F" # typically a location
|
||||||
|
yellow: "FAFAD2" # typically other slots/players
|
||||||
|
blue: "6495ED" # typically extra info (such as entrance)
|
||||||
|
magenta: "EE00EE" # typically your slot/player
|
||||||
|
cyan: "00EEEE" # typically regular item
|
||||||
|
slateblue: "6D8BE8" # typically useful item
|
||||||
|
plum: "AF99EF" # typically progression item
|
||||||
|
salmon: "FA8072" # typically trap item
|
||||||
|
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
||||||
|
<Label>:
|
||||||
|
color: "FFFFFF"
|
||||||
|
<TabbedPanel>:
|
||||||
tab_width: root.width / app.tab_count
|
tab_width: root.width / app.tab_count
|
||||||
<SelectableLabel>:
|
<SelectableLabel>:
|
||||||
canvas.before:
|
canvas.before:
|
||||||
@@ -13,6 +30,8 @@
|
|||||||
font_size: dp(20)
|
font_size: dp(20)
|
||||||
markup: True
|
markup: True
|
||||||
<UILog>:
|
<UILog>:
|
||||||
|
messages: 1000 # amount of messages stored in client logs.
|
||||||
|
cols: 1
|
||||||
viewclass: 'SelectableLabel'
|
viewclass: 'SelectableLabel'
|
||||||
scroll_y: 0
|
scroll_y: 0
|
||||||
scroll_type: ["content", "bars"]
|
scroll_type: ["content", "bars"]
|
||||||
|
|||||||
851
data/lua/ADVENTURE/adventure_connector.lua
Normal file
851
data/lua/ADVENTURE/adventure_connector.lua
Normal file
@@ -0,0 +1,851 @@
|
|||||||
|
local socket = require("socket")
|
||||||
|
local json = require('json')
|
||||||
|
local math = require('math')
|
||||||
|
|
||||||
|
local STATE_OK = "Ok"
|
||||||
|
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||||
|
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||||
|
local STATE_UNINITIALIZED = "Uninitialized"
|
||||||
|
|
||||||
|
local SCRIPT_VERSION = 1
|
||||||
|
|
||||||
|
local APItemValue = 0xA2
|
||||||
|
local APItemRam = 0xE7
|
||||||
|
local BatAPItemValue = 0xAB
|
||||||
|
local BatAPItemRam = 0xEA
|
||||||
|
local PlayerRoomAddr = 0x8A -- if in number room, we're not in play mode
|
||||||
|
local WinAddr = 0xDE -- if not 0 (I think if 0xff specifically), we won (and should update once, immediately)
|
||||||
|
|
||||||
|
-- If any of these are 2, that dragon ate the player (should send update immediately
|
||||||
|
-- once, and reset that when none of them are 2 again)
|
||||||
|
|
||||||
|
local DragonState = {0xA8, 0xAD, 0xB2}
|
||||||
|
local last_dragon_state = {0, 0, 0}
|
||||||
|
local carryAddress = 0x9D -- uses rom object table
|
||||||
|
local batRoomAddr = 0xCB
|
||||||
|
local batCarryAddress = 0xD0 -- uses ram object location
|
||||||
|
local batInvalidCarryItem = 0x78
|
||||||
|
local batItemCheckAddr = 0xf69f
|
||||||
|
local batMatrixLen = 11 -- number of pairs
|
||||||
|
local last_carry_item = 0xB4
|
||||||
|
local frames_with_no_item = 0
|
||||||
|
local ItemTableStart = 0xfe9d
|
||||||
|
local PlayerSlotAddress = 0xfff9
|
||||||
|
|
||||||
|
local itemMessages = {}
|
||||||
|
|
||||||
|
local nullObjectId = 0xB4
|
||||||
|
local ItemsReceived = nil
|
||||||
|
local sha256hash = nil
|
||||||
|
local foreign_items = nil
|
||||||
|
local foreign_items_by_room = {}
|
||||||
|
local bat_no_touch_locations_by_room = {}
|
||||||
|
local bat_no_touch_items = {}
|
||||||
|
local autocollect_items = {}
|
||||||
|
local localItemLocations = {}
|
||||||
|
|
||||||
|
local prev_bat_room = 0xff
|
||||||
|
local prev_player_room = 0
|
||||||
|
local prev_ap_room_index = nil
|
||||||
|
|
||||||
|
local pending_foreign_items_collected = {}
|
||||||
|
local pending_local_items_collected = {}
|
||||||
|
local rendering_foreign_item = nil
|
||||||
|
local skip_inventory_items = {}
|
||||||
|
|
||||||
|
local inventory = {}
|
||||||
|
local next_inventory_item = nil
|
||||||
|
|
||||||
|
local input_button_address = 0xD7
|
||||||
|
|
||||||
|
local deathlink_rec = nil
|
||||||
|
local deathlink_send = 0
|
||||||
|
|
||||||
|
local deathlink_sent = false
|
||||||
|
|
||||||
|
local prevstate = ""
|
||||||
|
local curstate = STATE_UNINITIALIZED
|
||||||
|
local atariSocket = nil
|
||||||
|
local frame = 0
|
||||||
|
|
||||||
|
local ItemIndex = 0
|
||||||
|
|
||||||
|
local yorgle_speed_address = 0xf725
|
||||||
|
local grundle_speed_address = 0xf740
|
||||||
|
local rhindle_speed_address = 0xf70A
|
||||||
|
|
||||||
|
local read_switch_a = 0xf780
|
||||||
|
local read_switch_b = 0xf764
|
||||||
|
|
||||||
|
local yorgle_speed = nil
|
||||||
|
local grundle_speed = nil
|
||||||
|
local rhindle_speed = nil
|
||||||
|
|
||||||
|
local slow_yorgle_id = tostring(118000000 + 0x103)
|
||||||
|
local slow_grundle_id = tostring(118000000 + 0x104)
|
||||||
|
local slow_rhindle_id = tostring(118000000 + 0x105)
|
||||||
|
|
||||||
|
local yorgle_dead = false
|
||||||
|
local grundle_dead = false
|
||||||
|
local rhindle_dead = false
|
||||||
|
|
||||||
|
local diff_a_locked = false
|
||||||
|
local diff_b_locked = false
|
||||||
|
|
||||||
|
local bat_logic = 0
|
||||||
|
|
||||||
|
local is_dead = 0
|
||||||
|
local freeincarnates_available = 0
|
||||||
|
local send_freeincarnate_used = false
|
||||||
|
local current_bat_ap_item = nil
|
||||||
|
|
||||||
|
local was_in_number_room = false
|
||||||
|
|
||||||
|
local u8 = nil
|
||||||
|
local wU8 = nil
|
||||||
|
local u16
|
||||||
|
|
||||||
|
local bizhawk_version = client.getversion()
|
||||||
|
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
|
||||||
|
local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8")
|
||||||
|
|
||||||
|
u8 = memory.read_u8
|
||||||
|
wU8 = memory.write_u8
|
||||||
|
u16 = memory.read_u16_le
|
||||||
|
function uRangeRam(address, bytes)
|
||||||
|
data = memory.read_bytes_as_array(address, bytes, "Main RAM")
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
function uRangeRom(address, bytes)
|
||||||
|
data = memory.read_bytes_as_array(address+0xf000, bytes, "System Bus")
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
function uRangeAddress(address, bytes)
|
||||||
|
data = memory.read_bytes_as_array(address, bytes, "System Bus")
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function table.empty (self)
|
||||||
|
for _, _ in pairs(self) do
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function slice (tbl, s, e)
|
||||||
|
local pos, new = 1, {}
|
||||||
|
for i = s + 1, e do
|
||||||
|
new[pos] = tbl[i]
|
||||||
|
pos = pos + 1
|
||||||
|
end
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
local function createForeignItemsByRoom()
|
||||||
|
foreign_items_by_room = {}
|
||||||
|
if foreign_items == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, foreign_item in pairs(foreign_items) do
|
||||||
|
if foreign_items_by_room[foreign_item.room_id] == nil then
|
||||||
|
foreign_items_by_room[foreign_item.room_id] = {}
|
||||||
|
end
|
||||||
|
new_foreign_item = {}
|
||||||
|
new_foreign_item.room_id = foreign_item.room_id
|
||||||
|
new_foreign_item.room_x = foreign_item.room_x
|
||||||
|
new_foreign_item.room_y = foreign_item.room_y
|
||||||
|
new_foreign_item.short_location_id = foreign_item.short_location_id
|
||||||
|
|
||||||
|
table.insert(foreign_items_by_room[foreign_item.room_id], new_foreign_item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function debugPrintNoTouchLocations()
|
||||||
|
for room_id, list in pairs(bat_no_touch_locations_by_room) do
|
||||||
|
for index, notouch_location in ipairs(list) do
|
||||||
|
print("ROOM "..tostring(room_id).. "["..tostring(index).."]: "..tostring(notouch_location.short_location_id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function processBlock(block)
|
||||||
|
if block == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local block_identified = 0
|
||||||
|
local msgBlock = block['messages']
|
||||||
|
if msgBlock ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
for i, v in pairs(msgBlock) do
|
||||||
|
if itemMessages[i] == nil then
|
||||||
|
local msg = {TTL=450, message=v, color=0xFFFF0000}
|
||||||
|
itemMessages[i] = msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local itemsBlock = block["items"]
|
||||||
|
if itemsBlock ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
ItemsReceived = itemsBlock
|
||||||
|
end
|
||||||
|
local apItemsBlock = block["foreign_items"]
|
||||||
|
if apItemsBlock ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
print("got foreign items block")
|
||||||
|
foreign_items = apItemsBlock
|
||||||
|
createForeignItemsByRoom()
|
||||||
|
end
|
||||||
|
local autocollectItems = block["autocollect_items"]
|
||||||
|
if autocollectItems ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
autocollect_items = {}
|
||||||
|
for _, acitem in pairs(autocollectItems) do
|
||||||
|
if autocollect_items[acitem.room_id] == nil then
|
||||||
|
autocollect_items[acitem.room_id] = {}
|
||||||
|
end
|
||||||
|
table.insert(autocollect_items[acitem.room_id], acitem)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local localLocalItemLocations = block["local_item_locations"]
|
||||||
|
if localLocalItemLocations ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
localItemLocations = localLocalItemLocations
|
||||||
|
print("got local item locations")
|
||||||
|
end
|
||||||
|
local checkedLocationsBlock = block["checked_locations"]
|
||||||
|
if checkedLocationsBlock ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
for room_id, foreign_item_list in pairs(foreign_items_by_room) do
|
||||||
|
for i, foreign_item in pairs(foreign_item_list) do
|
||||||
|
short_id = foreign_item.short_location_id
|
||||||
|
for j, checked_id in pairs(checkedLocationsBlock) do
|
||||||
|
if checked_id == short_id then
|
||||||
|
table.remove(foreign_item_list, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if foreign_items ~= nil then
|
||||||
|
for i, foreign_item in pairs(foreign_items) do
|
||||||
|
short_id = foreign_item.short_location_id
|
||||||
|
for j, checked_id in pairs(checkedLocationsBlock) do
|
||||||
|
if checked_id == short_id then
|
||||||
|
foreign_items[i] = nil
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local dragon_speeds_block = block["dragon_speeds"]
|
||||||
|
if dragon_speeds_block ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
yorgle_speed = dragon_speeds_block[slow_yorgle_id]
|
||||||
|
grundle_speed = dragon_speeds_block[slow_grundle_id]
|
||||||
|
rhindle_speed = dragon_speeds_block[slow_rhindle_id]
|
||||||
|
end
|
||||||
|
local diff_a_block = block["difficulty_a_locked"]
|
||||||
|
if diff_a_block ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
diff_a_locked = diff_a_block
|
||||||
|
end
|
||||||
|
local diff_b_block = block["difficulty_b_locked"]
|
||||||
|
if diff_b_block ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
diff_b_locked = diff_b_block
|
||||||
|
end
|
||||||
|
local freeincarnates_available_block = block["freeincarnates_available"]
|
||||||
|
if freeincarnates_available_block ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
if freeincarnates_available ~= freeincarnates_available_block then
|
||||||
|
freeincarnates_available = freeincarnates_available_block
|
||||||
|
local msg = {TTL=450, message="freeincarnates: "..tostring(freeincarnates_available), color=0xFFFF0000}
|
||||||
|
itemMessages[-2] = msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local bat_logic_block = block["bat_logic"]
|
||||||
|
if bat_logic_block ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
bat_logic = bat_logic_block
|
||||||
|
end
|
||||||
|
local bat_no_touch_locations_block = block["bat_no_touch_locations"]
|
||||||
|
if bat_no_touch_locations_block ~= nil then
|
||||||
|
block_identified = 1
|
||||||
|
for _, notouch_location in pairs(bat_no_touch_locations_block) do
|
||||||
|
local room_id = tonumber(notouch_location.room_id)
|
||||||
|
if bat_no_touch_locations_by_room[room_id] == nil then
|
||||||
|
bat_no_touch_locations_by_room[room_id] = {}
|
||||||
|
end
|
||||||
|
table.insert(bat_no_touch_locations_by_room[room_id], notouch_location)
|
||||||
|
|
||||||
|
if notouch_location.local_item ~= nil and notouch_location.local_item ~= 255 then
|
||||||
|
bat_no_touch_items[tonumber(notouch_location.local_item)] = true
|
||||||
|
-- print("no touch: "..tostring(notouch_location.local_item))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- debugPrintNoTouchLocations()
|
||||||
|
end
|
||||||
|
deathlink_rec = deathlink_rec or block["deathlink"]
|
||||||
|
if( block_identified == 0 ) then
|
||||||
|
print("unidentified block")
|
||||||
|
print(block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clearScreen()
|
||||||
|
if is23Or24Or25 then
|
||||||
|
return
|
||||||
|
elseif is26To28 then
|
||||||
|
drawText(0, 0, "", "black")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getMaxMessageLength()
|
||||||
|
if is23Or24Or25 then
|
||||||
|
return client.screenwidth()/11
|
||||||
|
elseif is26To28 then
|
||||||
|
return client.screenwidth()/12
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function drawText(x, y, message, color)
|
||||||
|
if is23Or24Or25 then
|
||||||
|
gui.addmessage(message)
|
||||||
|
elseif is26To28 then
|
||||||
|
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawMessages()
|
||||||
|
if table.empty(itemMessages) then
|
||||||
|
clearScreen()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local y = 10
|
||||||
|
found = false
|
||||||
|
maxMessageLength = getMaxMessageLength()
|
||||||
|
for k, v in pairs(itemMessages) do
|
||||||
|
if v["TTL"] > 0 then
|
||||||
|
message = v["message"]
|
||||||
|
while true do
|
||||||
|
drawText(5, y, message:sub(1, maxMessageLength), v["color"])
|
||||||
|
y = y + 16
|
||||||
|
|
||||||
|
message = message:sub(maxMessageLength + 1, message:len())
|
||||||
|
if message:len() == 0 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
newTTL = 0
|
||||||
|
if is26To28 then
|
||||||
|
newTTL = itemMessages[k]["TTL"] - 1
|
||||||
|
end
|
||||||
|
itemMessages[k]["TTL"] = newTTL
|
||||||
|
found = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if found == false then
|
||||||
|
clearScreen()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function difference(a, b)
|
||||||
|
local aa = {}
|
||||||
|
for k,v in pairs(a) do aa[v]=true end
|
||||||
|
for k,v in pairs(b) do aa[v]=nil end
|
||||||
|
local ret = {}
|
||||||
|
local n = 0
|
||||||
|
for k,v in pairs(a) do
|
||||||
|
if aa[v] then n=n+1 ret[n]=v end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
function getAllRam()
|
||||||
|
uRangeRAM(0,128);
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function arrayEqual(a1, a2)
|
||||||
|
if #a1 ~= #a2 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in ipairs(a1) do
|
||||||
|
if v ~= a2[i] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function alive_mode()
|
||||||
|
return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function generateLocationsChecked()
|
||||||
|
list_of_locations = {}
|
||||||
|
for s, f in pairs(pending_foreign_items_collected) do
|
||||||
|
table.insert(list_of_locations, f.short_location_id + 118000000)
|
||||||
|
end
|
||||||
|
for s, f in pairs(pending_local_items_collected) do
|
||||||
|
table.insert(list_of_locations, f + 118000000)
|
||||||
|
end
|
||||||
|
return list_of_locations
|
||||||
|
end
|
||||||
|
|
||||||
|
function receive()
|
||||||
|
l, e = atariSocket:receive()
|
||||||
|
if e == 'closed' then
|
||||||
|
if curstate == STATE_OK then
|
||||||
|
print("Connection closed")
|
||||||
|
end
|
||||||
|
curstate = STATE_UNINITIALIZED
|
||||||
|
return
|
||||||
|
elseif e == 'timeout' then
|
||||||
|
return
|
||||||
|
elseif e ~= nil then
|
||||||
|
print(e)
|
||||||
|
curstate = STATE_UNINITIALIZED
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if l ~= nil then
|
||||||
|
processBlock(json.decode(l))
|
||||||
|
end
|
||||||
|
-- Determine Message to send back
|
||||||
|
|
||||||
|
newSha256 = memory.hash_region(0xF000, 0x1000, "System Bus")
|
||||||
|
if (sha256hash ~= nil and sha256hash ~= newSha256) then
|
||||||
|
print("ROM changed, quitting")
|
||||||
|
curstate = STATE_UNINITIALIZED
|
||||||
|
return
|
||||||
|
end
|
||||||
|
sha256hash = newSha256
|
||||||
|
local retTable = {}
|
||||||
|
retTable["scriptVersion"] = SCRIPT_VERSION
|
||||||
|
retTable["romhash"] = sha256hash
|
||||||
|
if (alive_mode()) then
|
||||||
|
retTable["locations"] = generateLocationsChecked()
|
||||||
|
end
|
||||||
|
if (u8(WinAddr) ~= 0x00) then
|
||||||
|
retTable["victory"] = 1
|
||||||
|
end
|
||||||
|
if( deathlink_sent or deathlink_send == 0 ) then
|
||||||
|
retTable["deathLink"] = 0
|
||||||
|
else
|
||||||
|
print("Sending deathlink "..tostring(deathlink_send))
|
||||||
|
retTable["deathLink"] = deathlink_send
|
||||||
|
deathlink_sent = true
|
||||||
|
end
|
||||||
|
deathlink_send = 0
|
||||||
|
|
||||||
|
if send_freeincarnate_used == true then
|
||||||
|
print("Sending freeincarnate used")
|
||||||
|
retTable["freeincarnate"] = true
|
||||||
|
send_freeincarnate_used = false
|
||||||
|
end
|
||||||
|
|
||||||
|
msg = json.encode(retTable).."\n"
|
||||||
|
local ret, error = atariSocket:send(msg)
|
||||||
|
if ret == nil then
|
||||||
|
print(error)
|
||||||
|
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
|
||||||
|
curstate = STATE_TENTATIVELY_CONNECTED
|
||||||
|
elseif curstate == STATE_TENTATIVELY_CONNECTED then
|
||||||
|
print("Connected!")
|
||||||
|
curstate = STATE_OK
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutocollectFromRoom()
|
||||||
|
if autocollect_items ~= nil and autocollect_items[prev_player_room] ~= nil then
|
||||||
|
for _, item in pairs(autocollect_items[prev_player_room]) do
|
||||||
|
pending_foreign_items_collected[item.short_location_id] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SetYorgleSpeed()
|
||||||
|
if yorgle_speed ~= nil then
|
||||||
|
emu.setregister("A", yorgle_speed);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SetGrundleSpeed()
|
||||||
|
if grundle_speed ~= nil then
|
||||||
|
emu.setregister("A", grundle_speed);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SetRhindleSpeed()
|
||||||
|
if rhindle_speed ~= nil then
|
||||||
|
emu.setregister("A", rhindle_speed);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SetDifficultySwitchB()
|
||||||
|
if diff_b_locked then
|
||||||
|
local a = emu.getregister("A")
|
||||||
|
if a < 128 then
|
||||||
|
emu.setregister("A", a + 128)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SetDifficultySwitchA()
|
||||||
|
if diff_a_locked then
|
||||||
|
local a = emu.getregister("A")
|
||||||
|
if (a > 128 and a < 128 + 64) or (a < 64) then
|
||||||
|
emu.setregister("A", a + 64)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TryFreeincarnate()
|
||||||
|
if freeincarnates_available > 0 then
|
||||||
|
freeincarnates_available = freeincarnates_available - 1
|
||||||
|
for index, state_addr in pairs(DragonState) do
|
||||||
|
if last_dragon_state[index] == 1 then
|
||||||
|
send_freeincarnate_used = true
|
||||||
|
memory.write_u8(state_addr, 1, "System Bus")
|
||||||
|
local msg = {TTL=450, message="used freeincarnate", color=0xFF00FF00}
|
||||||
|
itemMessages[-1] = msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetLinkedObject()
|
||||||
|
if emu.getregister("X") == batRoomAddr then
|
||||||
|
bat_interest_item = emu.getregister("A")
|
||||||
|
-- if the bat can't touch that item, we'll switch it to the number item, which should never be
|
||||||
|
-- in the same room as the bat.
|
||||||
|
if bat_no_touch_items[bat_interest_item] ~= nil then
|
||||||
|
emu.setregister("A", 0xDD )
|
||||||
|
emu.setregister("Y", 0xDD )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function CheckCollectAPItem(carry_item, target_item_value, target_item_ram, rendering_foreign_item)
|
||||||
|
if( carry_item == target_item_value and rendering_foreign_item ~= nil ) then
|
||||||
|
memory.write_u8(carryAddress, nullObjectId, "System Bus")
|
||||||
|
memory.write_u8(target_item_ram, 0xFF, "System Bus")
|
||||||
|
pending_foreign_items_collected[rendering_foreign_item.short_location_id] = rendering_foreign_item
|
||||||
|
for index, fi in pairs(foreign_items_by_room[rendering_foreign_item.room_id]) do
|
||||||
|
if( fi.short_location_id == rendering_foreign_item.short_location_id ) then
|
||||||
|
table.remove(foreign_items_by_room[rendering_foreign_item.room_id], index)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for index, fi in pairs(foreign_items) do
|
||||||
|
if( fi.short_location_id == rendering_foreign_item.short_location_id ) then
|
||||||
|
foreign_items[index] = nil
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
prev_ap_room_index = 0
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function BatCanTouchForeign(foreign_item, bat_room)
|
||||||
|
if bat_no_touch_locations_by_room[bat_room] == nil or bat_no_touch_locations_by_room[bat_room][1] == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
for index, location in ipairs(bat_no_touch_locations_by_room[bat_room]) do
|
||||||
|
if location.short_location_id == foreign_item.short_location_id then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
function main()
|
||||||
|
memory.usememorydomain("System Bus")
|
||||||
|
if (is23Or24Or25 or is26To28) == false then
|
||||||
|
print("Must use a version of bizhawk 2.3.1 or higher")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local playerSlot = memory.read_u8(PlayerSlotAddress)
|
||||||
|
local port = 17242 + playerSlot
|
||||||
|
print("Using port"..tostring(port))
|
||||||
|
server, error = socket.bind('localhost', port)
|
||||||
|
if( error ~= nil ) then
|
||||||
|
print(error)
|
||||||
|
end
|
||||||
|
event.onmemoryexecute(SetYorgleSpeed, yorgle_speed_address);
|
||||||
|
event.onmemoryexecute(SetGrundleSpeed, grundle_speed_address);
|
||||||
|
event.onmemoryexecute(SetRhindleSpeed, rhindle_speed_address);
|
||||||
|
event.onmemoryexecute(SetDifficultySwitchA, read_switch_a)
|
||||||
|
event.onmemoryexecute(SetDifficultySwitchB, read_switch_b)
|
||||||
|
event.onmemoryexecute(GetLinkedObject, batItemCheckAddr)
|
||||||
|
-- TODO: Add an onmemoryexecute event to intercept the bat reading item rooms, and don't 'see' an item in the
|
||||||
|
-- room if it is in bat_no_touch_locations_by_room. Although realistically, I may have to handle this in the rom
|
||||||
|
-- for it to be totally reliable, because it won't work before the script connects (I might have to reset them?)
|
||||||
|
-- TODO: Also remove those items from the bat_no_touch_locations_by_room if they have been collected
|
||||||
|
while true do
|
||||||
|
frame = frame + 1
|
||||||
|
drawMessages()
|
||||||
|
if not (curstate == prevstate) then
|
||||||
|
print("Current state: "..curstate)
|
||||||
|
prevstate = curstate
|
||||||
|
end
|
||||||
|
|
||||||
|
local current_player_room = u8(PlayerRoomAddr)
|
||||||
|
local bat_room = u8(batRoomAddr)
|
||||||
|
local bat_carrying_item = u8(batCarryAddress)
|
||||||
|
local bat_carrying_ap_item = (BatAPItemRam == bat_carrying_item)
|
||||||
|
|
||||||
|
if current_player_room == 0x1E then
|
||||||
|
if u8(PlayerRoomAddr + 1) > 0x4B then
|
||||||
|
memory.write_u8(PlayerRoomAddr + 1, 0x4B)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_player_room == 0x00 then
|
||||||
|
if not was_in_number_room then
|
||||||
|
print("reset "..tostring(bat_carrying_ap_item).." "..tostring(bat_carrying_item))
|
||||||
|
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||||
|
memory.write_u8(batCarryAddress+ 1, 0)
|
||||||
|
createForeignItemsByRoom()
|
||||||
|
memory.write_u8(BatAPItemRam, 0xff)
|
||||||
|
memory.write_u8(APItemRam, 0xff)
|
||||||
|
prev_ap_room_index = 0
|
||||||
|
prev_player_room = 0
|
||||||
|
rendering_foreign_item = nil
|
||||||
|
was_in_number_room = true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
was_in_number_room = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if bat_room ~= prev_bat_room then
|
||||||
|
if bat_carrying_ap_item then
|
||||||
|
if foreign_items_by_room[prev_bat_room] ~= nil then
|
||||||
|
for r,f in pairs(foreign_items_by_room[prev_bat_room]) do
|
||||||
|
if f.short_location_id == current_bat_ap_item.short_location_id then
|
||||||
|
-- print("removing item from "..tostring(r).." in "..tostring(prev_bat_room))
|
||||||
|
table.remove(foreign_items_by_room[prev_bat_room], r)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if foreign_items_by_room[bat_room] == nil then
|
||||||
|
foreign_items_by_room[bat_room] = {}
|
||||||
|
end
|
||||||
|
-- print("adding item to "..tostring(bat_room))
|
||||||
|
table.insert(foreign_items_by_room[bat_room], current_bat_ap_item)
|
||||||
|
else
|
||||||
|
-- set AP item room and position for new room, or to invalid room
|
||||||
|
if foreign_items_by_room[bat_room] ~= nil and foreign_items_by_room[bat_room][1] ~= nil
|
||||||
|
and BatCanTouchForeign(foreign_items_by_room[bat_room][1], bat_room) then
|
||||||
|
if current_bat_ap_item ~= foreign_items_by_room[bat_room][1] then
|
||||||
|
current_bat_ap_item = foreign_items_by_room[bat_room][1]
|
||||||
|
-- print("Changing bat item to "..tostring(current_bat_ap_item.short_location_id))
|
||||||
|
end
|
||||||
|
memory.write_u8(BatAPItemRam, bat_room)
|
||||||
|
memory.write_u8(BatAPItemRam + 1, current_bat_ap_item.room_x)
|
||||||
|
memory.write_u8(BatAPItemRam + 2, current_bat_ap_item.room_y)
|
||||||
|
else
|
||||||
|
memory.write_u8(BatAPItemRam, 0xff)
|
||||||
|
if current_bat_ap_item ~= nil then
|
||||||
|
-- print("clearing bat item")
|
||||||
|
end
|
||||||
|
current_bat_ap_item = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
prev_bat_room = bat_room
|
||||||
|
|
||||||
|
-- update foreign_items_by_room position and room id for bat item if bat carrying an item
|
||||||
|
if bat_carrying_ap_item then
|
||||||
|
-- this is setting the item using the bat's position, which is somewhat wrong, but I think
|
||||||
|
-- there will be more problems with the room not matching sometimes if I use the actual item position
|
||||||
|
current_bat_ap_item.room_id = bat_room
|
||||||
|
current_bat_ap_item.room_x = u8(batRoomAddr + 1)
|
||||||
|
current_bat_ap_item.room_y = u8(batRoomAddr + 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
if (alive_mode()) then
|
||||||
|
if (current_player_room ~= prev_player_room) then
|
||||||
|
memory.write_u8(APItemRam, 0xFF, "System Bus")
|
||||||
|
prev_ap_room_index = 0
|
||||||
|
prev_player_room = current_player_room
|
||||||
|
AutocollectFromRoom()
|
||||||
|
end
|
||||||
|
local carry_item = memory.read_u8(carryAddress, "System Bus")
|
||||||
|
bat_no_touch_items[carry_item] = nil
|
||||||
|
if (next_inventory_item ~= nil) then
|
||||||
|
if ( carry_item == nullObjectId and last_carry_item == nullObjectId ) then
|
||||||
|
frames_with_no_item = frames_with_no_item + 1
|
||||||
|
if (frames_with_no_item > 10) then
|
||||||
|
frames_with_no_item = 10
|
||||||
|
local input_value = memory.read_u8(input_button_address, "System Bus")
|
||||||
|
if( input_value >= 64 and input_value < 128 ) then -- high bit clear, second highest bit set
|
||||||
|
memory.write_u8(carryAddress, next_inventory_item)
|
||||||
|
local item_ram_location = memory.read_u8(ItemTableStart + next_inventory_item)
|
||||||
|
if( memory.read_u8(batCarryAddress) ~= 0x78 and
|
||||||
|
memory.read_u8(batCarryAddress) == item_ram_location) then
|
||||||
|
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||||
|
memory.write_u8(batCarryAddress+ 1, 0)
|
||||||
|
memory.write_u8(item_ram_location, current_player_room)
|
||||||
|
memory.write_u8(item_ram_location + 1, memory.read_u8(PlayerRoomAddr + 1))
|
||||||
|
memory.write_u8(item_ram_location + 2, memory.read_u8(PlayerRoomAddr + 2))
|
||||||
|
end
|
||||||
|
ItemIndex = ItemIndex + 1
|
||||||
|
next_inventory_item = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
frames_with_no_item = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if( carry_item ~= last_carry_item ) then
|
||||||
|
if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then
|
||||||
|
pending_local_items_collected[localItemLocations[tostring(carry_item)]] =
|
||||||
|
localItemLocations[tostring(carry_item)]
|
||||||
|
table.remove(localItemLocations, tostring(carry_item))
|
||||||
|
skip_inventory_items[carry_item] = carry_item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
last_carry_item = carry_item
|
||||||
|
|
||||||
|
CheckCollectAPItem(carry_item, APItemValue, APItemRam, rendering_foreign_item)
|
||||||
|
if CheckCollectAPItem(carry_item, BatAPItemValue, BatAPItemRam, current_bat_ap_item) and bat_carrying_ap_item then
|
||||||
|
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||||
|
memory.write_u8(batCarryAddress+ 1, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
rendering_foreign_item = nil
|
||||||
|
if( foreign_items_by_room[current_player_room] ~= nil ) then
|
||||||
|
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil ) and memory.read_u8(APItemRam) ~= 0xff then
|
||||||
|
foreign_items_by_room[current_player_room][prev_ap_room_index].room_x = memory.read_u8(APItemRam + 1)
|
||||||
|
foreign_items_by_room[current_player_room][prev_ap_room_index].room_y = memory.read_u8(APItemRam + 2)
|
||||||
|
end
|
||||||
|
prev_ap_room_index = prev_ap_room_index + 1
|
||||||
|
local invalid_index = -1
|
||||||
|
if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then
|
||||||
|
prev_ap_room_index = 1
|
||||||
|
end
|
||||||
|
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and current_bat_ap_item ~= nil and
|
||||||
|
foreign_items_by_room[current_player_room][prev_ap_room_index].short_location_id == current_bat_ap_item.short_location_id) then
|
||||||
|
invalid_index = prev_ap_room_index
|
||||||
|
prev_ap_room_index = prev_ap_room_index + 1
|
||||||
|
if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then
|
||||||
|
prev_ap_room_index = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and prev_ap_room_index ~= invalid_index ) then
|
||||||
|
memory.write_u8(APItemRam, current_player_room)
|
||||||
|
rendering_foreign_item = foreign_items_by_room[current_player_room][prev_ap_room_index]
|
||||||
|
memory.write_u8(APItemRam + 1, rendering_foreign_item.room_x)
|
||||||
|
memory.write_u8(APItemRam + 2, rendering_foreign_item.room_y)
|
||||||
|
else
|
||||||
|
memory.write_u8(APItemRam, 0xFF, "System Bus")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if is_dead == 0 then
|
||||||
|
dragons_revived = false
|
||||||
|
player_dead = false
|
||||||
|
new_dragon_state = {0,0,0}
|
||||||
|
for index, dragon_state_addr in pairs(DragonState) do
|
||||||
|
new_dragon_state[index] = memory.read_u8(dragon_state_addr, "System Bus" )
|
||||||
|
if last_dragon_state[index] == 1 and new_dragon_state[index] ~= 1 then
|
||||||
|
dragons_revived = true
|
||||||
|
elseif last_dragon_state[index] ~= 1 and new_dragon_state[index] == 1 then
|
||||||
|
dragon_real_index = index - 1
|
||||||
|
print("Killed dragon: "..tostring(dragon_real_index))
|
||||||
|
local dragon_item = {}
|
||||||
|
dragon_item["short_location_id"] = 0xD0 + dragon_real_index
|
||||||
|
pending_foreign_items_collected[dragon_item.short_location_id] = dragon_item
|
||||||
|
end
|
||||||
|
if new_dragon_state[index] == 2 then
|
||||||
|
player_dead = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if dragons_revived and player_dead == false then
|
||||||
|
TryFreeincarnate()
|
||||||
|
end
|
||||||
|
last_dragon_state = new_dragon_state
|
||||||
|
end
|
||||||
|
elseif (u8(PlayerRoomAddr) == 0x00) then -- not alive mode, in number room
|
||||||
|
ItemIndex = 0 -- reset our inventory
|
||||||
|
next_inventory_item = nil
|
||||||
|
skip_inventory_items = {}
|
||||||
|
end
|
||||||
|
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||||
|
if (frame % 5 == 0) then
|
||||||
|
receive()
|
||||||
|
if alive_mode() then
|
||||||
|
local was_dead = is_dead
|
||||||
|
is_dead = 0
|
||||||
|
for index, dragonStateAddr in pairs(DragonState) do
|
||||||
|
local dragonstateval = memory.read_u8(dragonStateAddr, "System Bus")
|
||||||
|
if ( dragonstateval == 2) then
|
||||||
|
is_dead = index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if was_dead ~= 0 and is_dead == 0 then
|
||||||
|
TryFreeincarnate()
|
||||||
|
end
|
||||||
|
if deathlink_rec == true and is_dead == 0 then
|
||||||
|
print("setting dead from deathlink")
|
||||||
|
deathlink_rec = false
|
||||||
|
deathlink_sent = true
|
||||||
|
is_dead = 1
|
||||||
|
memory.write_u8(carryAddress, nullObjectId, "System Bus")
|
||||||
|
memory.write_u8(DragonState[1], 2, "System Bus")
|
||||||
|
end
|
||||||
|
if (is_dead > 0 and deathlink_send == 0 and not deathlink_sent) then
|
||||||
|
deathlink_send = is_dead
|
||||||
|
print("setting deathlink_send to "..tostring(is_dead))
|
||||||
|
elseif (is_dead == 0) then
|
||||||
|
deathlink_send = 0
|
||||||
|
deathlink_sent = false
|
||||||
|
end
|
||||||
|
if ItemsReceived ~= nil and ItemsReceived[ItemIndex + 1] ~= nil then
|
||||||
|
while ItemsReceived[ItemIndex + 1] ~= nil and skip_inventory_items[ItemsReceived[ItemIndex + 1]] ~= nil do
|
||||||
|
print("skip")
|
||||||
|
ItemIndex = ItemIndex + 1
|
||||||
|
end
|
||||||
|
local static_id = ItemsReceived[ItemIndex + 1]
|
||||||
|
if static_id ~= nil then
|
||||||
|
inventory[static_id] = 1
|
||||||
|
if next_inventory_item == nil then
|
||||||
|
next_inventory_item = static_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (curstate == STATE_UNINITIALIZED) then
|
||||||
|
if (frame % 60 == 0) then
|
||||||
|
|
||||||
|
print("Waiting for client.")
|
||||||
|
|
||||||
|
emu.frameadvance()
|
||||||
|
server:settimeout(2)
|
||||||
|
print("Attempting to connect")
|
||||||
|
local client, timeout = server:accept()
|
||||||
|
if timeout == nil then
|
||||||
|
print("Initial connection made")
|
||||||
|
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||||
|
atariSocket = client
|
||||||
|
atariSocket:settimeout(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
emu.frameadvance()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
main()
|
||||||
380
data/lua/ADVENTURE/json.lua
Normal file
380
data/lua/ADVENTURE/json.lua
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
--
|
||||||
|
-- json.lua
|
||||||
|
--
|
||||||
|
-- Copyright (c) 2015 rxi
|
||||||
|
--
|
||||||
|
-- This library is free software; you can redistribute it and/or modify it
|
||||||
|
-- under the terms of the MIT license. See LICENSE for details.
|
||||||
|
--
|
||||||
|
|
||||||
|
local json = { _version = "0.1.0" }
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Encode
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local encode
|
||||||
|
|
||||||
|
local escape_char_map = {
|
||||||
|
[ "\\" ] = "\\\\",
|
||||||
|
[ "\"" ] = "\\\"",
|
||||||
|
[ "\b" ] = "\\b",
|
||||||
|
[ "\f" ] = "\\f",
|
||||||
|
[ "\n" ] = "\\n",
|
||||||
|
[ "\r" ] = "\\r",
|
||||||
|
[ "\t" ] = "\\t",
|
||||||
|
}
|
||||||
|
|
||||||
|
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||||
|
for k, v in pairs(escape_char_map) do
|
||||||
|
escape_char_map_inv[v] = k
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function escape_char(c)
|
||||||
|
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_nil(val)
|
||||||
|
return "null"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_table(val, stack)
|
||||||
|
local res = {}
|
||||||
|
stack = stack or {}
|
||||||
|
|
||||||
|
-- Circular reference?
|
||||||
|
if stack[val] then error("circular reference") end
|
||||||
|
|
||||||
|
stack[val] = true
|
||||||
|
|
||||||
|
if val[1] ~= nil or next(val) == nil then
|
||||||
|
-- Treat as array -- check keys are valid and it is not sparse
|
||||||
|
local n = 0
|
||||||
|
for k in pairs(val) do
|
||||||
|
if type(k) ~= "number" then
|
||||||
|
error("invalid table: mixed or invalid key types")
|
||||||
|
end
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
if n ~= #val then
|
||||||
|
error("invalid table: sparse array")
|
||||||
|
end
|
||||||
|
-- Encode
|
||||||
|
for i, v in ipairs(val) do
|
||||||
|
table.insert(res, encode(v, stack))
|
||||||
|
end
|
||||||
|
stack[val] = nil
|
||||||
|
return "[" .. table.concat(res, ",") .. "]"
|
||||||
|
|
||||||
|
else
|
||||||
|
-- Treat as an object
|
||||||
|
for k, v in pairs(val) do
|
||||||
|
if type(k) ~= "string" then
|
||||||
|
error("invalid table: mixed or invalid key types")
|
||||||
|
end
|
||||||
|
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||||
|
end
|
||||||
|
stack[val] = nil
|
||||||
|
return "{" .. table.concat(res, ",") .. "}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_string(val)
|
||||||
|
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_number(val)
|
||||||
|
-- Check for NaN, -inf and inf
|
||||||
|
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||||
|
error("unexpected number value '" .. tostring(val) .. "'")
|
||||||
|
end
|
||||||
|
return string.format("%.14g", val)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local type_func_map = {
|
||||||
|
[ "nil" ] = encode_nil,
|
||||||
|
[ "table" ] = encode_table,
|
||||||
|
[ "string" ] = encode_string,
|
||||||
|
[ "number" ] = encode_number,
|
||||||
|
[ "boolean" ] = tostring,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
encode = function(val, stack)
|
||||||
|
local t = type(val)
|
||||||
|
local f = type_func_map[t]
|
||||||
|
if f then
|
||||||
|
return f(val, stack)
|
||||||
|
end
|
||||||
|
error("unexpected type '" .. t .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function json.encode(val)
|
||||||
|
return ( encode(val) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Decode
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local parse
|
||||||
|
|
||||||
|
local function create_set(...)
|
||||||
|
local res = {}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
res[ select(i, ...) ] = true
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||||
|
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||||
|
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||||
|
local literals = create_set("true", "false", "null")
|
||||||
|
|
||||||
|
local literal_map = {
|
||||||
|
[ "true" ] = true,
|
||||||
|
[ "false" ] = false,
|
||||||
|
[ "null" ] = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function next_char(str, idx, set, negate)
|
||||||
|
for i = idx, #str do
|
||||||
|
if set[str:sub(i, i)] ~= negate then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return #str + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function decode_error(str, idx, msg)
|
||||||
|
--local line_count = 1
|
||||||
|
--local col_count = 1
|
||||||
|
--for i = 1, idx - 1 do
|
||||||
|
-- col_count = col_count + 1
|
||||||
|
-- if str:sub(i, i) == "\n" then
|
||||||
|
-- line_count = line_count + 1
|
||||||
|
-- col_count = 1
|
||||||
|
-- end
|
||||||
|
-- end
|
||||||
|
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function codepoint_to_utf8(n)
|
||||||
|
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||||
|
local f = math.floor
|
||||||
|
if n <= 0x7f then
|
||||||
|
return string.char(n)
|
||||||
|
elseif n <= 0x7ff then
|
||||||
|
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||||
|
elseif n <= 0xffff then
|
||||||
|
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||||
|
elseif n <= 0x10ffff then
|
||||||
|
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||||
|
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||||
|
end
|
||||||
|
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_unicode_escape(s)
|
||||||
|
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||||
|
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||||
|
-- Surrogate pair?
|
||||||
|
if n2 then
|
||||||
|
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||||
|
else
|
||||||
|
return codepoint_to_utf8(n1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_string(str, i)
|
||||||
|
local has_unicode_escape = false
|
||||||
|
local has_surrogate_escape = false
|
||||||
|
local has_escape = false
|
||||||
|
local last
|
||||||
|
for j = i + 1, #str do
|
||||||
|
local x = str:byte(j)
|
||||||
|
|
||||||
|
if x < 32 then
|
||||||
|
decode_error(str, j, "control character in string")
|
||||||
|
end
|
||||||
|
|
||||||
|
if last == 92 then -- "\\" (escape char)
|
||||||
|
if x == 117 then -- "u" (unicode escape sequence)
|
||||||
|
local hex = str:sub(j + 1, j + 5)
|
||||||
|
if not hex:find("%x%x%x%x") then
|
||||||
|
decode_error(str, j, "invalid unicode escape in string")
|
||||||
|
end
|
||||||
|
if hex:find("^[dD][89aAbB]") then
|
||||||
|
has_surrogate_escape = true
|
||||||
|
else
|
||||||
|
has_unicode_escape = true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local c = string.char(x)
|
||||||
|
if not escape_chars[c] then
|
||||||
|
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||||
|
end
|
||||||
|
has_escape = true
|
||||||
|
end
|
||||||
|
last = nil
|
||||||
|
|
||||||
|
elseif x == 34 then -- '"' (end of string)
|
||||||
|
local s = str:sub(i + 1, j - 1)
|
||||||
|
if has_surrogate_escape then
|
||||||
|
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||||
|
end
|
||||||
|
if has_unicode_escape then
|
||||||
|
s = s:gsub("\\u....", parse_unicode_escape)
|
||||||
|
end
|
||||||
|
if has_escape then
|
||||||
|
s = s:gsub("\\.", escape_char_map_inv)
|
||||||
|
end
|
||||||
|
return s, j + 1
|
||||||
|
|
||||||
|
else
|
||||||
|
last = x
|
||||||
|
end
|
||||||
|
end
|
||||||
|
decode_error(str, i, "expected closing quote for string")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_number(str, i)
|
||||||
|
local x = next_char(str, i, delim_chars)
|
||||||
|
local s = str:sub(i, x - 1)
|
||||||
|
local n = tonumber(s)
|
||||||
|
if not n then
|
||||||
|
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||||
|
end
|
||||||
|
return n, x
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_literal(str, i)
|
||||||
|
local x = next_char(str, i, delim_chars)
|
||||||
|
local word = str:sub(i, x - 1)
|
||||||
|
if not literals[word] then
|
||||||
|
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||||
|
end
|
||||||
|
return literal_map[word], x
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_array(str, i)
|
||||||
|
local res = {}
|
||||||
|
local n = 1
|
||||||
|
i = i + 1
|
||||||
|
while 1 do
|
||||||
|
local x
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
-- Empty / end of array?
|
||||||
|
if str:sub(i, i) == "]" then
|
||||||
|
i = i + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- Read token
|
||||||
|
x, i = parse(str, i)
|
||||||
|
res[n] = x
|
||||||
|
n = n + 1
|
||||||
|
-- Next token
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
local chr = str:sub(i, i)
|
||||||
|
i = i + 1
|
||||||
|
if chr == "]" then break end
|
||||||
|
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||||
|
end
|
||||||
|
return res, i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_object(str, i)
|
||||||
|
local res = {}
|
||||||
|
i = i + 1
|
||||||
|
while 1 do
|
||||||
|
local key, val
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
-- Empty / end of object?
|
||||||
|
if str:sub(i, i) == "}" then
|
||||||
|
i = i + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- Read key
|
||||||
|
if str:sub(i, i) ~= '"' then
|
||||||
|
decode_error(str, i, "expected string for key")
|
||||||
|
end
|
||||||
|
key, i = parse(str, i)
|
||||||
|
-- Read ':' delimiter
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
if str:sub(i, i) ~= ":" then
|
||||||
|
decode_error(str, i, "expected ':' after key")
|
||||||
|
end
|
||||||
|
i = next_char(str, i + 1, space_chars, true)
|
||||||
|
-- Read value
|
||||||
|
val, i = parse(str, i)
|
||||||
|
-- Set
|
||||||
|
res[key] = val
|
||||||
|
-- Next token
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
local chr = str:sub(i, i)
|
||||||
|
i = i + 1
|
||||||
|
if chr == "}" then break end
|
||||||
|
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||||
|
end
|
||||||
|
return res, i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local char_func_map = {
|
||||||
|
[ '"' ] = parse_string,
|
||||||
|
[ "0" ] = parse_number,
|
||||||
|
[ "1" ] = parse_number,
|
||||||
|
[ "2" ] = parse_number,
|
||||||
|
[ "3" ] = parse_number,
|
||||||
|
[ "4" ] = parse_number,
|
||||||
|
[ "5" ] = parse_number,
|
||||||
|
[ "6" ] = parse_number,
|
||||||
|
[ "7" ] = parse_number,
|
||||||
|
[ "8" ] = parse_number,
|
||||||
|
[ "9" ] = parse_number,
|
||||||
|
[ "-" ] = parse_number,
|
||||||
|
[ "t" ] = parse_literal,
|
||||||
|
[ "f" ] = parse_literal,
|
||||||
|
[ "n" ] = parse_literal,
|
||||||
|
[ "[" ] = parse_array,
|
||||||
|
[ "{" ] = parse_object,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
parse = function(str, idx)
|
||||||
|
local chr = str:sub(idx, idx)
|
||||||
|
local f = char_func_map[chr]
|
||||||
|
if f then
|
||||||
|
return f(str, idx)
|
||||||
|
end
|
||||||
|
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function json.decode(str)
|
||||||
|
if type(str) ~= "string" then
|
||||||
|
error("expected argument of type string, got " .. type(str))
|
||||||
|
end
|
||||||
|
return ( parse(str, next_char(str, 1, space_chars, true)) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return json
|
||||||
132
data/lua/ADVENTURE/socket.lua
Normal file
132
data/lua/ADVENTURE/socket.lua
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- LuaSocket helper module
|
||||||
|
-- Author: Diego Nehab
|
||||||
|
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Declare module and import dependencies
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
local base = _G
|
||||||
|
local string = require("string")
|
||||||
|
local math = require("math")
|
||||||
|
local socket = require("socket.core")
|
||||||
|
module("socket")
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Exported auxiliar functions
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
function connect(address, port, laddress, lport)
|
||||||
|
local sock, err = socket.tcp()
|
||||||
|
if not sock then return nil, err end
|
||||||
|
if laddress then
|
||||||
|
local res, err = sock:bind(laddress, lport, -1)
|
||||||
|
if not res then return nil, err end
|
||||||
|
end
|
||||||
|
local res, err = sock:connect(address, port)
|
||||||
|
if not res then return nil, err end
|
||||||
|
return sock
|
||||||
|
end
|
||||||
|
|
||||||
|
function bind(host, port, backlog)
|
||||||
|
local sock, err = socket.tcp()
|
||||||
|
if not sock then return nil, err end
|
||||||
|
sock:setoption("reuseaddr", true)
|
||||||
|
local res, err = sock:bind(host, port)
|
||||||
|
if not res then return nil, err end
|
||||||
|
res, err = sock:listen(backlog)
|
||||||
|
if not res then return nil, err end
|
||||||
|
return sock
|
||||||
|
end
|
||||||
|
|
||||||
|
try = newtry()
|
||||||
|
|
||||||
|
function choose(table)
|
||||||
|
return function(name, opt1, opt2)
|
||||||
|
if base.type(name) ~= "string" then
|
||||||
|
name, opt1, opt2 = "default", name, opt1
|
||||||
|
end
|
||||||
|
local f = table[name or "nil"]
|
||||||
|
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||||
|
else return f(opt1, opt2) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Socket sources and sinks, conforming to LTN12
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- create namespaces inside LuaSocket namespace
|
||||||
|
sourcet = {}
|
||||||
|
sinkt = {}
|
||||||
|
|
||||||
|
BLOCKSIZE = 2048
|
||||||
|
|
||||||
|
sinkt["close-when-done"] = function(sock)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function(self, chunk, err)
|
||||||
|
if not chunk then
|
||||||
|
sock:close()
|
||||||
|
return 1
|
||||||
|
else return sock:send(chunk) end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sinkt["keep-open"] = function(sock)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function(self, chunk, err)
|
||||||
|
if chunk then return sock:send(chunk)
|
||||||
|
else return 1 end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sinkt["default"] = sinkt["keep-open"]
|
||||||
|
|
||||||
|
sink = choose(sinkt)
|
||||||
|
|
||||||
|
sourcet["by-length"] = function(sock, length)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function()
|
||||||
|
if length <= 0 then return nil end
|
||||||
|
local size = math.min(socket.BLOCKSIZE, length)
|
||||||
|
local chunk, err = sock:receive(size)
|
||||||
|
if err then return nil, err end
|
||||||
|
length = length - string.len(chunk)
|
||||||
|
return chunk
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sourcet["until-closed"] = function(sock)
|
||||||
|
local done
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function()
|
||||||
|
if done then return nil end
|
||||||
|
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||||
|
if not err then return chunk
|
||||||
|
elseif err == "closed" then
|
||||||
|
sock:close()
|
||||||
|
done = 1
|
||||||
|
return partial
|
||||||
|
else return nil, err end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
sourcet["default"] = sourcet["until-closed"]
|
||||||
|
|
||||||
|
source = choose(sourcet)
|
||||||
@@ -7,7 +7,7 @@ local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
|||||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||||
local STATE_UNINITIALIZED = "Uninitialized"
|
local STATE_UNINITIALIZED = "Uninitialized"
|
||||||
|
|
||||||
local SCRIPT_VERSION = 1
|
local SCRIPT_VERSION = 3
|
||||||
|
|
||||||
local APIndex = 0x1A6E
|
local APIndex = 0x1A6E
|
||||||
local APDeathLinkAddress = 0x00FD
|
local APDeathLinkAddress = 0x00FD
|
||||||
@@ -16,7 +16,8 @@ local EventFlagAddress = 0x1735
|
|||||||
local MissableAddress = 0x161A
|
local MissableAddress = 0x161A
|
||||||
local HiddenItemsAddress = 0x16DE
|
local HiddenItemsAddress = 0x16DE
|
||||||
local RodAddress = 0x1716
|
local RodAddress = 0x1716
|
||||||
local InGame = 0x1A71
|
local DexSanityAddress = 0x1A71
|
||||||
|
local InGameAddress = 0x1A84
|
||||||
local ClientCompatibilityAddress = 0xFF00
|
local ClientCompatibilityAddress = 0xFF00
|
||||||
|
|
||||||
local ItemsReceived = nil
|
local ItemsReceived = nil
|
||||||
@@ -34,6 +35,7 @@ local frame = 0
|
|||||||
local u8 = nil
|
local u8 = nil
|
||||||
local wU8 = nil
|
local wU8 = nil
|
||||||
local u16
|
local u16
|
||||||
|
local compat = nil
|
||||||
|
|
||||||
local function defineMemoryFunctions()
|
local function defineMemoryFunctions()
|
||||||
local memDomain = {}
|
local memDomain = {}
|
||||||
@@ -70,18 +72,6 @@ function slice (tbl, s, e)
|
|||||||
return new
|
return new
|
||||||
end
|
end
|
||||||
|
|
||||||
function processBlock(block)
|
|
||||||
if block == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local itemsBlock = block["items"]
|
|
||||||
memDomain.wram()
|
|
||||||
if itemsBlock ~= nil then
|
|
||||||
ItemsReceived = itemsBlock
|
|
||||||
end
|
|
||||||
deathlink_rec = block["deathlink"]
|
|
||||||
end
|
|
||||||
|
|
||||||
function difference(a, b)
|
function difference(a, b)
|
||||||
local aa = {}
|
local aa = {}
|
||||||
for k,v in pairs(a) do aa[v]=true end
|
for k,v in pairs(a) do aa[v]=true end
|
||||||
@@ -99,6 +89,7 @@ function generateLocationsChecked()
|
|||||||
events = uRange(EventFlagAddress, 0x140)
|
events = uRange(EventFlagAddress, 0x140)
|
||||||
missables = uRange(MissableAddress, 0x20)
|
missables = uRange(MissableAddress, 0x20)
|
||||||
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
|
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
|
||||||
|
dexsanity = uRange(DexSanityAddress, 19)
|
||||||
rod = u8(RodAddress)
|
rod = u8(RodAddress)
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
@@ -108,6 +99,9 @@ function generateLocationsChecked()
|
|||||||
table.foreach(hiddenitems, function(k, v) table.insert(data, v) end)
|
table.foreach(hiddenitems, function(k, v) table.insert(data, v) end)
|
||||||
table.insert(data, rod)
|
table.insert(data, rod)
|
||||||
|
|
||||||
|
if compat > 1 then
|
||||||
|
table.foreach(dexsanity, function(k, v) table.insert(data, v) end)
|
||||||
|
end
|
||||||
return data
|
return data
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -141,7 +135,15 @@ function receive()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if l ~= nil then
|
if l ~= nil then
|
||||||
processBlock(json.decode(l))
|
block = json.decode(l)
|
||||||
|
if block ~= nil then
|
||||||
|
local itemsBlock = block["items"]
|
||||||
|
if itemsBlock ~= nil then
|
||||||
|
ItemsReceived = itemsBlock
|
||||||
|
end
|
||||||
|
deathlink_rec = block["deathlink"]
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
-- Determine Message to send back
|
-- Determine Message to send back
|
||||||
memDomain.rom()
|
memDomain.rom()
|
||||||
@@ -156,15 +158,31 @@ function receive()
|
|||||||
seedName = newSeedName
|
seedName = newSeedName
|
||||||
local retTable = {}
|
local retTable = {}
|
||||||
retTable["scriptVersion"] = SCRIPT_VERSION
|
retTable["scriptVersion"] = SCRIPT_VERSION
|
||||||
retTable["clientCompatibilityVersion"] = u8(ClientCompatibilityAddress)
|
|
||||||
|
if compat == nil then
|
||||||
|
compat = u8(ClientCompatibilityAddress)
|
||||||
|
if compat < 2 then
|
||||||
|
InGameAddress = 0x1A71
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
retTable["clientCompatibilityVersion"] = compat
|
||||||
retTable["playerName"] = playerName
|
retTable["playerName"] = playerName
|
||||||
retTable["seedName"] = seedName
|
retTable["seedName"] = seedName
|
||||||
memDomain.wram()
|
memDomain.wram()
|
||||||
if u8(InGame) == 0xAC then
|
|
||||||
|
in_game = u8(InGameAddress)
|
||||||
|
if in_game == 0x2A or in_game == 0xAC then
|
||||||
retTable["locations"] = generateLocationsChecked()
|
retTable["locations"] = generateLocationsChecked()
|
||||||
|
elseif in_game ~= 0 then
|
||||||
|
print("Game may have crashed")
|
||||||
|
curstate = STATE_UNINITIALIZED
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
retTable["deathLink"] = deathlink_send
|
retTable["deathLink"] = deathlink_send
|
||||||
deathlink_send = false
|
deathlink_send = false
|
||||||
|
|
||||||
msg = json.encode(retTable).."\n"
|
msg = json.encode(retTable).."\n"
|
||||||
local ret, error = gbSocket:send(msg)
|
local ret, error = gbSocket:send(msg)
|
||||||
if ret == nil then
|
if ret == nil then
|
||||||
@@ -193,16 +211,23 @@ function main()
|
|||||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||||
if (frame % 5 == 0) then
|
if (frame % 5 == 0) then
|
||||||
receive()
|
receive()
|
||||||
if u8(InGame) == 0xAC and u8(APItemAddress) == 0x00 then
|
in_game = u8(InGameAddress)
|
||||||
ItemIndex = u16(APIndex)
|
if in_game == 0x2A or in_game == 0xAC then
|
||||||
if deathlink_rec == true then
|
if u8(APItemAddress) == 0x00 then
|
||||||
wU8(APDeathLinkAddress, 1)
|
ItemIndex = u16(APIndex)
|
||||||
elseif u8(APDeathLinkAddress) == 3 then
|
if deathlink_rec == true then
|
||||||
wU8(APDeathLinkAddress, 0)
|
wU8(APDeathLinkAddress, 1)
|
||||||
deathlink_send = true
|
elseif u8(APDeathLinkAddress) == 3 then
|
||||||
end
|
wU8(APDeathLinkAddress, 0)
|
||||||
if ItemsReceived[ItemIndex + 1] ~= nil then
|
deathlink_send = true
|
||||||
wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000)
|
end
|
||||||
|
if ItemsReceived[ItemIndex + 1] ~= nil then
|
||||||
|
item_id = ItemsReceived[ItemIndex + 1] - 172000000
|
||||||
|
if item_id > 255 then
|
||||||
|
item_id = item_id - 256
|
||||||
|
end
|
||||||
|
wU8(APItemAddress, item_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
702
data/lua/TLoZ/TheLegendOfZeldaConnector.lua
Normal file
702
data/lua/TLoZ/TheLegendOfZeldaConnector.lua
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
--Shamelessly based off the FF1 lua
|
||||||
|
|
||||||
|
local socket = require("socket")
|
||||||
|
local json = require('json')
|
||||||
|
local math = require('math')
|
||||||
|
|
||||||
|
local STATE_OK = "Ok"
|
||||||
|
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||||
|
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||||
|
local STATE_UNINITIALIZED = "Uninitialized"
|
||||||
|
|
||||||
|
local itemMessages = {}
|
||||||
|
local consumableStacks = nil
|
||||||
|
local prevstate = ""
|
||||||
|
local curstate = STATE_UNINITIALIZED
|
||||||
|
local zeldaSocket = nil
|
||||||
|
local frame = 0
|
||||||
|
local gameMode = 0
|
||||||
|
|
||||||
|
local cave_index
|
||||||
|
local triforce_byte
|
||||||
|
local game_state
|
||||||
|
|
||||||
|
local u8 = nil
|
||||||
|
local wU8 = nil
|
||||||
|
local isNesHawk = false
|
||||||
|
|
||||||
|
local shopsChecked = {}
|
||||||
|
local shopSlotLeft = 0x0628
|
||||||
|
local shopSlotMiddle = 0x0629
|
||||||
|
local shopSlotRight = 0x062A
|
||||||
|
|
||||||
|
--N.B.: you won't find these in a RAM map. They're flag values that the base patch derives from the cave ID.
|
||||||
|
local blueRingShopBit = 0x40
|
||||||
|
local potionShopBit = 0x02
|
||||||
|
local arrowShopBit = 0x08
|
||||||
|
local candleShopBit = 0x10
|
||||||
|
local shieldShopBit = 0x20
|
||||||
|
local takeAnyCaveBit = 0x01
|
||||||
|
|
||||||
|
|
||||||
|
local sword = 0x0657
|
||||||
|
local bombs = 0x0658
|
||||||
|
local maxBombs = 0x067C
|
||||||
|
local keys = 0x066E
|
||||||
|
local arrow = 0x0659
|
||||||
|
local bow = 0x065A
|
||||||
|
local candle = 0x065B
|
||||||
|
local recorder = 0x065C
|
||||||
|
local food = 0x065D
|
||||||
|
local waterOfLife = 0x065E
|
||||||
|
local magicalRod = 0x065F
|
||||||
|
local raft = 0x0660
|
||||||
|
local bookOfMagic = 0x0661
|
||||||
|
local ring = 0x0662
|
||||||
|
local stepladder = 0x0663
|
||||||
|
local magicalKey = 0x0664
|
||||||
|
local powerBracelet = 0x0665
|
||||||
|
local letter = 0x0666
|
||||||
|
local clockItem = 0x066C
|
||||||
|
local heartContainers = 0x066F
|
||||||
|
local partialHearts = 0x0670
|
||||||
|
local triforceFragments = 0x0671
|
||||||
|
local boomerang = 0x0674
|
||||||
|
local magicalBoomerang = 0x0675
|
||||||
|
local magicalShield = 0x0676
|
||||||
|
local rupeesToAdd = 0x067D
|
||||||
|
local rupeesToSubtract = 0x067E
|
||||||
|
local itemsObtained = 0x0677
|
||||||
|
local takeAnyCavesChecked = 0x0678
|
||||||
|
local localTriforce = 0x0679
|
||||||
|
local bonusItemsObtained = 0x067A
|
||||||
|
|
||||||
|
itemAPids = {
|
||||||
|
["Boomerang"] = 7100,
|
||||||
|
["Bow"] = 7101,
|
||||||
|
["Magical Boomerang"] = 7102,
|
||||||
|
["Raft"] = 7103,
|
||||||
|
["Stepladder"] = 7104,
|
||||||
|
["Recorder"] = 7105,
|
||||||
|
["Magical Rod"] = 7106,
|
||||||
|
["Red Candle"] = 7107,
|
||||||
|
["Book of Magic"] = 7108,
|
||||||
|
["Magical Key"] = 7109,
|
||||||
|
["Red Ring"] = 7110,
|
||||||
|
["Silver Arrow"] = 7111,
|
||||||
|
["Sword"] = 7112,
|
||||||
|
["White Sword"] = 7113,
|
||||||
|
["Magical Sword"] = 7114,
|
||||||
|
["Heart Container"] = 7115,
|
||||||
|
["Letter"] = 7116,
|
||||||
|
["Magical Shield"] = 7117,
|
||||||
|
["Candle"] = 7118,
|
||||||
|
["Arrow"] = 7119,
|
||||||
|
["Food"] = 7120,
|
||||||
|
["Water of Life (Blue)"] = 7121,
|
||||||
|
["Water of Life (Red)"] = 7122,
|
||||||
|
["Blue Ring"] = 7123,
|
||||||
|
["Triforce Fragment"] = 7124,
|
||||||
|
["Power Bracelet"] = 7125,
|
||||||
|
["Small Key"] = 7126,
|
||||||
|
["Bomb"] = 7127,
|
||||||
|
["Recovery Heart"] = 7128,
|
||||||
|
["Five Rupees"] = 7129,
|
||||||
|
["Rupee"] = 7130,
|
||||||
|
["Clock"] = 7131,
|
||||||
|
["Fairy"] = 7132
|
||||||
|
}
|
||||||
|
|
||||||
|
itemCodes = {
|
||||||
|
["Boomerang"] = 0x1D,
|
||||||
|
["Bow"] = 0x0A,
|
||||||
|
["Magical Boomerang"] = 0x1E,
|
||||||
|
["Raft"] = 0x0C,
|
||||||
|
["Stepladder"] = 0x0D,
|
||||||
|
["Recorder"] = 0x05,
|
||||||
|
["Magical Rod"] = 0x10,
|
||||||
|
["Red Candle"] = 0x07,
|
||||||
|
["Book of Magic"] = 0x11,
|
||||||
|
["Magical Key"] = 0x0B,
|
||||||
|
["Red Ring"] = 0x13,
|
||||||
|
["Silver Arrow"] = 0x09,
|
||||||
|
["Sword"] = 0x01,
|
||||||
|
["White Sword"] = 0x02,
|
||||||
|
["Magical Sword"] = 0x03,
|
||||||
|
["Heart Container"] = 0x1A,
|
||||||
|
["Letter"] = 0x15,
|
||||||
|
["Magical Shield"] = 0x1C,
|
||||||
|
["Candle"] = 0x06,
|
||||||
|
["Arrow"] = 0x08,
|
||||||
|
["Food"] = 0x04,
|
||||||
|
["Water of Life (Blue)"] = 0x1F,
|
||||||
|
["Water of Life (Red)"] = 0x20,
|
||||||
|
["Blue Ring"] = 0x12,
|
||||||
|
["Triforce Fragment"] = 0x1B,
|
||||||
|
["Power Bracelet"] = 0x14,
|
||||||
|
["Small Key"] = 0x19,
|
||||||
|
["Bomb"] = 0x00,
|
||||||
|
["Recovery Heart"] = 0x22,
|
||||||
|
["Five Rupees"] = 0x0F,
|
||||||
|
["Rupee"] = 0x18,
|
||||||
|
["Clock"] = 0x21,
|
||||||
|
["Fairy"] = 0x23
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
|
||||||
|
local function defineMemoryFunctions()
|
||||||
|
local memDomain = {}
|
||||||
|
local domains = memory.getmemorydomainlist()
|
||||||
|
if domains[1] == "System Bus" then
|
||||||
|
--NesHawk
|
||||||
|
isNesHawk = true
|
||||||
|
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
|
||||||
|
memDomain["ram"] = function() memory.usememorydomain("RAM") end
|
||||||
|
memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
|
||||||
|
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
|
||||||
|
elseif domains[1] == "WRAM" then
|
||||||
|
--QuickNES
|
||||||
|
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
|
||||||
|
memDomain["ram"] = function() memory.usememorydomain("RAM") end
|
||||||
|
memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
|
||||||
|
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
|
||||||
|
end
|
||||||
|
return memDomain
|
||||||
|
end
|
||||||
|
|
||||||
|
local memDomain = defineMemoryFunctions()
|
||||||
|
u8 = memory.read_u8
|
||||||
|
wU8 = memory.write_u8
|
||||||
|
uRange = memory.readbyterange
|
||||||
|
|
||||||
|
itemIDNames = {}
|
||||||
|
|
||||||
|
for key, value in pairs(itemAPids) do
|
||||||
|
itemIDNames[value] = key
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
local function determineItem(array)
|
||||||
|
memdomain.ram()
|
||||||
|
currentItemsObtained = u8(itemsObtained)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotSword()
|
||||||
|
local currentSword = u8(sword)
|
||||||
|
wU8(sword, math.max(currentSword, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotWhiteSword()
|
||||||
|
local currentSword = u8(sword)
|
||||||
|
wU8(sword, math.max(currentSword, 2))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotMagicalSword()
|
||||||
|
wU8(sword, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotBomb()
|
||||||
|
local currentBombs = u8(bombs)
|
||||||
|
local currentMaxBombs = u8(maxBombs)
|
||||||
|
wU8(bombs, math.min(currentBombs + 4, currentMaxBombs))
|
||||||
|
wU8(0x505, 0x29) -- Fake bomb to show item get.
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotArrow()
|
||||||
|
local currentArrow = u8(arrow)
|
||||||
|
wU8(arrow, math.max(currentArrow, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotSilverArrow()
|
||||||
|
wU8(arrow, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotBow()
|
||||||
|
wU8(bow, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotCandle()
|
||||||
|
local currentCandle = u8(candle)
|
||||||
|
wU8(candle, math.max(currentCandle, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotRedCandle()
|
||||||
|
wU8(candle, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotRecorder()
|
||||||
|
wU8(recorder, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotFood()
|
||||||
|
wU8(food, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotWaterOfLifeBlue()
|
||||||
|
local currentWaterOfLife = u8(waterOfLife)
|
||||||
|
wU8(waterOfLife, math.max(currentWaterOfLife, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotWaterOfLifeRed()
|
||||||
|
wU8(waterOfLife, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotMagicalRod()
|
||||||
|
wU8(magicalRod, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotBookOfMagic()
|
||||||
|
wU8(bookOfMagic, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotRaft()
|
||||||
|
wU8(raft, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotBlueRing()
|
||||||
|
local currentRing = u8(ring)
|
||||||
|
wU8(ring, math.max(currentRing, 1))
|
||||||
|
memDomain.saveram()
|
||||||
|
local currentTunicColor = u8(0x0B92)
|
||||||
|
if currentTunicColor == 0x29 then
|
||||||
|
wU8(0x0B92, 0x32)
|
||||||
|
wU8(0x0804, 0x32)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotRedRing()
|
||||||
|
wU8(ring, 2)
|
||||||
|
memDomain.saveram()
|
||||||
|
wU8(0x0B92, 0x16)
|
||||||
|
wU8(0x0804, 0x16)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotStepladder()
|
||||||
|
wU8(stepladder, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotMagicalKey()
|
||||||
|
wU8(magicalKey, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotPowerBracelet()
|
||||||
|
wU8(powerBracelet, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotLetter()
|
||||||
|
wU8(letter, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotHeartContainer()
|
||||||
|
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
|
||||||
|
if currentHeartContainers < 16 then
|
||||||
|
currentHeartContainers = math.min(currentHeartContainers + 1, 16)
|
||||||
|
local currentHearts = bit.band(u8(heartContainers), 0x0F) + 1
|
||||||
|
wU8(heartContainers, bit.lshift(currentHeartContainers, 4) + currentHearts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotTriforceFragment()
|
||||||
|
local triforceByte = 0xFF
|
||||||
|
local newTriforceCount = u8(localTriforce) + 1
|
||||||
|
wU8(localTriforce, newTriforceCount)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotBoomerang()
|
||||||
|
wU8(boomerang, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotMagicalBoomerang()
|
||||||
|
wU8(magicalBoomerang, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotMagicalShield()
|
||||||
|
wU8(magicalShield, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotRecoveryHeart()
|
||||||
|
local currentHearts = bit.band(u8(heartContainers), 0x0F)
|
||||||
|
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
|
||||||
|
if currentHearts < currentHeartContainers then
|
||||||
|
currentHearts = currentHearts + 1
|
||||||
|
else
|
||||||
|
wU8(partialHearts, 0xFF)
|
||||||
|
end
|
||||||
|
currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts)
|
||||||
|
wU8(heartContainers, currentHearts)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotFairy()
|
||||||
|
local currentHearts = bit.band(u8(heartContainers), 0x0F)
|
||||||
|
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
|
||||||
|
if currentHearts < currentHeartContainers then
|
||||||
|
currentHearts = currentHearts + 3
|
||||||
|
if currentHearts > currentHeartContainers then
|
||||||
|
currentHearts = currentHeartContainers
|
||||||
|
wU8(partialHearts, 0xFF)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
wU8(partialHearts, 0xFF)
|
||||||
|
end
|
||||||
|
currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts)
|
||||||
|
wU8(heartContainers, currentHearts)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotClock()
|
||||||
|
wU8(clockItem, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotFiveRupees()
|
||||||
|
local currentRupeesToAdd = u8(rupeesToAdd)
|
||||||
|
wU8(rupeesToAdd, math.min(currentRupeesToAdd + 5, 255))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotSmallKey()
|
||||||
|
wU8(keys, math.min(u8(keys) + 1, 9))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gotItem(item)
|
||||||
|
--Write itemCode to itemToLift
|
||||||
|
--Write 128 to itemLiftTimer
|
||||||
|
--Write 4 to sound effect queue
|
||||||
|
itemName = itemIDNames[item]
|
||||||
|
itemCode = itemCodes[itemName]
|
||||||
|
wU8(0x505, itemCode)
|
||||||
|
wU8(0x506, 128)
|
||||||
|
wU8(0x602, 4)
|
||||||
|
numberObtained = u8(itemsObtained) + 1
|
||||||
|
wU8(itemsObtained, numberObtained)
|
||||||
|
if itemName == "Boomerang" then gotBoomerang() end
|
||||||
|
if itemName == "Bow" then gotBow() end
|
||||||
|
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
|
||||||
|
if itemName == "Raft" then gotRaft() end
|
||||||
|
if itemName == "Stepladder" then gotStepladder() end
|
||||||
|
if itemName == "Recorder" then gotRecorder() end
|
||||||
|
if itemName == "Magical Rod" then gotMagicalRod() end
|
||||||
|
if itemName == "Red Candle" then gotRedCandle() end
|
||||||
|
if itemName == "Book of Magic" then gotBookOfMagic() end
|
||||||
|
if itemName == "Magical Key" then gotMagicalKey() end
|
||||||
|
if itemName == "Red Ring" then gotRedRing() end
|
||||||
|
if itemName == "Silver Arrow" then gotSilverArrow() end
|
||||||
|
if itemName == "Sword" then gotSword() end
|
||||||
|
if itemName == "White Sword" then gotWhiteSword() end
|
||||||
|
if itemName == "Magical Sword" then gotMagicalSword() end
|
||||||
|
if itemName == "Heart Container" then gotHeartContainer() end
|
||||||
|
if itemName == "Letter" then gotLetter() end
|
||||||
|
if itemName == "Magical Shield" then gotMagicalShield() end
|
||||||
|
if itemName == "Candle" then gotCandle() end
|
||||||
|
if itemName == "Arrow" then gotArrow() end
|
||||||
|
if itemName == "Food" then gotFood() end
|
||||||
|
if itemName == "Water of Life (Blue)" then gotWaterOfLifeBlue() end
|
||||||
|
if itemName == "Water of Life (Red)" then gotWaterOfLifeRed() end
|
||||||
|
if itemName == "Blue Ring" then gotBlueRing() end
|
||||||
|
if itemName == "Triforce Fragment" then gotTriforceFragment() end
|
||||||
|
if itemName == "Power Bracelet" then gotPowerBracelet() end
|
||||||
|
if itemName == "Small Key" then gotSmallKey() end
|
||||||
|
if itemName == "Bomb" then gotBomb() end
|
||||||
|
if itemName == "Recovery Heart" then gotRecoveryHeart() end
|
||||||
|
if itemName == "Five Rupees" then gotFiveRupees() end
|
||||||
|
if itemName == "Fairy" then gotFairy() end
|
||||||
|
if itemName == "Clock" then gotClock() end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function StateOKForMainLoop()
|
||||||
|
memDomain.ram()
|
||||||
|
local gameMode = u8(0x12)
|
||||||
|
return gameMode == 5
|
||||||
|
end
|
||||||
|
|
||||||
|
local function checkCaveItemObtained()
|
||||||
|
memDomain.ram()
|
||||||
|
local returnTable = {}
|
||||||
|
returnTable["slot1"] = u8(shopSlotLeft)
|
||||||
|
returnTable["slot2"] = u8(shopSlotMiddle)
|
||||||
|
returnTable["slot3"] = u8(shopSlotRight)
|
||||||
|
returnTable["takeAnys"] = u8(takeAnyCavesChecked)
|
||||||
|
return returnTable
|
||||||
|
end
|
||||||
|
|
||||||
|
function table.empty (self)
|
||||||
|
for _, _ in pairs(self) do
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function slice (tbl, s, e)
|
||||||
|
local pos, new = 1, {}
|
||||||
|
for i = s + 1, e do
|
||||||
|
new[pos] = tbl[i]
|
||||||
|
pos = pos + 1
|
||||||
|
end
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
local bizhawk_version = client.getversion()
|
||||||
|
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
|
||||||
|
local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8")
|
||||||
|
|
||||||
|
local function getMaxMessageLength()
|
||||||
|
if is23Or24Or25 then
|
||||||
|
return client.screenwidth()/11
|
||||||
|
elseif is26To28 then
|
||||||
|
return client.screenwidth()/12
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawText(x, y, message, color)
|
||||||
|
if is23Or24Or25 then
|
||||||
|
gui.addmessage(message)
|
||||||
|
elseif is26To28 then
|
||||||
|
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clearScreen()
|
||||||
|
if is23Or24Or25 then
|
||||||
|
return
|
||||||
|
elseif is26To28 then
|
||||||
|
drawText(0, 0, "", "black")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawMessages()
|
||||||
|
if table.empty(itemMessages) then
|
||||||
|
clearScreen()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local y = 10
|
||||||
|
found = false
|
||||||
|
maxMessageLength = getMaxMessageLength()
|
||||||
|
for k, v in pairs(itemMessages) do
|
||||||
|
if v["TTL"] > 0 then
|
||||||
|
message = v["message"]
|
||||||
|
while true do
|
||||||
|
drawText(5, y, message:sub(1, maxMessageLength), v["color"])
|
||||||
|
y = y + 16
|
||||||
|
|
||||||
|
message = message:sub(maxMessageLength + 1, message:len())
|
||||||
|
if message:len() == 0 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
newTTL = 0
|
||||||
|
if is26To28 then
|
||||||
|
newTTL = itemMessages[k]["TTL"] - 1
|
||||||
|
end
|
||||||
|
itemMessages[k]["TTL"] = newTTL
|
||||||
|
found = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if found == false then
|
||||||
|
clearScreen()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function generateOverworldLocationChecked()
|
||||||
|
memDomain.ram()
|
||||||
|
data = uRange(0x067E, 0x81)
|
||||||
|
data[0] = nil
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function getHCLocation()
|
||||||
|
memDomain.rom()
|
||||||
|
data = u8(0x1789A)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function getPBLocation()
|
||||||
|
memDomain.rom()
|
||||||
|
data = u8(0x10CB2)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function generateUnderworld16LocationChecked()
|
||||||
|
memDomain.ram()
|
||||||
|
data = uRange(0x06FE, 0x81)
|
||||||
|
data[0] = nil
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function generateUnderworld79LocationChecked()
|
||||||
|
memDomain.ram()
|
||||||
|
data = uRange(0x077E, 0x81)
|
||||||
|
data[0] = nil
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateTriforceFragments()
|
||||||
|
memDomain.ram()
|
||||||
|
local triforceByte = 0xFF
|
||||||
|
totalTriforceCount = u8(localTriforce)
|
||||||
|
local currentPieces = bit.rshift(triforceByte, 8 - math.min(8, totalTriforceCount))
|
||||||
|
wU8(triforceFragments, currentPieces)
|
||||||
|
end
|
||||||
|
|
||||||
|
function processBlock(block)
|
||||||
|
if block ~= nil then
|
||||||
|
local msgBlock = block['messages']
|
||||||
|
if msgBlock ~= nil then
|
||||||
|
for i, v in pairs(msgBlock) do
|
||||||
|
if itemMessages[i] == nil then
|
||||||
|
local msg = {TTL=450, message=v, color=0xFFFF0000}
|
||||||
|
itemMessages[i] = msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local bonusItems = block["bonusItems"]
|
||||||
|
if bonusItems ~= nil and isInGame then
|
||||||
|
for i, item in ipairs(bonusItems) do
|
||||||
|
memDomain.ram()
|
||||||
|
if i > u8(bonusItemsObtained) then
|
||||||
|
if u8(0x505) == 0 then
|
||||||
|
gotItem(item)
|
||||||
|
wU8(itemsObtained, u8(itemsObtained) - 1)
|
||||||
|
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local itemsBlock = block["items"]
|
||||||
|
memDomain.saveram()
|
||||||
|
isInGame = StateOKForMainLoop()
|
||||||
|
updateTriforceFragments()
|
||||||
|
if itemsBlock ~= nil and isInGame then
|
||||||
|
memDomain.ram()
|
||||||
|
--get item from item code
|
||||||
|
--get function from item
|
||||||
|
--do function
|
||||||
|
for i, item in ipairs(itemsBlock) do
|
||||||
|
memDomain.ram()
|
||||||
|
if u8(0x505) == 0 then
|
||||||
|
if i > u8(itemsObtained) then
|
||||||
|
gotItem(item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local shopsBlock = block["shops"]
|
||||||
|
if shopsBlock ~= nil then
|
||||||
|
wU8(shopSlotLeft, bit.bor(u8(shopSlotLeft), shopsBlock["left"]))
|
||||||
|
wU8(shopSlotMiddle, bit.bor(u8(shopSlotMiddle), shopsBlock["middle"]))
|
||||||
|
wU8(shopSlotRight, bit.bor(u8(shopSlotRight), shopsBlock["right"]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function difference(a, b)
|
||||||
|
local aa = {}
|
||||||
|
for k,v in pairs(a) do aa[v]=true end
|
||||||
|
for k,v in pairs(b) do aa[v]=nil end
|
||||||
|
local ret = {}
|
||||||
|
local n = 0
|
||||||
|
for k,v in pairs(a) do
|
||||||
|
if aa[v] then n=n+1 ret[n]=v end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
function receive()
|
||||||
|
l, e = zeldaSocket:receive()
|
||||||
|
if e == 'closed' then
|
||||||
|
if curstate == STATE_OK then
|
||||||
|
print("Connection closed")
|
||||||
|
end
|
||||||
|
curstate = STATE_UNINITIALIZED
|
||||||
|
return
|
||||||
|
elseif e == 'timeout' then
|
||||||
|
print("timeout")
|
||||||
|
return
|
||||||
|
elseif e ~= nil then
|
||||||
|
print(e)
|
||||||
|
curstate = STATE_UNINITIALIZED
|
||||||
|
return
|
||||||
|
end
|
||||||
|
processBlock(json.decode(l))
|
||||||
|
|
||||||
|
-- Determine Message to send back
|
||||||
|
memDomain.rom()
|
||||||
|
local playerName = uRange(0x1F, 0x11)
|
||||||
|
playerName[0] = nil
|
||||||
|
local retTable = {}
|
||||||
|
retTable["playerName"] = playerName
|
||||||
|
if StateOKForMainLoop() then
|
||||||
|
retTable["overworld"] = generateOverworldLocationChecked()
|
||||||
|
retTable["underworld1"] = generateUnderworld16LocationChecked()
|
||||||
|
retTable["underworld2"] = generateUnderworld79LocationChecked()
|
||||||
|
end
|
||||||
|
retTable["caves"] = checkCaveItemObtained()
|
||||||
|
memDomain.ram()
|
||||||
|
if gameMode ~= 19 then
|
||||||
|
gameMode = u8(0x12)
|
||||||
|
end
|
||||||
|
retTable["gameMode"] = gameMode
|
||||||
|
retTable["overworldHC"] = getHCLocation()
|
||||||
|
retTable["overworldPB"] = getPBLocation()
|
||||||
|
retTable["itemsObtained"] = u8(itemsObtained)
|
||||||
|
msg = json.encode(retTable).."\n"
|
||||||
|
local ret, error = zeldaSocket:send(msg)
|
||||||
|
if ret == nil then
|
||||||
|
print(error)
|
||||||
|
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
|
||||||
|
curstate = STATE_TENTATIVELY_CONNECTED
|
||||||
|
elseif curstate == STATE_TENTATIVELY_CONNECTED then
|
||||||
|
print("Connected!")
|
||||||
|
itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
|
||||||
|
curstate = STATE_OK
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function main()
|
||||||
|
if (is23Or24Or25 or is26To28) == false then
|
||||||
|
print("Must use a version of bizhawk 2.3.1 or higher")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
server, error = socket.bind('localhost', 52980)
|
||||||
|
|
||||||
|
while true do
|
||||||
|
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||||
|
frame = frame + 1
|
||||||
|
drawMessages()
|
||||||
|
if not (curstate == prevstate) then
|
||||||
|
-- console.log("Current state: "..curstate)
|
||||||
|
prevstate = curstate
|
||||||
|
end
|
||||||
|
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||||
|
if (frame % 60 == 0) then
|
||||||
|
gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
|
||||||
|
receive()
|
||||||
|
else
|
||||||
|
gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
|
||||||
|
end
|
||||||
|
elseif (curstate == STATE_UNINITIALIZED) then
|
||||||
|
gui.drawEllipse(248, 9, 6, 6, "Black", "White")
|
||||||
|
if (frame % 60 == 0) then
|
||||||
|
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||||
|
|
||||||
|
drawText(5, 8, "Waiting for client", 0xFFFF0000)
|
||||||
|
drawText(5, 32, "Please start Zelda1Client.exe", 0xFFFF0000)
|
||||||
|
|
||||||
|
-- Advance so the messages are drawn
|
||||||
|
emu.frameadvance()
|
||||||
|
server:settimeout(2)
|
||||||
|
print("Attempting to connect")
|
||||||
|
local client, timeout = server:accept()
|
||||||
|
if timeout == nil then
|
||||||
|
-- print('Initial Connection Made')
|
||||||
|
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||||
|
zeldaSocket = client
|
||||||
|
zeldaSocket:settimeout(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
emu.frameadvance()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
main()
|
||||||
BIN
data/lua/TLoZ/core.dll
Normal file
BIN
data/lua/TLoZ/core.dll
Normal file
Binary file not shown.
380
data/lua/TLoZ/json.lua
Normal file
380
data/lua/TLoZ/json.lua
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
--
|
||||||
|
-- json.lua
|
||||||
|
--
|
||||||
|
-- Copyright (c) 2015 rxi
|
||||||
|
--
|
||||||
|
-- This library is free software; you can redistribute it and/or modify it
|
||||||
|
-- under the terms of the MIT license. See LICENSE for details.
|
||||||
|
--
|
||||||
|
|
||||||
|
local json = { _version = "0.1.0" }
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Encode
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local encode
|
||||||
|
|
||||||
|
local escape_char_map = {
|
||||||
|
[ "\\" ] = "\\\\",
|
||||||
|
[ "\"" ] = "\\\"",
|
||||||
|
[ "\b" ] = "\\b",
|
||||||
|
[ "\f" ] = "\\f",
|
||||||
|
[ "\n" ] = "\\n",
|
||||||
|
[ "\r" ] = "\\r",
|
||||||
|
[ "\t" ] = "\\t",
|
||||||
|
}
|
||||||
|
|
||||||
|
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||||
|
for k, v in pairs(escape_char_map) do
|
||||||
|
escape_char_map_inv[v] = k
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function escape_char(c)
|
||||||
|
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_nil(val)
|
||||||
|
return "null"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_table(val, stack)
|
||||||
|
local res = {}
|
||||||
|
stack = stack or {}
|
||||||
|
|
||||||
|
-- Circular reference?
|
||||||
|
if stack[val] then error("circular reference") end
|
||||||
|
|
||||||
|
stack[val] = true
|
||||||
|
|
||||||
|
if val[1] ~= nil or next(val) == nil then
|
||||||
|
-- Treat as array -- check keys are valid and it is not sparse
|
||||||
|
local n = 0
|
||||||
|
for k in pairs(val) do
|
||||||
|
if type(k) ~= "number" then
|
||||||
|
error("invalid table: mixed or invalid key types")
|
||||||
|
end
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
if n ~= #val then
|
||||||
|
error("invalid table: sparse array")
|
||||||
|
end
|
||||||
|
-- Encode
|
||||||
|
for i, v in ipairs(val) do
|
||||||
|
table.insert(res, encode(v, stack))
|
||||||
|
end
|
||||||
|
stack[val] = nil
|
||||||
|
return "[" .. table.concat(res, ",") .. "]"
|
||||||
|
|
||||||
|
else
|
||||||
|
-- Treat as an object
|
||||||
|
for k, v in pairs(val) do
|
||||||
|
if type(k) ~= "string" then
|
||||||
|
error("invalid table: mixed or invalid key types")
|
||||||
|
end
|
||||||
|
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||||
|
end
|
||||||
|
stack[val] = nil
|
||||||
|
return "{" .. table.concat(res, ",") .. "}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_string(val)
|
||||||
|
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_number(val)
|
||||||
|
-- Check for NaN, -inf and inf
|
||||||
|
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||||
|
error("unexpected number value '" .. tostring(val) .. "'")
|
||||||
|
end
|
||||||
|
return string.format("%.14g", val)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local type_func_map = {
|
||||||
|
[ "nil" ] = encode_nil,
|
||||||
|
[ "table" ] = encode_table,
|
||||||
|
[ "string" ] = encode_string,
|
||||||
|
[ "number" ] = encode_number,
|
||||||
|
[ "boolean" ] = tostring,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
encode = function(val, stack)
|
||||||
|
local t = type(val)
|
||||||
|
local f = type_func_map[t]
|
||||||
|
if f then
|
||||||
|
return f(val, stack)
|
||||||
|
end
|
||||||
|
error("unexpected type '" .. t .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function json.encode(val)
|
||||||
|
return ( encode(val) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Decode
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local parse
|
||||||
|
|
||||||
|
local function create_set(...)
|
||||||
|
local res = {}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
res[ select(i, ...) ] = true
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||||
|
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||||
|
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||||
|
local literals = create_set("true", "false", "null")
|
||||||
|
|
||||||
|
local literal_map = {
|
||||||
|
[ "true" ] = true,
|
||||||
|
[ "false" ] = false,
|
||||||
|
[ "null" ] = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function next_char(str, idx, set, negate)
|
||||||
|
for i = idx, #str do
|
||||||
|
if set[str:sub(i, i)] ~= negate then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return #str + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function decode_error(str, idx, msg)
|
||||||
|
--local line_count = 1
|
||||||
|
--local col_count = 1
|
||||||
|
--for i = 1, idx - 1 do
|
||||||
|
-- col_count = col_count + 1
|
||||||
|
-- if str:sub(i, i) == "\n" then
|
||||||
|
-- line_count = line_count + 1
|
||||||
|
-- col_count = 1
|
||||||
|
-- end
|
||||||
|
-- end
|
||||||
|
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function codepoint_to_utf8(n)
|
||||||
|
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||||
|
local f = math.floor
|
||||||
|
if n <= 0x7f then
|
||||||
|
return string.char(n)
|
||||||
|
elseif n <= 0x7ff then
|
||||||
|
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||||
|
elseif n <= 0xffff then
|
||||||
|
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||||
|
elseif n <= 0x10ffff then
|
||||||
|
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||||
|
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||||
|
end
|
||||||
|
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_unicode_escape(s)
|
||||||
|
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||||
|
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||||
|
-- Surrogate pair?
|
||||||
|
if n2 then
|
||||||
|
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||||
|
else
|
||||||
|
return codepoint_to_utf8(n1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_string(str, i)
|
||||||
|
local has_unicode_escape = false
|
||||||
|
local has_surrogate_escape = false
|
||||||
|
local has_escape = false
|
||||||
|
local last
|
||||||
|
for j = i + 1, #str do
|
||||||
|
local x = str:byte(j)
|
||||||
|
|
||||||
|
if x < 32 then
|
||||||
|
decode_error(str, j, "control character in string")
|
||||||
|
end
|
||||||
|
|
||||||
|
if last == 92 then -- "\\" (escape char)
|
||||||
|
if x == 117 then -- "u" (unicode escape sequence)
|
||||||
|
local hex = str:sub(j + 1, j + 5)
|
||||||
|
if not hex:find("%x%x%x%x") then
|
||||||
|
decode_error(str, j, "invalid unicode escape in string")
|
||||||
|
end
|
||||||
|
if hex:find("^[dD][89aAbB]") then
|
||||||
|
has_surrogate_escape = true
|
||||||
|
else
|
||||||
|
has_unicode_escape = true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local c = string.char(x)
|
||||||
|
if not escape_chars[c] then
|
||||||
|
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||||
|
end
|
||||||
|
has_escape = true
|
||||||
|
end
|
||||||
|
last = nil
|
||||||
|
|
||||||
|
elseif x == 34 then -- '"' (end of string)
|
||||||
|
local s = str:sub(i + 1, j - 1)
|
||||||
|
if has_surrogate_escape then
|
||||||
|
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||||
|
end
|
||||||
|
if has_unicode_escape then
|
||||||
|
s = s:gsub("\\u....", parse_unicode_escape)
|
||||||
|
end
|
||||||
|
if has_escape then
|
||||||
|
s = s:gsub("\\.", escape_char_map_inv)
|
||||||
|
end
|
||||||
|
return s, j + 1
|
||||||
|
|
||||||
|
else
|
||||||
|
last = x
|
||||||
|
end
|
||||||
|
end
|
||||||
|
decode_error(str, i, "expected closing quote for string")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_number(str, i)
|
||||||
|
local x = next_char(str, i, delim_chars)
|
||||||
|
local s = str:sub(i, x - 1)
|
||||||
|
local n = tonumber(s)
|
||||||
|
if not n then
|
||||||
|
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||||
|
end
|
||||||
|
return n, x
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_literal(str, i)
|
||||||
|
local x = next_char(str, i, delim_chars)
|
||||||
|
local word = str:sub(i, x - 1)
|
||||||
|
if not literals[word] then
|
||||||
|
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||||
|
end
|
||||||
|
return literal_map[word], x
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_array(str, i)
|
||||||
|
local res = {}
|
||||||
|
local n = 1
|
||||||
|
i = i + 1
|
||||||
|
while 1 do
|
||||||
|
local x
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
-- Empty / end of array?
|
||||||
|
if str:sub(i, i) == "]" then
|
||||||
|
i = i + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- Read token
|
||||||
|
x, i = parse(str, i)
|
||||||
|
res[n] = x
|
||||||
|
n = n + 1
|
||||||
|
-- Next token
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
local chr = str:sub(i, i)
|
||||||
|
i = i + 1
|
||||||
|
if chr == "]" then break end
|
||||||
|
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||||
|
end
|
||||||
|
return res, i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_object(str, i)
|
||||||
|
local res = {}
|
||||||
|
i = i + 1
|
||||||
|
while 1 do
|
||||||
|
local key, val
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
-- Empty / end of object?
|
||||||
|
if str:sub(i, i) == "}" then
|
||||||
|
i = i + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- Read key
|
||||||
|
if str:sub(i, i) ~= '"' then
|
||||||
|
decode_error(str, i, "expected string for key")
|
||||||
|
end
|
||||||
|
key, i = parse(str, i)
|
||||||
|
-- Read ':' delimiter
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
if str:sub(i, i) ~= ":" then
|
||||||
|
decode_error(str, i, "expected ':' after key")
|
||||||
|
end
|
||||||
|
i = next_char(str, i + 1, space_chars, true)
|
||||||
|
-- Read value
|
||||||
|
val, i = parse(str, i)
|
||||||
|
-- Set
|
||||||
|
res[key] = val
|
||||||
|
-- Next token
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
local chr = str:sub(i, i)
|
||||||
|
i = i + 1
|
||||||
|
if chr == "}" then break end
|
||||||
|
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||||
|
end
|
||||||
|
return res, i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local char_func_map = {
|
||||||
|
[ '"' ] = parse_string,
|
||||||
|
[ "0" ] = parse_number,
|
||||||
|
[ "1" ] = parse_number,
|
||||||
|
[ "2" ] = parse_number,
|
||||||
|
[ "3" ] = parse_number,
|
||||||
|
[ "4" ] = parse_number,
|
||||||
|
[ "5" ] = parse_number,
|
||||||
|
[ "6" ] = parse_number,
|
||||||
|
[ "7" ] = parse_number,
|
||||||
|
[ "8" ] = parse_number,
|
||||||
|
[ "9" ] = parse_number,
|
||||||
|
[ "-" ] = parse_number,
|
||||||
|
[ "t" ] = parse_literal,
|
||||||
|
[ "f" ] = parse_literal,
|
||||||
|
[ "n" ] = parse_literal,
|
||||||
|
[ "[" ] = parse_array,
|
||||||
|
[ "{" ] = parse_object,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
parse = function(str, idx)
|
||||||
|
local chr = str:sub(idx, idx)
|
||||||
|
local f = char_func_map[chr]
|
||||||
|
if f then
|
||||||
|
return f(str, idx)
|
||||||
|
end
|
||||||
|
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function json.decode(str)
|
||||||
|
if type(str) ~= "string" then
|
||||||
|
error("expected argument of type string, got " .. type(str))
|
||||||
|
end
|
||||||
|
return ( parse(str, next_char(str, 1, space_chars, true)) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return json
|
||||||
132
data/lua/TLoZ/socket.lua
Normal file
132
data/lua/TLoZ/socket.lua
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- LuaSocket helper module
|
||||||
|
-- Author: Diego Nehab
|
||||||
|
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Declare module and import dependencies
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
local base = _G
|
||||||
|
local string = require("string")
|
||||||
|
local math = require("math")
|
||||||
|
local socket = require("socket.core")
|
||||||
|
module("socket")
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Exported auxiliar functions
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
function connect(address, port, laddress, lport)
|
||||||
|
local sock, err = socket.tcp()
|
||||||
|
if not sock then return nil, err end
|
||||||
|
if laddress then
|
||||||
|
local res, err = sock:bind(laddress, lport, -1)
|
||||||
|
if not res then return nil, err end
|
||||||
|
end
|
||||||
|
local res, err = sock:connect(address, port)
|
||||||
|
if not res then return nil, err end
|
||||||
|
return sock
|
||||||
|
end
|
||||||
|
|
||||||
|
function bind(host, port, backlog)
|
||||||
|
local sock, err = socket.tcp()
|
||||||
|
if not sock then return nil, err end
|
||||||
|
sock:setoption("reuseaddr", true)
|
||||||
|
local res, err = sock:bind(host, port)
|
||||||
|
if not res then return nil, err end
|
||||||
|
res, err = sock:listen(backlog)
|
||||||
|
if not res then return nil, err end
|
||||||
|
return sock
|
||||||
|
end
|
||||||
|
|
||||||
|
try = newtry()
|
||||||
|
|
||||||
|
function choose(table)
|
||||||
|
return function(name, opt1, opt2)
|
||||||
|
if base.type(name) ~= "string" then
|
||||||
|
name, opt1, opt2 = "default", name, opt1
|
||||||
|
end
|
||||||
|
local f = table[name or "nil"]
|
||||||
|
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||||
|
else return f(opt1, opt2) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Socket sources and sinks, conforming to LTN12
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- create namespaces inside LuaSocket namespace
|
||||||
|
sourcet = {}
|
||||||
|
sinkt = {}
|
||||||
|
|
||||||
|
BLOCKSIZE = 2048
|
||||||
|
|
||||||
|
sinkt["close-when-done"] = function(sock)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function(self, chunk, err)
|
||||||
|
if not chunk then
|
||||||
|
sock:close()
|
||||||
|
return 1
|
||||||
|
else return sock:send(chunk) end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sinkt["keep-open"] = function(sock)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function(self, chunk, err)
|
||||||
|
if chunk then return sock:send(chunk)
|
||||||
|
else return 1 end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sinkt["default"] = sinkt["keep-open"]
|
||||||
|
|
||||||
|
sink = choose(sinkt)
|
||||||
|
|
||||||
|
sourcet["by-length"] = function(sock, length)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function()
|
||||||
|
if length <= 0 then return nil end
|
||||||
|
local size = math.min(socket.BLOCKSIZE, length)
|
||||||
|
local chunk, err = sock:receive(size)
|
||||||
|
if err then return nil, err end
|
||||||
|
length = length - string.len(chunk)
|
||||||
|
return chunk
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sourcet["until-closed"] = function(sock)
|
||||||
|
local done
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function()
|
||||||
|
if done then return nil end
|
||||||
|
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||||
|
if not err then return chunk
|
||||||
|
elseif err == "closed" then
|
||||||
|
sock:close()
|
||||||
|
done = 1
|
||||||
|
return partial
|
||||||
|
else return nil, err end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
sourcet["default"] = sourcet["until-closed"]
|
||||||
|
|
||||||
|
source = choose(sourcet)
|
||||||
137
data/lua/connector_ladx_bizhawk.lua
Normal file
137
data/lua/connector_ladx_bizhawk.lua
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
-- SPDX-FileCopyrightText: 2023 Wilhelm Schürmann <wimschuermann@googlemail.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- This script attempts to implement the basic functionality needed in order for
|
||||||
|
-- the LADXR Archipelago client to be able to talk to BizHawk instead of RetroArch
|
||||||
|
-- by reproducing the RetroArch API with BizHawk's Lua interface.
|
||||||
|
--
|
||||||
|
-- RetroArch UDP API: https://github.com/libretro/RetroArch/blob/master/command.c
|
||||||
|
--
|
||||||
|
-- Only
|
||||||
|
-- VERSION
|
||||||
|
-- GET_STATUS
|
||||||
|
-- READ_CORE_MEMORY
|
||||||
|
-- WRITE_CORE_MEMORY
|
||||||
|
-- commands are supported right now.
|
||||||
|
--
|
||||||
|
-- USAGE:
|
||||||
|
-- Load this script in BizHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script")
|
||||||
|
--
|
||||||
|
-- All inconsistencies (like missing newlines for some commands) of the RetroArch
|
||||||
|
-- UDP API (network_cmd_enable) are reproduced as-is in order for clients written to work with
|
||||||
|
-- RetroArch's current API to "just work"(tm).
|
||||||
|
--
|
||||||
|
-- This script has only been tested on GB(C). If you have made sure it works for N64 or other
|
||||||
|
-- cores supported by BizHawk, please let me know. Note that GET_STATUS, at the very least, will
|
||||||
|
-- have to be adjusted.
|
||||||
|
--
|
||||||
|
--
|
||||||
|
-- NOTE:
|
||||||
|
-- BizHawk's Lua API is very trigger-happy on throwing exceptions.
|
||||||
|
-- Emulation will continue fine, but the RetroArch API layer will stop working. This
|
||||||
|
-- is indicated only by an exception visible in the Lua console, which most players
|
||||||
|
-- will probably not have in the foreground.
|
||||||
|
--
|
||||||
|
-- pcall(), the usual way to catch exceptions in Lua, doesn't appear to be supported at all,
|
||||||
|
-- meaning that error/exception handling is not easily possible.
|
||||||
|
--
|
||||||
|
-- This means that a lot more error checking would need to happen before e.g. reading/writing
|
||||||
|
-- memory. Since the end goal, according to AP's Discord, seems to be SNI integration of GB(C),
|
||||||
|
-- no further fault-proofing has been done on this.
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
local socket = require("socket")
|
||||||
|
local udp = socket.udp()
|
||||||
|
|
||||||
|
udp:setsockname('127.0.0.1', 55355)
|
||||||
|
udp:settimeout(0)
|
||||||
|
|
||||||
|
|
||||||
|
while true do
|
||||||
|
-- Attempt to lessen the CPU load by only polling the UDP socket every x frames.
|
||||||
|
-- x = 10 is entirely arbitrary, very little thought went into it.
|
||||||
|
-- We could try to make use of client.get_approx_framerate() here, but the values returned
|
||||||
|
-- seemed more or less arbitrary as well.
|
||||||
|
--
|
||||||
|
-- NOTE: Never mind the above, the LADXR Archipelago client appears to run into problems with
|
||||||
|
-- interwoven GET_STATUS calls, leading to stopped communication.
|
||||||
|
-- For GB(C), polling the socket on every frame is OK-ish, so we just do that.
|
||||||
|
--
|
||||||
|
--while emu.framecount() % 10 ~= 0 do
|
||||||
|
-- emu.frameadvance()
|
||||||
|
--end
|
||||||
|
|
||||||
|
local data, msg_or_ip, port_or_nil = udp:receivefrom()
|
||||||
|
if data then
|
||||||
|
-- "data" format is "COMMAND [PARAMETERS] [...]"
|
||||||
|
local command = string.match(data, "%S+")
|
||||||
|
if command == "VERSION" then
|
||||||
|
-- 1.14 is the latest RetroArch release at the time of writing this, no other reason
|
||||||
|
-- for choosing this here.
|
||||||
|
udp:sendto("1.14.0\n", msg_or_ip, port_or_nil)
|
||||||
|
elseif command == "GET_STATUS" then
|
||||||
|
local status = "PLAYING"
|
||||||
|
if client.ispaused() then
|
||||||
|
status = "PAUSED"
|
||||||
|
end
|
||||||
|
|
||||||
|
if emu.getsystemid() == "GBC" then
|
||||||
|
-- Actual reply from RetroArch's API:
|
||||||
|
-- "GET_STATUS PLAYING game_boy,AP_62468482466172374046_P1_Lonk,crc32=3ecb7b6f"
|
||||||
|
-- CRC32 isn't readily available through the Lua API. We could calculate
|
||||||
|
-- it ourselves, but since LADXR doesn't make use of this field it is
|
||||||
|
-- simply replaced by the hash that BizHawk _does_ make available.
|
||||||
|
|
||||||
|
udp:sendto(
|
||||||
|
"GET_STATUS " .. status .. " game_boy," ..
|
||||||
|
string.gsub(gameinfo.getromname(), "[%s,]", "_") ..
|
||||||
|
",romhash=" ..
|
||||||
|
gameinfo.getromhash() .. "\n",
|
||||||
|
msg_or_ip, port_or_nil
|
||||||
|
)
|
||||||
|
else -- No ROM loaded
|
||||||
|
-- NOTE: No newline is intentional here for 1:1 RetroArch compatibility
|
||||||
|
udp:sendto("GET_STATUS CONTENTLESS", msg_or_ip, port_or_nil)
|
||||||
|
end
|
||||||
|
elseif command == "READ_CORE_MEMORY" then
|
||||||
|
local _, address, length = string.match(data, "(%S+) (%S+) (%S+)")
|
||||||
|
address = tonumber(address, 16)
|
||||||
|
length = tonumber(length)
|
||||||
|
|
||||||
|
-- NOTE: mainmemory.read_bytes_as_array() would seem to be the obvious choice
|
||||||
|
-- here instead, but it isn't. At least for Sameboy and Gambatte, the "main"
|
||||||
|
-- memory differs (ROM vs WRAM).
|
||||||
|
-- Using memory.read_bytes_as_array() and explicitly using the System Bus
|
||||||
|
-- as the active memory domain solves this incompatibility, allowing us
|
||||||
|
-- to hopefully use whatever GB(C) emulator we want.
|
||||||
|
local mem = memory.read_bytes_as_array(address, length, "System Bus")
|
||||||
|
local hex_string = ""
|
||||||
|
for _, v in ipairs(mem) do
|
||||||
|
hex_string = hex_string .. string.format("%02X ", v)
|
||||||
|
end
|
||||||
|
hex_string = hex_string:sub(1, -2) -- Hang head in shame, remove last " "
|
||||||
|
local reply = string.format("%s %02x %s\n", command, address, hex_string)
|
||||||
|
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||||
|
elseif command == "WRITE_CORE_MEMORY" then
|
||||||
|
local _, address = string.match(data, "(%S+) (%S+)")
|
||||||
|
address = tonumber(address, 16)
|
||||||
|
|
||||||
|
local to_write = {}
|
||||||
|
local i = 1
|
||||||
|
for byte_str in string.gmatch(data, "%S+") do
|
||||||
|
if i > 2 then
|
||||||
|
table.insert(to_write, tonumber(byte_str, 16))
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
memory.write_bytes_as_array(address, to_write, "System Bus")
|
||||||
|
local reply = string.format("%s %02x %d\n", command, address, i - 3)
|
||||||
|
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
emu.frameadvance()
|
||||||
|
end
|
||||||
BIN
data/lua/core.dll
Normal file
BIN
data/lua/core.dll
Normal file
Binary file not shown.
132
data/lua/socket.lua
Normal file
132
data/lua/socket.lua
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- LuaSocket helper module
|
||||||
|
-- Author: Diego Nehab
|
||||||
|
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Declare module and import dependencies
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
local base = _G
|
||||||
|
local string = require("string")
|
||||||
|
local math = require("math")
|
||||||
|
local socket = require("socket.core")
|
||||||
|
module("socket")
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Exported auxiliar functions
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
function connect(address, port, laddress, lport)
|
||||||
|
local sock, err = socket.tcp()
|
||||||
|
if not sock then return nil, err end
|
||||||
|
if laddress then
|
||||||
|
local res, err = sock:bind(laddress, lport, -1)
|
||||||
|
if not res then return nil, err end
|
||||||
|
end
|
||||||
|
local res, err = sock:connect(address, port)
|
||||||
|
if not res then return nil, err end
|
||||||
|
return sock
|
||||||
|
end
|
||||||
|
|
||||||
|
function bind(host, port, backlog)
|
||||||
|
local sock, err = socket.tcp()
|
||||||
|
if not sock then return nil, err end
|
||||||
|
sock:setoption("reuseaddr", true)
|
||||||
|
local res, err = sock:bind(host, port)
|
||||||
|
if not res then return nil, err end
|
||||||
|
res, err = sock:listen(backlog)
|
||||||
|
if not res then return nil, err end
|
||||||
|
return sock
|
||||||
|
end
|
||||||
|
|
||||||
|
try = newtry()
|
||||||
|
|
||||||
|
function choose(table)
|
||||||
|
return function(name, opt1, opt2)
|
||||||
|
if base.type(name) ~= "string" then
|
||||||
|
name, opt1, opt2 = "default", name, opt1
|
||||||
|
end
|
||||||
|
local f = table[name or "nil"]
|
||||||
|
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||||
|
else return f(opt1, opt2) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- Socket sources and sinks, conforming to LTN12
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
-- create namespaces inside LuaSocket namespace
|
||||||
|
sourcet = {}
|
||||||
|
sinkt = {}
|
||||||
|
|
||||||
|
BLOCKSIZE = 2048
|
||||||
|
|
||||||
|
sinkt["close-when-done"] = function(sock)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function(self, chunk, err)
|
||||||
|
if not chunk then
|
||||||
|
sock:close()
|
||||||
|
return 1
|
||||||
|
else return sock:send(chunk) end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sinkt["keep-open"] = function(sock)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function(self, chunk, err)
|
||||||
|
if chunk then return sock:send(chunk)
|
||||||
|
else return 1 end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sinkt["default"] = sinkt["keep-open"]
|
||||||
|
|
||||||
|
sink = choose(sinkt)
|
||||||
|
|
||||||
|
sourcet["by-length"] = function(sock, length)
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function()
|
||||||
|
if length <= 0 then return nil end
|
||||||
|
local size = math.min(socket.BLOCKSIZE, length)
|
||||||
|
local chunk, err = sock:receive(size)
|
||||||
|
if err then return nil, err end
|
||||||
|
length = length - string.len(chunk)
|
||||||
|
return chunk
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
sourcet["until-closed"] = function(sock)
|
||||||
|
local done
|
||||||
|
return base.setmetatable({
|
||||||
|
getfd = function() return sock:getfd() end,
|
||||||
|
dirty = function() return sock:dirty() end
|
||||||
|
}, {
|
||||||
|
__call = function()
|
||||||
|
if done then return nil end
|
||||||
|
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||||
|
if not err then return chunk
|
||||||
|
elseif err == "closed" then
|
||||||
|
sock:close()
|
||||||
|
done = 1
|
||||||
|
return partial
|
||||||
|
else return nil, err end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
sourcet["default"] = sourcet["until-closed"]
|
||||||
|
|
||||||
|
source = choose(sourcet)
|
||||||
BIN
data/sprites/ladx/Bowwow.bdiff
Normal file
BIN
data/sprites/ladx/Bowwow.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Bunny.bdiff
Normal file
BIN
data/sprites/ladx/Bunny.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Luigi.bdiff
Normal file
BIN
data/sprites/ladx/Luigi.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Mario.bdiff
Normal file
BIN
data/sprites/ladx/Mario.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Matty_LA.bdiff
Normal file
BIN
data/sprites/ladx/Matty_LA.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Richard.bdiff
Normal file
BIN
data/sprites/ladx/Richard.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Tarin.bdiff
Normal file
BIN
data/sprites/ladx/Tarin.bdiff
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 535 KiB |
@@ -75,6 +75,18 @@ flowchart LR
|
|||||||
end
|
end
|
||||||
SNI <-- Various, depending on SNES device --> DK3
|
SNI <-- Various, depending on SNES device --> DK3
|
||||||
|
|
||||||
|
%% Super Mario World
|
||||||
|
subgraph Super Mario World
|
||||||
|
SMW[SNES]
|
||||||
|
end
|
||||||
|
SNI <-- Various, depending on SNES device --> SMW
|
||||||
|
|
||||||
|
%% Lufia II Ancient Cave
|
||||||
|
subgraph Lufia II Ancient Cave
|
||||||
|
L2AC[SNES]
|
||||||
|
end
|
||||||
|
SNI <-- Various, depending on SNES device --> L2AC
|
||||||
|
|
||||||
%% Native Clients or Games
|
%% Native Clients or Games
|
||||||
%% Games or clients which compile to native or which the client is integrated in the game.
|
%% Games or clients which compile to native or which the client is integrated in the game.
|
||||||
subgraph "Native"
|
subgraph "Native"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 91 KiB |
@@ -9,7 +9,7 @@ These steps should be followed in order to establish a gameplay connection with
|
|||||||
5. Client sends [Connect](#Connect) packet in order to authenticate with the server.
|
5. Client sends [Connect](#Connect) packet in order to authenticate with the server.
|
||||||
6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused).
|
6. Server validates the client's packet and responds with [Connected](#Connected) or [ConnectionRefused](#ConnectionRefused).
|
||||||
7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it.
|
7. Server may send [ReceivedItems](#ReceivedItems) to the client, in the case that the client is missing items that are queued up for it.
|
||||||
8. Server sends [Print](#Print) to all players to notify them of the new client connection.
|
8. Server sends [PrintJSON](#PrintJSON) to all players to notify them of the new client connection.
|
||||||
|
|
||||||
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
|
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
|
||||||
|
|
||||||
@@ -54,7 +54,6 @@ These packets are are sent from the multiworld server to the client. They are no
|
|||||||
* [ReceivedItems](#ReceivedItems)
|
* [ReceivedItems](#ReceivedItems)
|
||||||
* [LocationInfo](#LocationInfo)
|
* [LocationInfo](#LocationInfo)
|
||||||
* [RoomUpdate](#RoomUpdate)
|
* [RoomUpdate](#RoomUpdate)
|
||||||
* [Print](#Print)
|
|
||||||
* [PrintJSON](#PrintJSON)
|
* [PrintJSON](#PrintJSON)
|
||||||
* [DataPackage](#DataPackage)
|
* [DataPackage](#DataPackage)
|
||||||
* [Bounced](#Bounced)
|
* [Bounced](#Bounced)
|
||||||
@@ -65,18 +64,19 @@ These packets are are sent from the multiworld server to the client. They are no
|
|||||||
### RoomInfo
|
### RoomInfo
|
||||||
Sent to clients when they connect to an Archipelago server.
|
Sent to clients when they connect to an Archipelago server.
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
|-----------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
|
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
|
||||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||||
| password | bool | Denoted whether a password is required to join this room.|
|
| password | bool | Denoted whether a password is required to join this room. |
|
||||||
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
|
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". |
|
||||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. |
|
||||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
| location_check_points | int | The amount of hint points you receive per item/location check completed. |
|
||||||
| games | list\[str\] | List of games present in this multiworld. |
|
| games | list\[str\] | List of games present in this multiworld. |
|
||||||
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). |
|
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** |
|
||||||
| seed_name | str | uniquely identifying name of this generation |
|
| datapackage_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. |
|
||||||
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
| seed_name | str | Uniquely identifying name of this generation |
|
||||||
|
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
||||||
|
|
||||||
#### release
|
#### release
|
||||||
Dictates what is allowed when it comes to a player releasing their run. A release is an action which distributes the rest of the items in a player's run to those other players awaiting them.
|
Dictates what is allowed when it comes to a player releasing their run. A release is an action which distributes the rest of the items in a player's run to those other players awaiting them.
|
||||||
@@ -107,8 +107,8 @@ Dictates what is allowed when it comes to a player querying the items remaining
|
|||||||
### ConnectionRefused
|
### ConnectionRefused
|
||||||
Sent to clients when the server refuses connection. This is sent during the initial connection handshake.
|
Sent to clients when the server refuses connection. This is sent during the initial connection handshake.
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
|--------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `IncompatibleVersion`, `InvalidPassword`, or `InvalidItemsHandling`. |
|
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `IncompatibleVersion`, `InvalidPassword`, or `InvalidItemsHandling`. |
|
||||||
|
|
||||||
InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server.
|
InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server.
|
||||||
@@ -160,35 +160,44 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
|
|||||||
|
|
||||||
All arguments for this packet are optional, only changes are sent.
|
All arguments for this packet are optional, only changes are sent.
|
||||||
|
|
||||||
### Print
|
|
||||||
Sent to clients purely to display a message to the player.
|
|
||||||
* *Deprecation warning: clients that connect with version 0.3.5 or higher will nolonger recieve Print packets, instead all messsages are send as [PrintJSON](#PrintJSON)*
|
|
||||||
#### Arguments
|
|
||||||
| Name | Type | Notes |
|
|
||||||
| ---- | ---- | ----- |
|
|
||||||
| text | str | Message to display to player. |
|
|
||||||
|
|
||||||
### PrintJSON
|
### PrintJSON
|
||||||
Sent to clients purely to display a message to the player. This packet differs from [Print](#Print) in that the data being sent with this packet allows for more configurable or specific messaging.
|
Sent to clients purely to display a message to the player. While various message types provide additional arguments, clients only need to evaluate the `data` argument to construct the human-readable message text. All other arguments may be ignored safely.
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Message Types | Contents |
|
||||||
| ---- | ---- | ----- |
|
| ---- | ---- | ------------- | -------- |
|
||||||
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
|
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | (all) | Textual content of this message |
|
||||||
| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. |
|
| type | str | (any) | [PrintJsonType](#PrintJsonType) of this message (optional) |
|
||||||
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
|
| receiving | int | ItemSend, ItemCheat, Hint | Destination player's ID |
|
||||||
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
|
| item | [NetworkItem](#NetworkItem) | ItemSend, ItemCheat, Hint | Source player's ID, location ID, item ID and item flags |
|
||||||
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
|
| found | bool | Hint | Whether the location hinted for was checked |
|
||||||
| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. |
|
| team | int | Join, Part, Chat, TagsChanged, Goal, Release, Collect, ItemCheat | Team of the triggering player |
|
||||||
|
| slot | int | Join, Part, Chat, TagsChanged, Goal, Release, Collect | Slot of the triggering player |
|
||||||
|
| message | str | Chat, ServerChat | Original chat message without sender prefix |
|
||||||
|
| tags | list\[str\] | Join, TagsChanged | Tags of the triggering player |
|
||||||
|
| countdown | int | Countdown | Amount of seconds remaining on the countdown |
|
||||||
|
|
||||||
##### PrintJsonType
|
#### PrintJsonType
|
||||||
PrintJsonType indicates the type of [PrintJson](#PrintJson) packet, different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown type the data's list\[[JSONMessagePart](#JSONMessagePart)\] should still be printed as normal.
|
PrintJsonType indicates the type of a [PrintJSON](#PrintJSON) packet. Different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown or missing type, the `data`'s list\[[JSONMessagePart](#JSONMessagePart)\] should still be displayed to the player as normal text.
|
||||||
|
|
||||||
Currently defined types are:
|
Currently defined types are:
|
||||||
| Type | Notes |
|
|
||||||
| ---- | ----- |
|
| Type | Subject |
|
||||||
| ItemSend | The message is in response to a player receiving an item. |
|
| ---- | ------- |
|
||||||
| Hint | The message is in response to a player hinting. |
|
| ItemSend | A player received an item. |
|
||||||
| Countdown | The message contains information about the current server Countdown. |
|
| ItemCheat | A player used the `!getitem` command. |
|
||||||
|
| Hint | A player hinted. |
|
||||||
|
| Join | A player connected. |
|
||||||
|
| Part | A player disconnected. |
|
||||||
|
| Chat | A player sent a chat message. |
|
||||||
|
| ServerChat | The server broadcasted a message. |
|
||||||
|
| Tutorial | The client has triggered a tutorial message, such as when first connecting. |
|
||||||
|
| TagsChanged | A player changed their tags. |
|
||||||
|
| CommandResult | Someone (usually the client) entered an `!` command. |
|
||||||
|
| AdminCommandResult | The client entered an `!admin` command. |
|
||||||
|
| Goal | A player reached their goal. |
|
||||||
|
| Release | A player released the remaining items in their world. |
|
||||||
|
| Collect | A player collected the remaining items for their world. |
|
||||||
|
| Countdown | The current server countdown has progressed. |
|
||||||
|
|
||||||
### DataPackage
|
### DataPackage
|
||||||
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
|
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
|
||||||
@@ -546,12 +555,16 @@ Color options:
|
|||||||
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
|
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
|
||||||
|
|
||||||
### Client States
|
### Client States
|
||||||
An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate).
|
An enumeration containing the possible client states that may be used to inform
|
||||||
|
the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets
|
||||||
|
the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection
|
||||||
|
to a slot.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import enum
|
import enum
|
||||||
class ClientStatus(enum.IntEnum):
|
class ClientStatus(enum.IntEnum):
|
||||||
CLIENT_UNKNOWN = 0
|
CLIENT_UNKNOWN = 0
|
||||||
|
CLIENT_CONNECTED = 5
|
||||||
CLIENT_READY = 10
|
CLIENT_READY = 10
|
||||||
CLIENT_PLAYING = 20
|
CLIENT_PLAYING = 20
|
||||||
CLIENT_GOAL = 30
|
CLIENT_GOAL = 30
|
||||||
@@ -636,11 +649,12 @@ Note:
|
|||||||
#### GameData
|
#### GameData
|
||||||
GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation.
|
GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation.
|
||||||
|
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
|---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
|
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
|
||||||
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
|
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
|
||||||
| version | int | Version number of this game's data |
|
| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. |
|
||||||
|
| checksum | str | A checksum hash of this game's data. |
|
||||||
|
|
||||||
### Tags
|
### Tags
|
||||||
Tags are represented as a list of strings, the common Client tags follow:
|
Tags are represented as a list of strings, the common Client tags follow:
|
||||||
|
|||||||
188
docs/options api.md
Normal file
188
docs/options api.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Archipelago Options API
|
||||||
|
|
||||||
|
This document covers some of the generic options available using Archipelago's options handling system.
|
||||||
|
|
||||||
|
For more information on where these options go in your world please refer to:
|
||||||
|
- [world api.md](/docs/world%20api.md)
|
||||||
|
|
||||||
|
Archipelago will be abbreviated as "AP" from now on.
|
||||||
|
|
||||||
|
## Option Definitions
|
||||||
|
Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you
|
||||||
|
need to create:
|
||||||
|
- A new option class with a docstring detailing what the option will do to your user.
|
||||||
|
- A `display_name` to be displayed on the webhost.
|
||||||
|
- A new entry in the `option_definitions` dict for your World.
|
||||||
|
By style and convention, the internal names should be snake_case. If the option supports having multiple sub_options
|
||||||
|
such as Choice options, these can be defined with `option_my_sub_option`, where the preceding `option_` is required and
|
||||||
|
stripped for users, so will show as `my_sub_option` in yaml files and if `auto_display_name` is True `My Sub Option`
|
||||||
|
on the webhost. All options support `random` as a generic option. `random` chooses from any of the available
|
||||||
|
values for that option, and is reserved by AP. You can set this as your default value but you cannot define your own
|
||||||
|
new `option_random`.
|
||||||
|
|
||||||
|
### Option Creation
|
||||||
|
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
|
||||||
|
create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our
|
||||||
|
options:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Options.py
|
||||||
|
class StartingSword(Toggle):
|
||||||
|
"""Adds a sword to your starting inventory."""
|
||||||
|
display_name = "Start With Sword"
|
||||||
|
|
||||||
|
|
||||||
|
example_options = {
|
||||||
|
"starting_sword": StartingSword
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
|
||||||
|
to our world's `__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
from .Options import options
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleWorld(World):
|
||||||
|
option_definitions = options
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option Checking
|
||||||
|
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
|
||||||
|
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
|
||||||
|
`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to
|
||||||
|
relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is
|
||||||
|
the option class's `value` attribute. For our example above we can do a simple check:
|
||||||
|
```python
|
||||||
|
if self.multiworld.starting_sword[self.player]:
|
||||||
|
do_some_things()
|
||||||
|
```
|
||||||
|
|
||||||
|
or if I need a boolean object, such as in my slot_data I can access it as:
|
||||||
|
```python
|
||||||
|
start_with_sword = bool(self.multiworld.starting_sword[self.player].value)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic Option Classes
|
||||||
|
These options are generically available to every game automatically, but can be overridden for slightly different
|
||||||
|
behavior, if desired. See `worlds/soe/Options.py` for an example.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
Sets rules for availability of locations for the player. `Items` is for all items available but not necessarily all
|
||||||
|
locations, such as self-locking keys, but needs to be set by the world for this to be different from locations access.
|
||||||
|
|
||||||
|
### ProgressionBalancing
|
||||||
|
Algorithm for moving progression items into earlier spheres to make the gameplay experience a bit smoother. Can be
|
||||||
|
overridden if you want a different default value.
|
||||||
|
|
||||||
|
### LocalItems
|
||||||
|
Forces the players' items local to their world.
|
||||||
|
|
||||||
|
### NonLocalItems
|
||||||
|
Forces the players' items outside their world.
|
||||||
|
|
||||||
|
### StartInventory
|
||||||
|
Allows the player to define a dictionary of starting items with item name and quantity.
|
||||||
|
|
||||||
|
### StartHints
|
||||||
|
Gives the player starting hints for where the items defined here are.
|
||||||
|
|
||||||
|
### StartLocationHints
|
||||||
|
Gives the player starting hints for the items on locations defined here.
|
||||||
|
|
||||||
|
### ExcludeLocations
|
||||||
|
Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
|
||||||
|
|
||||||
|
### PriorityLocations
|
||||||
|
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
|
||||||
|
|
||||||
|
### ItemLinks
|
||||||
|
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||||
|
two players will combine their items in the link into a single item, which then gets replaced with `World.create_filler()`.
|
||||||
|
|
||||||
|
## Basic Option Classes
|
||||||
|
### Toggle
|
||||||
|
The example above. This simply has 0 and 1 as its available results with 0 (false) being the default value. Cannot be
|
||||||
|
compared to strings but can be directly compared to True and False.
|
||||||
|
|
||||||
|
### DefaultOnToggle
|
||||||
|
Like Toggle, but 1 (true) is the default value.
|
||||||
|
|
||||||
|
### Choice
|
||||||
|
A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do
|
||||||
|
comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with:
|
||||||
|
```python
|
||||||
|
if self.multiworld.sword_availability[self.player] == "early_sword":
|
||||||
|
do_early_sword_things()
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
```python
|
||||||
|
from .Options import SwordAvailability
|
||||||
|
|
||||||
|
if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword:
|
||||||
|
do_early_sword_things()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Range
|
||||||
|
A numeric option allowing a variety of integers including the endpoints. Has a default `range_start` of 0 and default
|
||||||
|
`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string
|
||||||
|
comparisons.
|
||||||
|
|
||||||
|
### SpecialRange
|
||||||
|
Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value.
|
||||||
|
For example:
|
||||||
|
```python
|
||||||
|
special_range_names: {
|
||||||
|
"normal": 20,
|
||||||
|
"extreme": 99,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
will let users use the names "normal" or "extreme" in their options selections, but will still return those as integers
|
||||||
|
to you. Useful if you want special handling regarding those specified values.
|
||||||
|
|
||||||
|
## More Advanced Options
|
||||||
|
### FreeText
|
||||||
|
This is an option that allows the user to enter any possible string value. Can only be compared with strings, and has
|
||||||
|
no validation step, so if this needs to be validated, you can either add a validation step to the option class or
|
||||||
|
within the world.
|
||||||
|
|
||||||
|
### TextChoice
|
||||||
|
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
|
||||||
|
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
|
||||||
|
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
|
||||||
|
point, `self.multiworld.my_option[self.player].current_key` will always return a string.
|
||||||
|
|
||||||
|
### PlandoBosses
|
||||||
|
An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports
|
||||||
|
everything it does, as well as having multiple validation steps to automatically support boss plando from users. If
|
||||||
|
using this class, you must define `bosses`, a set of valid boss names, and `locations`, a set of valid boss location
|
||||||
|
names, and `def can_place_boss`, which passes a boss and location, allowing you to check if that placement is valid for
|
||||||
|
your game. When this function is called, `bosses`, `locations`, and the passed strings will all be lowercase. There is
|
||||||
|
also a `duplicate_bosses` attribute allowing you to define if a boss can be placed multiple times in your world. False
|
||||||
|
by default, and will reject duplicate boss names from the user. For an example of using this class, refer to
|
||||||
|
`worlds.alttp.options.py`
|
||||||
|
|
||||||
|
### OptionDict
|
||||||
|
This option returns a dictionary. Setting a default here is recommended as it will output the dictionary to the
|
||||||
|
template. If you set a [Schema](https://pypi.org/project/schema/) on the class with `schema = Schema()`, then the
|
||||||
|
options system will automatically validate the user supplied data against the schema to ensure it's in the correct
|
||||||
|
format.
|
||||||
|
|
||||||
|
### ItemDict
|
||||||
|
Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world.
|
||||||
|
|
||||||
|
### OptionList
|
||||||
|
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You
|
||||||
|
can define a set of keys in `valid_keys`, and a default list if you want certain options to be available without editing
|
||||||
|
for this. If `valid_keys_casefold` is true, the verification will be case-insensitive; `verify_item_name` will check
|
||||||
|
that each value is a valid item name; and`verify_location_name` will check that each value is a valid location name.
|
||||||
|
|
||||||
|
### OptionSet
|
||||||
|
Like OptionList, but returns a set, preventing duplicates.
|
||||||
|
|
||||||
|
### ItemSet
|
||||||
|
Like OptionSet, but will verify that all the items in the set are a valid name for an item for your world.
|
||||||
@@ -7,10 +7,11 @@ use that version. These steps are for developers or platforms without compiled r
|
|||||||
## General
|
## General
|
||||||
|
|
||||||
What you'll need:
|
What you'll need:
|
||||||
* Python 3.8.7 or newer
|
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||||
* pip (Depending on platform may come included)
|
* **Python 3.11 does not work currently**
|
||||||
* A C compiler
|
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||||
* possibly optional, read OS-specific sections
|
* Matching C compiler
|
||||||
|
* possibly optional, read operating system specific sections
|
||||||
|
|
||||||
Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the
|
Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the
|
||||||
required modules and after pressing enter proceed to install everything automatically.
|
required modules and after pressing enter proceed to install everything automatically.
|
||||||
@@ -29,6 +30,8 @@ After this, you should be able to run the programs.
|
|||||||
|
|
||||||
Recommended steps
|
Recommended steps
|
||||||
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
|
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
|
||||||
|
* **Python 3.11 does not work currently**
|
||||||
|
|
||||||
* Download and install full Visual Studio from
|
* Download and install full Visual Studio from
|
||||||
[Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/)
|
[Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/)
|
||||||
or an older "Build Tools for Visual Studio" from
|
or an older "Build Tools for Visual Studio" from
|
||||||
@@ -40,6 +43,8 @@ Recommended steps
|
|||||||
|
|
||||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||||
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
||||||
|
* In PyCharm: right-click Generate.py and select `Run 'Generate'`
|
||||||
|
* Without PyCharm: open a command prompt in the source folder and type `py Generate.py`
|
||||||
|
|
||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
@@ -59,7 +64,7 @@ setting in host.yaml at your Enemizer executable.
|
|||||||
|
|
||||||
## Optional: SNI
|
## Optional: SNI
|
||||||
|
|
||||||
SNI is required to use SNIClient. If not integrated into the project, it has to be started manually.
|
[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually.
|
||||||
|
|
||||||
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
|
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
|
||||||
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
|
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
* Use type annotations where possible for function signatures and class members.
|
* Use type annotations where possible for function signatures and class members.
|
||||||
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
|
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
|
||||||
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
|
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
|
||||||
|
* Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier.
|
||||||
|
|
||||||
|
|
||||||
## Markdown
|
## Markdown
|
||||||
|
|||||||
@@ -48,5 +48,5 @@
|
|||||||
# TODO
|
# TODO
|
||||||
#JSON_AS_ASCII: false
|
#JSON_AS_ASCII: false
|
||||||
|
|
||||||
# Patch target. This is the address encoded into the patch that will be used for client auto-connect.
|
# Host Address. This is the address encoded into the patch that will be used for client auto-connect.
|
||||||
#PATCH_TARGET: archipelago.gg
|
#HOST_ADDRESS: archipelago.gg
|
||||||
|
|||||||
@@ -364,14 +364,9 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
|
|||||||
|
|
||||||
class MyGameWorld(World):
|
class MyGameWorld(World):
|
||||||
"""Insert description of the world/game here."""
|
"""Insert description of the world/game here."""
|
||||||
game: str = "My Game" # name of the game/world
|
game = "My Game" # name of the game/world
|
||||||
option_definitions = mygame_options # options the player can set
|
option_definitions = mygame_options # options the player can set
|
||||||
topology_present: bool = True # show path to required location checks in spoiler
|
topology_present = True # show path to required location checks in spoiler
|
||||||
|
|
||||||
# data_version is used to signal that items, locations or their names
|
|
||||||
# changed. Set this to 0 during development so other games' clients do not
|
|
||||||
# cache any texts, then increase by 1 for each release that makes changes.
|
|
||||||
data_version = 0
|
|
||||||
|
|
||||||
# ID of first item and location, could be hard-coded but code may be easier
|
# ID of first item and location, could be hard-coded but code may be easier
|
||||||
# to read with this as a propery.
|
# to read with this as a propery.
|
||||||
@@ -402,40 +397,43 @@ The world has to provide the following things for generation
|
|||||||
* additions to the regions list: at least one called "Menu"
|
* additions to the regions list: at least one called "Menu"
|
||||||
* locations placed inside those regions
|
* locations placed inside those regions
|
||||||
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
||||||
* applying `self.world.push_precollected` for start inventory
|
* applying `self.multiworld.push_precollected` for start inventory
|
||||||
* a `def generate_output(self, output_directory: str)` that creates the output
|
* `required_client_version: Tuple(int, int, int)`
|
||||||
files if there is output to be generated. When this is
|
Optional client version as tuple of 3 ints to make sure the client is compatible to
|
||||||
called, `self.world.get_locations(self.player)` has all locations for the player, with
|
this world (e.g. implements all required features) when connecting.
|
||||||
attribute `item` pointing to the item.
|
|
||||||
`location.item.player` can be used to see if it's a local item.
|
|
||||||
|
|
||||||
In addition, the following methods can be implemented and attributes can be set
|
In addition, the following methods can be implemented and are called in this order during generation
|
||||||
|
|
||||||
|
* `stage_assert_generate(cls, multiworld)` is a class method called at the start of
|
||||||
|
generation to check the existence of prerequisite files, usually a ROM for
|
||||||
|
games which require one.
|
||||||
* `def generate_early(self)`
|
* `def generate_early(self)`
|
||||||
called per player before any items or locations are created. You can set
|
called per player before any items or locations are created. You can set
|
||||||
properties on your world here. Already has access to player options and RNG.
|
properties on your world here. Already has access to player options and RNG.
|
||||||
* `def create_regions(self)`
|
* `def create_regions(self)`
|
||||||
called to place player's regions and their locations into the MultiWorld's regions list. If it's
|
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 `basic` as well.
|
hard to separate, this can be done during `generate_early` or `create_items` as well.
|
||||||
* `def create_items(self)`
|
* `def create_items(self)`
|
||||||
called to place player's items into the MultiWorld's itempool.
|
called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in
|
||||||
|
the MultiWorld's regions and itempool, and these lists should not be modified afterwards.
|
||||||
* `def set_rules(self)`
|
* `def 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.
|
||||||
Locations have to be defined before this, or rule application can miss them.
|
Locations have to be defined before this, or rule application can miss them.
|
||||||
* `def generate_basic(self)`
|
* `def generate_basic(self)`
|
||||||
called after the previous steps. Some placement and player specific
|
called after the previous steps. Some placement and player specific
|
||||||
randomizations can be done here. After this step all regions and items have
|
randomizations can be done here.
|
||||||
to be in the MultiWorld's regions and itempool.
|
|
||||||
* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement
|
* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement
|
||||||
before, during and after the regular fill process, before `generate_output`.
|
before, during and after the regular fill process, before `generate_output`.
|
||||||
|
If items need to be placed during pre_fill, these items can be determined
|
||||||
|
and created using `get_prefill_items`
|
||||||
|
* `def generate_output(self, output_directory: str)` that creates the output
|
||||||
|
files if there is output to be generated. When this is
|
||||||
|
called, `self.multiworld.get_locations(self.player)` has all locations for the player, with
|
||||||
|
attribute `item` pointing to the item.
|
||||||
|
`location.item.player` can be used to see if it's a local item.
|
||||||
* `fill_slot_data` and `modify_multidata` can be used to modify the data that
|
* `fill_slot_data` and `modify_multidata` can be used to modify the data that
|
||||||
will be used by the server to host the MultiWorld.
|
will be used by the server to host the MultiWorld.
|
||||||
* `required_client_version: Tuple(int, int, int)`
|
|
||||||
Client version as tuple of 3 ints to make sure the client is compatible to
|
|
||||||
this world (e.g. implements all required features) when connecting.
|
|
||||||
* `assert_generate(cls, world)` is a class method called at the start of
|
|
||||||
generation to check the existence of prerequisite files, usually a ROM for
|
|
||||||
games which require one.
|
|
||||||
|
|
||||||
#### generate_early
|
#### generate_early
|
||||||
|
|
||||||
@@ -497,21 +495,21 @@ def create_items(self) -> None:
|
|||||||
```python
|
```python
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
# Add regions to the multiworld. "Menu" is the required starting point.
|
# Add regions to the multiworld. "Menu" is the required starting point.
|
||||||
# Arguments to Region() are name, type, human_readable_name, player, world
|
# Arguments to Region() are name, player, world, and optionally hint_text
|
||||||
r = Region("Menu", RegionType.Generic, "Menu", self.player, self.multiworld)
|
r = Region("Menu", self.player, self.multiworld)
|
||||||
# Set Region.exits to a list of entrances that are reachable from region
|
# Set Region.exits to a list of entrances that are reachable from region
|
||||||
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
|
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
|
||||||
# Append region to MultiWorld's regions
|
# Append region to MultiWorld's regions
|
||||||
self.multiworld.regions.append(r) # or use += [r...]
|
self.multiworld.regions.append(r) # or use += [r...]
|
||||||
|
|
||||||
r = Region("Main Area", RegionType.Generic, "Main Area", self.player, self.multiworld)
|
r = Region("Main Area", self.player, self.multiworld)
|
||||||
# Add main area's locations to main area (all but final boss)
|
# Add main area's locations to main area (all but final boss)
|
||||||
r.locations = [MyGameLocation(self.player, location.name,
|
r.locations = [MyGameLocation(self.player, location.name,
|
||||||
self.location_name_to_id[location.name], r)]
|
self.location_name_to_id[location.name], r)]
|
||||||
r.exits = [Entrance(self.player, "Boss Door", r)]
|
r.exits = [Entrance(self.player, "Boss Door", r)]
|
||||||
self.multiworld.regions.append(r)
|
self.multiworld.regions.append(r)
|
||||||
|
|
||||||
r = Region("Boss Room", RegionType.Generic, "Boss Room", self.player, self.multiworld)
|
r = Region("Boss Room", self.player, self.multiworld)
|
||||||
# add event to Boss Room
|
# add event to Boss Room
|
||||||
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
|
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
|
||||||
self.multiworld.regions.append(r)
|
self.multiworld.regions.append(r)
|
||||||
@@ -680,3 +678,60 @@ def generate_output(self, output_directory: str):
|
|||||||
generate_mod(src, out_file, data)
|
generate_mod(src, out_file, data)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading
|
||||||
|
the `.md` files in your world's `/docs` directory.
|
||||||
|
|
||||||
|
#### Game Info
|
||||||
|
The game info page is for a short breakdown of what your game is and how it works in Archipelago. Any additional
|
||||||
|
information that may be useful to the player when learning your randomizer should also go here. The file name format
|
||||||
|
is `<language key>_<game name>.md`. While you can write these docs for multiple languages, currently only the english
|
||||||
|
version is displayed on the website.
|
||||||
|
|
||||||
|
#### Tutorials
|
||||||
|
Your game can have as many tutorials in as many languages as you like, with each one having a relevant `Tutorial`
|
||||||
|
defined in the `WebWorld`. The file name you use aren't particularly important, but it should be descriptive of what
|
||||||
|
the tutorial is covering, and the name of the file must match the relative URL provided in the `Tutorial`. Currently,
|
||||||
|
the JS that determines this ignores the provided file name and will search for `game/document_lang.md`, where
|
||||||
|
`game/document/lang` is the provided URL.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
Each world is expected to include unit tests that cover its logic, to ensure no logic bug regressions occur. This can be
|
||||||
|
done by creating a `/test` package within your world package. The `__init__.py` within this folder is where the world's
|
||||||
|
TestBase should be defined. This can be inherited from the main TestBase, which will automatically set up a solo
|
||||||
|
multiworld for each test written using it. Within subsequent modules, classes should be defined which inherit the world
|
||||||
|
TestBase, and can then define options to test in the class body, and run tests in each test method.
|
||||||
|
|
||||||
|
Example `__init__.py`
|
||||||
|
```python
|
||||||
|
from test.TestBase import WorldTestBase
|
||||||
|
|
||||||
|
|
||||||
|
class MyGameTestBase(WorldTestBase):
|
||||||
|
game = "My Game"
|
||||||
|
```
|
||||||
|
|
||||||
|
Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
|
||||||
|
|
||||||
|
Example `testChestAccess.py`
|
||||||
|
```python
|
||||||
|
from . import MyGameTestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestChestAccess(MyGameTestBase):
|
||||||
|
def testSwordChests(self):
|
||||||
|
"""Test locations that require a sword"""
|
||||||
|
locations = ["Chest1", "Chest2"]
|
||||||
|
items = [["Sword"]]
|
||||||
|
# this will test that each location can't be accessed without the "Sword", but can be accessed once obtained.
|
||||||
|
self.assertAccessDependency(locations, items)
|
||||||
|
|
||||||
|
def testAnyWeaponChests(self):
|
||||||
|
"""Test locations that require any weapon"""
|
||||||
|
locations = [f"Chest{i}" for i in range(3, 6)]
|
||||||
|
items = [["Sword"], ["Axe"], ["Spear"]]
|
||||||
|
# this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them.
|
||||||
|
self.assertAccessDependency(locations, items)
|
||||||
|
```
|
||||||
|
|||||||
41
host.yaml
41
host.yaml
@@ -93,6 +93,10 @@ sni_options:
|
|||||||
lttp_options:
|
lttp_options:
|
||||||
# File name of the v1.0 J rom
|
# File name of the v1.0 J rom
|
||||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||||
|
ladx_options:
|
||||||
|
# File name of the Link's Awakening DX rom
|
||||||
|
rom_file: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
|
||||||
|
|
||||||
lufia2ac_options:
|
lufia2ac_options:
|
||||||
# File name of the US rom
|
# File name of the US rom
|
||||||
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
|
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
|
||||||
@@ -107,7 +111,7 @@ factorio_options:
|
|||||||
filter_item_sends: false
|
filter_item_sends: false
|
||||||
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
||||||
bridge_chat_out: true
|
bridge_chat_out: true
|
||||||
minecraft_options:
|
minecraft_options:
|
||||||
forge_directory: "Minecraft Forge server"
|
forge_directory: "Minecraft Forge server"
|
||||||
max_heap_size: "2G"
|
max_heap_size: "2G"
|
||||||
# release channel, currently "release", or "beta"
|
# release channel, currently "release", or "beta"
|
||||||
@@ -125,6 +129,15 @@ soe_options:
|
|||||||
rom_file: "Secret of Evermore (USA).sfc"
|
rom_file: "Secret of Evermore (USA).sfc"
|
||||||
ffr_options:
|
ffr_options:
|
||||||
display_msgs: true
|
display_msgs: true
|
||||||
|
tloz_options:
|
||||||
|
# File name of the Zelda 1
|
||||||
|
rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
# true for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .nes file with
|
||||||
|
rom_start: true
|
||||||
|
# Display message inside of Bizhawk
|
||||||
|
display_msgs: true
|
||||||
dkc3_options:
|
dkc3_options:
|
||||||
# File name of the DKC3 US rom
|
# File name of the DKC3 US rom
|
||||||
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||||
@@ -139,6 +152,12 @@ pokemon_rb_options:
|
|||||||
# True for operating system default program
|
# True for operating system default program
|
||||||
# Alternatively, a path to a program to open the .gb file with
|
# Alternatively, a path to a program to open the .gb file with
|
||||||
rom_start: true
|
rom_start: true
|
||||||
|
|
||||||
|
wargroove_options:
|
||||||
|
# Locate the Wargroove root directory on your system.
|
||||||
|
# This is used by the Wargroove client, so it knows where to send communication files to
|
||||||
|
root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||||
|
|
||||||
zillion_options:
|
zillion_options:
|
||||||
# File name of the Zillion US rom
|
# File name of the Zillion US rom
|
||||||
rom_file: "Zillion (UE) [!].sms"
|
rom_file: "Zillion (UE) [!].sms"
|
||||||
@@ -148,3 +167,23 @@ zillion_options:
|
|||||||
# RetroArch doesn't make it easy to launch a game from the command line.
|
# RetroArch doesn't make it easy to launch a game from the command line.
|
||||||
# You have to know the path to the emulator core library on the user's computer.
|
# You have to know the path to the emulator core library on the user's computer.
|
||||||
rom_start: "retroarch"
|
rom_start: "retroarch"
|
||||||
|
|
||||||
|
adventure_options:
|
||||||
|
# File name of the standard NTSC Adventure rom.
|
||||||
|
# The licensed "The 80 Classic Games" CD-ROM contains this.
|
||||||
|
# It may also have a .a26 extension
|
||||||
|
rom_file: "ADVNTURE.BIN"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
# True for operating system default program for '.a26'
|
||||||
|
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
||||||
|
rom_start: true
|
||||||
|
# Optional, additional args passed into rom_start before the .bin file
|
||||||
|
# For example, this can be used to autoload the connector script in BizHawk
|
||||||
|
# (see BizHawk --lua= option)
|
||||||
|
rom_args: " "
|
||||||
|
# Set this to true to display item received messages in Emuhawk
|
||||||
|
display_msgs: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
180
inno_setup.iss
180
inno_setup.iss
@@ -25,9 +25,9 @@ OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText}
|
|||||||
Compression=lzma2
|
Compression=lzma2
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
LZMANumBlockThreads=8
|
LZMANumBlockThreads=8
|
||||||
ArchitecturesInstallIn64BitMode=x64
|
ArchitecturesInstallIn64BitMode=x64 arm64
|
||||||
ChangesAssociations=yes
|
ChangesAssociations=yes
|
||||||
ArchitecturesAllowed=x64
|
ArchitecturesAllowed=x64 arm64
|
||||||
AllowNoIcons=yes
|
AllowNoIcons=yes
|
||||||
SetupIconFile={#MyAppIcon}
|
SetupIconFile={#MyAppIcon}
|
||||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||||
@@ -63,6 +63,8 @@ Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full
|
|||||||
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
||||||
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
|
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
|
||||||
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
|
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
|
||||||
|
Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting
|
||||||
|
Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning
|
||||||
Name: "server"; Description: "Server"; Types: full hosting
|
Name: "server"; Description: "Server"; Types: full hosting
|
||||||
Name: "client"; Description: "Clients"; Types: full playing
|
Name: "client"; Description: "Clients"; Types: full playing
|
||||||
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
||||||
@@ -72,15 +74,20 @@ Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch
|
|||||||
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||||
|
Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
|
||||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||||
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
|
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
|
||||||
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
|
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
|
||||||
Name: "client/pkmn"; Description: "Pokemon Client"
|
Name: "client/pkmn"; Description: "Pokemon Client"
|
||||||
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||||
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||||
|
Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||||
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
|
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
|
||||||
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
||||||
|
Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
|
||||||
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
||||||
|
Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
|
||||||
|
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
||||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||||
|
|
||||||
[Dirs]
|
[Dirs]
|
||||||
@@ -97,15 +104,20 @@ Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda
|
|||||||
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
||||||
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
||||||
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
|
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
|
||||||
|
Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
|
||||||
|
Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz
|
||||||
|
Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn
|
||||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
||||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||||
|
|
||||||
|
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||||
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||||
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||||
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
||||||
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
|
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
|
||||||
|
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
|
||||||
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
|
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
|
||||||
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||||
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||||
@@ -115,6 +127,10 @@ Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: igno
|
|||||||
Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
|
Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
|
||||||
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
|
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
|
||||||
Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
|
Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
|
||||||
|
Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz
|
||||||
|
Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
|
||||||
|
Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
|
||||||
|
Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
|
||||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
@@ -130,6 +146,9 @@ Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Archipelag
|
|||||||
Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
|
Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
|
||||||
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
|
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
|
||||||
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
|
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
|
||||||
|
Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
|
||||||
|
Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
||||||
|
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
||||||
|
|
||||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
||||||
@@ -142,6 +161,10 @@ Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Ar
|
|||||||
Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
|
Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
|
||||||
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
|
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
|
||||||
Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
|
Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
|
||||||
|
Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
|
||||||
|
Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
|
||||||
|
Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
|
||||||
|
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
|
|
||||||
@@ -219,6 +242,21 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Ar
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
|
|
||||||
|
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx
|
||||||
|
|
||||||
|
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz
|
||||||
|
|
||||||
|
Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
|
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
|
||||||
@@ -286,6 +324,15 @@ var RedROMFilePage: TInputFileWizardPage;
|
|||||||
var bluerom: string;
|
var bluerom: string;
|
||||||
var BlueROMFilePage: TInputFileWizardPage;
|
var BlueROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
|
var ladxrom: string;
|
||||||
|
var LADXROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
|
var tlozrom: string;
|
||||||
|
var TLoZROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
|
var advnrom: string;
|
||||||
|
var AdvnROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
function GetSNESMD5OfFile(const rom: string): string;
|
function GetSNESMD5OfFile(const rom: string): string;
|
||||||
var data: AnsiString;
|
var data: AnsiString;
|
||||||
begin
|
begin
|
||||||
@@ -346,6 +393,25 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function CheckNESRom(name: string; hash: string): string;
|
||||||
|
var rom: string;
|
||||||
|
begin
|
||||||
|
log('Handling ' + name)
|
||||||
|
rom := FileSearch(name, WizardDirValue());
|
||||||
|
if Length(rom) > 0 then
|
||||||
|
begin
|
||||||
|
log('existing ROM found');
|
||||||
|
log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
|
||||||
|
if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
|
||||||
|
begin
|
||||||
|
log('existing ROM verified');
|
||||||
|
Result := rom;
|
||||||
|
exit;
|
||||||
|
end;
|
||||||
|
log('existing ROM failed verification');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function AddRomPage(name: string): TInputFileWizardPage;
|
function AddRomPage(name: string): TInputFileWizardPage;
|
||||||
begin
|
begin
|
||||||
Result :=
|
Result :=
|
||||||
@@ -392,6 +458,21 @@ begin
|
|||||||
'.sms');
|
'.sms');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function AddNESRomPage(name: string): TInputFileWizardPage;
|
||||||
|
begin
|
||||||
|
Result :=
|
||||||
|
CreateInputFilePage(
|
||||||
|
wpSelectComponents,
|
||||||
|
'Select ROM File',
|
||||||
|
'Where is your ' + name + ' located?',
|
||||||
|
'Select the file, then click Next.');
|
||||||
|
|
||||||
|
Result.Add(
|
||||||
|
'Location of ROM file:',
|
||||||
|
'NES ROM files|*.nes|All files|*.*',
|
||||||
|
'.nes');
|
||||||
|
end;
|
||||||
|
|
||||||
procedure AddOoTRomPage();
|
procedure AddOoTRomPage();
|
||||||
begin
|
begin
|
||||||
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
|
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
|
||||||
@@ -422,6 +503,21 @@ begin
|
|||||||
'.z64');
|
'.z64');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function AddA26Page(name: string): TInputFileWizardPage;
|
||||||
|
begin
|
||||||
|
Result :=
|
||||||
|
CreateInputFilePage(
|
||||||
|
wpSelectComponents,
|
||||||
|
'Select ROM File',
|
||||||
|
'Where is your ' + name + ' located?',
|
||||||
|
'Select the file, then click Next.');
|
||||||
|
|
||||||
|
Result.Add(
|
||||||
|
'Location of ROM file:',
|
||||||
|
'A2600 ROM files|*.BIN;*.a26|All files|*.*',
|
||||||
|
'.BIN');
|
||||||
|
end;
|
||||||
|
|
||||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||||
begin
|
begin
|
||||||
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
|
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
|
||||||
@@ -440,6 +536,16 @@ begin
|
|||||||
Result := not (OoTROMFilePage.Values[0] = '')
|
Result := not (OoTROMFilePage.Values[0] = '')
|
||||||
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
||||||
Result := not (ZlROMFilePage.Values[0] = '')
|
Result := not (ZlROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
|
||||||
|
Result := not (RedROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then
|
||||||
|
Result := not (BlueROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then
|
||||||
|
Result := not (LADXROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then
|
||||||
|
Result := not (TLoZROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then
|
||||||
|
Result := not (AdvnROMFilePage.Values[0] = '')
|
||||||
else
|
else
|
||||||
Result := True;
|
Result := True;
|
||||||
end;
|
end;
|
||||||
@@ -576,7 +682,7 @@ function GetRedROMPath(Param: string): string;
|
|||||||
begin
|
begin
|
||||||
if Length(redrom) > 0 then
|
if Length(redrom) > 0 then
|
||||||
Result := redrom
|
Result := redrom
|
||||||
else if Assigned(RedRomFilePage) then
|
else if Assigned(RedROMFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
|
R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
@@ -592,7 +698,7 @@ function GetBlueROMPath(Param: string): string;
|
|||||||
begin
|
begin
|
||||||
if Length(bluerom) > 0 then
|
if Length(bluerom) > 0 then
|
||||||
Result := bluerom
|
Result := bluerom
|
||||||
else if Assigned(BlueRomFilePage) then
|
else if Assigned(BlueROMFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
|
R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
@@ -603,6 +709,54 @@ begin
|
|||||||
else
|
else
|
||||||
Result := '';
|
Result := '';
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function GetTLoZROMPath(Param: string): string;
|
||||||
|
begin
|
||||||
|
if Length(tlozrom) > 0 then
|
||||||
|
Result := tlozrom
|
||||||
|
else if Assigned(TLoZROMFilePage) then
|
||||||
|
begin
|
||||||
|
R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0');
|
||||||
|
if R <> 0 then
|
||||||
|
MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
Result := TLoZROMFilePage.Values[0]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function GetLADXROMPath(Param: string): string;
|
||||||
|
begin
|
||||||
|
if Length(ladxrom) > 0 then
|
||||||
|
Result := ladxrom
|
||||||
|
else if Assigned(LADXROMFilePage) then
|
||||||
|
begin
|
||||||
|
R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f')
|
||||||
|
if R <> 0 then
|
||||||
|
MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
Result := LADXROMFilePage.Values[0]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function GetAdvnROMPath(Param: string): string;
|
||||||
|
begin
|
||||||
|
if Length(advnrom) > 0 then
|
||||||
|
Result := advnrom
|
||||||
|
else if Assigned(AdvnROMFilePage) then
|
||||||
|
begin
|
||||||
|
R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284');
|
||||||
|
if R <> 0 then
|
||||||
|
MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
Result := AdvnROMFilePage.Values[0]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
procedure InitializeWizard();
|
procedure InitializeWizard();
|
||||||
begin
|
begin
|
||||||
@@ -640,9 +794,21 @@ begin
|
|||||||
if Length(bluerom) = 0 then
|
if Length(bluerom) = 0 then
|
||||||
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
|
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
|
||||||
|
|
||||||
|
ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
|
||||||
|
if Length(ladxrom) = 0 then
|
||||||
|
LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
|
||||||
|
|
||||||
l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
|
l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
|
||||||
if Length(l2acrom) = 0 then
|
if Length(l2acrom) = 0 then
|
||||||
L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
|
L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
|
||||||
|
|
||||||
|
tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0');
|
||||||
|
if Length(tlozrom) = 0 then
|
||||||
|
TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes');
|
||||||
|
|
||||||
|
advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284');
|
||||||
|
if Length(advnrom) = 0 then
|
||||||
|
AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
@@ -669,4 +835,10 @@ begin
|
|||||||
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
|
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
|
||||||
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
|
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
|
||||||
Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
|
Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
|
||||||
|
if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
|
||||||
|
Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
|
||||||
|
if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
|
||||||
|
Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz'));
|
||||||
|
if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then
|
||||||
|
Result := not (WizardIsComponentSelected('client/advn'));
|
||||||
end;
|
end;
|
||||||
|
|||||||
38
kvui.py
38
kvui.py
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
import asyncio
|
|
||||||
|
|
||||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||||
@@ -26,6 +25,7 @@ from kivy.base import ExceptionHandler, ExceptionManager
|
|||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty
|
from kivy.properties import BooleanProperty, ObjectProperty
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
from kivy.uix.layout import Layout
|
from kivy.uix.layout import Layout
|
||||||
@@ -148,9 +148,11 @@ class ServerLabel(HovererableLabel):
|
|||||||
for permission_name, permission_data in ctx.permissions.items():
|
for permission_name, permission_data in ctx.permissions.items():
|
||||||
text += f"\n {permission_name}: {permission_data}"
|
text += f"\n {permission_name}: {permission_data}"
|
||||||
if ctx.hint_cost is not None and ctx.total_locations:
|
if ctx.hint_cost is not None and ctx.total_locations:
|
||||||
|
min_cost = int(ctx.server_version >= (0, 3, 9))
|
||||||
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
||||||
f"For you this means every {max(0, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
f"For you this means every " \
|
||||||
"location checks."
|
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))}" \
|
||||||
|
f" location checks."
|
||||||
elif ctx.hint_cost == 0:
|
elif ctx.hint_cost == 0:
|
||||||
text += "\n!hint is free to use."
|
text += "\n!hint is free to use."
|
||||||
|
|
||||||
@@ -486,6 +488,10 @@ class GameManager(App):
|
|||||||
if hasattr(self, "energy_link_label"):
|
if hasattr(self, "energy_link_label"):
|
||||||
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
||||||
|
|
||||||
|
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||||
|
def open_settings(self, *largs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LogtoUI(logging.Handler):
|
class LogtoUI(logging.Handler):
|
||||||
def __init__(self, on_log):
|
def __init__(self, on_log):
|
||||||
@@ -508,7 +514,7 @@ class LogtoUI(logging.Handler):
|
|||||||
|
|
||||||
|
|
||||||
class UILog(RecycleView):
|
class UILog(RecycleView):
|
||||||
cols = 1
|
messages: typing.ClassVar[int] # comes from kv file
|
||||||
|
|
||||||
def __init__(self, *loggers_to_handle, **kwargs):
|
def __init__(self, *loggers_to_handle, **kwargs):
|
||||||
super(UILog, self).__init__(**kwargs)
|
super(UILog, self).__init__(**kwargs)
|
||||||
@@ -518,9 +524,15 @@ class UILog(RecycleView):
|
|||||||
|
|
||||||
def on_log(self, record: str) -> None:
|
def on_log(self, record: str) -> None:
|
||||||
self.data.append({"text": escape_markup(record)})
|
self.data.append({"text": escape_markup(record)})
|
||||||
|
self.clean_old()
|
||||||
|
|
||||||
def on_message_markup(self, text):
|
def on_message_markup(self, text):
|
||||||
self.data.append({"text": text})
|
self.data.append({"text": text})
|
||||||
|
self.clean_old()
|
||||||
|
|
||||||
|
def clean_old(self):
|
||||||
|
if len(self.data) > self.messages:
|
||||||
|
self.data.pop(0)
|
||||||
|
|
||||||
def fix_heights(self):
|
def fix_heights(self):
|
||||||
"""Workaround fix for divergent texture and layout heights"""
|
"""Workaround fix for divergent texture and layout heights"""
|
||||||
@@ -538,6 +550,19 @@ class E(ExceptionHandler):
|
|||||||
|
|
||||||
|
|
||||||
class KivyJSONtoTextParser(JSONtoTextParser):
|
class KivyJSONtoTextParser(JSONtoTextParser):
|
||||||
|
# dummy class to absorb kvlang definitions
|
||||||
|
class TextColors(Widget):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries
|
||||||
|
colors = self.TextColors()
|
||||||
|
color_codes = self.color_codes.copy()
|
||||||
|
for name, code in color_codes.items():
|
||||||
|
color_codes[name] = getattr(colors, name, code)
|
||||||
|
self.color_codes = color_codes
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
self.ref_count = 0
|
self.ref_count = 0
|
||||||
return super(KivyJSONtoTextParser, self).__call__(*args, **kwargs)
|
return super(KivyJSONtoTextParser, self).__call__(*args, **kwargs)
|
||||||
@@ -587,3 +612,8 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
|||||||
ExceptionManager.add_handler(E())
|
ExceptionManager.add_handler(E())
|
||||||
|
|
||||||
Builder.load_file(Utils.local_path("data", "client.kv"))
|
Builder.load_file(Utils.local_path("data", "client.kv"))
|
||||||
|
user_file = Utils.user_path("data", "user.kv")
|
||||||
|
if os.path.exists(user_file):
|
||||||
|
logging.info("Loading user.kv into builder.")
|
||||||
|
Builder.load_file(user_file)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user