Compare commits
241 Commits
0.4.0
...
tests_apwo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1db6b67953 | ||
|
|
fa3c132304 | ||
|
|
6226713c4d | ||
|
|
b56da79890 | ||
|
|
1d6345d3a2 | ||
|
|
51a639ceaf | ||
|
|
7ecb1e6d6c | ||
|
|
c9fb443c64 | ||
|
|
325299286b | ||
|
|
776b5fab7c | ||
|
|
18e0d25051 | ||
|
|
dfb3df4a8f | ||
|
|
d0db728850 | ||
|
|
77b0852dca | ||
|
|
3fba94f000 | ||
|
|
85582b9458 | ||
|
|
122d404145 | ||
|
|
07e3fbe845 | ||
|
|
76cace725b | ||
|
|
99656bf059 | ||
|
|
332eab9569 | ||
|
|
8c2584f872 | ||
|
|
1ced726d31 | ||
|
|
d51e0ec0ab | ||
|
|
36b5b1207c | ||
|
|
a4e485e297 | ||
|
|
a7bc8846cd | ||
|
|
125ee8b198 | ||
|
|
553fe0be19 | ||
|
|
71bfb6babd | ||
|
|
1698c17caa | ||
|
|
751e5cec63 | ||
|
|
dc46e96e3f | ||
|
|
0934e5c711 | ||
|
|
aa8ffa247d | ||
|
|
a45e8730cb | ||
|
|
46f2f3d7cd | ||
|
|
a96ff8de16 | ||
|
|
f3e2e429b8 | ||
|
|
46b13e0b53 | ||
|
|
7a4e903906 | ||
|
|
f1ccf1b663 | ||
|
|
ec0822c5eb | ||
|
|
78b981228a | ||
|
|
f3c788d0cc | ||
|
|
59ad9e97e5 | ||
|
|
abd8eaf36e | ||
|
|
f36468fc25 | ||
|
|
a939f50480 | ||
|
|
b04b105bd8 | ||
|
|
845502ad39 | ||
|
|
afe9e12ef4 | ||
|
|
a75159b57e | ||
|
|
61fc80505e | ||
|
|
25f285b242 | ||
|
|
c4e28a8736 | ||
|
|
422ccdaa4c | ||
|
|
1e7c650159 | ||
|
|
ab64173600 | ||
|
|
36499b8983 | ||
|
|
923ff033b1 | ||
|
|
599d0ac81b | ||
|
|
ce2433b247 | ||
|
|
f6cb90daf9 | ||
|
|
54b200451d | ||
|
|
b98080afee | ||
|
|
5401e485aa | ||
|
|
58cf9783eb | ||
|
|
fad0fe16f4 | ||
|
|
c2884e9eb0 | ||
|
|
1809823308 | ||
|
|
df7462efcc | ||
|
|
00e3c44400 | ||
|
|
abf4b3bcbc | ||
|
|
c9f217943e | ||
|
|
e9f8b1ed28 | ||
|
|
c46d8afcfa | ||
|
|
f4d9c294a3 | ||
|
|
42d8fb8409 | ||
|
|
127d4812b5 | ||
|
|
527f30d91a | ||
|
|
1d565b9aaf | ||
|
|
6814bc158a | ||
|
|
e80f3206b6 | ||
|
|
54ea917c48 | ||
|
|
5e9bf4b007 | ||
|
|
c8453035da | ||
|
|
a2ddd5c9e8 | ||
|
|
97ba631b80 | ||
|
|
be4c597c8d | ||
|
|
324d3cf042 | ||
|
|
b1c5456d18 | ||
|
|
f474b81f40 | ||
|
|
5255bc5cd8 | ||
|
|
18127a75f5 | ||
|
|
899de428df | ||
|
|
f401702e7c | ||
|
|
68bfe1705d | ||
|
|
98b0bf7456 | ||
|
|
7674e62ba7 | ||
|
|
0b33c25b39 | ||
|
|
62f4e62d71 | ||
|
|
48add4687c | ||
|
|
cc08e853a0 | ||
|
|
7e3fa5058d | ||
|
|
c74577d708 | ||
|
|
a8b76b1310 | ||
|
|
c8ebad1dfe | ||
|
|
d3447a3983 | ||
|
|
11b2b5ed2f | ||
|
|
a0464ecea1 | ||
|
|
97fd78ba1b | ||
|
|
a60f370224 | ||
|
|
0363630f61 | ||
|
|
39c7c7291e | ||
|
|
a368520200 | ||
|
|
5d25f908a4 | ||
|
|
9edab76567 | ||
|
|
91b60f2e21 | ||
|
|
41b59488e3 | ||
|
|
42da24cb5e | ||
|
|
28c5e9ee65 | ||
|
|
b55174ccdf | ||
|
|
7bcf299412 | ||
|
|
a7816d186f | ||
|
|
9d40471dee | ||
|
|
b704070de5 | ||
|
|
6c459066a7 | ||
|
|
4c3eaf2996 | ||
|
|
bb56f7b400 | ||
|
|
22ed7ff9c3 | ||
|
|
173513c9f4 | ||
|
|
c0cf35edda | ||
|
|
dcc628f878 | ||
|
|
b950af09a6 | ||
|
|
58aea7ca58 | ||
|
|
06a25a903e | ||
|
|
62a265cc31 | ||
|
|
67c3076572 | ||
|
|
ab5cb7adad | ||
|
|
5e84f91d2f | ||
|
|
a7a17a5a4d | ||
|
|
a6ea3e1953 | ||
|
|
0515acc8fe | ||
|
|
bf5c1cbbbf | ||
|
|
c8fb46a5e6 | ||
|
|
a38a2903d5 | ||
|
|
4ef7e43521 | ||
|
|
e1f17fadfc | ||
|
|
4dc934729d | ||
|
|
722757e18a | ||
|
|
0ca3c5e6a2 | ||
|
|
664bbd86bb | ||
|
|
7a9d4272be | ||
|
|
7559adbb14 | ||
|
|
1a7bc4ffd4 | ||
|
|
be74a4a71a | ||
|
|
cb634fa8d4 | ||
|
|
f6758524d5 | ||
|
|
f395a6d184 | ||
|
|
ea03c90152 | ||
|
|
50d9ab041a | ||
|
|
acd3cb45bf | ||
|
|
8a78062825 | ||
|
|
599cd2c82e | ||
|
|
89ec31708e | ||
|
|
ef211da27f | ||
|
|
0122eb38ab | ||
|
|
3d8bc0bb67 | ||
|
|
d85c13ef0e | ||
|
|
27cb93d319 | ||
|
|
b0e8c8db6b | ||
|
|
5a7d20d393 | ||
|
|
808203a50f | ||
|
|
d8f79b4a42 | ||
|
|
02ef6cee47 | ||
|
|
e716b50f8c | ||
|
|
3fdf07677c | ||
|
|
d3baca9251 | ||
|
|
f52ca2571f | ||
|
|
469807ba01 | ||
|
|
8ada91939c | ||
|
|
054d14baa4 | ||
|
|
f0324e60f8 | ||
|
|
70ff19ac8c | ||
|
|
b02b329181 | ||
|
|
8b7ffaf671 | ||
|
|
c711d803f8 | ||
|
|
3c3954f5e8 | ||
|
|
05d398a51d | ||
|
|
5eadbc9840 | ||
|
|
0c1e3097c3 | ||
|
|
cdf7ca1dcc | ||
|
|
77fbd0eb2b | ||
|
|
c7284f90d9 | ||
|
|
8d559daa35 | ||
|
|
e49ffc64f2 | ||
|
|
94a02510c0 | ||
|
|
9d73988030 | ||
|
|
81411a191c | ||
|
|
6059b5ef66 | ||
|
|
0bc5a3bc8d | ||
|
|
11fdb29357 | ||
|
|
bbef7a4cbc | ||
|
|
8e7bbb4ea8 | ||
|
|
6628e8c85d | ||
|
|
84402a1b55 | ||
|
|
f4035b8621 | ||
|
|
bbf8546867 | ||
|
|
67a22b8b43 | ||
|
|
8e6ec85532 | ||
|
|
8fc50510a0 | ||
|
|
aa6ad5d34f | ||
|
|
ccb89dd65c | ||
|
|
a86c0aa37d | ||
|
|
ece6598b09 | ||
|
|
eef8f7af1a | ||
|
|
c626618221 | ||
|
|
47989325f8 | ||
|
|
815e7e6b0a | ||
|
|
a61a1f58c6 | ||
|
|
4c24872264 | ||
|
|
e778e49574 | ||
|
|
25f7413881 | ||
|
|
397ce8343e | ||
|
|
37fdc00517 | ||
|
|
03aa9b3604 | ||
|
|
8f52e4654f | ||
|
|
a86fd37860 | ||
|
|
eb503adb13 | ||
|
|
cbf72becc1 | ||
|
|
cdd460ae15 | ||
|
|
ffd968d89d | ||
|
|
8d73746d5b | ||
|
|
5ed56db48a | ||
|
|
5f447f4e6b | ||
|
|
f015cf4298 | ||
|
|
e43bb99622 | ||
|
|
34de5a57af | ||
|
|
510a460d84 | ||
|
|
6e271b643d |
80
.github/workflows/analyze-modified-files.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: Analyze modified files
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "**.py"
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "**.py"
|
||||||
|
|
||||||
|
env:
|
||||||
|
BASE: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD: ${{ github.event.pull_request.head.sha }}
|
||||||
|
BEFORE: ${{ github.event.before }}
|
||||||
|
AFTER: ${{ github.event.after }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
flake8-or-mypy:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
task: [flake8, mypy]
|
||||||
|
|
||||||
|
name: ${{ matrix.task }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: "Determine modified files (pull_request)"
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
git fetch origin $BASE $HEAD
|
||||||
|
DIFF=$(git diff --diff-filter=d --name-only $BASE...$HEAD -- "*.py")
|
||||||
|
echo "modified files:"
|
||||||
|
echo "$DIFF"
|
||||||
|
echo "diff=${DIFF//$'\n'/$' '}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: "Determine modified files (push)"
|
||||||
|
if: github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000'
|
||||||
|
run: |
|
||||||
|
git fetch origin $BEFORE $AFTER
|
||||||
|
DIFF=$(git diff --diff-filter=d --name-only $BEFORE..$AFTER -- "*.py")
|
||||||
|
echo "modified files:"
|
||||||
|
echo "$DIFF"
|
||||||
|
echo "diff=${DIFF//$'\n'/$' '}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: "Treat all files as modified (new branch)"
|
||||||
|
if: github.event_name == 'push' && github.event.before == '0000000000000000000000000000000000000000'
|
||||||
|
run: |
|
||||||
|
echo "diff=." >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
if: env.diff != ''
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
|
||||||
|
- name: "Install dependencies"
|
||||||
|
if: env.diff != ''
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip ${{ matrix.task }}
|
||||||
|
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
|
||||||
|
|
||||||
|
- name: "flake8: Stop the build if there are Python syntax errors or undefined names"
|
||||||
|
continue-on-error: false
|
||||||
|
if: env.diff != '' && matrix.task == 'flake8'
|
||||||
|
run: |
|
||||||
|
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }}
|
||||||
|
|
||||||
|
- name: "flake8: Lint modified files"
|
||||||
|
continue-on-error: true
|
||||||
|
if: env.diff != '' && matrix.task == 'flake8'
|
||||||
|
run: |
|
||||||
|
flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
|
||||||
|
|
||||||
|
- name: "mypy: Type check modified files"
|
||||||
|
continue-on-error: true
|
||||||
|
if: env.diff != '' && matrix.task == 'mypy'
|
||||||
|
run: |
|
||||||
|
mypy --follow-imports=silent --install-types --non-interactive --strict ${{ env.diff }}
|
||||||
4
.github/workflows/build.yml
vendored
@@ -52,8 +52,8 @@ jobs:
|
|||||||
path: dist/${{ env.ZIP_NAME }}
|
path: dist/${{ env.ZIP_NAME }}
|
||||||
retention-days: 7 # keep for 7 days, should be enough
|
retention-days: 7 # keep for 7 days, should be enough
|
||||||
|
|
||||||
build-ubuntu1804:
|
build-ubuntu2004:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
# - copy code below to release.yml -
|
# - copy code below to release.yml -
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
35
.github/workflows/lint.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
|
||||||
|
|
||||||
name: lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- '**.py'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**.py'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
flake8:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up Python 3.9
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip wheel
|
|
||||||
pip install flake8
|
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
||||||
- name: Lint with flake8
|
|
||||||
run: |
|
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
|
||||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
||||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
|
||||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
|
||||||
4
.github/workflows/release.yml
vendored
@@ -29,8 +29,8 @@ jobs:
|
|||||||
# build-release-windows: # this is done by hand because of signing
|
# build-release-windows: # this is done by hand because of signing
|
||||||
# build-release-macos: # LF volunteer
|
# build-release-macos: # LF volunteer
|
||||||
|
|
||||||
build-release-ubuntu1804:
|
build-release-ubuntu2004:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -28,6 +28,7 @@
|
|||||||
*.apsave
|
*.apsave
|
||||||
*.BIN
|
*.BIN
|
||||||
|
|
||||||
|
setups
|
||||||
build
|
build
|
||||||
bundle/components.wxs
|
bundle/components.wxs
|
||||||
dist
|
dist
|
||||||
@@ -176,6 +177,9 @@ minecraft_versions.json
|
|||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
|
#undertale stuff
|
||||||
|
/Undertale/
|
||||||
|
|
||||||
# OS General Files
|
# OS General Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ from worlds.adventure.Offsets import static_item_element_size, connector_port_of
|
|||||||
SYSTEM_MESSAGE_ID = 0
|
SYSTEM_MESSAGE_ID = 0
|
||||||
|
|
||||||
CONNECTION_TIMING_OUT_STATUS = \
|
CONNECTION_TIMING_OUT_STATUS = \
|
||||||
"Connection timing out. Please restart your emulator, then restart adventure_connector.lua"
|
"Connection timing out. Please restart your emulator, then restart connector_adventure.lua"
|
||||||
CONNECTION_REFUSED_STATUS = \
|
CONNECTION_REFUSED_STATUS = \
|
||||||
"Connection Refused. Please start your emulator and make sure adventure_connector.lua is running"
|
"Connection Refused. Please start your emulator and make sure connector_adventure.lua is running"
|
||||||
CONNECTION_RESET_STATUS = \
|
CONNECTION_RESET_STATUS = \
|
||||||
"Connection was reset. Please restart your emulator, then restart adventure_connector.lua"
|
"Connection was reset. Please restart your emulator, then restart connector_adventure.lua"
|
||||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||||
@@ -396,7 +396,7 @@ async def atari_sync_task(ctx: AdventureContext):
|
|||||||
ctx.atari_streams = await asyncio.wait_for(
|
ctx.atari_streams = await asyncio.wait_for(
|
||||||
asyncio.open_connection("localhost",
|
asyncio.open_connection("localhost",
|
||||||
port),
|
port),
|
||||||
timeout=10)
|
timeout=10)
|
||||||
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
|
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
logger.debug("Connection Timed Out, Trying Again")
|
logger.debug("Connection Timed Out, Trying Again")
|
||||||
@@ -436,7 +436,7 @@ async def patch_and_run_game(patch_file, ctx):
|
|||||||
logger.info(msg, extra={'compact_gui': True})
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
ctx.gui_error('Error', msg)
|
ctx.gui_error('Error', msg)
|
||||||
|
|
||||||
with open(Utils.user_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
|
with open(Utils.local_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
|
||||||
basepatch = bytes(file.read())
|
basepatch = bytes(file.read())
|
||||||
|
|
||||||
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)
|
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)
|
||||||
|
|||||||
192
BaseClasses.py
@@ -7,9 +7,9 @@ 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, ChainMap
|
from collections import ChainMap, Counter, deque
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
import Options
|
import Options
|
||||||
@@ -28,15 +28,15 @@ class Group(TypedDict, total=False):
|
|||||||
link_replacement: bool
|
link_replacement: bool
|
||||||
|
|
||||||
|
|
||||||
class ThreadBarrierProxy():
|
class ThreadBarrierProxy:
|
||||||
"""Passes through getattr while passthrough is True"""
|
"""Passes through getattr while passthrough is True"""
|
||||||
def __init__(self, obj: Any):
|
def __init__(self, obj: object) -> None:
|
||||||
self.passthrough = True
|
self.passthrough = True
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
|
|
||||||
def __getattr__(self, item):
|
def __getattr__(self, name: str) -> Any:
|
||||||
if self.passthrough:
|
if self.passthrough:
|
||||||
return getattr(self.obj, item)
|
return getattr(self.obj, name)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("You are in a threaded context and global random state was removed for your safety. "
|
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.")
|
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
|
||||||
@@ -96,7 +96,6 @@ class MultiWorld():
|
|||||||
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
|
||||||
self.algorithm = 'balanced'
|
self.algorithm = 'balanced'
|
||||||
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
|
|
||||||
self.groups = {}
|
self.groups = {}
|
||||||
self.regions = []
|
self.regions = []
|
||||||
self.shops = []
|
self.shops = []
|
||||||
@@ -113,7 +112,6 @@ class MultiWorld():
|
|||||||
self.dark_world_light_cone = False
|
self.dark_world_light_cone = False
|
||||||
self.rupoor_cost = 10
|
self.rupoor_cost = 10
|
||||||
self.aga_randomness = True
|
self.aga_randomness = True
|
||||||
self.lock_aga_door_in_escape = False
|
|
||||||
self.save_and_quit_from_boss = True
|
self.save_and_quit_from_boss = True
|
||||||
self.custom = False
|
self.custom = False
|
||||||
self.customitemarray = []
|
self.customitemarray = []
|
||||||
@@ -122,6 +120,7 @@ class MultiWorld():
|
|||||||
self.early_items = {player: {} for player in self.player_ids}
|
self.early_items = {player: {} for player in self.player_ids}
|
||||||
self.local_early_items = {player: {} for player in self.player_ids}
|
self.local_early_items = {player: {} for player in self.player_ids}
|
||||||
self.indirect_connections = {}
|
self.indirect_connections = {}
|
||||||
|
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||||
self.fix_trock_doors = self.AttributeProxy(
|
self.fix_trock_doors = self.AttributeProxy(
|
||||||
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||||
self.fix_skullwoods_exit = self.AttributeProxy(
|
self.fix_skullwoods_exit = self.AttributeProxy(
|
||||||
@@ -135,7 +134,6 @@ class MultiWorld():
|
|||||||
def set_player_attr(attr, val):
|
def set_player_attr(attr, val):
|
||||||
self.__dict__.setdefault(attr, {})[player] = val
|
self.__dict__.setdefault(attr, {})[player] = val
|
||||||
|
|
||||||
set_player_attr('tech_tree_layout_prerequisites', {})
|
|
||||||
set_player_attr('_region_cache', {})
|
set_player_attr('_region_cache', {})
|
||||||
set_player_attr('shuffle', "vanilla")
|
set_player_attr('shuffle', "vanilla")
|
||||||
set_player_attr('logic', "noglitches")
|
set_player_attr('logic', "noglitches")
|
||||||
@@ -387,12 +385,6 @@ class MultiWorld():
|
|||||||
self._recache()
|
self._recache()
|
||||||
return self._location_cache[location, player]
|
return self._location_cache[location, player]
|
||||||
|
|
||||||
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
|
|
||||||
try:
|
|
||||||
return self.dungeons[dungeonname, player]
|
|
||||||
except KeyError as e:
|
|
||||||
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
|
|
||||||
|
|
||||||
def get_all_state(self, use_cache: bool) -> CollectionState:
|
def get_all_state(self, use_cache: bool) -> CollectionState:
|
||||||
cached = getattr(self, "_all_state", None)
|
cached = getattr(self, "_all_state", None)
|
||||||
if use_cache and cached:
|
if use_cache and cached:
|
||||||
@@ -445,7 +437,6 @@ class MultiWorld():
|
|||||||
self.state.collect(item, True)
|
self.state.collect(item, True)
|
||||||
|
|
||||||
def push_item(self, location: Location, item: Item, collect: bool = True):
|
def push_item(self, location: Location, item: Item, collect: bool = True):
|
||||||
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
|
|
||||||
location.item = item
|
location.item = item
|
||||||
item.location = location
|
item.location = location
|
||||||
if collect:
|
if collect:
|
||||||
@@ -742,9 +733,11 @@ class CollectionState():
|
|||||||
return self.prog_items[item, player] >= count
|
return self.prog_items[item, player] >= count
|
||||||
|
|
||||||
def has_all(self, items: Set[str], player: int) -> bool:
|
def has_all(self, items: Set[str], player: int) -> bool:
|
||||||
|
"""Returns True if each item name of items is in state at least once."""
|
||||||
return all(self.prog_items[item, player] for item in items)
|
return all(self.prog_items[item, player] for item in items)
|
||||||
|
|
||||||
def has_any(self, items: Set[str], player: int) -> bool:
|
def has_any(self, items: Set[str], player: int) -> bool:
|
||||||
|
"""Returns True if at least one item name of items is in state at least once."""
|
||||||
return any(self.prog_items[item, player] for item in items)
|
return any(self.prog_items[item, player] for item in items)
|
||||||
|
|
||||||
def count(self, item: str, player: int) -> int:
|
def count(self, item: str, player: int) -> int:
|
||||||
@@ -801,7 +794,6 @@ class Region:
|
|||||||
entrances: List[Entrance]
|
entrances: List[Entrance]
|
||||||
exits: List[Entrance]
|
exits: List[Entrance]
|
||||||
locations: List[Location]
|
locations: List[Location]
|
||||||
dungeon: Optional[Dungeon] = None
|
|
||||||
|
|
||||||
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -836,6 +828,29 @@ class Region:
|
|||||||
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
|
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
|
||||||
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
||||||
|
|
||||||
|
def add_locations(self, locations: Dict[str, Optional[int]], location_type: Optional[typing.Type[Location]] = None) -> None:
|
||||||
|
"""Adds locations to the Region object, where location_type is your Location class and locations is a dict of
|
||||||
|
location names to address."""
|
||||||
|
if location_type is None:
|
||||||
|
location_type = Location
|
||||||
|
for location, address in locations.items():
|
||||||
|
self.locations.append(location_type(self.player, location, address, self))
|
||||||
|
|
||||||
|
def add_exits(self, exits: Dict[str, Optional[str]], rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||||
|
|
||||||
|
:param exits: exits from the region. format is {"connecting_region", "exit_name"}
|
||||||
|
:param rules: rules for the exits from this region. format is {"connecting_region", rule}
|
||||||
|
"""
|
||||||
|
for exiting_region, name in exits.items():
|
||||||
|
ret = Entrance(self.player, name, self) if name \
|
||||||
|
else Entrance(self.player, f"{self.name} -> {exiting_region}", self)
|
||||||
|
if rules and exiting_region in rules:
|
||||||
|
ret.access_rule = rules[exiting_region]
|
||||||
|
self.exits.append(ret)
|
||||||
|
ret.connect(self.multiworld.get_region(exiting_region, self.player))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
@@ -881,63 +896,6 @@ class Entrance:
|
|||||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
|
|
||||||
class Dungeon(object):
|
|
||||||
def __init__(self, name: str, regions: List[Region], big_key: Item, small_keys: List[Item],
|
|
||||||
dungeon_items: List[Item], player: int):
|
|
||||||
self.name = name
|
|
||||||
self.regions = regions
|
|
||||||
self.big_key = big_key
|
|
||||||
self.small_keys = small_keys
|
|
||||||
self.dungeon_items = dungeon_items
|
|
||||||
self.bosses = dict()
|
|
||||||
self.player = player
|
|
||||||
self.multiworld = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def boss(self) -> Optional[Boss]:
|
|
||||||
return self.bosses.get(None, None)
|
|
||||||
|
|
||||||
@boss.setter
|
|
||||||
def boss(self, value: Optional[Boss]):
|
|
||||||
self.bosses[None] = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def keys(self) -> List[Item]:
|
|
||||||
return self.small_keys + ([self.big_key] if self.big_key else [])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_items(self) -> List[Item]:
|
|
||||||
return self.dungeon_items + self.keys
|
|
||||||
|
|
||||||
def is_dungeon_item(self, item: Item) -> bool:
|
|
||||||
return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items)
|
|
||||||
|
|
||||||
def __eq__(self, other: Dungeon) -> bool:
|
|
||||||
if not other:
|
|
||||||
return False
|
|
||||||
return self.name == other.name and self.player == other.player
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.__str__()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
|
||||||
|
|
||||||
|
|
||||||
class Boss():
|
|
||||||
def __init__(self, name: str, enemizer_name: str, defeat_rule: Callable, player: int):
|
|
||||||
self.name = name
|
|
||||||
self.enemizer_name = enemizer_name
|
|
||||||
self.defeat_rule = defeat_rule
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
def can_defeat(self, state) -> bool:
|
|
||||||
return self.defeat_rule(state, self.player)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"Boss({self.name})"
|
|
||||||
|
|
||||||
|
|
||||||
class LocationProgressType(IntEnum):
|
class LocationProgressType(IntEnum):
|
||||||
DEFAULT = 1
|
DEFAULT = 1
|
||||||
PRIORITY = 2
|
PRIORITY = 2
|
||||||
@@ -1070,15 +1028,19 @@ class Item:
|
|||||||
def flags(self) -> int:
|
def flags(self) -> int:
|
||||||
return self.classification.as_flag()
|
return self.classification.as_flag()
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Item):
|
||||||
|
return NotImplemented
|
||||||
return self.name == other.name and self.player == other.player
|
return self.name == other.name and self.player == other.player
|
||||||
|
|
||||||
def __lt__(self, other: Item) -> bool:
|
def __lt__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Item):
|
||||||
|
return NotImplemented
|
||||||
if other.player != self.player:
|
if other.player != self.player:
|
||||||
return other.player < self.player
|
return other.player < self.player
|
||||||
return self.name < other.name
|
return self.name < other.name
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash((self.name, self.player))
|
return hash((self.name, self.player))
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -1090,33 +1052,44 @@ class Item:
|
|||||||
return f"{self.name} (Player {self.player})"
|
return f"{self.name} (Player {self.player})"
|
||||||
|
|
||||||
|
|
||||||
class Spoiler():
|
class EntranceInfo(TypedDict, total=False):
|
||||||
multiworld: MultiWorld
|
player: int
|
||||||
unreachables: Set[Location]
|
entrance: str
|
||||||
|
exit: str
|
||||||
|
direction: str
|
||||||
|
|
||||||
def __init__(self, world):
|
|
||||||
self.multiworld = world
|
class Spoiler:
|
||||||
|
multiworld: MultiWorld
|
||||||
|
hashes: Dict[int, str]
|
||||||
|
entrances: Dict[Tuple[str, str, int], EntranceInfo]
|
||||||
|
playthrough: Dict[str, Union[List[str], Dict[str, str]]] # sphere "0" is list, others are dict
|
||||||
|
unreachables: Set[Location]
|
||||||
|
paths: Dict[str, List[Union[Tuple[str, str], Tuple[str, None]]]] # last step takes no further exits
|
||||||
|
|
||||||
|
def __init__(self, multiworld: MultiWorld) -> None:
|
||||||
|
self.multiworld = multiworld
|
||||||
self.hashes = {}
|
self.hashes = {}
|
||||||
self.entrances = OrderedDict()
|
self.entrances = {}
|
||||||
self.playthrough = {}
|
self.playthrough = {}
|
||||||
self.unreachables = set()
|
self.unreachables = set()
|
||||||
self.paths = {}
|
self.paths = {}
|
||||||
|
|
||||||
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int):
|
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int) -> None:
|
||||||
if self.multiworld.players == 1:
|
if self.multiworld.players == 1:
|
||||||
self.entrances[(entrance, direction, player)] = OrderedDict(
|
self.entrances[(entrance, direction, player)] = \
|
||||||
[('entrance', entrance), ('exit', exit_), ('direction', direction)])
|
{"entrance": entrance, "exit": exit_, "direction": direction}
|
||||||
else:
|
else:
|
||||||
self.entrances[(entrance, direction, player)] = OrderedDict(
|
self.entrances[(entrance, direction, player)] = \
|
||||||
[('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)])
|
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
|
||||||
|
|
||||||
def create_playthrough(self, create_paths: bool = True):
|
def create_playthrough(self, create_paths: bool = True) -> None:
|
||||||
"""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
|
||||||
# get locations containing progress items
|
# get locations containing progress items
|
||||||
multiworld = self.multiworld
|
multiworld = self.multiworld
|
||||||
prog_locations = {location for location in multiworld.get_filled_locations() if location.item.advancement}
|
prog_locations = {location for location in multiworld.get_filled_locations() if location.item.advancement}
|
||||||
state_cache = [None]
|
state_cache: List[Optional[CollectionState]] = [None]
|
||||||
collection_spheres: List[Set[Location]] = []
|
collection_spheres: List[Set[Location]] = []
|
||||||
state = CollectionState(multiworld)
|
state = CollectionState(multiworld)
|
||||||
sphere_candidates = set(prog_locations)
|
sphere_candidates = set(prog_locations)
|
||||||
@@ -1225,17 +1198,17 @@ class Spoiler():
|
|||||||
for item in removed_precollected:
|
for item in removed_precollected:
|
||||||
multiworld.push_precollected(item)
|
multiworld.push_precollected(item)
|
||||||
|
|
||||||
def create_paths(self, state: CollectionState, collection_spheres: List[Set[Location]]):
|
def create_paths(self, state: CollectionState, collection_spheres: List[Set[Location]]) -> None:
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
multiworld = self.multiworld
|
multiworld = self.multiworld
|
||||||
|
|
||||||
def flist_to_iter(node):
|
def flist_to_iter(path_value: Optional[PathValue]) -> Iterator[str]:
|
||||||
while node:
|
while path_value:
|
||||||
value, node = node
|
region_or_entrance, path_value = path_value
|
||||||
yield value
|
yield region_or_entrance
|
||||||
|
|
||||||
def get_path(state, region):
|
def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, str], Tuple[str, None]]]:
|
||||||
reversed_path_as_flist = state.path.get(region, (region, None))
|
reversed_path_as_flist: PathValue = state.path.get(region, (str(region), None))
|
||||||
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
||||||
# Now we combine the flat string list into (region, exit) pairs
|
# Now we combine the flat string list into (region, exit) pairs
|
||||||
pathsiter = iter(string_path_flat)
|
pathsiter = iter(string_path_flat)
|
||||||
@@ -1261,14 +1234,11 @@ 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_file(self, filename: str):
|
def to_file(self, filename: str) -> None:
|
||||||
def write_option(option_key: str, option_obj: type(Options.Option)):
|
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
|
||||||
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:
|
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
|
||||||
outfile.write(f'{display_name + ":":33}{res.current_option_name}\n')
|
|
||||||
except:
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
||||||
outfile.write(
|
outfile.write(
|
||||||
@@ -1301,15 +1271,15 @@ class Spoiler():
|
|||||||
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")
|
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]
|
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 location, item in locations]))
|
['%s: %s' % (location, item) for location, item in locations]))
|
||||||
|
|
||||||
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 [
|
[f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else
|
||||||
f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
[f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||||
if self.unreachables:
|
if self.unreachables:
|
||||||
outfile.write('\n\nUnreachable Items:\n\n')
|
outfile.write('\n\nUnreachable Items:\n\n')
|
||||||
outfile.write(
|
outfile.write(
|
||||||
@@ -1370,23 +1340,21 @@ class PlandoOptions(IntFlag):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions:
|
def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions:
|
||||||
try:
|
try:
|
||||||
part = cls[part]
|
return base | cls[part]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise KeyError(f"{part} is not a recognized name for a plando module. "
|
raise KeyError(f"{part} is not a recognized name for a plando module. "
|
||||||
f"Known options: {', '.join(flag.name for flag in cls)}") from e
|
f"Known options: {', '.join(str(flag.name) for flag in cls)}") from e
|
||||||
else:
|
|
||||||
return base | part
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if self.value:
|
if self.value:
|
||||||
return ", ".join(flag.name for flag in PlandoOptions if self.value & flag.value)
|
return ", ".join(str(flag.name) for flag in PlandoOptions if self.value & flag.value)
|
||||||
return "None"
|
return "None"
|
||||||
|
|
||||||
|
|
||||||
seeddigits = 20
|
seeddigits = 20
|
||||||
|
|
||||||
|
|
||||||
def get_seed(seed=None) -> int:
|
def get_seed(seed: Optional[int] = None) -> int:
|
||||||
if seed is None:
|
if seed is None:
|
||||||
random.seed(None)
|
random.seed(None)
|
||||||
return random.randint(0, pow(10, seeddigits) - 1)
|
return random.randint(0, pow(10, seeddigits) - 1)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
|||||||
from Utils import Version, stream_input, async_start
|
from Utils import Version, stream_input, async_start
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import network_data_package, AutoWorldRegister
|
||||||
import os
|
import os
|
||||||
|
import ssl
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
import kvui
|
import kvui
|
||||||
@@ -33,6 +34,12 @@ logger = logging.getLogger("Client")
|
|||||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||||
|
|
||||||
|
|
||||||
|
@Utils.cache_argsless
|
||||||
|
def get_ssl_context():
|
||||||
|
import certifi
|
||||||
|
return ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
||||||
|
|
||||||
|
|
||||||
class ClientCommandProcessor(CommandProcessor):
|
class ClientCommandProcessor(CommandProcessor):
|
||||||
def __init__(self, ctx: CommonContext):
|
def __init__(self, ctx: CommonContext):
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
@@ -68,14 +75,17 @@ class ClientCommandProcessor(CommandProcessor):
|
|||||||
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
|
||||||
|
|
||||||
def _cmd_missing(self) -> bool:
|
def _cmd_missing(self, filter_text = "") -> bool:
|
||||||
"""List all missing location checks, from your local game state"""
|
"""List all missing location checks, from your local game state.
|
||||||
|
Can be given text, which will be used as filter."""
|
||||||
if not self.ctx.game:
|
if not self.ctx.game:
|
||||||
self.output("No game set, cannot determine missing checks.")
|
self.output("No game set, cannot determine missing checks.")
|
||||||
return False
|
return False
|
||||||
count = 0
|
count = 0
|
||||||
checked_count = 0
|
checked_count = 0
|
||||||
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
|
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
|
||||||
|
if filter_text and filter_text not in location:
|
||||||
|
continue
|
||||||
if location_id < 0:
|
if location_id < 0:
|
||||||
continue
|
continue
|
||||||
if location_id not in self.ctx.locations_checked:
|
if location_id not in self.ctx.locations_checked:
|
||||||
@@ -154,6 +164,7 @@ class CommonContext:
|
|||||||
disconnected_intentionally: bool = False
|
disconnected_intentionally: bool = False
|
||||||
server: typing.Optional[Endpoint] = None
|
server: typing.Optional[Endpoint] = None
|
||||||
server_version: Version = Version(0, 0, 0)
|
server_version: Version = Version(0, 0, 0)
|
||||||
|
generator_version: Version = Version(0, 0, 0)
|
||||||
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
|
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
|
||||||
|
|
||||||
last_death_link: float = time.time() # last send/received death link on AP layer
|
last_death_link: float = time.time() # last send/received death link on AP layer
|
||||||
@@ -163,6 +174,7 @@ class CommonContext:
|
|||||||
server_address: typing.Optional[str]
|
server_address: typing.Optional[str]
|
||||||
password: typing.Optional[str]
|
password: typing.Optional[str]
|
||||||
hint_cost: typing.Optional[int]
|
hint_cost: typing.Optional[int]
|
||||||
|
hint_points: typing.Optional[int]
|
||||||
player_names: typing.Dict[int, str]
|
player_names: typing.Dict[int, str]
|
||||||
|
|
||||||
finished_game: bool
|
finished_game: bool
|
||||||
@@ -179,6 +191,10 @@ class CommonContext:
|
|||||||
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
|
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
|
||||||
locations_info: typing.Dict[int, NetworkItem]
|
locations_info: typing.Dict[int, NetworkItem]
|
||||||
|
|
||||||
|
# data storage
|
||||||
|
stored_data: typing.Dict[str, typing.Any]
|
||||||
|
stored_data_notification_keys: typing.Set[str]
|
||||||
|
|
||||||
# internals
|
# internals
|
||||||
# current message box through kvui
|
# current message box through kvui
|
||||||
_messagebox: typing.Optional["kvui.MessageBox"] = None
|
_messagebox: typing.Optional["kvui.MessageBox"] = None
|
||||||
@@ -214,6 +230,9 @@ class CommonContext:
|
|||||||
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
|
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
|
||||||
self.locations_info = {}
|
self.locations_info = {}
|
||||||
|
|
||||||
|
self.stored_data = {}
|
||||||
|
self.stored_data_notification_keys = set()
|
||||||
|
|
||||||
self.input_queue = asyncio.Queue()
|
self.input_queue = asyncio.Queue()
|
||||||
self.input_requests = 0
|
self.input_requests = 0
|
||||||
|
|
||||||
@@ -256,6 +275,7 @@ class CommonContext:
|
|||||||
self.items_received = []
|
self.items_received = []
|
||||||
self.locations_info = {}
|
self.locations_info = {}
|
||||||
self.server_version = Version(0, 0, 0)
|
self.server_version = Version(0, 0, 0)
|
||||||
|
self.generator_version = Version(0, 0, 0)
|
||||||
self.server = None
|
self.server = None
|
||||||
self.server_task = None
|
self.server_task = None
|
||||||
self.hint_cost = None
|
self.hint_cost = None
|
||||||
@@ -454,6 +474,21 @@ class CommonContext:
|
|||||||
for game, game_data in data_package["games"].items():
|
for game, game_data in data_package["games"].items():
|
||||||
Utils.store_data_package_for_checksum(game, game_data)
|
Utils.store_data_package_for_checksum(game, game_data)
|
||||||
|
|
||||||
|
# data storage
|
||||||
|
|
||||||
|
def set_notify(self, *keys: str) -> None:
|
||||||
|
"""Subscribe to be notified of changes to selected data storage keys.
|
||||||
|
|
||||||
|
The values can be accessed via the "stored_data" attribute of this context, which is a dictionary mapping the
|
||||||
|
names of the data storage keys to the latest values received from the server.
|
||||||
|
"""
|
||||||
|
if new_keys := (set(keys) - self.stored_data_notification_keys):
|
||||||
|
self.stored_data_notification_keys.update(new_keys)
|
||||||
|
async_start(self.send_msgs([{"cmd": "Get",
|
||||||
|
"keys": list(new_keys)},
|
||||||
|
{"cmd": "SetNotify",
|
||||||
|
"keys": list(new_keys)}]))
|
||||||
|
|
||||||
# DeathLink hooks
|
# DeathLink hooks
|
||||||
|
|
||||||
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||||
@@ -583,7 +618,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
|||||||
|
|
||||||
logger.info(f'Connecting to Archipelago server at {address}')
|
logger.info(f'Connecting to Archipelago server at {address}')
|
||||||
try:
|
try:
|
||||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
|
||||||
|
ssl=get_ssl_context() if address.startswith("wss://") else None)
|
||||||
if ctx.ui is not None:
|
if ctx.ui is not None:
|
||||||
ctx.ui.update_address_bar(server_url.netloc)
|
ctx.ui.update_address_bar(server_url.netloc)
|
||||||
ctx.server = Endpoint(socket)
|
ctx.server = Endpoint(socket)
|
||||||
@@ -598,6 +634,7 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
|||||||
except websockets.InvalidMessage:
|
except websockets.InvalidMessage:
|
||||||
# probably encrypted
|
# probably encrypted
|
||||||
if address.startswith("ws://"):
|
if address.startswith("ws://"):
|
||||||
|
# try wss
|
||||||
await server_loop(ctx, "ws" + address[1:])
|
await server_loop(ctx, "ws" + address[1:])
|
||||||
else:
|
else:
|
||||||
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
|
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
|
||||||
@@ -642,11 +679,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
logger.info('Room Information:')
|
logger.info('Room Information:')
|
||||||
logger.info('--------------------------------')
|
logger.info('--------------------------------')
|
||||||
version = args["version"]
|
version = args["version"]
|
||||||
ctx.server_version = tuple(version)
|
ctx.server_version = Version(*version)
|
||||||
version = ".".join(str(item) for item in version)
|
|
||||||
|
|
||||||
logger.info(f'Server protocol version: {version}')
|
if "generator_version" in args:
|
||||||
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
|
ctx.generator_version = Version(*args["generator_version"])
|
||||||
|
logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, '
|
||||||
|
f'generator version: {ctx.generator_version.as_simple_string()}, '
|
||||||
|
f'tags: {", ".join(args["tags"])}')
|
||||||
|
else:
|
||||||
|
logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, '
|
||||||
|
f'tags: {", ".join(args["tags"])}')
|
||||||
if args['password']:
|
if args['password']:
|
||||||
logger.info('Password required')
|
logger.info('Password required')
|
||||||
ctx.update_permissions(args.get("permissions", {}))
|
ctx.update_permissions(args.get("permissions", {}))
|
||||||
@@ -708,6 +750,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
ctx.slot = args["slot"]
|
ctx.slot = args["slot"]
|
||||||
# int keys get lost in JSON transfer
|
# int keys get lost in JSON transfer
|
||||||
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
|
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
|
||||||
|
ctx.hint_points = args.get("hint_points", 0)
|
||||||
ctx.consume_players_package(args["players"])
|
ctx.consume_players_package(args["players"])
|
||||||
msgs = []
|
msgs = []
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
@@ -716,6 +759,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
if ctx.locations_scouted:
|
if ctx.locations_scouted:
|
||||||
msgs.append({"cmd": "LocationScouts",
|
msgs.append({"cmd": "LocationScouts",
|
||||||
"locations": list(ctx.locations_scouted)})
|
"locations": list(ctx.locations_scouted)})
|
||||||
|
if ctx.stored_data_notification_keys:
|
||||||
|
msgs.append({"cmd": "Get",
|
||||||
|
"keys": list(ctx.stored_data_notification_keys)})
|
||||||
|
msgs.append({"cmd": "SetNotify",
|
||||||
|
"keys": list(ctx.stored_data_notification_keys)})
|
||||||
if msgs:
|
if msgs:
|
||||||
await ctx.send_msgs(msgs)
|
await ctx.send_msgs(msgs)
|
||||||
if ctx.finished_game:
|
if ctx.finished_game:
|
||||||
@@ -779,7 +827,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
|
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
|
||||||
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
|
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
|
||||||
ctx.on_deathlink(args["data"])
|
ctx.on_deathlink(args["data"])
|
||||||
|
|
||||||
|
elif cmd == "Retrieved":
|
||||||
|
ctx.stored_data.update(args["keys"])
|
||||||
|
|
||||||
elif cmd == "SetReply":
|
elif cmd == "SetReply":
|
||||||
|
ctx.stored_data[args["key"]] = args["value"]
|
||||||
if args["key"] == "EnergyLink":
|
if args["key"] == "EnergyLink":
|
||||||
ctx.current_energy_link_value = args["value"]
|
ctx.current_energy_link_value = args["value"]
|
||||||
if ctx.ui:
|
if ctx.ui:
|
||||||
@@ -820,10 +873,9 @@ def get_base_parser(description: typing.Optional[str] = None):
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def run_as_textclient():
|
||||||
# Text Mode to use !hint and such with games that have no text entry
|
|
||||||
|
|
||||||
class TextContext(CommonContext):
|
class TextContext(CommonContext):
|
||||||
|
# Text Mode to use !hint and such with games that have no text entry
|
||||||
tags = {"AP", "TextOnly"}
|
tags = {"AP", "TextOnly"}
|
||||||
game = "" # empty matches any game since 0.3.2
|
game = "" # empty matches any game since 0.3.2
|
||||||
items_handling = 0b111 # receive all items for /received
|
items_handling = 0b111 # receive all items for /received
|
||||||
@@ -838,12 +890,11 @@ if __name__ == '__main__':
|
|||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
self.game = self.slot_info[self.slot].game
|
self.game = self.slot_info[self.slot].game
|
||||||
|
|
||||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||||
self.game = ""
|
self.game = ""
|
||||||
await super().disconnect(allow_autoreconnect)
|
await super().disconnect(allow_autoreconnect)
|
||||||
|
|
||||||
|
|
||||||
async def main(args):
|
async def main(args):
|
||||||
ctx = TextContext(args.connect, args.password)
|
ctx = TextContext(args.connect, args.password)
|
||||||
ctx.auth = args.name
|
ctx.auth = args.name
|
||||||
@@ -856,7 +907,6 @@ if __name__ == '__main__':
|
|||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
await ctx.shutdown()
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
||||||
@@ -876,3 +926,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_as_textclient()
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP
|
|||||||
|
|
||||||
SYSTEM_MESSAGE_ID = 0
|
SYSTEM_MESSAGE_ID = 0
|
||||||
|
|
||||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua"
|
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_ff1.lua"
|
||||||
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running"
|
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_ff1.lua is running"
|
||||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua"
|
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_ff1.lua"
|
||||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||||
@@ -33,7 +33,7 @@ class FF1CommandProcessor(ClientCommandProcessor):
|
|||||||
logger.info(f"NES Status: {self.ctx.nes_status}")
|
logger.info(f"NES Status: {self.ctx.nes_status}")
|
||||||
|
|
||||||
def _cmd_toggle_msgs(self):
|
def _cmd_toggle_msgs(self):
|
||||||
"""Toggle displaying messages in bizhawk"""
|
"""Toggle displaying messages in EmuHawk"""
|
||||||
global DISPLAY_MSGS
|
global DISPLAY_MSGS
|
||||||
DISPLAY_MSGS = not DISPLAY_MSGS
|
DISPLAY_MSGS = not DISPLAY_MSGS
|
||||||
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
||||||
|
|||||||
@@ -1,553 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
import string
|
|
||||||
import copy
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import typing
|
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
import factorio_rcon
|
from worlds.factorio.Client import check_stdin, launch
|
||||||
import colorama
|
|
||||||
import asyncio
|
|
||||||
from queue import Queue
|
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
def check_stdin() -> None:
|
|
||||||
if Utils.is_windows and sys.stdin:
|
|
||||||
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Utils.init_logging("FactorioClient", exception_logger="Client")
|
Utils.init_logging("FactorioClient", exception_logger="Client")
|
||||||
check_stdin()
|
check_stdin()
|
||||||
|
launch()
|
||||||
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
|
|
||||||
from MultiServer import mark_raw
|
|
||||||
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
|
||||||
from Utils import async_start
|
|
||||||
|
|
||||||
from worlds.factorio import Factorio
|
|
||||||
|
|
||||||
|
|
||||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
|
||||||
ctx: FactorioContext
|
|
||||||
|
|
||||||
def _cmd_energy_link(self):
|
|
||||||
"""Print the status of the energy link."""
|
|
||||||
self.output(f"Energy Link: {self.ctx.energy_link_status}")
|
|
||||||
|
|
||||||
@mark_raw
|
|
||||||
def _cmd_factorio(self, text: str) -> bool:
|
|
||||||
"""Send the following command to the bound Factorio Server."""
|
|
||||||
if self.ctx.rcon_client:
|
|
||||||
# TODO: Print the command non-silently only for race seeds, or otherwise block anything but /factorio /save in race seeds.
|
|
||||||
self.ctx.print_to_game(f"/factorio {text}")
|
|
||||||
result = self.ctx.rcon_client.send_command(text)
|
|
||||||
if result:
|
|
||||||
self.output(result)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _cmd_resync(self):
|
|
||||||
"""Manually trigger a resync."""
|
|
||||||
self.ctx.awaiting_bridge = True
|
|
||||||
|
|
||||||
def _cmd_toggle_send_filter(self):
|
|
||||||
"""Toggle filtering of item sends that get displayed in-game to only those that involve you."""
|
|
||||||
self.ctx.toggle_filter_item_sends()
|
|
||||||
|
|
||||||
def _cmd_toggle_chat(self):
|
|
||||||
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
|
|
||||||
self.ctx.toggle_bridge_chat_out()
|
|
||||||
|
|
||||||
class FactorioContext(CommonContext):
|
|
||||||
command_processor = FactorioCommandProcessor
|
|
||||||
game = "Factorio"
|
|
||||||
items_handling = 0b111 # full remote
|
|
||||||
|
|
||||||
# updated by spinup server
|
|
||||||
mod_version: Utils.Version = Utils.Version(0, 0, 0)
|
|
||||||
|
|
||||||
def __init__(self, server_address, password):
|
|
||||||
super(FactorioContext, self).__init__(server_address, password)
|
|
||||||
self.send_index: int = 0
|
|
||||||
self.rcon_client = None
|
|
||||||
self.awaiting_bridge = False
|
|
||||||
self.write_data_path = None
|
|
||||||
self.death_link_tick: int = 0 # last send death link on Factorio layer
|
|
||||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
|
||||||
self.energy_link_increment = 0
|
|
||||||
self.last_deplete = 0
|
|
||||||
self.filter_item_sends: bool = False
|
|
||||||
self.multiplayer: bool = False # whether multiple different players have connected
|
|
||||||
self.bridge_chat_out: bool = True
|
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool = False):
|
|
||||||
if password_requested and not self.password:
|
|
||||||
await super(FactorioContext, self).server_auth(password_requested)
|
|
||||||
|
|
||||||
if self.rcon_client:
|
|
||||||
await get_info(self, self.rcon_client) # retrieve current auth code
|
|
||||||
else:
|
|
||||||
raise Exception("Cannot connect to a server with unknown own identity, "
|
|
||||||
"bridge to Factorio first.")
|
|
||||||
|
|
||||||
await self.send_connect()
|
|
||||||
|
|
||||||
def on_print(self, args: dict):
|
|
||||||
super(FactorioContext, self).on_print(args)
|
|
||||||
if self.rcon_client:
|
|
||||||
if not args['text'].startswith(self.player_names[self.slot] + ":"):
|
|
||||||
self.print_to_game(args['text'])
|
|
||||||
|
|
||||||
def on_print_json(self, args: dict):
|
|
||||||
if self.rcon_client:
|
|
||||||
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"]))
|
|
||||||
if not text.startswith(self.player_names[self.slot] + ":"): # TODO: Remove string heuristic in the future.
|
|
||||||
self.print_to_game(text)
|
|
||||||
super(FactorioContext, self).on_print_json(args)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def savegame_name(self) -> str:
|
|
||||||
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
|
|
||||||
|
|
||||||
def print_to_game(self, text):
|
|
||||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
|
||||||
f"{text}")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def energy_link_status(self) -> str:
|
|
||||||
if not self.energy_link_increment:
|
|
||||||
return "Disabled"
|
|
||||||
elif self.current_energy_link_value is None:
|
|
||||||
return "Standby"
|
|
||||||
else:
|
|
||||||
return f"{Utils.format_SI_prefix(self.current_energy_link_value)}J"
|
|
||||||
|
|
||||||
def on_deathlink(self, data: dict):
|
|
||||||
if self.rcon_client:
|
|
||||||
self.rcon_client.send_command(f"/ap-deathlink {data['source']}")
|
|
||||||
super(FactorioContext, self).on_deathlink(data)
|
|
||||||
|
|
||||||
def on_package(self, cmd: str, args: dict):
|
|
||||||
if cmd in {"Connected", "RoomUpdate"}:
|
|
||||||
# catch up sync anything that is already cleared.
|
|
||||||
if "checked_locations" in args and args["checked_locations"]:
|
|
||||||
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
|
|
||||||
item_name in args["checked_locations"]})
|
|
||||||
if cmd == "Connected" and self.energy_link_increment:
|
|
||||||
async_start(self.send_msgs([{
|
|
||||||
"cmd": "SetNotify", "keys": ["EnergyLink"]
|
|
||||||
}]))
|
|
||||||
elif cmd == "SetReply":
|
|
||||||
if args["key"] == "EnergyLink":
|
|
||||||
if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete:
|
|
||||||
# it's our deplete request
|
|
||||||
gained = int(args["original_value"] - args["value"])
|
|
||||||
gained_text = Utils.format_SI_prefix(gained) + "J"
|
|
||||||
if gained:
|
|
||||||
logger.debug(f"EnergyLink: Received {gained_text}. "
|
|
||||||
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
|
|
||||||
self.rcon_client.send_command(f"/ap-energylink {gained}")
|
|
||||||
|
|
||||||
def on_user_say(self, text: str) -> typing.Optional[str]:
|
|
||||||
# Mirror chat sent from the UI to the Factorio server.
|
|
||||||
self.print_to_game(f"{self.player_names[self.slot]}: {text}")
|
|
||||||
return text
|
|
||||||
|
|
||||||
async def chat_from_factorio(self, user: str, message: str) -> None:
|
|
||||||
if not self.bridge_chat_out:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Pass through commands
|
|
||||||
if message.startswith("!"):
|
|
||||||
await self.send_msgs([{"cmd": "Say", "text": message}])
|
|
||||||
return
|
|
||||||
|
|
||||||
# Omit messages that contain local coordinates
|
|
||||||
if "[gps=" in message:
|
|
||||||
return
|
|
||||||
|
|
||||||
prefix = f"({user}) " if self.multiplayer else ""
|
|
||||||
await self.send_msgs([{"cmd": "Say", "text": f"{prefix}{message}"}])
|
|
||||||
|
|
||||||
def toggle_filter_item_sends(self) -> None:
|
|
||||||
self.filter_item_sends = not self.filter_item_sends
|
|
||||||
if self.filter_item_sends:
|
|
||||||
announcement = "Item sends are now filtered."
|
|
||||||
else:
|
|
||||||
announcement = "Item sends are no longer filtered."
|
|
||||||
logger.info(announcement)
|
|
||||||
self.print_to_game(announcement)
|
|
||||||
|
|
||||||
def toggle_bridge_chat_out(self) -> None:
|
|
||||||
self.bridge_chat_out = not self.bridge_chat_out
|
|
||||||
if self.bridge_chat_out:
|
|
||||||
announcement = "Chat is now bridged to Archipelago."
|
|
||||||
else:
|
|
||||||
announcement = "Chat is no longer bridged to Archipelago."
|
|
||||||
logger.info(announcement)
|
|
||||||
self.print_to_game(announcement)
|
|
||||||
|
|
||||||
def run_gui(self):
|
|
||||||
from kvui import GameManager
|
|
||||||
|
|
||||||
class FactorioManager(GameManager):
|
|
||||||
logging_pairs = [
|
|
||||||
("Client", "Archipelago"),
|
|
||||||
("FactorioServer", "Factorio Server Log"),
|
|
||||||
("FactorioWatcher", "Bridge Data Log"),
|
|
||||||
]
|
|
||||||
base_title = "Archipelago Factorio Client"
|
|
||||||
|
|
||||||
self.ui = FactorioManager(self)
|
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
|
||||||
|
|
||||||
|
|
||||||
async def game_watcher(ctx: FactorioContext):
|
|
||||||
bridge_logger = logging.getLogger("FactorioWatcher")
|
|
||||||
next_bridge = time.perf_counter() + 1
|
|
||||||
try:
|
|
||||||
while not ctx.exit_event.is_set():
|
|
||||||
# TODO: restore on-demand refresh
|
|
||||||
if ctx.rcon_client and time.perf_counter() > next_bridge:
|
|
||||||
next_bridge = time.perf_counter() + 1
|
|
||||||
ctx.awaiting_bridge = False
|
|
||||||
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
|
|
||||||
if not ctx.auth:
|
|
||||||
pass # auth failed, wait for new attempt
|
|
||||||
elif data["slot_name"] != ctx.auth:
|
|
||||||
bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
|
|
||||||
elif data["seed_name"] != ctx.seed_name:
|
|
||||||
bridge_logger.warning(
|
|
||||||
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
|
|
||||||
else:
|
|
||||||
data = data["info"]
|
|
||||||
research_data = data["research_done"]
|
|
||||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
|
||||||
victory = data["victory"]
|
|
||||||
await ctx.update_death_link(data["death_link"])
|
|
||||||
ctx.multiplayer = data.get("multiplayer", False)
|
|
||||||
|
|
||||||
if not ctx.finished_game and victory:
|
|
||||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
|
||||||
ctx.finished_game = True
|
|
||||||
|
|
||||||
if ctx.locations_checked != research_data:
|
|
||||||
bridge_logger.debug(
|
|
||||||
f"New researches done: "
|
|
||||||
f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}")
|
|
||||||
ctx.locations_checked = research_data
|
|
||||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
|
||||||
death_link_tick = data.get("death_link_tick", 0)
|
|
||||||
if death_link_tick != ctx.death_link_tick:
|
|
||||||
ctx.death_link_tick = death_link_tick
|
|
||||||
if "DeathLink" in ctx.tags:
|
|
||||||
async_start(ctx.send_death())
|
|
||||||
if ctx.energy_link_increment:
|
|
||||||
in_world_bridges = data["energy_bridges"]
|
|
||||||
if in_world_bridges:
|
|
||||||
in_world_energy = data["energy"]
|
|
||||||
if in_world_energy < (ctx.energy_link_increment * in_world_bridges):
|
|
||||||
# attempt to refill
|
|
||||||
ctx.last_deplete = time.time()
|
|
||||||
async_start(ctx.send_msgs([{
|
|
||||||
"cmd": "Set", "key": "EnergyLink", "operations":
|
|
||||||
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
|
|
||||||
{"operation": "max", "value": 0}],
|
|
||||||
"last_deplete": ctx.last_deplete
|
|
||||||
}]))
|
|
||||||
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
|
|
||||||
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
|
|
||||||
ctx.energy_link_increment*in_world_bridges:
|
|
||||||
value = ctx.energy_link_increment * in_world_bridges
|
|
||||||
async_start(ctx.send_msgs([{
|
|
||||||
"cmd": "Set", "key": "EnergyLink", "operations":
|
|
||||||
[{"operation": "add", "value": value}]
|
|
||||||
}]))
|
|
||||||
ctx.rcon_client.send_command(
|
|
||||||
f"/ap-energylink -{value}")
|
|
||||||
logger.debug(f"EnergyLink: Sent {Utils.format_SI_prefix(value)}J")
|
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception(e)
|
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
|
||||||
|
|
||||||
|
|
||||||
def stream_factorio_output(pipe, queue, process):
|
|
||||||
pipe.reconfigure(errors="replace")
|
|
||||||
|
|
||||||
def queuer():
|
|
||||||
while process.poll() is None:
|
|
||||||
text = pipe.readline().strip()
|
|
||||||
if text:
|
|
||||||
queue.put_nowait(text)
|
|
||||||
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
|
|
||||||
thread.start()
|
|
||||||
return thread
|
|
||||||
|
|
||||||
|
|
||||||
async def factorio_server_watcher(ctx: FactorioContext):
|
|
||||||
savegame_name = os.path.abspath(ctx.savegame_name)
|
|
||||||
if not os.path.exists(savegame_name):
|
|
||||||
logger.info(f"Creating savegame {savegame_name}")
|
|
||||||
subprocess.run((
|
|
||||||
executable, "--create", savegame_name, "--preset", "archipelago"
|
|
||||||
))
|
|
||||||
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
|
|
||||||
*(str(elem) for elem in server_args)),
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
encoding="utf-8")
|
|
||||||
factorio_server_logger.info("Started Factorio Server")
|
|
||||||
factorio_queue = Queue()
|
|
||||||
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
|
|
||||||
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
|
|
||||||
try:
|
|
||||||
while not ctx.exit_event.is_set():
|
|
||||||
if factorio_process.poll() is not None:
|
|
||||||
factorio_server_logger.info("Factorio server has exited.")
|
|
||||||
ctx.exit_event.set()
|
|
||||||
|
|
||||||
while not factorio_queue.empty():
|
|
||||||
msg = factorio_queue.get()
|
|
||||||
factorio_queue.task_done()
|
|
||||||
|
|
||||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
|
||||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
|
||||||
if not ctx.server:
|
|
||||||
logger.info("Established bridge to Factorio Server. "
|
|
||||||
"Ready to connect to Archipelago via /connect")
|
|
||||||
check_stdin()
|
|
||||||
|
|
||||||
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
|
|
||||||
ctx.awaiting_bridge = True
|
|
||||||
factorio_server_logger.debug(msg)
|
|
||||||
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command energy-link$", msg):
|
|
||||||
factorio_server_logger.debug(msg)
|
|
||||||
ctx.print_to_game(f"Energy Link: {ctx.energy_link_status}")
|
|
||||||
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-send-filter$", msg):
|
|
||||||
factorio_server_logger.debug(msg)
|
|
||||||
ctx.toggle_filter_item_sends()
|
|
||||||
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-chat$", msg):
|
|
||||||
factorio_server_logger.debug(msg)
|
|
||||||
ctx.toggle_bridge_chat_out()
|
|
||||||
else:
|
|
||||||
factorio_server_logger.info(msg)
|
|
||||||
match = re.match(r"^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[CHAT\] ([^:]+): (.*)$", msg)
|
|
||||||
if match:
|
|
||||||
await ctx.chat_from_factorio(match.group(1), match.group(2))
|
|
||||||
if ctx.rcon_client:
|
|
||||||
commands = {}
|
|
||||||
while ctx.send_index < len(ctx.items_received):
|
|
||||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
|
||||||
item_id = transfer_item.item
|
|
||||||
player_name = ctx.player_names[transfer_item.player]
|
|
||||||
if item_id not in Factorio.item_id_to_name:
|
|
||||||
factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
|
|
||||||
else:
|
|
||||||
item_name = Factorio.item_id_to_name[item_id]
|
|
||||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
|
|
||||||
commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
|
|
||||||
ctx.send_index += 1
|
|
||||||
if commands:
|
|
||||||
ctx.rcon_client.send_commands(commands)
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception(e)
|
|
||||||
logging.error("Aborted Factorio Server Bridge")
|
|
||||||
ctx.exit_event.set()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if factorio_process.poll() is not None:
|
|
||||||
if ctx.rcon_client:
|
|
||||||
ctx.rcon_client.close()
|
|
||||||
ctx.rcon_client = None
|
|
||||||
return
|
|
||||||
|
|
||||||
sent_quit = False
|
|
||||||
if ctx.rcon_client:
|
|
||||||
# Attempt clean quit through RCON.
|
|
||||||
try:
|
|
||||||
ctx.rcon_client.send_command("/quit")
|
|
||||||
except factorio_rcon.RCONNetworkError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
sent_quit = True
|
|
||||||
ctx.rcon_client.close()
|
|
||||||
ctx.rcon_client = None
|
|
||||||
if not sent_quit:
|
|
||||||
# Attempt clean quit using SIGTERM. (Note that on Windows this kills the process instead.)
|
|
||||||
factorio_process.terminate()
|
|
||||||
|
|
||||||
try:
|
|
||||||
factorio_process.wait(10)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
factorio_process.kill()
|
|
||||||
|
|
||||||
|
|
||||||
async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
|
|
||||||
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
|
|
||||||
ctx.auth = info["slot_name"]
|
|
||||||
ctx.seed_name = info["seed_name"]
|
|
||||||
# 0.2.0 addition, not present earlier
|
|
||||||
death_link = bool(info.get("death_link", False))
|
|
||||||
ctx.energy_link_increment = info.get("energy_link", 0)
|
|
||||||
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
|
|
||||||
if ctx.energy_link_increment and ctx.ui:
|
|
||||||
ctx.ui.enable_energy_link()
|
|
||||||
await ctx.update_death_link(death_link)
|
|
||||||
|
|
||||||
|
|
||||||
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
|
||||||
savegame_name = os.path.abspath("Archipelago.zip")
|
|
||||||
if not os.path.exists(savegame_name):
|
|
||||||
logger.info(f"Creating savegame {savegame_name}")
|
|
||||||
subprocess.run((
|
|
||||||
executable, "--create", savegame_name
|
|
||||||
))
|
|
||||||
factorio_process = subprocess.Popen(
|
|
||||||
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stdin=subprocess.DEVNULL,
|
|
||||||
encoding="utf-8")
|
|
||||||
factorio_server_logger.info("Started Information Exchange Factorio Server")
|
|
||||||
factorio_queue = Queue()
|
|
||||||
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
|
|
||||||
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
|
|
||||||
rcon_client = None
|
|
||||||
try:
|
|
||||||
while not ctx.auth:
|
|
||||||
while not factorio_queue.empty():
|
|
||||||
msg = factorio_queue.get()
|
|
||||||
factorio_server_logger.info(msg)
|
|
||||||
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
|
|
||||||
parts = msg.split()
|
|
||||||
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
|
||||||
elif "Write data path: " in msg:
|
|
||||||
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
|
|
||||||
if "AppData" in ctx.write_data_path:
|
|
||||||
logger.warning("It appears your mods are loaded from Appdata, "
|
|
||||||
"this can lead to problems with multiple Factorio instances. "
|
|
||||||
"If this is the case, you will get a file locked error running Factorio.")
|
|
||||||
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
|
||||||
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
|
||||||
if ctx.mod_version == ctx.__class__.mod_version:
|
|
||||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
|
||||||
await get_info(ctx, rcon_client)
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(e, extra={"compact_gui": True})
|
|
||||||
msg = "Aborted Factorio Server Bridge"
|
|
||||||
logger.error(msg)
|
|
||||||
ctx.gui_error(msg, e)
|
|
||||||
ctx.exit_event.set()
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
factorio_process.terminate()
|
|
||||||
factorio_process.wait(5)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def main(args):
|
|
||||||
ctx = FactorioContext(args.connect, args.password)
|
|
||||||
ctx.filter_item_sends = initial_filter_item_sends
|
|
||||||
ctx.bridge_chat_out = initial_bridge_chat_out
|
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
|
||||||
|
|
||||||
if gui_enabled:
|
|
||||||
ctx.run_gui()
|
|
||||||
ctx.run_cli()
|
|
||||||
|
|
||||||
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
|
|
||||||
successful_launch = await factorio_server_task
|
|
||||||
if successful_launch:
|
|
||||||
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
|
|
||||||
progression_watcher = asyncio.create_task(
|
|
||||||
game_watcher(ctx), name="FactorioProgressionWatcher")
|
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
|
||||||
ctx.server_address = None
|
|
||||||
|
|
||||||
await progression_watcher
|
|
||||||
await factorio_server_task
|
|
||||||
|
|
||||||
await ctx.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
class FactorioJSONtoTextParser(JSONtoTextParser):
|
|
||||||
def _handle_color(self, node: JSONMessagePart):
|
|
||||||
colors = node["color"].split(";")
|
|
||||||
for color in colors:
|
|
||||||
if color in self.color_codes:
|
|
||||||
node["text"] = f"[color=#{self.color_codes[color]}]{node['text']}[/color]"
|
|
||||||
return self._handle_text(node)
|
|
||||||
return self._handle_text(node)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
|
|
||||||
"Remaining arguments get passed into bound Factorio instance."
|
|
||||||
"Refer to Factorio --help for those.")
|
|
||||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
|
||||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
|
||||||
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
|
|
||||||
|
|
||||||
args, rest = parser.parse_known_args()
|
|
||||||
colorama.init()
|
|
||||||
rcon_port = args.rcon_port
|
|
||||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
|
||||||
random.choice(string.ascii_letters) for x in range(32))
|
|
||||||
|
|
||||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
|
||||||
options = Utils.get_options()
|
|
||||||
executable = options["factorio_options"]["executable"]
|
|
||||||
server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None)
|
|
||||||
if server_settings:
|
|
||||||
server_settings = os.path.abspath(server_settings)
|
|
||||||
if not isinstance(options["factorio_options"]["filter_item_sends"], bool):
|
|
||||||
logging.warning(f"Warning: Option filter_item_sends should be a bool.")
|
|
||||||
initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"])
|
|
||||||
if not isinstance(options["factorio_options"]["bridge_chat_out"], bool):
|
|
||||||
logging.warning(f"Warning: Option bridge_chat_out should be a bool.")
|
|
||||||
initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"])
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.dirname(executable)):
|
|
||||||
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
|
||||||
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
|
|
||||||
executable = os.path.join(executable, "factorio")
|
|
||||||
if not os.path.isfile(executable):
|
|
||||||
if os.path.isfile(executable + ".exe"):
|
|
||||||
executable = executable + ".exe"
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError(f"Path {executable} is not an executable file.")
|
|
||||||
|
|
||||||
if server_settings and os.path.isfile(server_settings):
|
|
||||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest)
|
|
||||||
else:
|
|
||||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
|
|
||||||
|
|
||||||
asyncio.run(main(args))
|
|
||||||
colorama.deinit()
|
|
||||||
|
|||||||
60
Fill.py
@@ -1,11 +1,10 @@
|
|||||||
import logging
|
|
||||||
import typing
|
|
||||||
import collections
|
import collections
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
|
import typing
|
||||||
from collections import Counter, deque
|
from collections import Counter, deque
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item, ItemClassification
|
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
|
||||||
|
|
||||||
from worlds.AutoWorld import call_all
|
from worlds.AutoWorld import call_all
|
||||||
from worlds.generic.Rules import add_item_rule
|
from worlds.generic.Rules import add_item_rule
|
||||||
|
|
||||||
@@ -40,8 +39,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
"""
|
"""
|
||||||
unplaced_items: typing.List[Item] = []
|
unplaced_items: typing.List[Item] = []
|
||||||
placements: typing.List[Location] = []
|
placements: typing.List[Location] = []
|
||||||
|
cleanup_required = False
|
||||||
|
|
||||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter()
|
||||||
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
|
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
|
||||||
for item in item_pool:
|
for item in item_pool:
|
||||||
reachable_items.setdefault(item.player, deque()).append(item)
|
reachable_items.setdefault(item.player, deque()).append(item)
|
||||||
@@ -85,25 +85,28 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
else:
|
else:
|
||||||
# we filled all reachable spots.
|
# we filled all reachable spots.
|
||||||
if swap:
|
if swap:
|
||||||
# try swapping this item with previously placed items
|
# try swapping this item with previously placed items in a safe way then in an unsafe way
|
||||||
for (i, location) in enumerate(placements):
|
swap_attempts = ((i, location, unsafe)
|
||||||
|
for unsafe in (False, True)
|
||||||
|
for i, location in enumerate(placements))
|
||||||
|
for (i, location, unsafe) in swap_attempts:
|
||||||
placed_item = location.item
|
placed_item = location.item
|
||||||
# Unplaceable items can sometimes be swapped infinitely. Limit the
|
# Unplaceable items can sometimes be swapped infinitely. Limit the
|
||||||
# number of times we will swap an individual item to prevent this
|
# number of times we will swap an individual item to prevent this
|
||||||
swap_count = swapped_items[placed_item.player,
|
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]
|
||||||
placed_item.name]
|
|
||||||
if swap_count > 1:
|
if swap_count > 1:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
location.item = None
|
location.item = None
|
||||||
placed_item.location = None
|
placed_item.location = None
|
||||||
swap_state = sweep_from_pool(base_state, [placed_item])
|
swap_state = sweep_from_pool(base_state, [placed_item] if unsafe else [])
|
||||||
# swap_state assumes we can collect placed item before item_to_place
|
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
|
||||||
|
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
|
||||||
|
# to clean that up later, so there is a chance generation fails.
|
||||||
if (not single_player_placement or location.player == item_to_place.player) \
|
if (not single_player_placement or location.player == item_to_place.player) \
|
||||||
and location.can_fill(swap_state, item_to_place, perform_access_check):
|
and location.can_fill(swap_state, item_to_place, perform_access_check):
|
||||||
|
|
||||||
# Verify that placing this item won't reduce available locations, which could happen with rules
|
# Verify placing this item won't reduce available locations, which would be a useless swap.
|
||||||
# that want to not have both items. Left in until removal is proven useful.
|
|
||||||
prev_state = swap_state.copy()
|
prev_state = swap_state.copy()
|
||||||
prev_loc_count = len(
|
prev_loc_count = len(
|
||||||
world.get_reachable_locations(prev_state))
|
world.get_reachable_locations(prev_state))
|
||||||
@@ -118,13 +121,15 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
spot_to_fill = placements.pop(i)
|
spot_to_fill = placements.pop(i)
|
||||||
|
|
||||||
swap_count += 1
|
swap_count += 1
|
||||||
swapped_items[placed_item.player,
|
swapped_items[placed_item.player, placed_item.name, unsafe] = swap_count
|
||||||
placed_item.name] = swap_count
|
|
||||||
|
|
||||||
reachable_items[placed_item.player].appendleft(
|
reachable_items[placed_item.player].appendleft(
|
||||||
placed_item)
|
placed_item)
|
||||||
item_pool.append(placed_item)
|
item_pool.append(placed_item)
|
||||||
|
|
||||||
|
# cleanup at the end to hopefully get better errors
|
||||||
|
cleanup_required = True
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Item can't be placed here, restore original item
|
# Item can't be placed here, restore original item
|
||||||
@@ -145,6 +150,16 @@ 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 cleanup_required:
|
||||||
|
# validate all placements and remove invalid ones
|
||||||
|
for placement in placements:
|
||||||
|
state = sweep_from_pool(base_state, [])
|
||||||
|
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
|
||||||
|
placement.item.location = None
|
||||||
|
unplaced_items.append(placement.item)
|
||||||
|
placement.item = None
|
||||||
|
locations.append(placement)
|
||||||
|
|
||||||
if allow_excluded:
|
if allow_excluded:
|
||||||
# check if partial fill is the result of excluded locations, in which case retry
|
# check if partial fill is the result of excluded locations, in which case retry
|
||||||
excluded_locations = [
|
excluded_locations = [
|
||||||
@@ -526,16 +541,16 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
|||||||
checked_locations: typing.Set[Location] = set()
|
checked_locations: typing.Set[Location] = set()
|
||||||
unchecked_locations: typing.Set[Location] = set(world.get_locations())
|
unchecked_locations: typing.Set[Location] = set(world.get_locations())
|
||||||
|
|
||||||
reachable_locations_count: typing.Dict[int, int] = {
|
|
||||||
player: 0
|
|
||||||
for player in world.player_ids
|
|
||||||
if len(world.get_filled_locations(player)) != 0
|
|
||||||
}
|
|
||||||
total_locations_count: typing.Counter[int] = Counter(
|
total_locations_count: typing.Counter[int] = Counter(
|
||||||
location.player
|
location.player
|
||||||
for location in world.get_locations()
|
for location in world.get_locations()
|
||||||
if not location.locked
|
if not location.locked
|
||||||
)
|
)
|
||||||
|
reachable_locations_count: typing.Dict[int, int] = {
|
||||||
|
player: 0
|
||||||
|
for player in world.player_ids
|
||||||
|
if total_locations_count[player] and len(world.get_filled_locations(player)) != 0
|
||||||
|
}
|
||||||
balanceable_players = {
|
balanceable_players = {
|
||||||
player: balanceable_players[player]
|
player: balanceable_players[player]
|
||||||
for player in balanceable_players
|
for player in balanceable_players
|
||||||
@@ -552,6 +567,10 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
|
|||||||
def item_percentage(player: int, num: int) -> float:
|
def item_percentage(player: int, num: int) -> float:
|
||||||
return num / total_locations_count[player]
|
return num / total_locations_count[player]
|
||||||
|
|
||||||
|
# If there are no locations that aren't locked, there's no point in attempting to balance progression.
|
||||||
|
if len(total_locations_count) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Gather non-locked locations.
|
# Gather non-locked locations.
|
||||||
# This ensures that only shuffled locations get counted for progression balancing,
|
# This ensures that only shuffled locations get counted for progression balancing,
|
||||||
@@ -825,7 +844,6 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
for player in worlds:
|
for player in worlds:
|
||||||
locations += non_early_locations[player]
|
locations += non_early_locations[player]
|
||||||
|
|
||||||
|
|
||||||
block['locations'] = locations
|
block['locations'] = locations
|
||||||
|
|
||||||
if not block['count']:
|
if not block['count']:
|
||||||
|
|||||||
83
Generate.py
@@ -7,8 +7,8 @@ import random
|
|||||||
import string
|
import string
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from collections import Counter, ChainMap
|
from collections import ChainMap, Counter
|
||||||
from typing import Dict, Tuple, Callable, Any, Union
|
from typing import Any, Callable, Dict, Tuple, Union
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
@@ -27,9 +27,6 @@ from worlds.AutoWorld import AutoWorldRegister
|
|||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mystery_argparse():
|
def mystery_argparse():
|
||||||
options = get_options()
|
options = get_options()
|
||||||
defaults = options["generator"]
|
defaults = options["generator"]
|
||||||
@@ -56,6 +53,8 @@ def mystery_argparse():
|
|||||||
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
|
||||||
parser.add_argument('--plando', default=defaults["plando_options"],
|
parser.add_argument('--plando', default=defaults["plando_options"],
|
||||||
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
|
||||||
|
parser.add_argument("--skip_prog_balancing", action="store_true",
|
||||||
|
help="Skip progression balancing step during generation.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if not os.path.isabs(args.weights_file_path):
|
if not os.path.isabs(args.weights_file_path):
|
||||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||||
@@ -74,10 +73,12 @@ def main(args=None, callback=ERmain):
|
|||||||
args, options = mystery_argparse()
|
args, options = mystery_argparse()
|
||||||
|
|
||||||
seed = get_seed(args.seed)
|
seed = get_seed(args.seed)
|
||||||
|
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||||
random.seed(seed)
|
random.seed(seed)
|
||||||
seed_name = get_seed_name(random)
|
seed_name = get_seed_name(random)
|
||||||
|
|
||||||
if args.race:
|
if args.race:
|
||||||
|
logging.info("Race mode enabled. Using non-deterministic random source.")
|
||||||
random.seed() # reset to time-based random source
|
random.seed() # reset to time-based random source
|
||||||
|
|
||||||
weights_cache: Dict[str, Tuple[Any, ...]] = {}
|
weights_cache: Dict[str, Tuple[Any, ...]] = {}
|
||||||
@@ -86,15 +87,15 @@ def main(args=None, callback=ERmain):
|
|||||||
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
|
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
|
||||||
print(f"Weights: {args.weights_file_path} >> "
|
logging.info(f"Weights: {args.weights_file_path} >> "
|
||||||
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
|
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
|
||||||
|
|
||||||
if args.meta_file_path and os.path.exists(args.meta_file_path):
|
if args.meta_file_path and os.path.exists(args.meta_file_path):
|
||||||
try:
|
try:
|
||||||
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
|
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
||||||
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
logging.info(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
||||||
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
|
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
|
||||||
del(meta_weights["meta_description"])
|
del(meta_weights["meta_description"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -120,17 +121,18 @@ def main(args=None, callback=ERmain):
|
|||||||
for filename, yaml_data in weights_cache.items():
|
for filename, yaml_data in weights_cache.items():
|
||||||
if filename not in {args.meta_file_path, args.weights_file_path}:
|
if filename not in {args.meta_file_path, args.weights_file_path}:
|
||||||
for yaml in yaml_data:
|
for yaml in yaml_data:
|
||||||
print(f"P{player_id} Weights: {filename} >> "
|
logging.info(f"P{player_id} Weights: {filename} >> "
|
||||||
f"{get_choice('description', yaml, 'No description specified')}")
|
f"{get_choice('description', yaml, 'No description specified')}")
|
||||||
player_files[player_id] = filename
|
player_files[player_id] = filename
|
||||||
player_id += 1
|
player_id += 1
|
||||||
|
|
||||||
args.multi = max(player_id - 1, args.multi)
|
args.multi = max(player_id - 1, args.multi)
|
||||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
logging.info(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, "
|
||||||
f"{args.plando}")
|
f"{seed_name} Seed {seed} with plando: {args.plando}")
|
||||||
|
|
||||||
if not weights_cache:
|
if not weights_cache:
|
||||||
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
raise Exception(f"No weights found. "
|
||||||
|
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
||||||
f"A mix is also permitted.")
|
f"A mix is also permitted.")
|
||||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||||
erargs.seed = seed
|
erargs.seed = seed
|
||||||
@@ -140,8 +142,7 @@ def main(args=None, callback=ERmain):
|
|||||||
erargs.race = args.race
|
erargs.race = args.race
|
||||||
erargs.outputname = seed_name
|
erargs.outputname = seed_name
|
||||||
erargs.outputpath = args.outputpath
|
erargs.outputpath = args.outputpath
|
||||||
|
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||||
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
|
||||||
|
|
||||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||||
@@ -449,6 +450,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||||
|
|
||||||
ret.game = get_choice("game", weights)
|
ret.game = get_choice("game", weights)
|
||||||
|
if ret.game not in AutoWorldRegister.world_types:
|
||||||
|
picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
|
||||||
|
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||||
|
f"Check your spelling or installation of that world.")
|
||||||
|
|
||||||
if ret.game not in weights:
|
if ret.game not in weights:
|
||||||
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
|
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
|
||||||
|
|
||||||
@@ -463,32 +469,29 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
for option_key, option in Options.common_options.items():
|
for option_key, option in Options.common_options.items():
|
||||||
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
||||||
|
|
||||||
if ret.game in AutoWorldRegister.world_types:
|
for option_key, option in world_type.option_definitions.items():
|
||||||
for option_key, option in world_type.option_definitions.items():
|
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||||
|
for option_key, option in Options.per_game_common_options.items():
|
||||||
|
# skip setting this option if already set from common_options, defaulting to root option
|
||||||
|
if option_key not in world_type.option_definitions and \
|
||||||
|
(option_key not in Options.common_options or option_key in game_weights):
|
||||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||||
for option_key, option in Options.per_game_common_options.items():
|
if PlandoOptions.items in plando_options:
|
||||||
# skip setting this option if already set from common_options, defaulting to root option
|
ret.plando_items = game_weights.get("plando_items", [])
|
||||||
if option_key not in world_type.option_definitions and \
|
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||||
(option_key not in Options.common_options or option_key in game_weights):
|
# bad hardcoded behavior to make this work for now
|
||||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
ret.plando_connections = []
|
||||||
if PlandoOptions.items in plando_options:
|
if PlandoOptions.connections in plando_options:
|
||||||
ret.plando_items = game_weights.get("plando_items", [])
|
options = game_weights.get("plando_connections", [])
|
||||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
for placement in options:
|
||||||
# bad hardcoded behavior to make this work for now
|
if roll_percentage(get_choice("percentage", placement, 100)):
|
||||||
ret.plando_connections = []
|
ret.plando_connections.append(PlandoConnection(
|
||||||
if PlandoOptions.connections in plando_options:
|
get_choice("entrance", placement),
|
||||||
options = game_weights.get("plando_connections", [])
|
get_choice("exit", placement),
|
||||||
for placement in options:
|
get_choice("direction", placement)
|
||||||
if roll_percentage(get_choice("percentage", placement, 100)):
|
))
|
||||||
ret.plando_connections.append(PlandoConnection(
|
elif ret.game == "A Link to the Past":
|
||||||
get_choice("entrance", placement),
|
roll_alttp_settings(ret, game_weights, plando_options)
|
||||||
get_choice("exit", placement),
|
|
||||||
get_choice("direction", placement)
|
|
||||||
))
|
|
||||||
elif ret.game == "A Link to the Past":
|
|
||||||
roll_alttp_settings(ret, game_weights, plando_options)
|
|
||||||
else:
|
|
||||||
raise Exception(f"Unsupported game {ret.game}")
|
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|||||||
262
KH2Client.py
@@ -53,79 +53,8 @@ class KH2Context(CommonContext):
|
|||||||
self.collectible_override_flags_address = 0
|
self.collectible_override_flags_address = 0
|
||||||
self.collectible_offsets = {}
|
self.collectible_offsets = {}
|
||||||
self.sending = []
|
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
|
# list used to keep track of locations+items player has. Used for disoneccting
|
||||||
self.kh2seedsave = {"checked_locations": {"0": []},
|
self.kh2seedsave = None
|
||||||
"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.slotDataProgressionNames = {}
|
||||||
self.kh2seedname = None
|
self.kh2seedname = None
|
||||||
self.kh2slotdata = None
|
self.kh2slotdata = None
|
||||||
@@ -202,14 +131,13 @@ class KH2Context(CommonContext):
|
|||||||
|
|
||||||
self.boost_set = set(CheckDupingItems["Boosts"])
|
self.boost_set = set(CheckDupingItems["Boosts"])
|
||||||
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
|
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
|
||||||
|
|
||||||
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
|
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
|
||||||
# Growth:[level 1,level 4,slot]
|
# Growth:[level 1,level 4,slot]
|
||||||
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE],
|
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25DA],
|
||||||
"Quick Run": [0x62, 0x65, 0x25D0],
|
"Quick Run": [0x62, 0x65, 0x25DC],
|
||||||
"Dodge Roll": [0x234, 0x237, 0x25D2],
|
"Dodge Roll": [0x234, 0x237, 0x25DE],
|
||||||
"Aerial Dodge": [0x066, 0x069, 0x25D4],
|
"Aerial Dodge": [0x066, 0x069, 0x25E0],
|
||||||
"Glide": [0x6A, 0x6D, 0x25D6]}
|
"Glide": [0x6A, 0x6D, 0x25E2]}
|
||||||
self.boost_to_anchor_dict = {
|
self.boost_to_anchor_dict = {
|
||||||
"Power Boost": 0x24F9,
|
"Power Boost": 0x24F9,
|
||||||
"Magic Boost": 0x24FA,
|
"Magic Boost": 0x24FA,
|
||||||
@@ -269,19 +197,66 @@ class KH2Context(CommonContext):
|
|||||||
if not os.path.exists(self.game_communication_path):
|
if not os.path.exists(self.game_communication_path):
|
||||||
os.makedirs(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"):
|
if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
||||||
|
self.kh2seedsave = {"itemIndex": -1,
|
||||||
|
# back of soras invo is 0x25E2. Growth should be moved there
|
||||||
|
# Character: [back of invo, front of invo]
|
||||||
|
"SoraInvo": [0x25D8, 0x2546],
|
||||||
|
"DonaldInvo": [0x26F4, 0x2658],
|
||||||
|
"GoofyInvo": [0x280A, 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
|
||||||
|
"LocationsChecked": [],
|
||||||
|
"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}
|
||||||
|
}
|
||||||
with open(os.path.join(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:
|
'wt') as f:
|
||||||
pass
|
pass
|
||||||
|
self.locations_checked = set()
|
||||||
elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
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:
|
with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f:
|
||||||
self.kh2seedsave = json.load(f)
|
self.kh2seedsave = json.load(f)
|
||||||
|
self.locations_checked = set(self.kh2seedsave["LocationsChecked"])
|
||||||
|
self.serverconneced = True
|
||||||
|
|
||||||
if cmd in {"Connected"}:
|
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.kh2slotdata = args['slot_data']
|
||||||
self.serverconneced = True
|
|
||||||
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
|
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
|
||||||
try:
|
try:
|
||||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||||
@@ -296,21 +271,29 @@ class KH2Context(CommonContext):
|
|||||||
|
|
||||||
if cmd in {"ReceivedItems"}:
|
if cmd in {"ReceivedItems"}:
|
||||||
start_index = args["index"]
|
start_index = args["index"]
|
||||||
if start_index != len(self.items_received):
|
if start_index == 0:
|
||||||
|
# resetting everything that were sent from the server
|
||||||
|
self.kh2seedsave["SoraInvo"][0] = 0x25D8
|
||||||
|
self.kh2seedsave["DonaldInvo"][0] = 0x26F4
|
||||||
|
self.kh2seedsave["GoofyInvo"][0] = 0x280A
|
||||||
|
self.kh2seedsave["itemIndex"] = - 1
|
||||||
|
self.kh2seedsave["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": {},
|
||||||
|
}
|
||||||
|
if start_index > self.kh2seedsave["itemIndex"]:
|
||||||
|
self.kh2seedsave["itemIndex"] = start_index
|
||||||
for item in args['items']:
|
for item in args['items']:
|
||||||
# starting invo from server
|
asyncio.create_task(self.give_item(item.item))
|
||||||
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 cmd in {"RoomUpdate"}:
|
||||||
if "checked_locations" in args:
|
if "checked_locations" in args:
|
||||||
@@ -326,12 +309,12 @@ class KH2Context(CommonContext):
|
|||||||
if currentworldint in self.worldid:
|
if currentworldint in self.worldid:
|
||||||
curworldid = self.worldid[currentworldint]
|
curworldid = self.worldid[currentworldint]
|
||||||
for location, data in curworldid.items():
|
for location, data in curworldid.items():
|
||||||
if location not in self.locations_checked \
|
locationId = kh2_loc_name_to_id[location]
|
||||||
|
if locationId not in self.locations_checked \
|
||||||
and (int.from_bytes(
|
and (int.from_bytes(
|
||||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||||
"big") & 0x1 << data.bitIndex) > 0:
|
"big") & 0x1 << data.bitIndex) > 0:
|
||||||
self.locations_checked.add(location)
|
self.sending = self.sending + [(int(locationId))]
|
||||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info("Line 285")
|
logger.info("Line 285")
|
||||||
if self.kh2connected:
|
if self.kh2connected:
|
||||||
@@ -344,12 +327,12 @@ class KH2Context(CommonContext):
|
|||||||
for location, data in SoraLevels.items():
|
for location, data in SoraLevels.items():
|
||||||
currentLevel = int.from_bytes(
|
currentLevel = int.from_bytes(
|
||||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
|
||||||
if location not in self.locations_checked \
|
locationId = kh2_loc_name_to_id[location]
|
||||||
|
if locationId not in self.locations_checked \
|
||||||
and currentLevel >= data.bitIndex:
|
and currentLevel >= data.bitIndex:
|
||||||
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
|
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
|
||||||
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
|
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
|
||||||
self.locations_checked.add(location)
|
self.sending = self.sending + [(int(locationId))]
|
||||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
|
||||||
formDict = {
|
formDict = {
|
||||||
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
|
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
|
||||||
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
|
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
|
||||||
@@ -357,12 +340,12 @@ class KH2Context(CommonContext):
|
|||||||
for location, data in formDict[i][1].items():
|
for location, data in formDict[i][1].items():
|
||||||
formlevel = int.from_bytes(
|
formlevel = int.from_bytes(
|
||||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
|
||||||
if location not in self.locations_checked \
|
locationId = kh2_loc_name_to_id[location]
|
||||||
|
if locationId not in self.locations_checked \
|
||||||
and formlevel >= data.bitIndex:
|
and formlevel >= data.bitIndex:
|
||||||
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
|
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
|
||||||
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
|
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
|
||||||
self.locations_checked.add(location)
|
self.sending = self.sending + [(int(locationId))]
|
||||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info("Line 312")
|
logger.info("Line 312")
|
||||||
if self.kh2connected:
|
if self.kh2connected:
|
||||||
@@ -373,18 +356,20 @@ class KH2Context(CommonContext):
|
|||||||
async def checkSlots(self):
|
async def checkSlots(self):
|
||||||
try:
|
try:
|
||||||
for location, data in weaponSlots.items():
|
for location, data in weaponSlots.items():
|
||||||
if location not in self.locations_checked:
|
locationId = kh2_loc_name_to_id[location]
|
||||||
|
if locationId not in self.locations_checked:
|
||||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||||
"big") > 0:
|
"big") > 0:
|
||||||
self.locations_checked.add(location)
|
self.sending = self.sending + [(int(locationId))]
|
||||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
|
||||||
|
|
||||||
for location, data in formSlots.items():
|
for location, data in formSlots.items():
|
||||||
if location not in self.locations_checked:
|
locationId = kh2_loc_name_to_id[location]
|
||||||
|
if locationId not in self.locations_checked:
|
||||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||||
"big") & 0x1 << data.bitIndex > 0:
|
"big") & 0x1 << data.bitIndex > 0:
|
||||||
self.locations_checked.add(location)
|
# self.locations_checked
|
||||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
self.sending = self.sending + [(int(locationId))]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.kh2connected:
|
if self.kh2connected:
|
||||||
logger.info("Line 333")
|
logger.info("Line 333")
|
||||||
@@ -394,8 +379,7 @@ class KH2Context(CommonContext):
|
|||||||
|
|
||||||
async def verifyChests(self):
|
async def verifyChests(self):
|
||||||
try:
|
try:
|
||||||
currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big"))
|
for location in self.locations_checked:
|
||||||
for location in self.kh2seedsave["worldIdChecks"][currentworld]:
|
|
||||||
locationName = self.lookup_id_to_Location[location]
|
locationName = self.lookup_id_to_Location[location]
|
||||||
if locationName in self.chest_set:
|
if locationName in self.chest_set:
|
||||||
if locationName in self.location_name_to_worlddata.keys():
|
if locationName in self.location_name_to_worlddata.keys():
|
||||||
@@ -428,24 +412,6 @@ class KH2Context(CommonContext):
|
|||||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
|
||||||
(self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1)
|
(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"):
|
async def give_item(self, item, ItemType="ServerItems"):
|
||||||
try:
|
try:
|
||||||
itemname = self.lookup_id_to_item[item]
|
itemname = self.lookup_id_to_item[item]
|
||||||
@@ -679,7 +645,21 @@ class KH2Context(CommonContext):
|
|||||||
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
||||||
ability = current & 0x0FFF
|
ability = current & 0x0FFF
|
||||||
if ability | 0x8000 != (0x8000 + itemData.memaddr):
|
if ability | 0x8000 != (0x8000 + itemData.memaddr):
|
||||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
|
if current - 0x8000 > 0:
|
||||||
|
self.kh2.write_short(self.kh2.base_address + self.Save + slot, (0x8000 + itemData.memaddr))
|
||||||
|
else:
|
||||||
|
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
|
||||||
|
# removes the duped ability if client gave faster than the game.
|
||||||
|
for charInvo in {"SoraInvo", "DonaldInvo", "GoofyInvo"}:
|
||||||
|
if self.kh2.read_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1]) != 0 and \
|
||||||
|
self.kh2seedsave[charInvo][1] + 2 < self.kh2seedsave[charInvo][0]:
|
||||||
|
self.kh2.write_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1], 0)
|
||||||
|
# remove the dummy level 1 growths if they are in these invo slots.
|
||||||
|
for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
|
||||||
|
current = self.kh2.read_short(self.kh2.base_address + self.Save + inventorySlot)
|
||||||
|
ability = current & 0x0FFF
|
||||||
|
if 0x05E <= ability <= 0x06D:
|
||||||
|
self.kh2.write_short(self.kh2.base_address + self.Save + inventorySlot, 0)
|
||||||
|
|
||||||
for itemName in self.master_growth:
|
for itemName in self.master_growth:
|
||||||
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
|
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
|
||||||
@@ -707,6 +687,10 @@ class KH2Context(CommonContext):
|
|||||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big")
|
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),
|
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
"big") & 0x1 << itemData.bitmask) == 0:
|
"big") & 0x1 << itemData.bitmask) == 0:
|
||||||
|
# when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game.
|
||||||
|
if itemName in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}:
|
||||||
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + 0x3410,
|
||||||
|
(0).to_bytes(1, 'big'), 1)
|
||||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
|
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
@@ -753,10 +737,13 @@ class KH2Context(CommonContext):
|
|||||||
if itemName in server_stat:
|
if itemName in server_stat:
|
||||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
|
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
|
||||||
|
|
||||||
|
# 0x130293 is Crit_1's location id for touching the computer
|
||||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||||
"big") != amountOfItems \
|
"big") != amountOfItems \
|
||||||
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
|
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
|
||||||
"big") >= 5:
|
"big") >= 5 and int.from_bytes(
|
||||||
|
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x23DF, 1),
|
||||||
|
"big") > 0:
|
||||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
amountOfItems.to_bytes(1, 'big'), 1)
|
amountOfItems.to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
@@ -777,7 +764,8 @@ class KH2Context(CommonContext):
|
|||||||
if itemName == "AP Boost":
|
if itemName == "AP Boost":
|
||||||
amountOfUsedBoosts -= 50
|
amountOfUsedBoosts -= 50
|
||||||
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
|
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
|
||||||
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][itemName] and amountOfBoostsInInvo < 255:
|
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][
|
||||||
|
itemName] and amountOfBoostsInInvo < 255:
|
||||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||||
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
|
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
|
||||||
|
|
||||||
@@ -859,9 +847,9 @@ async def kh2_watcher(ctx: KH2Context):
|
|||||||
location_ids = []
|
location_ids = []
|
||||||
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
|
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
|
||||||
for location 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.locations_checked:
|
||||||
if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]:
|
ctx.locations_checked.add(location)
|
||||||
ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location)
|
ctx.kh2seedsave["LocationsChecked"].append(location)
|
||||||
if location in ctx.kh2LocalItems:
|
if location in ctx.kh2LocalItems:
|
||||||
item = ctx.kh2slotdata["LocalItems"][str(location)]
|
item = ctx.kh2slotdata["LocalItems"][str(location)]
|
||||||
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
|
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
|
||||||
|
|||||||
129
Launcher.py
@@ -11,14 +11,18 @@ Scroll down to components= to add components to the launcher as well as setup.py
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import webbrowser
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Sequence, Union, Optional
|
from typing import Sequence, Union, Optional
|
||||||
|
|
||||||
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier
|
import Utils
|
||||||
|
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
@@ -38,7 +42,6 @@ def open_host_yaml():
|
|||||||
exe = which("open")
|
exe = which("open")
|
||||||
subprocess.Popen([exe, file])
|
subprocess.Popen([exe, file])
|
||||||
else:
|
else:
|
||||||
import webbrowser
|
|
||||||
webbrowser.open(file)
|
webbrowser.open(file)
|
||||||
|
|
||||||
|
|
||||||
@@ -53,39 +56,54 @@ def open_patch():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox('Error', str(e), error=True)
|
messagebox('Error', str(e), error=True)
|
||||||
else:
|
else:
|
||||||
file, _, component = identify(filename)
|
file, component = identify(filename)
|
||||||
if file and component:
|
if file and component:
|
||||||
launch([*get_exe(component), file], component.cli)
|
launch([*get_exe(component), file], component.cli)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_yamls():
|
||||||
|
from Options import generate_yaml_templates
|
||||||
|
|
||||||
|
target = Utils.user_path("Players", "Templates")
|
||||||
|
generate_yaml_templates(target, False)
|
||||||
|
open_folder(target)
|
||||||
|
|
||||||
|
|
||||||
def browse_files():
|
def browse_files():
|
||||||
file = user_path()
|
open_folder(user_path())
|
||||||
|
|
||||||
|
|
||||||
|
def open_folder(folder_path):
|
||||||
if is_linux:
|
if is_linux:
|
||||||
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
|
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||||
subprocess.Popen([exe, file])
|
subprocess.Popen([exe, folder_path])
|
||||||
elif is_macos:
|
elif is_macos:
|
||||||
exe = which("open")
|
exe = which("open")
|
||||||
subprocess.Popen([exe, file])
|
subprocess.Popen([exe, folder_path])
|
||||||
else:
|
else:
|
||||||
import webbrowser
|
webbrowser.open(folder_path)
|
||||||
webbrowser.open(file)
|
|
||||||
|
|
||||||
|
|
||||||
components.extend([
|
components.extend([
|
||||||
# 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("Generate Template Settings", func=generate_yamls),
|
||||||
|
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||||
|
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||||
|
Component("Browse Files", func=browse_files),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]):
|
def identify(path: Union[None, str]):
|
||||||
if path is None:
|
if path is None:
|
||||||
return None, None, None
|
return None, None
|
||||||
for component in components:
|
for component in components:
|
||||||
if component.handles_file(path):
|
if component.handles_file(path):
|
||||||
return path, component.script_name, component
|
return path, component
|
||||||
return (None, None, None) if '/' in path or '\\' in path else (None, path, None)
|
elif path == component.display_name or path == component.script_name:
|
||||||
|
return None, component
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
|
def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
|
||||||
@@ -132,16 +150,18 @@ def launch(exe, in_terminal=False):
|
|||||||
|
|
||||||
def run_gui():
|
def run_gui():
|
||||||
from kvui import App, ContainerLayout, GridLayout, Button, Label
|
from kvui import App, ContainerLayout, GridLayout, Button, Label
|
||||||
|
from kivy.uix.image import AsyncImage
|
||||||
|
from kivy.uix.relativelayout import RelativeLayout
|
||||||
|
|
||||||
class Launcher(App):
|
class Launcher(App):
|
||||||
base_title: str = "Archipelago Launcher"
|
base_title: str = "Archipelago Launcher"
|
||||||
container: ContainerLayout
|
container: ContainerLayout
|
||||||
grid: GridLayout
|
grid: GridLayout
|
||||||
|
|
||||||
_tools = {c.display_name: c for c in components if c.type == Type.TOOL and isfile(get_exe(c)[-1])}
|
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
|
||||||
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT and isfile(get_exe(c)[-1])}
|
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
|
||||||
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER and isfile(get_exe(c)[-1])}
|
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
|
||||||
_funcs = {c.display_name: c for c in components if c.type == Type.FUNC}
|
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
|
||||||
|
|
||||||
def __init__(self, ctx=None):
|
def __init__(self, ctx=None):
|
||||||
self.title = self.base_title
|
self.title = self.base_title
|
||||||
@@ -153,24 +173,44 @@ def run_gui():
|
|||||||
self.container = ContainerLayout()
|
self.container = ContainerLayout()
|
||||||
self.grid = GridLayout(cols=2)
|
self.grid = GridLayout(cols=2)
|
||||||
self.container.add_widget(self.grid)
|
self.container.add_widget(self.grid)
|
||||||
|
self.grid.add_widget(Label(text="General"))
|
||||||
|
self.grid.add_widget(Label(text="Clients"))
|
||||||
button_layout = self.grid # make buttons fill the window
|
button_layout = self.grid # make buttons fill the window
|
||||||
|
|
||||||
|
def build_button(component: Component):
|
||||||
|
"""
|
||||||
|
Builds a button widget for a given component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component (Component): The component associated with the button.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None. The button is added to the parent grid layout.
|
||||||
|
|
||||||
|
"""
|
||||||
|
button = Button(text=component.display_name)
|
||||||
|
button.component = component
|
||||||
|
button.bind(on_release=self.component_action)
|
||||||
|
if component.icon != "icon":
|
||||||
|
image = AsyncImage(source=icon_paths[component.icon],
|
||||||
|
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||||
|
box_layout = RelativeLayout()
|
||||||
|
box_layout.add_widget(button)
|
||||||
|
box_layout.add_widget(image)
|
||||||
|
button_layout.add_widget(box_layout)
|
||||||
|
else:
|
||||||
|
button_layout.add_widget(button)
|
||||||
|
|
||||||
for (tool, client) in itertools.zip_longest(itertools.chain(
|
for (tool, client) in itertools.zip_longest(itertools.chain(
|
||||||
self._tools.items(), self._funcs.items(), self._adjusters.items()), self._clients.items()):
|
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
|
||||||
# column 1
|
# column 1
|
||||||
if tool:
|
if tool:
|
||||||
button = Button(text=tool[0])
|
build_button(tool[1])
|
||||||
button.component = tool[1]
|
|
||||||
button.bind(on_release=self.component_action)
|
|
||||||
button_layout.add_widget(button)
|
|
||||||
else:
|
else:
|
||||||
button_layout.add_widget(Label())
|
button_layout.add_widget(Label())
|
||||||
# column 2
|
# column 2
|
||||||
if client:
|
if client:
|
||||||
button = Button(text=client[0])
|
build_button(client[1])
|
||||||
button.component = client[1]
|
|
||||||
button.bind(on_press=self.component_action)
|
|
||||||
button_layout.add_widget(button)
|
|
||||||
else:
|
else:
|
||||||
button_layout.add_widget(Label())
|
button_layout.add_widget(Label())
|
||||||
|
|
||||||
@@ -178,14 +218,29 @@ def run_gui():
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def component_action(button):
|
def component_action(button):
|
||||||
if button.component.type == Type.FUNC:
|
if button.component.func:
|
||||||
button.component.func()
|
button.component.func()
|
||||||
else:
|
else:
|
||||||
launch(get_exe(button.component), button.component.cli)
|
launch(get_exe(button.component), button.component.cli)
|
||||||
|
|
||||||
|
def _stop(self, *largs):
|
||||||
|
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
|
||||||
|
# Closing the window explicitly cleans it up.
|
||||||
|
self.root_window.close()
|
||||||
|
super()._stop(*largs)
|
||||||
|
|
||||||
Launcher().run()
|
Launcher().run()
|
||||||
|
|
||||||
|
|
||||||
|
def run_component(component: Component, *args):
|
||||||
|
if component.func:
|
||||||
|
component.func(*args)
|
||||||
|
elif component.script_name:
|
||||||
|
subprocess.run([*get_exe(component.script_name), *args])
|
||||||
|
else:
|
||||||
|
logging.warning(f"Component {component} does not appear to be executable.")
|
||||||
|
|
||||||
|
|
||||||
def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||||
if isinstance(args, argparse.Namespace):
|
if isinstance(args, argparse.Namespace):
|
||||||
args = {k: v for k, v in args._get_kwargs()}
|
args = {k: v for k, v in args._get_kwargs()}
|
||||||
@@ -193,24 +248,34 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
if "Patch|Game|Component" in args:
|
if "Patch|Game|Component" in args:
|
||||||
file, component, _ = identify(args["Patch|Game|Component"])
|
file, component = identify(args["Patch|Game|Component"])
|
||||||
if file:
|
if file:
|
||||||
args['file'] = file
|
args['file'] = file
|
||||||
if component:
|
if component:
|
||||||
args['component'] = component
|
args['component'] = component
|
||||||
|
if not component:
|
||||||
|
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
||||||
|
|
||||||
if 'file' in args:
|
if 'file' in args:
|
||||||
subprocess.run([*get_exe(args['component']), args['file'], *args['args']])
|
run_component(args["component"], args["file"], *args["args"])
|
||||||
elif 'component' in args:
|
elif 'component' in args:
|
||||||
subprocess.run([*get_exe(args['component']), *args['args']])
|
run_component(args["component"], *args["args"])
|
||||||
else:
|
else:
|
||||||
run_gui()
|
run_gui()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_logging('Launcher')
|
init_logging('Launcher')
|
||||||
|
Utils.freeze_support()
|
||||||
|
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||||
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
||||||
parser.add_argument('Patch|Game|Component', type=str, nargs='?',
|
parser.add_argument('Patch|Game|Component', type=str, nargs='?',
|
||||||
help="Pass either a patch file, a generated game or the name of a component to run.")
|
help="Pass either a patch file, a generated game or the name of a component to run.")
|
||||||
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
|
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
|
||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|
||||||
|
from worlds.LauncherComponents import processes
|
||||||
|
for process in processes:
|
||||||
|
# we await all child processes to close before we tear down the process host
|
||||||
|
# this makes it feel like each one is its own program, as the Launcher is closed now
|
||||||
|
process.join()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import typing
|
|||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
import struct
|
||||||
|
|
||||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||||
server_loop)
|
server_loop)
|
||||||
@@ -91,7 +91,7 @@ class LAClientConstants:
|
|||||||
# wLinkSendShopTarget = 0xDDFF
|
# wLinkSendShopTarget = 0xDDFF
|
||||||
|
|
||||||
|
|
||||||
wRecvIndex = 0xDDFE # 0xDB58
|
wRecvIndex = 0xDDFD # Two bytes
|
||||||
wCheckAddress = 0xC0FF - 0x4
|
wCheckAddress = 0xC0FF - 0x4
|
||||||
WRamCheckSize = 0x4
|
WRamCheckSize = 0x4
|
||||||
WRamSafetyValue = bytearray([0]*WRamCheckSize)
|
WRamSafetyValue = bytearray([0]*WRamCheckSize)
|
||||||
@@ -365,14 +365,13 @@ class LinksAwakeningClient():
|
|||||||
item_id, from_player])
|
item_id, from_player])
|
||||||
status |= 1
|
status |= 1
|
||||||
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
||||||
self.gameboy.write_memory(LAClientConstants.wRecvIndex, [next_index])
|
self.gameboy.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
|
||||||
|
|
||||||
async def wait_for_game_ready(self):
|
async def wait_for_game_ready(self):
|
||||||
logger.info("Waiting on game to be in valid state...")
|
logger.info("Waiting on game to be in valid state...")
|
||||||
while not await self.gameboy.check_safe_gameplay(throw=False):
|
while not await self.gameboy.check_safe_gameplay(throw=False):
|
||||||
pass
|
pass
|
||||||
logger.info("Ready!")
|
logger.info("Ready!")
|
||||||
last_index = 0
|
|
||||||
|
|
||||||
async def is_victory(self):
|
async def is_victory(self):
|
||||||
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
||||||
@@ -382,11 +381,6 @@ class LinksAwakeningClient():
|
|||||||
await self.item_tracker.readItems()
|
await self.item_tracker.readItems()
|
||||||
await self.gps_tracker.read_location()
|
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]
|
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
|
||||||
if self.deathlink_debounce and current_health != 0:
|
if self.deathlink_debounce and current_health != 0:
|
||||||
self.deathlink_debounce = False
|
self.deathlink_debounce = False
|
||||||
@@ -404,7 +398,7 @@ class LinksAwakeningClient():
|
|||||||
if await self.is_victory():
|
if await self.is_victory():
|
||||||
await win_cb()
|
await win_cb()
|
||||||
|
|
||||||
recv_index = (await self.gameboy.async_read_memory_safe(LAClientConstants.wRecvIndex))[0]
|
recv_index = struct.unpack(">H", self.gameboy.read_memory(LAClientConstants.wRecvIndex, 2))[0]
|
||||||
|
|
||||||
# Play back one at a time
|
# Play back one at a time
|
||||||
if recv_index in self.recvd_checks:
|
if recv_index in self.recvd_checks:
|
||||||
@@ -438,12 +432,16 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
found_checks = []
|
found_checks = []
|
||||||
last_resend = time.time()
|
last_resend = time.time()
|
||||||
|
|
||||||
magpie = MagpieBridge()
|
magpie_enabled = False
|
||||||
|
magpie = None
|
||||||
magpie_task = None
|
magpie_task = None
|
||||||
won = False
|
won = False
|
||||||
|
|
||||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
|
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
||||||
self.client = LinksAwakeningClient()
|
self.client = LinksAwakeningClient()
|
||||||
|
if magpie:
|
||||||
|
self.magpie_enabled = True
|
||||||
|
self.magpie = MagpieBridge()
|
||||||
super().__init__(server_address, password)
|
super().__init__(server_address, password)
|
||||||
|
|
||||||
def run_gui(self) -> None:
|
def run_gui(self) -> None:
|
||||||
@@ -462,16 +460,17 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
def build(self):
|
def build(self):
|
||||||
b = super().build()
|
b = super().build()
|
||||||
|
|
||||||
button = Button(text="", size=(30, 30), size_hint_x=None,
|
if self.ctx.magpie_enabled:
|
||||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
button = Button(text="", size=(30, 30), size_hint_x=None,
|
||||||
image = Image(size=(16, 16), texture=magpie_logo())
|
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||||
button.add_widget(image)
|
image = Image(size=(16, 16), texture=magpie_logo())
|
||||||
|
button.add_widget(image)
|
||||||
|
|
||||||
def set_center(_, center):
|
def set_center(_, center):
|
||||||
image.center = center
|
image.center = center
|
||||||
button.bind(center=set_center)
|
button.bind(center=set_center)
|
||||||
|
|
||||||
self.connect_layout.add_widget(button)
|
self.connect_layout.add_widget(button)
|
||||||
return b
|
return b
|
||||||
|
|
||||||
self.ui = LADXManager(self)
|
self.ui = LADXManager(self)
|
||||||
@@ -506,7 +505,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
def new_checks(self, item_ids, ladxr_ids):
|
def new_checks(self, item_ids, ladxr_ids):
|
||||||
self.found_checks += item_ids
|
self.found_checks += item_ids
|
||||||
create_task_log_exception(self.send_checks())
|
create_task_log_exception(self.send_checks())
|
||||||
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
if self.magpie_enabled:
|
||||||
|
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
||||||
|
|
||||||
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:
|
||||||
@@ -537,7 +537,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
async def deathlink():
|
async def deathlink():
|
||||||
await self.send_deathlink()
|
await self.send_deathlink()
|
||||||
|
|
||||||
self.magpie_task = asyncio.create_task(self.magpie.serve())
|
if self.magpie_enabled:
|
||||||
|
self.magpie_task = asyncio.create_task(self.magpie.serve())
|
||||||
|
|
||||||
# yield to allow UI to start
|
# yield to allow UI to start
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
@@ -558,9 +559,10 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
if self.last_resend + 5.0 < now:
|
if self.last_resend + 5.0 < now:
|
||||||
self.last_resend = now
|
self.last_resend = now
|
||||||
await self.send_checks()
|
await self.send_checks()
|
||||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
if self.magpie_enabled:
|
||||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
await self.magpie.send_gps(self.client.gps_tracker)
|
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||||
|
await self.magpie.send_gps(self.client.gps_tracker)
|
||||||
|
|
||||||
except GameboyException:
|
except GameboyException:
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
@@ -570,9 +572,11 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
async def main():
|
async def main():
|
||||||
parser = get_base_parser(description="Link's Awakening Client.")
|
parser = get_base_parser(description="Link's Awakening Client.")
|
||||||
parser.add_argument("--url", help="Archipelago connection url")
|
parser.add_argument("--url", help="Archipelago connection url")
|
||||||
|
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
|
||||||
|
|
||||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
help='Path to a .apladx Archipelago Binary Patch file')
|
help='Path to a .apladx Archipelago Binary Patch file')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
logger.info(args)
|
logger.info(args)
|
||||||
|
|
||||||
@@ -590,7 +594,7 @@ async def main():
|
|||||||
if url.password:
|
if url.password:
|
||||||
args.password = urllib.parse.unquote(url.password)
|
args.password = urllib.parse.unquote(url.password)
|
||||||
|
|
||||||
ctx = LinksAwakeningContext(args.connect, args.password)
|
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
|
||||||
|
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
|
|
||||||
|
|||||||
101
LttPAdjuster.py
@@ -44,7 +44,7 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
|||||||
return textwrap.dedent(action.help)
|
return textwrap.dedent(action.help)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def get_argparser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||||
|
|
||||||
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
|
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
|
||||||
@@ -85,9 +85,6 @@ def main():
|
|||||||
parser.add_argument('--ow_palettes', default='default',
|
parser.add_argument('--ow_palettes', default='default',
|
||||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||||
'sick'])
|
'sick'])
|
||||||
# parser.add_argument('--link_palettes', default='default',
|
|
||||||
# choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
|
||||||
# 'sick'])
|
|
||||||
parser.add_argument('--shield_palettes', default='default',
|
parser.add_argument('--shield_palettes', default='default',
|
||||||
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
|
||||||
'sick'])
|
'sick'])
|
||||||
@@ -107,8 +104,19 @@ def main():
|
|||||||
Alternatively, can be a ALttP Rom patched with a Link
|
Alternatively, can be a ALttP Rom patched with a Link
|
||||||
sprite that will be extracted.
|
sprite that will be extracted.
|
||||||
''')
|
''')
|
||||||
|
parser.add_argument('--oof', help='''\
|
||||||
|
Path to a sound effect to replace Link's "oof" sound.
|
||||||
|
Needs to be in a .brr format and have a length of no
|
||||||
|
more than 2673 bytes, created from a 16-bit signed PCM
|
||||||
|
.wav at 12khz. https://github.com/boldowa/snesbrr
|
||||||
|
''')
|
||||||
parser.add_argument('--names', default='', type=str)
|
parser.add_argument('--names', default='', type=str)
|
||||||
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = get_argparser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
args.music = not args.disablemusic
|
args.music = not args.disablemusic
|
||||||
# set up logger
|
# set up logger
|
||||||
@@ -126,6 +134,13 @@ def main():
|
|||||||
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
|
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
|
||||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if args.oof is not None and not os.path.isfile(args.oof):
|
||||||
|
input('Could not find oof sound effect at given location. \nPress Enter to exit.')
|
||||||
|
sys.exit(1)
|
||||||
|
if args.oof is not None and os.path.getsize(args.oof) > 2673:
|
||||||
|
input('"oof" sound effect cannot exceed 2673 bytes. \nPress Enter to exit.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
args, path = adjust(args=args)
|
args, path = adjust(args=args)
|
||||||
if isinstance(args.sprite, Sprite):
|
if isinstance(args.sprite, Sprite):
|
||||||
@@ -165,7 +180,7 @@ def adjust(args):
|
|||||||
world = getattr(args, "world")
|
world = getattr(args, "world")
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
|
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
|
||||||
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
|
args.sprite, args.oof, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
|
||||||
deathlink=args.deathlink, allowcollect=args.allowcollect)
|
deathlink=args.deathlink, allowcollect=args.allowcollect)
|
||||||
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
|
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
|
||||||
rom.write_to_file(path)
|
rom.write_to_file(path)
|
||||||
@@ -180,7 +195,7 @@ def adjustGUI():
|
|||||||
from tkinter import Tk, LEFT, BOTTOM, TOP, \
|
from tkinter import Tk, LEFT, BOTTOM, TOP, \
|
||||||
StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
|
StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from Main import __version__ as MWVersion
|
from Utils import __version__ as MWVersion
|
||||||
adjustWindow = Tk()
|
adjustWindow = Tk()
|
||||||
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
|
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
|
||||||
set_icon(adjustWindow)
|
set_icon(adjustWindow)
|
||||||
@@ -227,6 +242,7 @@ def adjustGUI():
|
|||||||
guiargs.sprite = rom_vars.sprite
|
guiargs.sprite = rom_vars.sprite
|
||||||
if rom_vars.sprite_pool:
|
if rom_vars.sprite_pool:
|
||||||
guiargs.world = AdjusterWorld(rom_vars.sprite_pool)
|
guiargs.world = AdjusterWorld(rom_vars.sprite_pool)
|
||||||
|
guiargs.oof = rom_vars.oof
|
||||||
|
|
||||||
try:
|
try:
|
||||||
guiargs, path = adjust(args=guiargs)
|
guiargs, path = adjust(args=guiargs)
|
||||||
@@ -265,6 +281,7 @@ def adjustGUI():
|
|||||||
else:
|
else:
|
||||||
guiargs.sprite = rom_vars.sprite
|
guiargs.sprite = rom_vars.sprite
|
||||||
guiargs.sprite_pool = rom_vars.sprite_pool
|
guiargs.sprite_pool = rom_vars.sprite_pool
|
||||||
|
guiargs.oof = rom_vars.oof
|
||||||
persistent_store("adjuster", GAME_ALTTP, guiargs)
|
persistent_store("adjuster", GAME_ALTTP, guiargs)
|
||||||
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
|
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
|
||||||
|
|
||||||
@@ -481,6 +498,36 @@ class BackgroundTaskProgressNullWindow(BackgroundTask):
|
|||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
|
class AttachTooltip(object):
|
||||||
|
|
||||||
|
def __init__(self, parent, text):
|
||||||
|
self._parent = parent
|
||||||
|
self._text = text
|
||||||
|
self._window = None
|
||||||
|
parent.bind('<Enter>', lambda event : self.show())
|
||||||
|
parent.bind('<Leave>', lambda event : self.hide())
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
if self._window or not self._text:
|
||||||
|
return
|
||||||
|
self._window = Toplevel(self._parent)
|
||||||
|
#remove window bar controls
|
||||||
|
self._window.wm_overrideredirect(1)
|
||||||
|
#adjust positioning
|
||||||
|
x, y, *_ = self._parent.bbox("insert")
|
||||||
|
x = x + self._parent.winfo_rootx() + 20
|
||||||
|
y = y + self._parent.winfo_rooty() + 20
|
||||||
|
self._window.wm_geometry("+{0}+{1}".format(x,y))
|
||||||
|
#show text
|
||||||
|
label = Label(self._window, text=self._text, justify=LEFT)
|
||||||
|
label.pack(ipadx=1)
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
if self._window:
|
||||||
|
self._window.destroy()
|
||||||
|
self._window = None
|
||||||
|
|
||||||
|
|
||||||
def get_rom_frame(parent=None):
|
def get_rom_frame(parent=None):
|
||||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||||
if not adjuster_settings:
|
if not adjuster_settings:
|
||||||
@@ -522,6 +569,7 @@ def get_rom_options_frame(parent=None):
|
|||||||
"reduceflashing": True,
|
"reduceflashing": True,
|
||||||
"deathlink": False,
|
"deathlink": False,
|
||||||
"sprite": None,
|
"sprite": None,
|
||||||
|
"oof": None,
|
||||||
"quickswap": True,
|
"quickswap": True,
|
||||||
"menuspeed": 'normal',
|
"menuspeed": 'normal',
|
||||||
"heartcolor": 'red',
|
"heartcolor": 'red',
|
||||||
@@ -598,12 +646,50 @@ def get_rom_options_frame(parent=None):
|
|||||||
spriteEntry.pack(side=LEFT)
|
spriteEntry.pack(side=LEFT)
|
||||||
spriteSelectButton.pack(side=LEFT)
|
spriteSelectButton.pack(side=LEFT)
|
||||||
|
|
||||||
|
oofDialogFrame = Frame(romOptionsFrame)
|
||||||
|
oofDialogFrame.grid(row=1, column=1)
|
||||||
|
baseOofLabel = Label(oofDialogFrame, text='"OOF" Sound:')
|
||||||
|
|
||||||
|
vars.oofNameVar = StringVar()
|
||||||
|
vars.oof = adjuster_settings.oof
|
||||||
|
|
||||||
|
def set_oof(oof_param):
|
||||||
|
nonlocal vars
|
||||||
|
if isinstance(oof_param, str) and os.path.isfile(oof_param) and os.path.getsize(oof_param) <= 2673:
|
||||||
|
vars.oof = oof_param
|
||||||
|
vars.oofNameVar.set(oof_param.rsplit('/',1)[-1])
|
||||||
|
else:
|
||||||
|
vars.oof = None
|
||||||
|
vars.oofNameVar.set('(unchanged)')
|
||||||
|
|
||||||
|
set_oof(adjuster_settings.oof)
|
||||||
|
oofEntry = Label(oofDialogFrame, textvariable=vars.oofNameVar)
|
||||||
|
|
||||||
|
def OofSelect():
|
||||||
|
nonlocal vars
|
||||||
|
oof_file = filedialog.askopenfilename(
|
||||||
|
filetypes=[("BRR files", ".brr"),
|
||||||
|
("All Files", "*")])
|
||||||
|
try:
|
||||||
|
set_oof(oof_file)
|
||||||
|
except Exception:
|
||||||
|
set_oof(None)
|
||||||
|
|
||||||
|
oofSelectButton = Button(oofDialogFrame, text='...', command=OofSelect)
|
||||||
|
AttachTooltip(oofSelectButton,
|
||||||
|
text="Select a .brr file no more than 2673 bytes.\n" + \
|
||||||
|
"This can be created from a <=0.394s 16-bit signed PCM .wav file at 12khz using snesbrr.")
|
||||||
|
|
||||||
|
baseOofLabel.pack(side=LEFT)
|
||||||
|
oofEntry.pack(side=LEFT)
|
||||||
|
oofSelectButton.pack(side=LEFT)
|
||||||
|
|
||||||
vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
|
vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
|
||||||
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
|
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
|
||||||
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
|
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
|
||||||
|
|
||||||
menuspeedFrame = Frame(romOptionsFrame)
|
menuspeedFrame = Frame(romOptionsFrame)
|
||||||
menuspeedFrame.grid(row=1, column=1, sticky=E)
|
menuspeedFrame.grid(row=6, column=1, sticky=E)
|
||||||
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
|
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
|
||||||
menuspeedLabel.pack(side=LEFT)
|
menuspeedLabel.pack(side=LEFT)
|
||||||
vars.menuspeedVar = StringVar()
|
vars.menuspeedVar = StringVar()
|
||||||
@@ -1056,7 +1142,6 @@ class SpriteSelector():
|
|||||||
def custom_sprite_dir(self):
|
def custom_sprite_dir(self):
|
||||||
return user_path("data", "sprites", "custom")
|
return user_path("data", "sprites", "custom")
|
||||||
|
|
||||||
|
|
||||||
def get_image_for_sprite(sprite, gif_only: bool = False):
|
def get_image_for_sprite(sprite, gif_only: bool = False):
|
||||||
if not sprite.valid:
|
if not sprite.valid:
|
||||||
return None
|
return None
|
||||||
|
|||||||
372
MMBN3Client.py
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
import asyncio
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import multiprocessing
|
||||||
|
import subprocess
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
|
||||||
|
import bsdiff4
|
||||||
|
|
||||||
|
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||||
|
ClientCommandProcessor, logger, get_base_parser
|
||||||
|
import Utils
|
||||||
|
from NetUtils import ClientStatus
|
||||||
|
from worlds.mmbn3.Items import items_by_id
|
||||||
|
from worlds.mmbn3.Rom import get_base_rom_path
|
||||||
|
from worlds.mmbn3.Locations import all_locations, scoutable_locations
|
||||||
|
|
||||||
|
SYSTEM_MESSAGE_ID = 0
|
||||||
|
|
||||||
|
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_mmbn3.lua"
|
||||||
|
CONNECTION_REFUSED_STATUS = \
|
||||||
|
"Connection refused. Please start your emulator and make sure connector_mmbn3.lua is running"
|
||||||
|
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_mmbn3.lua"
|
||||||
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||||
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||||
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||||
|
CONNECTION_INCORRECT_ROM = "Supplied Base Rom does not match US GBA Blue Version. Please provide the correct ROM version"
|
||||||
|
|
||||||
|
script_version: int = 2
|
||||||
|
|
||||||
|
debugEnabled = False
|
||||||
|
locations_checked = []
|
||||||
|
items_sent = []
|
||||||
|
itemIndex = 1
|
||||||
|
|
||||||
|
CHECKSUM_BLUE = "6fe31df0144759b34ad666badaacc442"
|
||||||
|
|
||||||
|
|
||||||
|
class MMBN3CommandProcessor(ClientCommandProcessor):
|
||||||
|
def __init__(self, ctx):
|
||||||
|
super().__init__(ctx)
|
||||||
|
|
||||||
|
def _cmd_gba(self):
|
||||||
|
"""Check GBA Connection State"""
|
||||||
|
if isinstance(self.ctx, MMBN3Context):
|
||||||
|
logger.info(f"GBA Status: {self.ctx.gba_status}")
|
||||||
|
|
||||||
|
def _cmd_debug(self):
|
||||||
|
"""Toggle the Debug Text overlay in ROM"""
|
||||||
|
global debugEnabled
|
||||||
|
debugEnabled = not debugEnabled
|
||||||
|
logger.info("Debug Overlay Enabled" if debugEnabled else "Debug Overlay Disabled")
|
||||||
|
|
||||||
|
|
||||||
|
class MMBN3Context(CommonContext):
|
||||||
|
command_processor = MMBN3CommandProcessor
|
||||||
|
game = "MegaMan Battle Network 3"
|
||||||
|
items_handling = 0b001 # full local
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super().__init__(server_address, password)
|
||||||
|
self.gba_streams: (StreamReader, StreamWriter) = None
|
||||||
|
self.gba_sync_task = None
|
||||||
|
self.gba_status = CONNECTION_INITIAL_STATUS
|
||||||
|
self.awaiting_rom = False
|
||||||
|
self.location_table = {}
|
||||||
|
self.version_warning = False
|
||||||
|
self.auth_name = None
|
||||||
|
self.slot_data = dict()
|
||||||
|
self.patching_error = False
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super(MMBN3Context, self).server_auth(password_requested)
|
||||||
|
|
||||||
|
if self.auth_name is None:
|
||||||
|
self.awaiting_rom = True
|
||||||
|
logger.info("No ROM detected, awaiting conection to Bizhawk to authenticate to the multiworld server")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Attempting to decode from ROM... ")
|
||||||
|
self.awaiting_rom = False
|
||||||
|
self.auth = self.auth_name.decode("utf8").replace('\x00', '')
|
||||||
|
logger.info("Connecting as "+self.auth)
|
||||||
|
await self.send_connect(name=self.auth)
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
from kvui import GameManager
|
||||||
|
|
||||||
|
class MMBN3Manager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
|
base_title = "Archipelago MegaMan Battle Network 3 Client"
|
||||||
|
|
||||||
|
self.ui = MMBN3Manager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd == 'Connected':
|
||||||
|
self.slot_data = args.get("slot_data", {})
|
||||||
|
print(self.slot_data)
|
||||||
|
|
||||||
|
class ItemInfo:
|
||||||
|
id = 0x00
|
||||||
|
sender = ""
|
||||||
|
type = ""
|
||||||
|
count = 1
|
||||||
|
itemName = "Unknown"
|
||||||
|
itemID = 0x00 # Item ID, Chip ID, etc.
|
||||||
|
subItemID = 0x00 # Code for chips, color for programs
|
||||||
|
itemIndex = 1
|
||||||
|
|
||||||
|
def __init__(self, id, sender, type):
|
||||||
|
self.id = id
|
||||||
|
self.sender = sender
|
||||||
|
self.type = type
|
||||||
|
|
||||||
|
def get_json(self):
|
||||||
|
json_data = {
|
||||||
|
"id": self.id,
|
||||||
|
"sender": self.sender,
|
||||||
|
"type": self.type,
|
||||||
|
"itemName": self.itemName,
|
||||||
|
"itemID": self.itemID,
|
||||||
|
"subItemID": self.subItemID,
|
||||||
|
"count": self.count,
|
||||||
|
"itemIndex": self.itemIndex
|
||||||
|
}
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_payload(ctx: MMBN3Context):
|
||||||
|
global debugEnabled
|
||||||
|
|
||||||
|
items_sent = []
|
||||||
|
for i, item in enumerate(ctx.items_received):
|
||||||
|
item_data = items_by_id[item.item]
|
||||||
|
new_item = ItemInfo(i, ctx.player_names[item.player], item_data.type)
|
||||||
|
new_item.itemIndex = i+1
|
||||||
|
new_item.itemName = item_data.itemName
|
||||||
|
new_item.type = item_data.type
|
||||||
|
new_item.itemID = item_data.itemID
|
||||||
|
new_item.subItemID = item_data.subItemID
|
||||||
|
new_item.count = item_data.count
|
||||||
|
items_sent.append(new_item)
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"items": [item.get_json() for item in items_sent],
|
||||||
|
"debug": debugEnabled
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_payload(payload: dict, ctx: MMBN3Context, force: bool):
|
||||||
|
# Game completion handling
|
||||||
|
if payload["gameComplete"] and not ctx.finished_game:
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "StatusUpdate",
|
||||||
|
"status": ClientStatus.CLIENT_GOAL
|
||||||
|
}])
|
||||||
|
ctx.finished_game = True
|
||||||
|
|
||||||
|
# Locations handling
|
||||||
|
if ctx.location_table != payload["locations"]:
|
||||||
|
ctx.location_table = payload["locations"]
|
||||||
|
locs = [loc.id for loc in all_locations
|
||||||
|
if check_location_packet(loc, ctx.location_table)]
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationChecks",
|
||||||
|
"locations": locs
|
||||||
|
}])
|
||||||
|
|
||||||
|
# If trade hinting is enabled, send scout checks
|
||||||
|
if ctx.slot_data.get("trade_quest_hinting", 0) == 2:
|
||||||
|
scouted_locs = [loc.id for loc in scoutable_locations
|
||||||
|
if check_location_scouted(loc, payload["locations"])]
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationScouts",
|
||||||
|
"locations": scouted_locs,
|
||||||
|
"create_as_hint": 2
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
|
def check_location_packet(location, memory):
|
||||||
|
if len(memory) == 0:
|
||||||
|
return False
|
||||||
|
# Our keys have to be strings to come through the JSON lua plugin so we have to turn our memory address into a string as well
|
||||||
|
location_key = hex(location.flag_byte)[2:]
|
||||||
|
byte = memory.get(location_key)
|
||||||
|
if byte is not None:
|
||||||
|
return byte & location.flag_mask
|
||||||
|
|
||||||
|
|
||||||
|
def check_location_scouted(location, memory):
|
||||||
|
if len(memory) == 0:
|
||||||
|
return False
|
||||||
|
location_key = hex(location.hint_flag)[2:]
|
||||||
|
byte = memory.get(location_key)
|
||||||
|
if byte is not None:
|
||||||
|
return byte & location.hint_flag_mask
|
||||||
|
|
||||||
|
|
||||||
|
async def gba_sync_task(ctx: MMBN3Context):
|
||||||
|
logger.info("Starting GBA connector. Use /gba for status information.")
|
||||||
|
if ctx.patching_error:
|
||||||
|
logger.error('Unable to Patch ROM. No ROM provided or ROM does not match US GBA Blue Version.')
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
error_status = None
|
||||||
|
if ctx.gba_streams:
|
||||||
|
(reader, writer) = ctx.gba_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 four fields
|
||||||
|
# 1. str: player name (always)
|
||||||
|
# 2. int: script version (always)
|
||||||
|
# 3. dict[str, byte]: value of location's memory byte
|
||||||
|
# 4. bool: whether the game currently registers as complete
|
||||||
|
data = await asyncio.wait_for(reader.readline(), timeout=10)
|
||||||
|
data_decoded = json.loads(data.decode())
|
||||||
|
reported_version = data_decoded.get("scriptVersion", 0)
|
||||||
|
if reported_version >= script_version:
|
||||||
|
if ctx.game is not None and "locations" in data_decoded:
|
||||||
|
# Not just a keep alive ping, parse
|
||||||
|
asyncio.create_task((parse_payload(data_decoded, ctx, False)))
|
||||||
|
if not ctx.auth:
|
||||||
|
ctx.auth_name = bytes(data_decoded["playerName"])
|
||||||
|
|
||||||
|
if ctx.awaiting_rom:
|
||||||
|
logger.info("Awaiting data from ROM...")
|
||||||
|
await ctx.server_auth(False)
|
||||||
|
else:
|
||||||
|
if not ctx.version_warning:
|
||||||
|
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}."
|
||||||
|
"Please update to the latest version."
|
||||||
|
"Your connection to the Archipelago server will not be accepted.")
|
||||||
|
ctx.version_warning = True
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.debug("Read Timed Out, Reconnecting")
|
||||||
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.gba_streams = None
|
||||||
|
except ConnectionResetError:
|
||||||
|
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.gba_streams = None
|
||||||
|
except TimeoutError:
|
||||||
|
logger.debug("Connection Timed Out, Reconnecting")
|
||||||
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.gba_streams = None
|
||||||
|
except ConnectionResetError:
|
||||||
|
logger.debug("Connection Lost, Reconnecting")
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
writer.close()
|
||||||
|
ctx.gba_streams = None
|
||||||
|
if ctx.gba_status == CONNECTION_TENTATIVE_STATUS:
|
||||||
|
if not error_status:
|
||||||
|
logger.info("Successfully Connected to GBA")
|
||||||
|
ctx.gba_status = CONNECTION_CONNECTED_STATUS
|
||||||
|
else:
|
||||||
|
ctx.gba_status = f"Was tentatively connected but error occurred: {error_status}"
|
||||||
|
elif error_status:
|
||||||
|
ctx.gba_status = error_status
|
||||||
|
logger.info("Lost connection to GBA and attempting to reconnect. Use /gba for status updates")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
logger.debug("Attempting to connect to GBA")
|
||||||
|
ctx.gba_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 28922), timeout=10)
|
||||||
|
ctx.gba_status = CONNECTION_TENTATIVE_STATUS
|
||||||
|
except TimeoutError:
|
||||||
|
logger.debug("Connection Timed Out, Trying Again")
|
||||||
|
ctx.gba_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
|
continue
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
logger.debug("Connection Refused, Trying Again")
|
||||||
|
ctx.gba_status = CONNECTION_REFUSED_STATUS
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
async def run_game(romfile):
|
||||||
|
options = Utils.get_options().get("mmbn3_options", None)
|
||||||
|
if options is None:
|
||||||
|
auto_start = True
|
||||||
|
else:
|
||||||
|
auto_start = options.get("rom_start", True)
|
||||||
|
if auto_start:
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open(romfile)
|
||||||
|
elif os.path.isfile(auto_start):
|
||||||
|
subprocess.Popen([auto_start, romfile],
|
||||||
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
|
||||||
|
async def patch_and_run_game(apmmbn3_file):
|
||||||
|
base_name = os.path.splitext(apmmbn3_file)[0]
|
||||||
|
|
||||||
|
with zipfile.ZipFile(apmmbn3_file, 'r') as patch_archive:
|
||||||
|
try:
|
||||||
|
with patch_archive.open("delta.bsdiff4", 'r') as stream:
|
||||||
|
patch_data = stream.read()
|
||||||
|
except KeyError:
|
||||||
|
raise FileNotFoundError("Patch file missing from archive.")
|
||||||
|
rom_file = get_base_rom_path()
|
||||||
|
|
||||||
|
with open(rom_file, 'rb') as rom:
|
||||||
|
rom_bytes = rom.read()
|
||||||
|
|
||||||
|
patched_bytes = bsdiff4.patch(rom_bytes, patch_data)
|
||||||
|
patched_rom_file = base_name+".gba"
|
||||||
|
with open(patched_rom_file, 'wb') as patched_rom:
|
||||||
|
patched_rom.write(patched_bytes)
|
||||||
|
|
||||||
|
asyncio.create_task(run_game(patched_rom_file))
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_checksum():
|
||||||
|
rom_file = get_base_rom_path()
|
||||||
|
if not os.path.exists(rom_file):
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(rom_file, 'rb') as rom:
|
||||||
|
rom_bytes = rom.read()
|
||||||
|
|
||||||
|
basemd5 = hashlib.md5()
|
||||||
|
basemd5.update(rom_bytes)
|
||||||
|
return CHECKSUM_BLUE == basemd5.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
Utils.init_logging("MMBN3Client")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
multiprocessing.freeze_support()
|
||||||
|
parser = get_base_parser()
|
||||||
|
parser.add_argument("patch_file", default="", type=str, nargs="?",
|
||||||
|
help="Path to an APMMBN3 file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
checksum_matches = confirm_checksum()
|
||||||
|
if checksum_matches:
|
||||||
|
if args.patch_file:
|
||||||
|
asyncio.create_task(patch_and_run_game(args.patch_file))
|
||||||
|
|
||||||
|
ctx = MMBN3Context(args.connect, args.password)
|
||||||
|
if not checksum_matches:
|
||||||
|
ctx.patching_error = True
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
|
||||||
|
ctx.gba_sync_task = asyncio.create_task(gba_sync_task(ctx), name="GBA Sync")
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
if ctx.gba_sync_task:
|
||||||
|
await ctx.gba_sync_task
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
colorama.deinit()
|
||||||
64
Main.py
@@ -1,23 +1,26 @@
|
|||||||
import collections
|
import collections
|
||||||
|
import concurrent.futures
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import zlib
|
|
||||||
import concurrent.futures
|
|
||||||
import pickle
|
import pickle
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Dict, List, Tuple, Optional, Set
|
import zlib
|
||||||
|
from typing import Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
from BaseClasses import Item, MultiWorld, CollectionState, Region, LocationProgressType, Location
|
|
||||||
import worlds
|
import worlds
|
||||||
from worlds.alttp.SubClasses import LTTPRegionType
|
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
|
||||||
from worlds.alttp.Regions import is_main_entrance
|
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
|
||||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
from Options import StartInventoryPool
|
||||||
from worlds.alttp.Shops import FillDisabledShopSlots
|
from Utils import __version__, get_options, output_path, version_tuple
|
||||||
from Utils import output_path, get_options, __version__, version_tuple
|
|
||||||
from worlds.generic.Rules import locality_rules, exclusion_rules
|
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
from worlds.alttp.Regions import is_main_entrance
|
||||||
|
from worlds.alttp.Shops import FillDisabledShopSlots
|
||||||
|
from worlds.alttp.SubClasses import LTTPRegionType
|
||||||
|
from worlds.generic.Rules import exclusion_rules, locality_rules
|
||||||
|
|
||||||
|
__all__ = ["main"]
|
||||||
|
|
||||||
ordered_areas = (
|
ordered_areas = (
|
||||||
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||||
@@ -116,6 +119,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
world.push_precollected(world.create_item(item_name, player))
|
world.push_precollected(world.create_item(item_name, player))
|
||||||
|
|
||||||
|
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
|
||||||
|
for _ in range(count):
|
||||||
|
world.push_precollected(world.create_item(item_name, player))
|
||||||
|
|
||||||
logger.info('Creating World.')
|
logger.info('Creating World.')
|
||||||
AutoWorld.call_all(world, "create_regions")
|
AutoWorld.call_all(world, "create_regions")
|
||||||
|
|
||||||
@@ -149,6 +156,37 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
|
|
||||||
AutoWorld.call_all(world, "generate_basic")
|
AutoWorld.call_all(world, "generate_basic")
|
||||||
|
|
||||||
|
# remove starting inventory from pool items.
|
||||||
|
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||||
|
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
|
||||||
|
new_items: List[Item] = []
|
||||||
|
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||||
|
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
|
||||||
|
for player, items in depletion_pool.items():
|
||||||
|
player_world: AutoWorld.World = world.worlds[player]
|
||||||
|
for count in items.values():
|
||||||
|
new_items.append(player_world.create_filler())
|
||||||
|
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
||||||
|
for i, item in enumerate(world.itempool):
|
||||||
|
if depletion_pool[item.player].get(item.name, 0):
|
||||||
|
target -= 1
|
||||||
|
depletion_pool[item.player][item.name] -= 1
|
||||||
|
# quick abort if we have found all items
|
||||||
|
if not target:
|
||||||
|
new_items.extend(world.itempool[i+1:])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
new_items.append(item)
|
||||||
|
|
||||||
|
# leftovers?
|
||||||
|
if target:
|
||||||
|
for player, remaining_items in depletion_pool.items():
|
||||||
|
remaining_items = {name: count for name, count in remaining_items.items() if count}
|
||||||
|
if remaining_items:
|
||||||
|
raise Exception(f"{world.get_player_name(player)}"
|
||||||
|
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||||
|
world.itempool[:] = new_items
|
||||||
|
|
||||||
# temporary home for item links, should be moved out of Main
|
# temporary home for item links, should be moved out of Main
|
||||||
for group_id, group in world.groups.items():
|
for group_id, group in world.groups.items():
|
||||||
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
||||||
@@ -247,8 +285,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
|
|
||||||
AutoWorld.call_all(world, 'post_fill')
|
AutoWorld.call_all(world, 'post_fill')
|
||||||
|
|
||||||
if world.players > 1:
|
if world.players > 1 and not args.skip_prog_balancing:
|
||||||
balance_multiworld_progression(world)
|
balance_multiworld_progression(world)
|
||||||
|
else:
|
||||||
|
logger.info("Progression balancing skipped.")
|
||||||
|
|
||||||
logger.info(f'Beginning output...')
|
logger.info(f'Beginning output...')
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import multiprocessing
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
local_dir = os.path.dirname(__file__)
|
local_dir = os.path.dirname(__file__)
|
||||||
@@ -9,7 +10,8 @@ requirements_files = {os.path.join(local_dir, 'requirements.txt')}
|
|||||||
if sys.version_info < (3, 8, 6):
|
if sys.version_info < (3, 8, 6):
|
||||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
||||||
|
|
||||||
update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
|
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||||
|
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
|
||||||
|
|
||||||
if not update_ran:
|
if not update_ran:
|
||||||
for entry in os.scandir(os.path.join(local_dir, "worlds")):
|
for entry in os.scandir(os.path.join(local_dir, "worlds")):
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
import functools
|
|
||||||
import logging
|
|
||||||
import zlib
|
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
@@ -162,7 +159,7 @@ class Context:
|
|||||||
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]
|
slot_info: typing.Dict[int, NetworkSlot]
|
||||||
|
generator_version = Version(0, 0, 0)
|
||||||
checksums: typing.Dict[str, str]
|
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]]]
|
||||||
@@ -226,7 +223,7 @@ class Context:
|
|||||||
self.save_dirty = False
|
self.save_dirty = False
|
||||||
self.tags = ['AP']
|
self.tags = ['AP']
|
||||||
self.games: typing.Dict[int, str] = {}
|
self.games: typing.Dict[int, str] = {}
|
||||||
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
self.minimum_client_versions: typing.Dict[int, Version] = {}
|
||||||
self.seed_name = ""
|
self.seed_name = ""
|
||||||
self.groups = {}
|
self.groups = {}
|
||||||
self.group_collected: typing.Dict[int, typing.Set[int]] = {}
|
self.group_collected: typing.Dict[int, typing.Set[int]] = {}
|
||||||
@@ -260,7 +257,8 @@ class Context:
|
|||||||
|
|
||||||
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"]
|
if "checksum" in game_package:
|
||||||
|
self.checksums[game_name] = game_package["checksum"]
|
||||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
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():
|
||||||
@@ -268,7 +266,7 @@ class Context:
|
|||||||
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] = \
|
self.all_location_and_group_names[game_name] = \
|
||||||
set(game_package["location_name_to_id"]) | set(self.location_name_groups[game_name])
|
set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(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
|
||||||
@@ -356,9 +354,7 @@ class Context:
|
|||||||
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
|
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
|
||||||
for text in texts]))
|
for text in texts]))
|
||||||
|
|
||||||
|
|
||||||
# loading
|
# loading
|
||||||
|
|
||||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||||
if multidatapath.lower().endswith(".zip"):
|
if multidatapath.lower().endswith(".zip"):
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -385,15 +381,17 @@ class Context:
|
|||||||
|
|
||||||
def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any],
|
def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any],
|
||||||
use_embedded_server_options: bool):
|
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 > version_tuple:
|
||||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
||||||
f"however this server is of version {Utils.version_tuple}")
|
f"however this server is of version {version_tuple}")
|
||||||
|
self.generator_version = Version(*decoded_obj["version"])
|
||||||
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||||
self.minimum_client_versions = {}
|
self.minimum_client_versions = {}
|
||||||
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(Version(*version), min_client_version)
|
||||||
|
|
||||||
self.slot_info = decoded_obj["slot_info"]
|
self.slot_info = decoded_obj["slot_info"]
|
||||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||||
@@ -446,9 +444,10 @@ class Context:
|
|||||||
logging.info(f"Loading embedded data package for game {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"]
|
||||||
self.location_name_groups[game_name] = data["location_name_groups"]
|
if "location_name_groups" in data:
|
||||||
|
self.location_name_groups[game_name] = data["location_name_groups"]
|
||||||
|
del data["location_name_groups"]
|
||||||
del data["item_name_groups"] # remove from data package, but keep in self.item_name_groups
|
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]
|
||||||
@@ -544,7 +543,7 @@ class Context:
|
|||||||
"stored_data": self.stored_data,
|
"stored_data": self.stored_data,
|
||||||
"game_options": {"hint_cost": self.hint_cost, "location_check_points": self.location_check_points,
|
"game_options": {"hint_cost": self.hint_cost, "location_check_points": self.location_check_points,
|
||||||
"server_password": self.server_password, "password": self.password,
|
"server_password": self.server_password, "password": self.password,
|
||||||
"forfeit_mode": self.release_mode, "release_mode": self.release_mode, # TODO remove forfeit_mode around 0.4
|
"release_mode": self.release_mode,
|
||||||
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
|
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
|
||||||
"item_cheat": self.item_cheat, "compatibility": self.compatibility}
|
"item_cheat": self.item_cheat, "compatibility": self.compatibility}
|
||||||
|
|
||||||
@@ -700,6 +699,10 @@ class Context:
|
|||||||
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||||
if targets:
|
if targets:
|
||||||
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
|
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
|
||||||
|
self.broadcast(self.clients[team][slot], [{
|
||||||
|
"cmd": "RoomUpdate",
|
||||||
|
"hint_points": get_slot_points(self, team, slot)
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
def update_aliases(ctx: Context, team: int):
|
def update_aliases(ctx: Context, team: int):
|
||||||
@@ -754,14 +757,15 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
# 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,
|
||||||
'version': Utils.version_tuple,
|
'version': version_tuple,
|
||||||
|
'generator_version': ctx.generator_version,
|
||||||
'permissions': get_permissions(ctx),
|
'permissions': get_permissions(ctx),
|
||||||
'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() if game in games},
|
in ctx.gamespackage.items() if game in games},
|
||||||
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
|
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
|
||||||
in ctx.gamespackage.items() if game in games},
|
in ctx.gamespackage.items() if game in games and "checksum" in game_data},
|
||||||
'seed_name': ctx.seed_name,
|
'seed_name': ctx.seed_name,
|
||||||
'time': time.time(),
|
'time': time.time(),
|
||||||
}])
|
}])
|
||||||
@@ -769,7 +773,6 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
|
|
||||||
def get_permissions(ctx) -> typing.Dict[str, Permission]:
|
def get_permissions(ctx) -> typing.Dict[str, Permission]:
|
||||||
return {
|
return {
|
||||||
"forfeit": Permission.from_text(ctx.release_mode), # TODO remove around 0.4
|
|
||||||
"release": Permission.from_text(ctx.release_mode),
|
"release": Permission.from_text(ctx.release_mode),
|
||||||
"remaining": Permission.from_text(ctx.remaining_mode),
|
"remaining": Permission.from_text(ctx.remaining_mode),
|
||||||
"collect": Permission.from_text(ctx.collect_mode)
|
"collect": Permission.from_text(ctx.collect_mode)
|
||||||
@@ -1327,27 +1330,41 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
"Sorry, !remaining requires you to have beaten the game on this server")
|
"Sorry, !remaining requires you to have beaten the game on this server")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cmd_missing(self) -> bool:
|
def _cmd_missing(self, filter_text="") -> bool:
|
||||||
"""List all missing location checks from the server's perspective"""
|
"""List all missing location checks from the server's perspective.
|
||||||
|
Can be given text, which will be used as filter."""
|
||||||
|
|
||||||
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
|
names = [self.ctx.location_names[location] for location in locations]
|
||||||
texts.append(f"Found {len(locations)} missing location checks")
|
if filter_text:
|
||||||
|
names = [name for name in names if filter_text in name]
|
||||||
|
texts = [f'Missing: {name}' for name in names]
|
||||||
|
if filter_text:
|
||||||
|
texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.")
|
||||||
|
else:
|
||||||
|
texts.append(f"Found {len(locations)} missing location checks")
|
||||||
self.output_multiple(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
|
||||||
|
|
||||||
def _cmd_checked(self) -> bool:
|
def _cmd_checked(self, filter_text="") -> bool:
|
||||||
"""List all done location checks from the server's perspective"""
|
"""List all done location checks from the server's perspective.
|
||||||
|
Can be given text, which will be used as filter."""
|
||||||
|
|
||||||
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
|
names = [self.ctx.location_names[location] for location in locations]
|
||||||
texts.append(f"Found {len(locations)} done location checks")
|
if filter_text:
|
||||||
|
names = [name for name in names if filter_text in name]
|
||||||
|
texts = [f'Checked: {name}' for name in names]
|
||||||
|
if filter_text:
|
||||||
|
texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.")
|
||||||
|
else:
|
||||||
|
texts.append(f"Found {len(locations)} done location checks")
|
||||||
self.output_multiple(texts)
|
self.output_multiple(texts)
|
||||||
else:
|
else:
|
||||||
self.output("No done location checks found.")
|
self.output("No done location checks found.")
|
||||||
@@ -1626,7 +1643,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
"players": ctx.get_players_package(),
|
"players": ctx.get_players_package(),
|
||||||
"missing_locations": get_missing_checks(ctx, team, slot),
|
"missing_locations": get_missing_checks(ctx, team, slot),
|
||||||
"checked_locations": get_checked_checks(ctx, team, slot),
|
"checked_locations": get_checked_checks(ctx, team, slot),
|
||||||
"slot_info": ctx.slot_info
|
"slot_info": ctx.slot_info,
|
||||||
|
"hint_points": get_slot_points(ctx, team, slot),
|
||||||
}
|
}
|
||||||
reply = [connected_packet]
|
reply = [connected_packet]
|
||||||
start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory)
|
start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory)
|
||||||
@@ -1728,6 +1746,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
|
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
|
||||||
locs.append(NetworkItem(target_item, location, target_player, flags))
|
locs.append(NetworkItem(target_item, location, target_player, flags))
|
||||||
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
|
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
|
||||||
|
if locs and create_as_hint:
|
||||||
|
ctx.save()
|
||||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||||
|
|
||||||
elif cmd == 'StatusUpdate':
|
elif cmd == 'StatusUpdate':
|
||||||
@@ -1786,6 +1806,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
targets.add(client)
|
targets.add(client)
|
||||||
if targets:
|
if targets:
|
||||||
ctx.broadcast(targets, [args])
|
ctx.broadcast(targets, [args])
|
||||||
|
ctx.save()
|
||||||
|
|
||||||
elif cmd == "SetNotify":
|
elif cmd == "SetNotify":
|
||||||
if "keys" not in args or type(args["keys"]) != list:
|
if "keys" not in args or type(args["keys"]) != list:
|
||||||
@@ -1803,6 +1824,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
|||||||
ctx.on_goal_achieved(client)
|
ctx.on_goal_achieved(client)
|
||||||
|
|
||||||
ctx.client_game_state[client.team, client.slot] = new_status
|
ctx.client_game_state[client.team, client.slot] = new_status
|
||||||
|
ctx.save()
|
||||||
|
|
||||||
|
|
||||||
class ServerCommandProcessor(CommonCommandProcessor):
|
class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
@@ -1843,7 +1865,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
def _cmd_exit(self) -> bool:
|
def _cmd_exit(self) -> bool:
|
||||||
"""Shutdown the server"""
|
"""Shutdown the server"""
|
||||||
async_start(self.ctx.server.ws_server._close())
|
self.ctx.server.ws_server.close()
|
||||||
if self.ctx.shutdown_task:
|
if self.ctx.shutdown_task:
|
||||||
self.ctx.shutdown_task.cancel()
|
self.ctx.shutdown_task.cancel()
|
||||||
self.ctx.exit_event.set()
|
self.ctx.exit_event.set()
|
||||||
@@ -2184,7 +2206,7 @@ async def auto_shutdown(ctx, to_cancel=None):
|
|||||||
await asyncio.sleep(ctx.auto_shutdown)
|
await asyncio.sleep(ctx.auto_shutdown)
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
if not ctx.client_activity_timers.values():
|
if not ctx.client_activity_timers.values():
|
||||||
async_start(ctx.server.ws_server._close())
|
ctx.server.ws_server.close()
|
||||||
ctx.exit_event.set()
|
ctx.exit_event.set()
|
||||||
if to_cancel:
|
if to_cancel:
|
||||||
for task in to_cancel:
|
for task in to_cancel:
|
||||||
@@ -2195,7 +2217,7 @@ async def auto_shutdown(ctx, to_cancel=None):
|
|||||||
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
|
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
|
||||||
seconds = ctx.auto_shutdown - delta.total_seconds()
|
seconds = ctx.auto_shutdown - delta.total_seconds()
|
||||||
if seconds < 0:
|
if seconds < 0:
|
||||||
async_start(ctx.server.ws_server._close())
|
ctx.server.ws_server.close()
|
||||||
ctx.exit_event.set()
|
ctx.exit_event.set()
|
||||||
if to_cancel:
|
if to_cancel:
|
||||||
for task in to_cancel:
|
for task in to_cancel:
|
||||||
@@ -2250,8 +2272,7 @@ async def main(args: argparse.Namespace):
|
|||||||
|
|
||||||
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
|
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
|
||||||
|
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None,
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ssl=ssl_context)
|
||||||
ping_interval=None, ssl=ssl_context)
|
|
||||||
ip = args.host if args.host else Utils.get_public_ipv4()
|
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||||
'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from json import JSONEncoder, JSONDecoder
|
|||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from Utils import Version
|
from Utils import ByValue, Version
|
||||||
|
|
||||||
|
|
||||||
class JSONMessagePart(typing.TypedDict, total=False):
|
class JSONMessagePart(typing.TypedDict, total=False):
|
||||||
@@ -20,7 +20,7 @@ class JSONMessagePart(typing.TypedDict, total=False):
|
|||||||
flags: int
|
flags: int
|
||||||
|
|
||||||
|
|
||||||
class ClientStatus(enum.IntEnum):
|
class ClientStatus(ByValue, enum.IntEnum):
|
||||||
CLIENT_UNKNOWN = 0
|
CLIENT_UNKNOWN = 0
|
||||||
CLIENT_CONNECTED = 5
|
CLIENT_CONNECTED = 5
|
||||||
CLIENT_READY = 10
|
CLIENT_READY = 10
|
||||||
@@ -28,7 +28,7 @@ class ClientStatus(enum.IntEnum):
|
|||||||
CLIENT_GOAL = 30
|
CLIENT_GOAL = 30
|
||||||
|
|
||||||
|
|
||||||
class SlotType(enum.IntFlag):
|
class SlotType(ByValue, enum.IntFlag):
|
||||||
spectator = 0b00
|
spectator = 0b00
|
||||||
player = 0b01
|
player = 0b01
|
||||||
group = 0b10
|
group = 0b10
|
||||||
@@ -39,7 +39,7 @@ class SlotType(enum.IntFlag):
|
|||||||
return self.value != 0b01
|
return self.value != 0b01
|
||||||
|
|
||||||
|
|
||||||
class Permission(enum.IntFlag):
|
class Permission(ByValue, enum.IntFlag):
|
||||||
disabled = 0b000 # 0, completely disables access
|
disabled = 0b000 # 0, completely disables access
|
||||||
enabled = 0b001 # 1, allows manual use
|
enabled = 0b001 # 1, allows manual use
|
||||||
goal = 0b010 # 2, allows manual use after goal completion
|
goal = 0b010 # 2, allows manual use after goal completion
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def adjustGUI():
|
|||||||
StringVar, IntVar, Checkbutton, Frame, Label, X, Entry, Button, \
|
StringVar, IntVar, Checkbutton, Frame, Label, X, Entry, Button, \
|
||||||
OptionMenu, filedialog, messagebox, ttk
|
OptionMenu, filedialog, messagebox, ttk
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from Main import __version__ as MWVersion
|
from Utils import __version__ as MWVersion
|
||||||
|
|
||||||
window = tk.Tk()
|
window = tk.Tk()
|
||||||
window.wm_title(f"Archipelago {MWVersion} OoT Adjuster")
|
window.wm_title(f"Archipelago {MWVersion} OoT Adjuster")
|
||||||
|
|||||||
27
OoTClient.py
@@ -17,9 +17,9 @@ from worlds.oot.N64Patch import apply_patch_file
|
|||||||
from worlds.oot.Utils import data_path
|
from worlds.oot.Utils import data_path
|
||||||
|
|
||||||
|
|
||||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart oot_connector.lua"
|
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_oot.lua"
|
||||||
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure oot_connector.lua is running"
|
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure connector_oot.lua is running"
|
||||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart oot_connector.lua"
|
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_oot.lua"
|
||||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||||
@@ -100,7 +100,7 @@ class OoTContext(CommonContext):
|
|||||||
await super(OoTContext, self).server_auth(password_requested)
|
await super(OoTContext, self).server_auth(password_requested)
|
||||||
if not self.auth:
|
if not self.auth:
|
||||||
self.awaiting_rom = True
|
self.awaiting_rom = True
|
||||||
logger.info('Awaiting connection to Bizhawk to get player information')
|
logger.info('Awaiting connection to EmuHawk to get player information')
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.send_connect()
|
await self.send_connect()
|
||||||
@@ -179,6 +179,12 @@ async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
|
|||||||
locations = payload['locations']
|
locations = payload['locations']
|
||||||
collectibles = payload['collectibles']
|
collectibles = payload['collectibles']
|
||||||
|
|
||||||
|
# The Lua JSON library serializes an empty table into a list instead of a dict. Verify types for safety:
|
||||||
|
if isinstance(locations, list):
|
||||||
|
locations = {}
|
||||||
|
if isinstance(collectibles, list):
|
||||||
|
collectibles = {}
|
||||||
|
|
||||||
if ctx.location_table != locations or ctx.collectible_table != collectibles:
|
if ctx.location_table != locations or ctx.collectible_table != collectibles:
|
||||||
ctx.location_table = locations
|
ctx.location_table = locations
|
||||||
ctx.collectible_table = collectibles
|
ctx.collectible_table = collectibles
|
||||||
@@ -293,10 +299,15 @@ async def patch_and_run_game(apz5_file):
|
|||||||
if not os.path.exists(rom_file_name):
|
if not os.path.exists(rom_file_name):
|
||||||
rom_file_name = Utils.user_path(rom_file_name)
|
rom_file_name = Utils.user_path(rom_file_name)
|
||||||
rom = Rom(rom_file_name)
|
rom = Rom(rom_file_name)
|
||||||
apply_patch_file(rom, apz5_file,
|
|
||||||
sub_file=(os.path.basename(base_name) + '.zpf'
|
sub_file = None
|
||||||
if zipfile.is_zipfile(apz5_file)
|
if zipfile.is_zipfile(apz5_file):
|
||||||
else None))
|
for name in zipfile.ZipFile(apz5_file).namelist():
|
||||||
|
if name.endswith('.zpf'):
|
||||||
|
sub_file = name
|
||||||
|
break
|
||||||
|
|
||||||
|
apply_patch_file(rom, apz5_file, sub_file=sub_file)
|
||||||
rom.write_to_file(decomp_path)
|
rom.write_to_file(decomp_path)
|
||||||
os.chdir(data_path("Compress"))
|
os.chdir(data_path("Compress"))
|
||||||
compress_rom_file(decomp_path, comp_path)
|
compress_rom_file(decomp_path, comp_path)
|
||||||
|
|||||||
87
Options.py
@@ -13,6 +13,7 @@ from Utils import get_fuzzy_results
|
|||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from worlds.AutoWorld import World
|
from worlds.AutoWorld import World
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
class AssembleOptions(abc.ABCMeta):
|
class AssembleOptions(abc.ABCMeta):
|
||||||
@@ -715,8 +716,16 @@ class SpecialRange(Range):
|
|||||||
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
||||||
|
|
||||||
|
|
||||||
class VerifyKeys:
|
class FreezeValidKeys(AssembleOptions):
|
||||||
valid_keys = frozenset()
|
def __new__(mcs, name, bases, attrs):
|
||||||
|
if "valid_keys" in attrs:
|
||||||
|
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
|
||||||
|
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyKeys(metaclass=FreezeValidKeys):
|
||||||
|
valid_keys: typing.Iterable = []
|
||||||
|
_valid_keys: frozenset # gets created by AssembleOptions from valid_keys
|
||||||
valid_keys_casefold: bool = False
|
valid_keys_casefold: bool = False
|
||||||
convert_name_groups: bool = False
|
convert_name_groups: bool = False
|
||||||
verify_item_name: bool = False
|
verify_item_name: bool = False
|
||||||
@@ -728,10 +737,10 @@ class VerifyKeys:
|
|||||||
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)
|
||||||
extra = dataset - cls.valid_keys
|
extra = dataset - cls._valid_keys
|
||||||
if extra:
|
if extra:
|
||||||
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: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> 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:
|
||||||
@@ -792,6 +801,10 @@ class ItemDict(OptionDict):
|
|||||||
|
|
||||||
|
|
||||||
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||||
|
# Supports duplicate entries and ordering.
|
||||||
|
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
|
||||||
|
# Not a docstring so it doesn't get grabbed by the options system.
|
||||||
|
|
||||||
default: typing.List[typing.Any] = []
|
default: typing.List[typing.Any] = []
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
|
|
||||||
@@ -897,6 +910,13 @@ class StartInventory(ItemDict):
|
|||||||
display_name = "Start Inventory"
|
display_name = "Start Inventory"
|
||||||
|
|
||||||
|
|
||||||
|
class StartInventoryPool(StartInventory):
|
||||||
|
"""Start with these items and don't place them in the world.
|
||||||
|
The game decides what the replacement items will be."""
|
||||||
|
verify_item_name = True
|
||||||
|
display_name = "Start Inventory from Pool"
|
||||||
|
|
||||||
|
|
||||||
class StartHints(ItemSet):
|
class StartHints(ItemSet):
|
||||||
"""Start with these item's locations prefilled into the !hint command."""
|
"""Start with these item's locations prefilled into the !hint command."""
|
||||||
display_name = "Start Hints"
|
display_name = "Start Hints"
|
||||||
@@ -904,6 +924,7 @@ class StartHints(ItemSet):
|
|||||||
|
|
||||||
class LocationSet(OptionSet):
|
class LocationSet(OptionSet):
|
||||||
verify_location_name = True
|
verify_location_name = True
|
||||||
|
convert_name_groups = True
|
||||||
|
|
||||||
|
|
||||||
class StartLocationHints(LocationSet):
|
class StartLocationHints(LocationSet):
|
||||||
@@ -1002,6 +1023,64 @@ per_game_common_options = {
|
|||||||
"item_links": ItemLinks
|
"item_links": ItemLinks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
|
||||||
|
import os
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
from worlds import AutoWorldRegister
|
||||||
|
from Utils import local_path, __version__
|
||||||
|
|
||||||
|
full_path: str
|
||||||
|
|
||||||
|
os.makedirs(target_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# clean out old
|
||||||
|
for file in os.listdir(target_folder):
|
||||||
|
full_path = os.path.join(target_folder, file)
|
||||||
|
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||||
|
os.unlink(full_path)
|
||||||
|
|
||||||
|
def dictify_range(option: typing.Union[Range, SpecialRange]):
|
||||||
|
data = {option.default: 50}
|
||||||
|
for sub_option in ["random", "random-low", "random-high"]:
|
||||||
|
if sub_option != option.default:
|
||||||
|
data[sub_option] = 0
|
||||||
|
|
||||||
|
notes = {}
|
||||||
|
for name, number in getattr(option, "special_range_names", {}).items():
|
||||||
|
notes[name] = f"equivalent to {number}"
|
||||||
|
if number in data:
|
||||||
|
data[name] = data[number]
|
||||||
|
del data[number]
|
||||||
|
else:
|
||||||
|
data[name] = 0
|
||||||
|
|
||||||
|
return data, notes
|
||||||
|
|
||||||
|
for game_name, world in AutoWorldRegister.world_types.items():
|
||||||
|
if not world.hidden or generate_hidden:
|
||||||
|
all_options: typing.Dict[str, AssembleOptions] = {
|
||||||
|
**per_game_common_options,
|
||||||
|
**world.option_definitions
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(local_path("data", "options.yaml")) as f:
|
||||||
|
file_data = f.read()
|
||||||
|
res = Template(file_data).render(
|
||||||
|
options=all_options,
|
||||||
|
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||||
|
dictify_range=dictify_range,
|
||||||
|
)
|
||||||
|
|
||||||
|
del file_data
|
||||||
|
|
||||||
|
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
|
||||||
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
from worlds.alttp.Options import Logic
|
from worlds.alttp.Options import Logic
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class GBContext(CommonContext):
|
|||||||
await super(GBContext, self).server_auth(password_requested)
|
await super(GBContext, self).server_auth(password_requested)
|
||||||
if not self.auth:
|
if not self.auth:
|
||||||
self.awaiting_rom = True
|
self.awaiting_rom = True
|
||||||
logger.info('Awaiting connection to Bizhawk to get Player information')
|
logger.info('Awaiting connection to EmuHawk to get Player information')
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.send_connect()
|
await self.send_connect()
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ Currently, the following games are supported:
|
|||||||
* The Legend of Zelda: Link's Awakening DX
|
* The Legend of Zelda: Link's Awakening DX
|
||||||
* Clique
|
* Clique
|
||||||
* Adventure
|
* Adventure
|
||||||
|
* DLC Quest
|
||||||
|
* Noita
|
||||||
|
* Undertale
|
||||||
|
* Bumper Stickers
|
||||||
|
* Mega Man Battle Network 3: Blue Version
|
||||||
|
* Muse Dash
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ def launch_sni() -> None:
|
|||||||
f"please start it yourself if it is not running")
|
f"please start it yourself if it is not running")
|
||||||
|
|
||||||
|
|
||||||
async def _snes_connect(ctx: SNIContext, address: str) -> WebSocketClientProtocol:
|
async def _snes_connect(ctx: SNIContext, address: str, retry: bool = True) -> WebSocketClientProtocol:
|
||||||
address = f"ws://{address}" if "://" not in address else address
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
snes_logger.info("Connecting to SNI at %s ..." % address)
|
snes_logger.info("Connecting to SNI at %s ..." % address)
|
||||||
seen_problems: typing.Set[str] = set()
|
seen_problems: typing.Set[str] = set()
|
||||||
@@ -336,6 +336,8 @@ async def _snes_connect(ctx: SNIContext, address: str) -> WebSocketClientProtoco
|
|||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
else:
|
else:
|
||||||
return snes_socket
|
return snes_socket
|
||||||
|
if not retry:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
class SNESRequest(typing.TypedDict):
|
class SNESRequest(typing.TypedDict):
|
||||||
@@ -684,6 +686,8 @@ async def main() -> None:
|
|||||||
logging.info(f"Wrote rom file to {romfile}")
|
logging.info(f"Wrote rom file to {romfile}")
|
||||||
if args.diff_file.endswith(".apsoe"):
|
if args.diff_file.endswith(".apsoe"):
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
async_start(run_game(romfile))
|
||||||
|
await _snes_connect(SNIContext(args.snes, args.connect, args.password), args.snes, False)
|
||||||
webbrowser.open(f"http://www.evermizer.com/apclient/#server={meta['server']}")
|
webbrowser.open(f"http://www.evermizer.com/apclient/#server={meta['server']}")
|
||||||
logging.info("Starting Evermizer Client in your Browser...")
|
logging.info("Starting Evermizer Client in your Browser...")
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -25,11 +25,10 @@ logger = logging.getLogger("Client")
|
|||||||
sc2_logger = logging.getLogger("Starcraft2")
|
sc2_logger = logging.getLogger("Starcraft2")
|
||||||
|
|
||||||
import nest_asyncio
|
import nest_asyncio
|
||||||
import sc2
|
from worlds._sc2common import bot
|
||||||
from sc2.bot_ai import BotAI
|
from worlds._sc2common.bot.data import Race
|
||||||
from sc2.data import Race
|
from worlds._sc2common.bot.main import run_game
|
||||||
from sc2.main import run_game
|
from worlds._sc2common.bot.player import Bot
|
||||||
from sc2.player import Bot
|
|
||||||
from worlds.sc2wol import SC2WoLWorld
|
from worlds.sc2wol import SC2WoLWorld
|
||||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
|
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
|
||||||
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
||||||
@@ -240,8 +239,6 @@ class SC2Context(CommonContext):
|
|||||||
from kivy.uix.floatlayout import FloatLayout
|
from kivy.uix.floatlayout import FloatLayout
|
||||||
from kivy.properties import StringProperty
|
from kivy.properties import StringProperty
|
||||||
|
|
||||||
import Utils
|
|
||||||
|
|
||||||
class HoverableButton(HoverBehavior, Button):
|
class HoverableButton(HoverBehavior, Button):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -544,11 +541,11 @@ async def starcraft_launch(ctx: SC2Context, mission_id: int):
|
|||||||
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
|
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
|
||||||
|
|
||||||
with DllDirectory(None):
|
with DllDirectory(None):
|
||||||
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
|
run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
|
||||||
name="Archipelago", fullscreen=True)], realtime=True)
|
name="Archipelago", fullscreen=True)], realtime=True)
|
||||||
|
|
||||||
|
|
||||||
class ArchipelagoBot(sc2.bot_ai.BotAI):
|
class ArchipelagoBot(bot.bot_ai.BotAI):
|
||||||
game_running: bool = False
|
game_running: bool = False
|
||||||
mission_completed: bool = False
|
mission_completed: bool = False
|
||||||
boni: typing.List[bool]
|
boni: typing.List[bool]
|
||||||
@@ -867,7 +864,7 @@ def check_game_install_path() -> bool:
|
|||||||
documentspath = buf.value
|
documentspath = buf.value
|
||||||
einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
|
einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
|
||||||
else:
|
else:
|
||||||
einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF]))
|
einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
|
||||||
|
|
||||||
# Check if the file exists.
|
# Check if the file exists.
|
||||||
if os.path.isfile(einfo):
|
if os.path.isfile(einfo):
|
||||||
@@ -883,7 +880,7 @@ def check_game_install_path() -> bool:
|
|||||||
f"try again.")
|
f"try again.")
|
||||||
return False
|
return False
|
||||||
if os.path.exists(base):
|
if os.path.exists(base):
|
||||||
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
|
executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
|
||||||
|
|
||||||
# Finally, check the path for an actual executable.
|
# Finally, check the path for an actual executable.
|
||||||
# If we find one, great. Set up the SC2PATH.
|
# If we find one, great. Set up the SC2PATH.
|
||||||
|
|||||||
498
UndertaleClient.py
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import typing
|
||||||
|
import bsdiff4
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
|
||||||
|
from NetUtils import NetworkItem, ClientStatus
|
||||||
|
from worlds import undertale
|
||||||
|
from MultiServer import mark_raw
|
||||||
|
from CommonClient import CommonContext, server_loop, \
|
||||||
|
gui_enabled, ClientCommandProcessor, get_base_parser
|
||||||
|
from Utils import async_start
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||||
|
def __init__(self, ctx):
|
||||||
|
super().__init__(ctx)
|
||||||
|
|
||||||
|
def _cmd_resync(self):
|
||||||
|
"""Manually trigger a resync."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
self.output(f"Syncing items.")
|
||||||
|
self.ctx.syncing = True
|
||||||
|
|
||||||
|
def _cmd_patch(self):
|
||||||
|
"""Patch the game."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||||
|
self.ctx.patch_game()
|
||||||
|
self.output("Patched.")
|
||||||
|
|
||||||
|
@mark_raw
|
||||||
|
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||||
|
"""Patch the game automatically."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||||
|
tempInstall = steaminstall
|
||||||
|
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
|
tempInstall = None
|
||||||
|
if tempInstall is None:
|
||||||
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||||
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
elif not os.path.exists(tempInstall):
|
||||||
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||||
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
|
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
|
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
|
||||||
|
" command. \"/auto_patch (Steam directory)\".")
|
||||||
|
else:
|
||||||
|
for file_name in os.listdir(tempInstall):
|
||||||
|
if file_name != "steam_api.dll":
|
||||||
|
shutil.copy(tempInstall+"\\"+file_name,
|
||||||
|
os.getcwd() + "\\Undertale\\" + file_name)
|
||||||
|
self.ctx.patch_game()
|
||||||
|
self.output("Patching successful!")
|
||||||
|
|
||||||
|
def _cmd_online(self):
|
||||||
|
"""Makes you no longer able to see other Undertale players."""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
self.ctx.update_online_mode(not ("Online" in self.ctx.tags))
|
||||||
|
if "Online" in self.ctx.tags:
|
||||||
|
self.output(f"Now online.")
|
||||||
|
else:
|
||||||
|
self.output(f"Now offline.")
|
||||||
|
|
||||||
|
def _cmd_deathlink(self):
|
||||||
|
"""Toggles deathlink"""
|
||||||
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
|
self.ctx.deathlink_status = not self.ctx.deathlink_status
|
||||||
|
if self.ctx.deathlink_status:
|
||||||
|
self.output(f"Deathlink enabled.")
|
||||||
|
else:
|
||||||
|
self.output(f"Deathlink disabled.")
|
||||||
|
|
||||||
|
|
||||||
|
class UndertaleContext(CommonContext):
|
||||||
|
tags = {"AP", "Online"}
|
||||||
|
game = "Undertale"
|
||||||
|
command_processor = UndertaleCommandProcessor
|
||||||
|
items_handling = 0b111
|
||||||
|
route = None
|
||||||
|
pieces_needed = None
|
||||||
|
completed_routes = None
|
||||||
|
completed_count = 0
|
||||||
|
save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
|
||||||
|
def __init__(self, server_address, password):
|
||||||
|
super().__init__(server_address, password)
|
||||||
|
self.pieces_needed = 0
|
||||||
|
self.game = "Undertale"
|
||||||
|
self.got_deathlink = False
|
||||||
|
self.syncing = False
|
||||||
|
self.deathlink_status = False
|
||||||
|
self.tem_armor = False
|
||||||
|
self.completed_count = 0
|
||||||
|
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
||||||
|
|
||||||
|
def patch_game(self):
|
||||||
|
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
|
||||||
|
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||||
|
with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
|
||||||
|
f.write(patchedFile)
|
||||||
|
os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
|
||||||
|
with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
|
||||||
|
"Which Character.txt"), "w") as f:
|
||||||
|
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
||||||
|
"line other than this one.\n", "frisk"])
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
async def server_auth(self, password_requested: bool = False):
|
||||||
|
if password_requested and not self.password:
|
||||||
|
await super().server_auth(password_requested)
|
||||||
|
await self.get_username()
|
||||||
|
await self.send_connect()
|
||||||
|
|
||||||
|
def clear_undertale_files(self):
|
||||||
|
path = self.save_game_folder
|
||||||
|
self.finished_game = False
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if "check.spot" == file or "scout" == file:
|
||||||
|
os.remove(os.path.join(root, file))
|
||||||
|
elif file.endswith((".item", ".victory", ".route", ".playerspot", ".mad",
|
||||||
|
".youDied", ".LV", ".mine", ".flag", ".hint")):
|
||||||
|
os.remove(os.path.join(root, file))
|
||||||
|
|
||||||
|
async def connect(self, address: typing.Optional[str] = None):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().connect(address)
|
||||||
|
|
||||||
|
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().disconnect(allow_autoreconnect)
|
||||||
|
|
||||||
|
async def connection_closed(self):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().connection_closed()
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
self.clear_undertale_files()
|
||||||
|
await super().shutdown()
|
||||||
|
|
||||||
|
def update_online_mode(self, online):
|
||||||
|
old_tags = self.tags.copy()
|
||||||
|
if online:
|
||||||
|
self.tags.add("Online")
|
||||||
|
else:
|
||||||
|
self.tags -= {"Online"}
|
||||||
|
if old_tags != self.tags and self.server and not self.server.socket.closed:
|
||||||
|
async_start(self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]))
|
||||||
|
|
||||||
|
def on_package(self, cmd: str, args: dict):
|
||||||
|
if cmd == "Connected":
|
||||||
|
self.game = self.slot_info[self.slot].game
|
||||||
|
async_start(process_undertale_cmd(self, cmd, args))
|
||||||
|
|
||||||
|
def run_gui(self):
|
||||||
|
from kvui import GameManager
|
||||||
|
|
||||||
|
class UTManager(GameManager):
|
||||||
|
logging_pairs = [
|
||||||
|
("Client", "Archipelago")
|
||||||
|
]
|
||||||
|
base_title = "Archipelago Undertale Client"
|
||||||
|
|
||||||
|
self.ui = UTManager(self)
|
||||||
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
def on_deathlink(self, data: typing.Dict[str, typing.Any]):
|
||||||
|
self.got_deathlink = True
|
||||||
|
super().on_deathlink(data)
|
||||||
|
|
||||||
|
|
||||||
|
def to_room_name(place_name: str):
|
||||||
|
if place_name == "Old Home Exit":
|
||||||
|
return "room_ruinsexit"
|
||||||
|
elif place_name == "Snowdin Forest":
|
||||||
|
return "room_tundra1"
|
||||||
|
elif place_name == "Snowdin Town Exit":
|
||||||
|
return "room_fogroom"
|
||||||
|
elif place_name == "Waterfall":
|
||||||
|
return "room_water1"
|
||||||
|
elif place_name == "Waterfall Exit":
|
||||||
|
return "room_fire2"
|
||||||
|
elif place_name == "Hotland":
|
||||||
|
return "room_fire_prelab"
|
||||||
|
elif place_name == "Hotland Exit":
|
||||||
|
return "room_fire_precore"
|
||||||
|
elif place_name == "Core":
|
||||||
|
return "room_fire_core1"
|
||||||
|
|
||||||
|
|
||||||
|
async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
||||||
|
if cmd == "Connected":
|
||||||
|
if not os.path.exists(ctx.save_game_folder):
|
||||||
|
os.mkdir(ctx.save_game_folder)
|
||||||
|
ctx.route = args["slot_data"]["route"]
|
||||||
|
ctx.pieces_needed = args["slot_data"]["key_pieces"]
|
||||||
|
ctx.tem_armor = args["slot_data"]["temy_armor_include"]
|
||||||
|
|
||||||
|
await ctx.send_msgs([{"cmd": "Get", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||||
|
str(ctx.slot)+" RoutesDone pacifist",
|
||||||
|
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||||
|
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||||
|
str(ctx.slot)+" RoutesDone pacifist",
|
||||||
|
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||||
|
if args["slot_data"]["only_flakes"]:
|
||||||
|
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
||||||
|
f.close()
|
||||||
|
if not args["slot_data"]["key_hunt"]:
|
||||||
|
ctx.pieces_needed = 0
|
||||||
|
if args["slot_data"]["rando_love"]:
|
||||||
|
filename = f"LOVErando.LV"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.close()
|
||||||
|
if args["slot_data"]["rando_stats"]:
|
||||||
|
filename = f"STATrando.LV"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.close()
|
||||||
|
filename = f"{ctx.route}.route"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.close()
|
||||||
|
filename = f"check.spot"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
||||||
|
for ss in ctx.checked_locations:
|
||||||
|
f.write(str(ss-12000)+"\n")
|
||||||
|
f.close()
|
||||||
|
elif cmd == "LocationInfo":
|
||||||
|
for l in args["locations"]:
|
||||||
|
locationid = l.location
|
||||||
|
filename = f"{str(locationid-12000)}.hint"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
toDraw = ""
|
||||||
|
for i in range(20):
|
||||||
|
if i < len(str(ctx.item_names[l.item])):
|
||||||
|
toDraw += str(ctx.item_names[l.item])[i]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
f.write(toDraw)
|
||||||
|
f.close()
|
||||||
|
elif cmd == "Retrieved":
|
||||||
|
if str(ctx.slot)+" RoutesDone neutral" in args["keys"]:
|
||||||
|
if args["keys"][str(ctx.slot)+" RoutesDone neutral"] is not None:
|
||||||
|
ctx.completed_routes["neutral"] = args["keys"][str(ctx.slot)+" RoutesDone neutral"]
|
||||||
|
if str(ctx.slot)+" RoutesDone genocide" in args["keys"]:
|
||||||
|
if args["keys"][str(ctx.slot)+" RoutesDone genocide"] is not None:
|
||||||
|
ctx.completed_routes["genocide"] = args["keys"][str(ctx.slot)+" RoutesDone genocide"]
|
||||||
|
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
||||||
|
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
||||||
|
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
||||||
|
elif cmd == "SetReply":
|
||||||
|
if args["value"] is not None:
|
||||||
|
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
||||||
|
ctx.completed_routes["pacifist"] = args["value"]
|
||||||
|
elif str(ctx.slot)+" RoutesDone genocide" == args["key"]:
|
||||||
|
ctx.completed_routes["genocide"] = args["value"]
|
||||||
|
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
||||||
|
ctx.completed_routes["neutral"] = args["value"]
|
||||||
|
elif cmd == "ReceivedItems":
|
||||||
|
start_index = args["index"]
|
||||||
|
|
||||||
|
if start_index == 0:
|
||||||
|
ctx.items_received = []
|
||||||
|
elif start_index != len(ctx.items_received):
|
||||||
|
sync_msg = [{"cmd": "Sync"}]
|
||||||
|
if ctx.locations_checked:
|
||||||
|
sync_msg.append({"cmd": "LocationChecks",
|
||||||
|
"locations": list(ctx.locations_checked)})
|
||||||
|
await ctx.send_msgs(sync_msg)
|
||||||
|
if start_index == len(ctx.items_received):
|
||||||
|
counter = -1
|
||||||
|
placedWeapon = 0
|
||||||
|
placedArmor = 0
|
||||||
|
for item in args["items"]:
|
||||||
|
id = NetworkItem(*item).location
|
||||||
|
while NetworkItem(*item).location < 0 and \
|
||||||
|
counter <= id:
|
||||||
|
id -= 1
|
||||||
|
if NetworkItem(*item).location < 0:
|
||||||
|
counter -= 1
|
||||||
|
filename = f"{str(id)}PLR{str(NetworkItem(*item).player)}.item"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
if NetworkItem(*item).item == 77701:
|
||||||
|
if placedWeapon == 0:
|
||||||
|
f.write(str(77013-11000))
|
||||||
|
elif placedWeapon == 1:
|
||||||
|
f.write(str(77014-11000))
|
||||||
|
elif placedWeapon == 2:
|
||||||
|
f.write(str(77025-11000))
|
||||||
|
elif placedWeapon == 3:
|
||||||
|
f.write(str(77045-11000))
|
||||||
|
elif placedWeapon == 4:
|
||||||
|
f.write(str(77049-11000))
|
||||||
|
elif placedWeapon == 5:
|
||||||
|
f.write(str(77047-11000))
|
||||||
|
elif placedWeapon == 6:
|
||||||
|
if str(ctx.route) == "genocide" or str(ctx.route) == "all_routes":
|
||||||
|
f.write(str(77052-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77051-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77003-11000))
|
||||||
|
placedWeapon += 1
|
||||||
|
elif NetworkItem(*item).item == 77702:
|
||||||
|
if placedArmor == 0:
|
||||||
|
f.write(str(77012-11000))
|
||||||
|
elif placedArmor == 1:
|
||||||
|
f.write(str(77015-11000))
|
||||||
|
elif placedArmor == 2:
|
||||||
|
f.write(str(77024-11000))
|
||||||
|
elif placedArmor == 3:
|
||||||
|
f.write(str(77044-11000))
|
||||||
|
elif placedArmor == 4:
|
||||||
|
f.write(str(77048-11000))
|
||||||
|
elif placedArmor == 5:
|
||||||
|
if str(ctx.route) == "genocide":
|
||||||
|
f.write(str(77053-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77046-11000))
|
||||||
|
elif placedArmor == 6 and ((not str(ctx.route) == "genocide") or ctx.tem_armor):
|
||||||
|
if str(ctx.route) == "all_routes":
|
||||||
|
f.write(str(77053-11000))
|
||||||
|
elif str(ctx.route) == "genocide":
|
||||||
|
f.write(str(77064-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77050-11000))
|
||||||
|
elif placedArmor == 7 and ctx.tem_armor and not str(ctx.route) == "genocide":
|
||||||
|
f.write(str(77064-11000))
|
||||||
|
else:
|
||||||
|
f.write(str(77004-11000))
|
||||||
|
placedArmor += 1
|
||||||
|
else:
|
||||||
|
f.write(str(NetworkItem(*item).item-11000))
|
||||||
|
f.close()
|
||||||
|
ctx.items_received.append(NetworkItem(*item))
|
||||||
|
if [item.item for item in ctx.items_received].count(77000) >= ctx.pieces_needed > 0:
|
||||||
|
filename = f"{str(-99999)}PLR{str(0)}.item"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.write(str(77787 - 11000))
|
||||||
|
f.close()
|
||||||
|
filename = f"{str(-99998)}PLR{str(0)}.item"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.write(str(77789 - 11000))
|
||||||
|
f.close()
|
||||||
|
ctx.watcher_event.set()
|
||||||
|
|
||||||
|
elif cmd == "RoomUpdate":
|
||||||
|
if "checked_locations" in args:
|
||||||
|
filename = f"check.spot"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
|
||||||
|
for ss in ctx.checked_locations:
|
||||||
|
f.write(str(ss-12000)+"\n")
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
elif cmd == "Bounced":
|
||||||
|
tags = args.get("tags", [])
|
||||||
|
if "Online" in tags:
|
||||||
|
data = args.get("worlds/undertale/data", {})
|
||||||
|
if data["player"] != ctx.slot and data["player"] is not None:
|
||||||
|
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||||
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
|
f.write(str(data["x"]) + str(data["y"]) + str(data["room"]) + str(
|
||||||
|
data["spr"]) + str(data["frm"]))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def multi_watcher(ctx: UndertaleContext):
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
path = ctx.save_game_folder
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if "spots.mine" in file and "Online" in ctx.tags:
|
||||||
|
with open(root + "/" + file, "r") as mine:
|
||||||
|
this_x = mine.readline()
|
||||||
|
this_y = mine.readline()
|
||||||
|
this_room = mine.readline()
|
||||||
|
this_sprite = mine.readline()
|
||||||
|
this_frame = mine.readline()
|
||||||
|
mine.close()
|
||||||
|
message = [{"cmd": "Bounce", "tags": ["Online"],
|
||||||
|
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
|
||||||
|
"spr": this_sprite, "frm": this_frame}}]
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
async def game_watcher(ctx: UndertaleContext):
|
||||||
|
while not ctx.exit_event.is_set():
|
||||||
|
await ctx.update_death_link(ctx.deathlink_status)
|
||||||
|
path = ctx.save_game_folder
|
||||||
|
if ctx.syncing:
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if ".item" in file:
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
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
|
||||||
|
if ctx.got_deathlink:
|
||||||
|
ctx.got_deathlink = False
|
||||||
|
with open(os.path.join(ctx.save_game_folder, "/WelcomeToTheDead.youDied"), "w") as f:
|
||||||
|
f.close()
|
||||||
|
sending = []
|
||||||
|
victory = False
|
||||||
|
found_routes = 0
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if "DontBeMad.mad" in file and "DeathLink" in ctx.tags:
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
await ctx.send_death()
|
||||||
|
if "scout" == file:
|
||||||
|
sending = []
|
||||||
|
with open(root+"/"+file, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for l in lines:
|
||||||
|
if ctx.server_locations.__contains__(int(l)+12000):
|
||||||
|
sending = sending + [int(l)+12000]
|
||||||
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
||||||
|
"create_as_hint": int(2)}])
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
if "check.spot" in file:
|
||||||
|
sending = []
|
||||||
|
with open(root+"/"+file, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for l in lines:
|
||||||
|
sending = sending+[(int(l))+12000]
|
||||||
|
message = [{"cmd": "LocationChecks", "locations": sending}]
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
if "victory" in file and str(ctx.route) in file:
|
||||||
|
victory = True
|
||||||
|
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||||
|
os.remove(root+"/"+file)
|
||||||
|
if "victory" in file:
|
||||||
|
if str(ctx.route) == "all_routes":
|
||||||
|
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
||||||
|
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone neutral",
|
||||||
|
"default": 0, "want_reply": True, "operations": [{"operation": "max",
|
||||||
|
"value": 1}]}])
|
||||||
|
elif "pacifist" in file and ctx.completed_routes["pacifist"] != 1:
|
||||||
|
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone pacifist",
|
||||||
|
"default": 0, "want_reply": True, "operations": [{"operation": "max",
|
||||||
|
"value": 1}]}])
|
||||||
|
elif "genocide" in file and ctx.completed_routes["genocide"] != 1:
|
||||||
|
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone genocide",
|
||||||
|
"default": 0, "want_reply": True, "operations": [{"operation": "max",
|
||||||
|
"value": 1}]}])
|
||||||
|
if str(ctx.route) == "all_routes":
|
||||||
|
found_routes += ctx.completed_routes["neutral"]
|
||||||
|
found_routes += ctx.completed_routes["pacifist"]
|
||||||
|
found_routes += ctx.completed_routes["genocide"]
|
||||||
|
if str(ctx.route) == "all_routes" and found_routes >= 3:
|
||||||
|
victory = True
|
||||||
|
ctx.locations_checked = sending
|
||||||
|
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 main():
|
||||||
|
Utils.init_logging("UndertaleClient", exception_logger="Client")
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
ctx = UndertaleContext(None, None)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
|
asyncio.create_task(
|
||||||
|
game_watcher(ctx), name="UndertaleProgressionWatcher")
|
||||||
|
|
||||||
|
asyncio.create_task(
|
||||||
|
multi_watcher(ctx), name="UndertaleMultiplayerWatcher")
|
||||||
|
|
||||||
|
if gui_enabled:
|
||||||
|
ctx.run_gui()
|
||||||
|
ctx.run_cli()
|
||||||
|
|
||||||
|
await ctx.exit_event.wait()
|
||||||
|
await ctx.shutdown()
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
asyncio.run(_main())
|
||||||
|
colorama.deinit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = get_base_parser(description="Undertale Client, for text interfacing.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
main()
|
||||||
79
Utils.py
@@ -38,8 +38,11 @@ class Version(typing.NamedTuple):
|
|||||||
minor: int
|
minor: int
|
||||||
build: int
|
build: int
|
||||||
|
|
||||||
|
def as_simple_string(self) -> str:
|
||||||
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
__version__ = "0.4.0"
|
|
||||||
|
__version__ = "0.4.2"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -336,6 +339,10 @@ def get_default_options() -> OptionsType:
|
|||||||
"wargroove_options": {
|
"wargroove_options": {
|
||||||
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||||
},
|
},
|
||||||
|
"mmbn3_options": {
|
||||||
|
"rom_file": "Mega Man Battle Network 3 - Blue Version (USA).gba",
|
||||||
|
"rom_start": True
|
||||||
|
},
|
||||||
"adventure_options": {
|
"adventure_options": {
|
||||||
"rom_file": "ADVNTURE.BIN",
|
"rom_file": "ADVNTURE.BIN",
|
||||||
"display_msgs": True,
|
"display_msgs": True,
|
||||||
@@ -505,6 +512,15 @@ def restricted_loads(s):
|
|||||||
return RestrictedUnpickler(io.BytesIO(s)).load()
|
return RestrictedUnpickler(io.BytesIO(s)).load()
|
||||||
|
|
||||||
|
|
||||||
|
class ByValue:
|
||||||
|
"""
|
||||||
|
Mixin for enums to pickle value instead of name (restores pre-3.11 behavior). Use as left-most parent.
|
||||||
|
See https://github.com/python/cpython/pull/26658 for why this exists.
|
||||||
|
"""
|
||||||
|
def __reduce_ex__(self, prot):
|
||||||
|
return self.__class__, (self._value_, )
|
||||||
|
|
||||||
|
|
||||||
class KeyedDefaultDict(collections.defaultdict):
|
class KeyedDefaultDict(collections.defaultdict):
|
||||||
"""defaultdict variant that uses the missing key as argument to default_factory"""
|
"""defaultdict variant that uses the missing key as argument to default_factory"""
|
||||||
default_factory: typing.Callable[[typing.Any], typing.Any]
|
default_factory: typing.Callable[[typing.Any], typing.Any]
|
||||||
@@ -537,6 +553,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
|||||||
root_logger.removeHandler(handler)
|
root_logger.removeHandler(handler)
|
||||||
handler.close()
|
handler.close()
|
||||||
root_logger.setLevel(loglevel)
|
root_logger.setLevel(loglevel)
|
||||||
|
logging.getLogger("websockets").setLevel(loglevel) # make sure level is applied for websockets
|
||||||
if "a" not in write_mode:
|
if "a" not in write_mode:
|
||||||
name += f"_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
|
name += f"_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
|
||||||
file_handler = logging.FileHandler(
|
file_handler = logging.FileHandler(
|
||||||
@@ -753,10 +770,10 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
|
|||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
|
|
||||||
_faf_tasks: "Set[asyncio.Task[None]]" = set()
|
_faf_tasks: "Set[asyncio.Task[typing.Any]]" = set()
|
||||||
|
|
||||||
|
|
||||||
def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None:
|
def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Use this to start a task when you don't keep a reference to it or immediately await it,
|
Use this to start a task when you don't keep a reference to it or immediately await it,
|
||||||
to prevent early garbage collection. "fire-and-forget"
|
to prevent early garbage collection. "fire-and-forget"
|
||||||
@@ -769,6 +786,60 @@ def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str]
|
|||||||
# ```
|
# ```
|
||||||
# This implementation follows the pattern given in that documentation.
|
# This implementation follows the pattern given in that documentation.
|
||||||
|
|
||||||
task = asyncio.create_task(co, name=name)
|
task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name)
|
||||||
_faf_tasks.add(task)
|
_faf_tasks.add(task)
|
||||||
task.add_done_callback(_faf_tasks.discard)
|
task.add_done_callback(_faf_tasks.discard)
|
||||||
|
|
||||||
|
|
||||||
|
def deprecate(message: str):
|
||||||
|
if __debug__:
|
||||||
|
raise Exception(message)
|
||||||
|
import warnings
|
||||||
|
warnings.warn(message)
|
||||||
|
|
||||||
|
def _extend_freeze_support() -> None:
|
||||||
|
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
|
||||||
|
# upstream issue: https://github.com/python/cpython/issues/76327
|
||||||
|
# code based on https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py#L26
|
||||||
|
import multiprocessing
|
||||||
|
import multiprocessing.spawn
|
||||||
|
|
||||||
|
def _freeze_support() -> None:
|
||||||
|
"""Minimal freeze_support. Only apply this if frozen."""
|
||||||
|
from subprocess import _args_from_interpreter_flags
|
||||||
|
|
||||||
|
# Prevent `spawn` from trying to read `__main__` in from the main script
|
||||||
|
multiprocessing.process.ORIGINAL_DIR = None
|
||||||
|
|
||||||
|
# Handle the first process that MP will create
|
||||||
|
if (
|
||||||
|
len(sys.argv) >= 2 and sys.argv[-2] == '-c' and sys.argv[-1].startswith((
|
||||||
|
'from multiprocessing.semaphore_tracker import main', # Py<3.8
|
||||||
|
'from multiprocessing.resource_tracker import main', # Py>=3.8
|
||||||
|
'from multiprocessing.forkserver import main'
|
||||||
|
)) and set(sys.argv[1:-2]) == set(_args_from_interpreter_flags())
|
||||||
|
):
|
||||||
|
exec(sys.argv[-1])
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# Handle the second process that MP will create
|
||||||
|
if multiprocessing.spawn.is_forking(sys.argv):
|
||||||
|
kwargs = {}
|
||||||
|
for arg in sys.argv[2:]:
|
||||||
|
name, value = arg.split('=')
|
||||||
|
if value == 'None':
|
||||||
|
kwargs[name] = None
|
||||||
|
else:
|
||||||
|
kwargs[name] = int(value)
|
||||||
|
multiprocessing.spawn.spawn_main(**kwargs)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if not is_windows and is_frozen():
|
||||||
|
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support
|
||||||
|
|
||||||
|
|
||||||
|
def freeze_support() -> None:
|
||||||
|
"""This behaves like multiprocessing.freeze_support but also works on Non-Windows."""
|
||||||
|
import multiprocessing
|
||||||
|
_extend_freeze_support()
|
||||||
|
multiprocessing.freeze_support()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
|
|||||||
# if you want to deploy, make sure you have a non-guessable secret key
|
# if you want to deploy, make sure you have a non-guessable secret key
|
||||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||||
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
||||||
app.config["JOB_THRESHOLD"] = 2
|
app.config["JOB_THRESHOLD"] = 1
|
||||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||||
app.config["JOB_TIME"] = 600
|
app.config["JOB_TIME"] = 600
|
||||||
app.config['SESSION_PERMANENT'] = True
|
app.config['SESSION_PERMANENT'] = True
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import json
|
|||||||
import pickle
|
import pickle
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import request, session, url_for, Markup
|
from flask import request, session, url_for
|
||||||
|
from markupsafe import Markup
|
||||||
from pony.orm import commit
|
from pony.orm import commit
|
||||||
|
|
||||||
from WebHostLib import app
|
from WebHostLib import app
|
||||||
@@ -48,9 +49,8 @@ def generate_api():
|
|||||||
if len(options) > app.config["MAX_ROLL"]:
|
if len(options) > app.config["MAX_ROLL"]:
|
||||||
return {"text": "Max size of multiworld exceeded",
|
return {"text": "Max size of multiworld exceeded",
|
||||||
"detail": app.config["MAX_ROLL"]}, 409
|
"detail": app.config["MAX_ROLL"]}, 409
|
||||||
meta = get_meta(meta_options_source)
|
meta = get_meta(meta_options_source, race)
|
||||||
meta["race"] = race
|
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||||
results, gen_options = roll_options(options, meta["plando_options"])
|
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
return {"text": str(results),
|
return {"text": str(results),
|
||||||
"detail": results}, 400
|
"detail": results}, 400
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def autogen(config: dict):
|
|||||||
with Locker("autogen"):
|
with Locker("autogen"):
|
||||||
|
|
||||||
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
||||||
initargs=(config["PONY"],)) as generator_pool:
|
initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool:
|
||||||
with db_session:
|
with db_session:
|
||||||
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
|
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import zipfile
|
import zipfile
|
||||||
from typing import *
|
from typing import *
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, render_template, Markup
|
from flask import request, flash, redirect, url_for, render_template
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
from WebHostLib import app
|
from WebHostLib import app
|
||||||
|
|
||||||
@@ -52,11 +53,12 @@ def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
|||||||
|
|
||||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||||
|
|
||||||
for file in infolist:
|
for file in infolist:
|
||||||
if file.filename.endswith(banned_zip_contents):
|
if file.filename.endswith(banned_zip_contents):
|
||||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||||
|
"Your file was deleted."
|
||||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||||
options[file.filename] = zfile.open(file, "r").read()
|
options[file.filename] = zfile.open(file, "r").read()
|
||||||
else:
|
else:
|
||||||
@@ -90,7 +92,7 @@ def roll_options(options: Dict[str, Union[dict, str]],
|
|||||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||||
plando_options=plando_options)
|
plando_options=plando_options)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
results[filename] = f"Failed to generate options in {filename}: {e}"
|
||||||
else:
|
else:
|
||||||
results[filename] = True
|
results[filename] = True
|
||||||
return results, rolled_results
|
return results, rolled_results
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from pony.orm import commit, db_session, select
|
|||||||
import Utils
|
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 restricted_loads, cache_argsless
|
||||||
from .models import Command, GameDataPackage, Room, db
|
from .models import Command, GameDataPackage, Room, db
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class WebHostContext(Context):
|
|||||||
|
|
||||||
multidata = self.decompress(room.seed.multidata)
|
multidata = self.decompress(room.seed.multidata)
|
||||||
game_data_packages = {}
|
game_data_packages = {}
|
||||||
for game in list(multidata["datapackage"]):
|
for game in list(multidata.get("datapackage", {})):
|
||||||
game_data = multidata["datapackage"][game]
|
game_data = multidata["datapackage"][game]
|
||||||
if "checksum" in game_data:
|
if "checksum" in game_data:
|
||||||
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||||
@@ -102,8 +102,9 @@ class WebHostContext(Context):
|
|||||||
# games package could be dropped from static data once all rooms embed data package
|
# games package could be dropped from static data once all rooms embed data package
|
||||||
del multidata["datapackage"][game]
|
del multidata["datapackage"][game]
|
||||||
else:
|
else:
|
||||||
data = Utils.restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
|
row = GameDataPackage.get(checksum=game_data["checksum"])
|
||||||
game_data_packages[game] = data
|
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete
|
||||||
|
game_data_packages[game] = Utils.restricted_loads(row.data)
|
||||||
|
|
||||||
return self._load(multidata, game_data_packages, True)
|
return self._load(multidata, game_data_packages, True)
|
||||||
|
|
||||||
@@ -168,13 +169,11 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||||
ping_interval=None, ssl=ssl_context)
|
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||||
ping_interval=None, ssl=ssl_context)
|
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
port = 0
|
port = 0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import tempfile
|
|||||||
import zipfile
|
import zipfile
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Dict, Optional, Any
|
from typing import Dict, Optional, Any, Union, List
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
from pony.orm import commit, db_session
|
from pony.orm import commit, db_session
|
||||||
@@ -22,7 +22,7 @@ from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID
|
|||||||
from .upload import upload_zip_to_db
|
from .upload import upload_zip_to_db
|
||||||
|
|
||||||
|
|
||||||
def get_meta(options_source: dict) -> dict:
|
def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[str], Dict[str, Any]]]:
|
||||||
plando_options = {
|
plando_options = {
|
||||||
options_source.get("plando_bosses", ""),
|
options_source.get("plando_bosses", ""),
|
||||||
options_source.get("plando_items", ""),
|
options_source.get("plando_items", ""),
|
||||||
@@ -39,7 +39,21 @@ def get_meta(options_source: dict) -> dict:
|
|||||||
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
|
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
|
||||||
"server_password": options_source.get("server_password", None),
|
"server_password": options_source.get("server_password", None),
|
||||||
}
|
}
|
||||||
return {"server_options": server_options, "plando_options": list(plando_options)}
|
generator_options = {
|
||||||
|
"spoiler": int(options_source.get("spoiler", 0)),
|
||||||
|
"race": race
|
||||||
|
}
|
||||||
|
|
||||||
|
if race:
|
||||||
|
server_options["item_cheat"] = False
|
||||||
|
server_options["remaining_mode"] = "disabled"
|
||||||
|
generator_options["spoiler"] = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"server_options": server_options,
|
||||||
|
"plando_options": list(plando_options),
|
||||||
|
"generator_options": generator_options,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate', methods=['GET', 'POST'])
|
@app.route('/generate', methods=['GET', 'POST'])
|
||||||
@@ -55,13 +69,8 @@ def generate(race=False):
|
|||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
meta = get_meta(request.form)
|
meta = get_meta(request.form, race)
|
||||||
meta["race"] = race
|
results, gen_options = roll_options(options, set(meta["plando_options"]))
|
||||||
results, gen_options = roll_options(options, meta["plando_options"])
|
|
||||||
|
|
||||||
if race:
|
|
||||||
meta["server_options"]["item_cheat"] = False
|
|
||||||
meta["server_options"]["remaining_mode"] = "disabled"
|
|
||||||
|
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
return render_template("checkResult.html", results=results)
|
return render_template("checkResult.html", results=results)
|
||||||
@@ -97,7 +106,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
|||||||
meta: Dict[str, Any] = {}
|
meta: Dict[str, Any] = {}
|
||||||
|
|
||||||
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
|
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
|
||||||
race = meta.setdefault("race", False)
|
race = meta.setdefault("generator_options", {}).setdefault("race", False)
|
||||||
|
|
||||||
def task():
|
def task():
|
||||||
target = tempfile.TemporaryDirectory()
|
target = tempfile.TemporaryDirectory()
|
||||||
@@ -114,13 +123,13 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
|||||||
erargs = parse_arguments(['--multi', str(playercount)])
|
erargs = parse_arguments(['--multi', str(playercount)])
|
||||||
erargs.seed = seed
|
erargs.seed = seed
|
||||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
|
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
|
||||||
erargs.spoiler = 0 if race else 3
|
erargs.spoiler = meta["generator_options"].get("spoiler", 0)
|
||||||
erargs.race = race
|
erargs.race = race
|
||||||
erargs.outputname = seedname
|
erargs.outputname = seedname
|
||||||
erargs.outputpath = target.name
|
erargs.outputpath = target.name
|
||||||
erargs.teams = 1
|
erargs.teams = 1
|
||||||
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
||||||
{"bosses", "items", "connections", "texts"}))
|
{"bosses", "items", "connections", "texts"}))
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||||
|
|||||||
@@ -116,7 +116,11 @@ def display_log(room: UUID):
|
|||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
if room.owner == session["_id"]:
|
if room.owner == session["_id"]:
|
||||||
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
file_path = os.path.join("logs", str(room.id) + ".txt")
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8")
|
||||||
|
return "Log File does not exist."
|
||||||
|
|
||||||
return "Access Denied", 403
|
return "Access Denied", 403
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Slot(db.Entity):
|
|||||||
class Room(db.Entity):
|
class Room(db.Entity):
|
||||||
id = PrimaryKey(UUID, default=uuid4)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
|
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
|
||||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
|
||||||
owner = Required(UUID, index=True)
|
owner = Required(UUID, index=True)
|
||||||
commands = Set('Command')
|
commands = Set('Command')
|
||||||
seed = Required('Seed', index=True)
|
seed = Required('Seed', index=True)
|
||||||
@@ -38,7 +38,7 @@ class Seed(db.Entity):
|
|||||||
rooms = Set(Room)
|
rooms = Set(Room)
|
||||||
multidata = Required(bytes, lazy=True)
|
multidata = Required(bytes, lazy=True)
|
||||||
owner = Required(UUID, index=True)
|
owner = Required(UUID, index=True)
|
||||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
|
||||||
slots = Set(Slot)
|
slots = Set(Slot)
|
||||||
spoiler = Optional(LongStr, lazy=True)
|
spoiler = Optional(LongStr, lazy=True)
|
||||||
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
|
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
|
||||||
|
|||||||
@@ -17,29 +17,8 @@ handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hin
|
|||||||
def create():
|
def create():
|
||||||
target_folder = local_path("WebHostLib", "static", "generated")
|
target_folder = local_path("WebHostLib", "static", "generated")
|
||||||
yaml_folder = os.path.join(target_folder, "configs")
|
yaml_folder = os.path.join(target_folder, "configs")
|
||||||
os.makedirs(yaml_folder, exist_ok=True)
|
|
||||||
|
|
||||||
for file in os.listdir(yaml_folder):
|
Options.generate_yaml_templates(yaml_folder)
|
||||||
full_path: str = os.path.join(yaml_folder, file)
|
|
||||||
if os.path.isfile(full_path):
|
|
||||||
os.unlink(full_path)
|
|
||||||
|
|
||||||
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
|
|
||||||
data = {option.default: 50}
|
|
||||||
for sub_option in ["random", "random-low", "random-high"]:
|
|
||||||
if sub_option != option.default:
|
|
||||||
data[sub_option] = 0
|
|
||||||
|
|
||||||
notes = {}
|
|
||||||
for name, number in getattr(option, "special_range_names", {}).items():
|
|
||||||
notes[name] = f"equivalent to {number}"
|
|
||||||
if number in data:
|
|
||||||
data[name] = data[number]
|
|
||||||
del data[number]
|
|
||||||
else:
|
|
||||||
data[name] = 0
|
|
||||||
|
|
||||||
return data, notes
|
|
||||||
|
|
||||||
def get_html_doc(option_type: type(Options.Option)) -> str:
|
def get_html_doc(option_type: type(Options.Option)) -> str:
|
||||||
if not option_type.__doc__:
|
if not option_type.__doc__:
|
||||||
@@ -61,23 +40,11 @@ def create():
|
|||||||
**Options.per_game_common_options,
|
**Options.per_game_common_options,
|
||||||
**world.option_definitions
|
**world.option_definitions
|
||||||
}
|
}
|
||||||
with open(local_path("WebHostLib", "templates", "options.yaml")) as f:
|
|
||||||
file_data = f.read()
|
|
||||||
res = Template(file_data).render(
|
|
||||||
options=all_options,
|
|
||||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
|
||||||
dictify_range=dictify_range,
|
|
||||||
)
|
|
||||||
|
|
||||||
del file_data
|
|
||||||
|
|
||||||
with open(os.path.join(target_folder, "configs", game_name + ".yaml"), "w", encoding="utf-8") as f:
|
|
||||||
f.write(res)
|
|
||||||
|
|
||||||
# Generate JSON files for player-settings pages
|
# Generate JSON files for player-settings pages
|
||||||
player_settings = {
|
player_settings = {
|
||||||
"baseOptions": {
|
"baseOptions": {
|
||||||
"description": "Generated by https://archipelago.gg/",
|
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||||
"game": game_name,
|
"game": game_name,
|
||||||
"name": "Player",
|
"name": "Player",
|
||||||
},
|
},
|
||||||
@@ -131,6 +98,7 @@ def create():
|
|||||||
"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),
|
||||||
|
"defaultValue": list(option.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
elif issubclass(option, Options.LocationSet):
|
elif issubclass(option, Options.LocationSet):
|
||||||
@@ -138,15 +106,17 @@ def create():
|
|||||||
"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),
|
||||||
|
"defaultValue": list(option.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
elif issubclass(option, Options.VerifyKeys):
|
elif issubclass(option, Options.VerifyKeys) and not issubclass(option, Options.OptionDict):
|
||||||
if option.valid_keys:
|
if option.valid_keys:
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
"type": "custom-list",
|
"type": "custom-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),
|
||||||
"options": list(option.valid_keys),
|
"options": list(option.valid_keys),
|
||||||
|
"defaultValue": list(option.default) if hasattr(option, "default") else []
|
||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
flask>=2.2.3
|
flask>=2.2.3
|
||||||
pony>=0.7.16
|
pony>=0.7.16; python_version <= '3.10'
|
||||||
|
pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11'
|
||||||
waitress>=2.1.2
|
waitress>=2.1.2
|
||||||
Flask-Caching>=2.0.2
|
Flask-Caching>=2.0.2
|
||||||
Flask-Compress>=1.13
|
Flask-Compress>=1.13
|
||||||
Flask-Limiter>=3.3.0
|
Flask-Limiter>=3.3.0
|
||||||
bokeh>=3.1.0
|
bokeh>=3.1.1
|
||||||
|
markupsafe>=2.1.3
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
|||||||
randomButton.classList.add('randomize-button');
|
randomButton.classList.add('randomize-button');
|
||||||
randomButton.setAttribute('data-key', setting);
|
randomButton.setAttribute('data-key', setting);
|
||||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, [select]));
|
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||||
if (currentSettings[gameName][setting] === 'random') {
|
if (currentSettings[gameName][setting] === 'random') {
|
||||||
randomButton.classList.add('active');
|
randomButton.classList.add('active');
|
||||||
select.disabled = true;
|
select.disabled = true;
|
||||||
@@ -185,7 +185,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
|||||||
randomButton.classList.add('randomize-button');
|
randomButton.classList.add('randomize-button');
|
||||||
randomButton.setAttribute('data-key', setting);
|
randomButton.setAttribute('data-key', setting);
|
||||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, [range]));
|
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||||
if (currentSettings[gameName][setting] === 'random') {
|
if (currentSettings[gameName][setting] === 'random') {
|
||||||
randomButton.classList.add('active');
|
randomButton.classList.add('active');
|
||||||
range.disabled = true;
|
range.disabled = true;
|
||||||
@@ -269,7 +269,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
|||||||
randomButton.setAttribute('data-key', setting);
|
randomButton.setAttribute('data-key', setting);
|
||||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||||
event, [specialRange, specialRangeSelect])
|
event, specialRange, specialRangeSelect)
|
||||||
);
|
);
|
||||||
if (currentSettings[gameName][setting] === 'random') {
|
if (currentSettings[gameName][setting] === 'random') {
|
||||||
randomButton.classList.add('active');
|
randomButton.classList.add('active');
|
||||||
@@ -294,23 +294,25 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
|||||||
return table;
|
return table;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRandomize = (event, inputElements) => {
|
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||||
const active = event.target.classList.contains('active');
|
const active = event.target.classList.contains('active');
|
||||||
const randomButton = event.target;
|
const randomButton = event.target;
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
randomButton.classList.remove('active');
|
randomButton.classList.remove('active');
|
||||||
for (const element of inputElements) {
|
inputElement.disabled = undefined;
|
||||||
element.disabled = undefined;
|
if (optionalSelectElement) {
|
||||||
updateGameSetting(element);
|
optionalSelectElement.disabled = undefined;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
randomButton.classList.add('active');
|
randomButton.classList.add('active');
|
||||||
for (const element of inputElements) {
|
inputElement.disabled = true;
|
||||||
element.disabled = true;
|
if (optionalSelectElement) {
|
||||||
updateGameSetting(randomButton);
|
optionalSelectElement.disabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateGameSetting(randomButton);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateBaseSetting = (event) => {
|
const updateBaseSetting = (event) => {
|
||||||
@@ -364,6 +366,7 @@ const generateGame = (raceMode = false) => {
|
|||||||
weights: { player: settings },
|
weights: { player: settings },
|
||||||
presetData: { player: settings },
|
presetData: { player: settings },
|
||||||
playerCount: 1,
|
playerCount: 1,
|
||||||
|
spoiler: 3,
|
||||||
race: raceMode ? '1' : '0',
|
race: raceMode ? '1' : '0',
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
window.location.href = response.data.url;
|
window.location.href = response.data.url;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const createDefaultSettings = (settingData) => {
|
|||||||
case 'items-list':
|
case 'items-list':
|
||||||
case 'locations-list':
|
case 'locations-list':
|
||||||
case 'custom-list':
|
case 'custom-list':
|
||||||
newSettings[game][gameSetting] = [];
|
newSettings[game][gameSetting] = setting.defaultValue;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1199,6 +1199,7 @@ const generateGame = (raceMode = false) => {
|
|||||||
weights: { player: JSON.stringify(settings) },
|
weights: { player: JSON.stringify(settings) },
|
||||||
presetData: { player: JSON.stringify(settings) },
|
presetData: { player: JSON.stringify(settings) },
|
||||||
playerCount: 1,
|
playerCount: 1,
|
||||||
|
spoiler: 3,
|
||||||
race: raceMode ? '1' : '0',
|
race: raceMode ? '1' : '0',
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
window.location.href = response.data.url;
|
window.location.href = response.data.url;
|
||||||
|
|||||||
@@ -119,6 +119,28 @@
|
|||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label for="spoiler">Spoiler Log:
|
||||||
|
<span class="interactive" data-tooltip="Generates a text listing all randomized elements.
|
||||||
|
Warning: playthrough can take a significant amount of time for larger multiworlds.">
|
||||||
|
(?)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="spoiler" id="spoiler">
|
||||||
|
{% if race -%}
|
||||||
|
<option value="0">Disabled in Race mode</option>
|
||||||
|
{%- else -%}
|
||||||
|
<option value="3">Enabled with playthrough and traversal</option>
|
||||||
|
<option value="2">Enabled with playthrough</option>
|
||||||
|
<option value="1">Enabled</option>
|
||||||
|
<option value="0">Disabled</option>
|
||||||
|
{%- endif -%}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,14 +30,14 @@
|
|||||||
<img src="/static/static/button-images/hamburger-menu-icon.png" alt="Menu" />
|
<img src="/static/static/button-images/hamburger-menu-icon.png" alt="Menu" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
</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 %}
|
||||||
|
|||||||
@@ -32,13 +32,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<form method=post>
|
<div style="display: flex; align-items: center;">
|
||||||
<div class="form-group">
|
<form method=post style="flex-grow: 1; margin-right: 1em;">
|
||||||
<label for="cmd"></label>
|
<div class="form-group">
|
||||||
<input class="form-control" type="text" id="cmd" name="cmd"
|
<label for="cmd"></label>
|
||||||
placeholder="Server Command. /help to list them, list gets appended to log.">
|
<input class="form-control" type="text" id="cmd" name="cmd"
|
||||||
</div>
|
placeholder="Server Command. /help to list them, list gets appended to log.">
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
<a href="{{ url_for("display_log", room=room.id) }}">
|
||||||
|
Open Log File...
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div id="logger"></div>
|
<div id="logger"></div>
|
||||||
<script type="application/ecmascript">
|
<script type="application/ecmascript">
|
||||||
let xmlhttp = new XMLHttpRequest();
|
let xmlhttp = new XMLHttpRequest();
|
||||||
|
|||||||
@@ -25,30 +25,34 @@
|
|||||||
<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 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.data %}
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
{% if patch.game == "Minecraft" %}
|
||||||
Download APMC File...</a>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
{% elif patch.game == "Factorio" %}
|
Download APMC File...</a>
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
{% elif patch.game == "Factorio" %}
|
||||||
Download Factorio Mod...</a>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
{% elif patch.game == "Kingdom Hearts 2" %}
|
Download Factorio Mod...</a>
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
{% elif patch.game == "Kingdom Hearts 2" %}
|
||||||
Download Kingdom Hearts 2 Mod...</a>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
{% elif patch.game == "Ocarina of Time" %}
|
Download Kingdom Hearts 2 Mod...</a>
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
{% elif patch.game == "Ocarina of Time" %}
|
||||||
Download APZ5 File...</a>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
{% elif patch.game == "VVVVVV" and room.seed.slots|length == 1 %}
|
Download APZ5 File...</a>
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
{% elif patch.game == "VVVVVV" and room.seed.slots|length == 1 %}
|
||||||
Download APV6 File...</a>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
|
Download APV6 File...</a>
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
|
||||||
Download APSM64EX File...</a>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
{% elif patch.game | supports_apdeltapatch %}
|
Download APSM64EX File...</a>
|
||||||
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
{% elif patch.game | supports_apdeltapatch %}
|
||||||
Download Patch File...</a>
|
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
||||||
{% elif patch.game == "Dark Souls III" and patch.data %}
|
Download Patch File...</a>
|
||||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
{% elif patch.game == "Dark Souls III" %}
|
||||||
Download JSON File...</a>
|
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||||
|
Download JSON File...</a>
|
||||||
|
{% else %}
|
||||||
|
No file to download for this game.
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
No file to download for this game.
|
No file to download for this game.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -31,12 +31,12 @@
|
|||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Game</th>
|
<th>Game</th>
|
||||||
|
<th>Status</th>
|
||||||
{% block custom_table_headers %}
|
{% block custom_table_headers %}
|
||||||
{# implement this block in game-specific multi trackers #}
|
{# implement this block in game-specific multi trackers #}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<th class="center-column">Checks</th>
|
<th class="center-column">Checks</th>
|
||||||
<th class="center-column">%</th>
|
<th class="center-column">%</th>
|
||||||
<th class="center-column">Status</th>
|
|
||||||
<th class="center-column hours">Last<br>Activity</th>
|
<th class="center-column hours">Last<br>Activity</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -47,13 +47,15 @@
|
|||||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||||
<td>{{ games[player] }}</td>
|
<td>{{ games[player] }}</td>
|
||||||
|
<td>{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing",
|
||||||
|
30: "Goal Completed"}.get(states[team, player], "Unknown State") }}</td>
|
||||||
{% block custom_table_row scoped %}
|
{% block custom_table_row scoped %}
|
||||||
{# implement this block in game-specific multi trackers #}
|
{# implement this block in game-specific multi trackers #}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<td class="center-column">{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}</td>
|
<td class="center-column" data-sort="{{ checks["Total"] }}">
|
||||||
|
{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}
|
||||||
|
</td>
|
||||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</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] -%}
|
{%- 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 -%}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div id="tracker-navigation">
|
<div id="tracker-navigation">
|
||||||
{% for enabled_tracker in enabled_multiworld_trackers %}
|
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
||||||
<a class="tracker-navigation-button{%- if enabled_tracker.current -%} selected{% endif %}"
|
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
|
||||||
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
<h2>Tutorials</h2>
|
<h2>Tutorials</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/tutorial/Archipelago/setup/en">Multiworld Setup Tutorial</a></li>
|
<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/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/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/advanced_settings/en">Advanced YAML Guide</a></li>
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import zipfile
|
|||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from io import BytesIO
|
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
|
||||||
|
from markupsafe import Markup
|
||||||
from pony.orm import commit, flush, select, rollback
|
from pony.orm import commit, flush, select, rollback
|
||||||
from pony.orm.core import TransactionIntegrityError
|
from pony.orm.core import TransactionIntegrityError
|
||||||
|
|
||||||
import MultiServer
|
import MultiServer
|
||||||
from NetUtils import NetworkSlot, SlotType
|
from NetUtils import 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
|
||||||
@@ -20,6 +21,41 @@ from .models import Seed, Room, Slot, GameDataPackage
|
|||||||
|
|
||||||
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||||
|
|
||||||
|
def process_multidata(compressed_multidata, files={}):
|
||||||
|
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
|
||||||
|
|
||||||
|
slots: typing.Set[Slot] = set()
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
for slot, slot_info in decompressed_multidata["slot_info"].items():
|
||||||
|
# Ignore Player Groups (e.g. item links)
|
||||||
|
if slot_info.type == SlotType.group:
|
||||||
|
continue
|
||||||
|
slots.add(Slot(data=files.get(slot, None),
|
||||||
|
player_name=slot_info.name,
|
||||||
|
player_id=slot,
|
||||||
|
game=slot_info.game))
|
||||||
|
flush() # commit slots
|
||||||
|
|
||||||
|
compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
|
||||||
|
return slots, compressed_multidata
|
||||||
|
|
||||||
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
||||||
if not owner:
|
if not owner:
|
||||||
@@ -29,7 +65,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||||||
flash(Markup("Error: Your .zip file only contains .yaml files. "
|
flash(Markup("Error: Your .zip file only contains .yaml files. "
|
||||||
'Did you mean to <a href="/generate">generate a game</a>?'))
|
'Did you mean to <a href="/generate">generate a game</a>?'))
|
||||||
return
|
return
|
||||||
slots: typing.Set[Slot] = set()
|
|
||||||
spoiler = ""
|
spoiler = ""
|
||||||
files = {}
|
files = {}
|
||||||
multidata = None
|
multidata = None
|
||||||
@@ -80,42 +116,7 @@ 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)
|
slots, multidata = process_multidata(multidata, files)
|
||||||
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:
|
|
||||||
for slot, slot_info in decompressed_multidata["slot_info"].items():
|
|
||||||
# Ignore Player Groups (e.g. item links)
|
|
||||||
if slot_info.type == SlotType.group:
|
|
||||||
continue
|
|
||||||
slots.add(Slot(data=files.get(slot, None),
|
|
||||||
player_name=slot_info.name,
|
|
||||||
player_id=slot,
|
|
||||||
game=slot_info.game))
|
|
||||||
|
|
||||||
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())
|
||||||
@@ -156,11 +157,11 @@ def uploads():
|
|||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
multidata = file.read()
|
multidata = file.read()
|
||||||
MultiServer.Context.decompress(multidata)
|
slots, multidata = process_multidata(multidata)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
|
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
|
||||||
else:
|
else:
|
||||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
seed = Seed(multidata=multidata, slots=slots, owner=session["_id"])
|
||||||
flush() # place into DB and generate ids
|
flush() # place into DB and generate ids
|
||||||
return redirect(url_for("view_seed", seed=seed.id))
|
return redirect(url_for("view_seed", seed=seed.id))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ from worlds.tloz import Items, Locations, Rom
|
|||||||
|
|
||||||
SYSTEM_MESSAGE_ID = 0
|
SYSTEM_MESSAGE_ID = 0
|
||||||
|
|
||||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua"
|
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_tloz.lua"
|
||||||
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running"
|
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_tloz.lua is running"
|
||||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua"
|
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_tloz.lua"
|
||||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||||
@@ -46,7 +46,7 @@ class ZeldaCommandProcessor(ClientCommandProcessor):
|
|||||||
logger.info(f"NES Status: {self.ctx.nes_status}")
|
logger.info(f"NES Status: {self.ctx.nes_status}")
|
||||||
|
|
||||||
def _cmd_toggle_msgs(self):
|
def _cmd_toggle_msgs(self):
|
||||||
"""Toggle displaying messages in bizhawk"""
|
"""Toggle displaying messages in EmuHawk"""
|
||||||
global DISPLAY_MSGS
|
global DISPLAY_MSGS
|
||||||
DISPLAY_MSGS = not DISPLAY_MSGS
|
DISPLAY_MSGS = not DISPLAY_MSGS
|
||||||
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
||||||
|
|||||||
BIN
data/discord-mark-blue.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
data/icon.ico
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 255 KiB |
BIN
data/icon.png
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
@@ -1,132 +0,0 @@
|
|||||||
-----------------------------------------------------------------------------
|
|
||||||
-- 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)
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
--
|
|
||||||
-- 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
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
-----------------------------------------------------------------------------
|
|
||||||
-- 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)
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
--
|
|
||||||
-- 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
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
-----------------------------------------------------------------------------
|
|
||||||
-- 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)
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
--
|
|
||||||
-- 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
|
|
||||||
|
|
||||||
function error(err)
|
|
||||||
print(err)
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
print("invalid table: sparse array")
|
|
||||||
print(n)
|
|
||||||
print("VAL:")
|
|
||||||
print(val)
|
|
||||||
print("STACK:")
|
|
||||||
print(stack)
|
|
||||||
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
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
-----------------------------------------------------------------------------
|
|
||||||
-- 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)
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
--
|
|
||||||
-- 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
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
-----------------------------------------------------------------------------
|
|
||||||
-- 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)
|
|
||||||
109
data/lua/common.lua
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
print("Loading AP lua connector script")
|
||||||
|
|
||||||
|
local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)")
|
||||||
|
lua_major = tonumber(lua_major)
|
||||||
|
lua_minor = tonumber(lua_minor)
|
||||||
|
-- lua compat shims
|
||||||
|
if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then
|
||||||
|
require("lua_5_3_compat")
|
||||||
|
end
|
||||||
|
|
||||||
|
function table.empty (self)
|
||||||
|
for _, _ in pairs(self) do
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local bizhawk_version = client.getversion()
|
||||||
|
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
|
||||||
|
bizhawk_major = tonumber(bizhawk_major)
|
||||||
|
bizhawk_minor = tonumber(bizhawk_minor)
|
||||||
|
if bizhawk_patch == "" then
|
||||||
|
bizhawk_patch = 0
|
||||||
|
else
|
||||||
|
bizhawk_patch = tonumber(bizhawk_patch)
|
||||||
|
end
|
||||||
|
|
||||||
|
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_major == 2 and bizhawk_minor >= 3 and bizhawk_minor <= 5)
|
||||||
|
local isGreaterOrEqualTo26 = bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor >= 6)
|
||||||
|
local isUntestedBizHawk = bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9)
|
||||||
|
local untestedBizHawkMessage = "Warning: this version of BizHawk is newer than we know about. If it doesn't work, consider downgrading to 2.9"
|
||||||
|
|
||||||
|
u8 = memory.read_u8
|
||||||
|
wU8 = memory.write_u8
|
||||||
|
u16 = memory.read_u16_le
|
||||||
|
uRange = memory.readbyterange
|
||||||
|
|
||||||
|
function getMaxMessageLength()
|
||||||
|
local denominator = 12
|
||||||
|
if is23Or24Or25 then
|
||||||
|
denominator = 11
|
||||||
|
end
|
||||||
|
return math.floor(client.screenwidth()/denominator)
|
||||||
|
end
|
||||||
|
|
||||||
|
function drawText(x, y, message, color)
|
||||||
|
if is23Or24Or25 then
|
||||||
|
gui.addmessage(message)
|
||||||
|
elseif isGreaterOrEqualTo26 then
|
||||||
|
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function clearScreen()
|
||||||
|
if is23Or24Or25 then
|
||||||
|
return
|
||||||
|
elseif isGreaterOrEqualTo26 then
|
||||||
|
drawText(0, 0, "", "black")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
itemMessages = {}
|
||||||
|
|
||||||
|
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 isGreaterOrEqualTo26 then
|
||||||
|
newTTL = itemMessages[k]["TTL"] - 1
|
||||||
|
end
|
||||||
|
itemMessages[k]["TTL"] = newTTL
|
||||||
|
found = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if found == false then
|
||||||
|
clearScreen()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function checkBizHawkVersion()
|
||||||
|
if not is23Or24Or25 and not isGreaterOrEqualTo26 then
|
||||||
|
print("Must use a version of BizHawk 2.3.1 or higher")
|
||||||
|
return false
|
||||||
|
elseif isUntestedBizHawk then
|
||||||
|
print(untestedBizHawkMessage)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function stripPrefix(s, p)
|
||||||
|
return (s:sub(0, #p) == p) and s:sub(#p+1) or s
|
||||||
|
end
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
local socket = require("socket")
|
local socket = require("socket")
|
||||||
local json = require('json')
|
local json = require('json')
|
||||||
local math = require('math')
|
local math = require('math')
|
||||||
|
require("common")
|
||||||
|
|
||||||
local STATE_OK = "Ok"
|
local STATE_OK = "Ok"
|
||||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||||
@@ -32,8 +33,6 @@ local frames_with_no_item = 0
|
|||||||
local ItemTableStart = 0xfe9d
|
local ItemTableStart = 0xfe9d
|
||||||
local PlayerSlotAddress = 0xfff9
|
local PlayerSlotAddress = 0xfff9
|
||||||
|
|
||||||
local itemMessages = {}
|
|
||||||
|
|
||||||
local nullObjectId = 0xB4
|
local nullObjectId = 0xB4
|
||||||
local ItemsReceived = nil
|
local ItemsReceived = nil
|
||||||
local sha256hash = nil
|
local sha256hash = nil
|
||||||
@@ -101,17 +100,6 @@ local current_bat_ap_item = nil
|
|||||||
|
|
||||||
local was_in_number_room = false
|
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)
|
function uRangeRam(address, bytes)
|
||||||
data = memory.read_bytes_as_array(address, bytes, "Main RAM")
|
data = memory.read_bytes_as_array(address, bytes, "Main RAM")
|
||||||
return data
|
return data
|
||||||
@@ -125,23 +113,6 @@ function uRangeAddress(address, bytes)
|
|||||||
return data
|
return data
|
||||||
end
|
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()
|
local function createForeignItemsByRoom()
|
||||||
foreign_items_by_room = {}
|
foreign_items_by_room = {}
|
||||||
if foreign_items == nil then
|
if foreign_items == nil then
|
||||||
@@ -294,94 +265,11 @@ function processBlock(block)
|
|||||||
end
|
end
|
||||||
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()
|
function getAllRam()
|
||||||
uRangeRAM(0,128);
|
uRangeRAM(0,128);
|
||||||
return data
|
return data
|
||||||
end
|
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()
|
local function alive_mode()
|
||||||
return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00)
|
return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00)
|
||||||
end
|
end
|
||||||
@@ -569,8 +457,7 @@ end
|
|||||||
|
|
||||||
function main()
|
function main()
|
||||||
memory.usememorydomain("System Bus")
|
memory.usememorydomain("System Bus")
|
||||||
if (is23Or24Or25 or is26To28) == false then
|
if not checkBizHawkVersion() then
|
||||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local playerSlot = memory.read_u8(PlayerSlotAddress)
|
local playerSlot = memory.read_u8(PlayerSlotAddress)
|
||||||
@@ -711,7 +598,7 @@ function main()
|
|||||||
if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then
|
if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then
|
||||||
pending_local_items_collected[localItemLocations[tostring(carry_item)]] =
|
pending_local_items_collected[localItemLocations[tostring(carry_item)]] =
|
||||||
localItemLocations[tostring(carry_item)]
|
localItemLocations[tostring(carry_item)]
|
||||||
table.remove(localItemLocations, tostring(carry_item))
|
localItemLocations[tostring(carry_item)] = nil
|
||||||
skip_inventory_items[carry_item] = carry_item
|
skip_inventory_items[carry_item] = carry_item
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
local socket = require("socket")
|
local socket = require("socket")
|
||||||
local json = require('json')
|
local json = require('json')
|
||||||
local math = require('math')
|
local math = require('math')
|
||||||
|
require("common")
|
||||||
|
|
||||||
local STATE_OK = "Ok"
|
local STATE_OK = "Ok"
|
||||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||||
@@ -102,15 +103,12 @@ local noOverworldItemsLookup = {
|
|||||||
[500] = 0x12,
|
[500] = 0x12,
|
||||||
}
|
}
|
||||||
|
|
||||||
local itemMessages = {}
|
|
||||||
local consumableStacks = nil
|
local consumableStacks = nil
|
||||||
local prevstate = ""
|
local prevstate = ""
|
||||||
local curstate = STATE_UNINITIALIZED
|
local curstate = STATE_UNINITIALIZED
|
||||||
local ff1Socket = nil
|
local ff1Socket = nil
|
||||||
local frame = 0
|
local frame = 0
|
||||||
|
|
||||||
local u8 = nil
|
|
||||||
local wU8 = nil
|
|
||||||
local isNesHawk = false
|
local isNesHawk = false
|
||||||
|
|
||||||
|
|
||||||
@@ -134,9 +132,6 @@ local function defineMemoryFunctions()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local memDomain = defineMemoryFunctions()
|
local memDomain = defineMemoryFunctions()
|
||||||
u8 = memory.read_u8
|
|
||||||
wU8 = memory.write_u8
|
|
||||||
uRange = memory.readbyterange
|
|
||||||
|
|
||||||
local function StateOKForMainLoop()
|
local function StateOKForMainLoop()
|
||||||
memDomain.saveram()
|
memDomain.saveram()
|
||||||
@@ -146,83 +141,6 @@ local function StateOKForMainLoop()
|
|||||||
return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2)
|
return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2)
|
||||||
end
|
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", nil, nil, 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 generateLocationChecked()
|
function generateLocationChecked()
|
||||||
memDomain.saveram()
|
memDomain.saveram()
|
||||||
data = uRange(0x01FF, 0x101)
|
data = uRange(0x01FF, 0x101)
|
||||||
@@ -316,7 +234,14 @@ function getEmptyArmorSlots()
|
|||||||
end
|
end
|
||||||
return ret
|
return ret
|
||||||
end
|
end
|
||||||
|
local 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
|
||||||
function processBlock(block)
|
function processBlock(block)
|
||||||
local msgBlock = block['messages']
|
local msgBlock = block['messages']
|
||||||
if msgBlock ~= nil then
|
if msgBlock ~= nil then
|
||||||
@@ -448,18 +373,6 @@ function processBlock(block)
|
|||||||
end
|
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()
|
function receive()
|
||||||
l, e = ff1Socket:receive()
|
l, e = ff1Socket:receive()
|
||||||
if e == 'closed' then
|
if e == 'closed' then
|
||||||
@@ -501,8 +414,7 @@ function receive()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function main()
|
function main()
|
||||||
if (is23Or24Or25 or is26To28) == false then
|
if not checkBizHawkVersion() then
|
||||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
server, error = socket.bind('localhost', 52980)
|
server, error = socket.bind('localhost', 52980)
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
-- This script attempts to implement the basic functionality needed in order for
|
-- 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
|
-- the LADXR Archipelago client to be able to talk to EmuHawk instead of RetroArch
|
||||||
-- by reproducing the RetroArch API with BizHawk's Lua interface.
|
-- by reproducing the RetroArch API with EmuHawk's Lua interface.
|
||||||
--
|
--
|
||||||
-- RetroArch UDP API: https://github.com/libretro/RetroArch/blob/master/command.c
|
-- RetroArch UDP API: https://github.com/libretro/RetroArch/blob/master/command.c
|
||||||
--
|
--
|
||||||
@@ -16,19 +16,19 @@
|
|||||||
-- commands are supported right now.
|
-- commands are supported right now.
|
||||||
--
|
--
|
||||||
-- USAGE:
|
-- USAGE:
|
||||||
-- Load this script in BizHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script")
|
-- Load this script in EmuHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script", or drag+drop)
|
||||||
--
|
--
|
||||||
-- All inconsistencies (like missing newlines for some commands) of the RetroArch
|
-- 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
|
-- 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).
|
-- 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
|
-- 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
|
-- cores supported by EmuHawk, please let me know. Note that GET_STATUS, at the very least, will
|
||||||
-- have to be adjusted.
|
-- have to be adjusted.
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
-- NOTE:
|
-- NOTE:
|
||||||
-- BizHawk's Lua API is very trigger-happy on throwing exceptions.
|
-- EmuHawk's Lua API is very trigger-happy on throwing exceptions.
|
||||||
-- Emulation will continue fine, but the RetroArch API layer will stop working. This
|
-- 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
|
-- is indicated only by an exception visible in the Lua console, which most players
|
||||||
-- will probably not have in the foreground.
|
-- will probably not have in the foreground.
|
||||||
@@ -43,12 +43,12 @@
|
|||||||
|
|
||||||
|
|
||||||
local socket = require("socket")
|
local socket = require("socket")
|
||||||
local udp = socket.udp()
|
local udp = socket.socket.udp()
|
||||||
|
require('common')
|
||||||
|
|
||||||
udp:setsockname('127.0.0.1', 55355)
|
udp:setsockname('127.0.0.1', 55355)
|
||||||
udp:settimeout(0)
|
udp:settimeout(0)
|
||||||
|
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
-- Attempt to lessen the CPU load by only polling the UDP socket every x frames.
|
-- 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.
|
-- x = 10 is entirely arbitrary, very little thought went into it.
|
||||||
@@ -82,7 +82,7 @@ while true do
|
|||||||
-- "GET_STATUS PLAYING game_boy,AP_62468482466172374046_P1_Lonk,crc32=3ecb7b6f"
|
-- "GET_STATUS PLAYING game_boy,AP_62468482466172374046_P1_Lonk,crc32=3ecb7b6f"
|
||||||
-- CRC32 isn't readily available through the Lua API. We could calculate
|
-- 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
|
-- it ourselves, but since LADXR doesn't make use of this field it is
|
||||||
-- simply replaced by the hash that BizHawk _does_ make available.
|
-- simply replaced by the hash that EmuHawk _does_ make available.
|
||||||
|
|
||||||
udp:sendto(
|
udp:sendto(
|
||||||
"GET_STATUS " .. status .. " game_boy," ..
|
"GET_STATUS " .. status .. " game_boy," ..
|
||||||
@@ -97,6 +97,7 @@ while true do
|
|||||||
end
|
end
|
||||||
elseif command == "READ_CORE_MEMORY" then
|
elseif command == "READ_CORE_MEMORY" then
|
||||||
local _, address, length = string.match(data, "(%S+) (%S+) (%S+)")
|
local _, address, length = string.match(data, "(%S+) (%S+) (%S+)")
|
||||||
|
address = stripPrefix(address, "0x")
|
||||||
address = tonumber(address, 16)
|
address = tonumber(address, 16)
|
||||||
length = tonumber(length)
|
length = tonumber(length)
|
||||||
|
|
||||||
@@ -116,12 +117,14 @@ while true do
|
|||||||
udp:sendto(reply, msg_or_ip, port_or_nil)
|
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||||
elseif command == "WRITE_CORE_MEMORY" then
|
elseif command == "WRITE_CORE_MEMORY" then
|
||||||
local _, address = string.match(data, "(%S+) (%S+)")
|
local _, address = string.match(data, "(%S+) (%S+)")
|
||||||
|
address = stripPrefix(address, "0x")
|
||||||
address = tonumber(address, 16)
|
address = tonumber(address, 16)
|
||||||
|
|
||||||
local to_write = {}
|
local to_write = {}
|
||||||
local i = 1
|
local i = 1
|
||||||
for byte_str in string.gmatch(data, "%S+") do
|
for byte_str in string.gmatch(data, "%S+") do
|
||||||
if i > 2 then
|
if i > 2 then
|
||||||
|
byte_str = stripPrefix(byte_str, "0x")
|
||||||
table.insert(to_write, tonumber(byte_str, 16))
|
table.insert(to_write, tonumber(byte_str, 16))
|
||||||
end
|
end
|
||||||
i = i + 1
|
i = i + 1
|
||||||
|
|||||||
723
data/lua/connector_mmbn3.lua
Normal file
@@ -0,0 +1,723 @@
|
|||||||
|
local socket = require("socket")
|
||||||
|
local json = require('json')
|
||||||
|
local math = require('math')
|
||||||
|
require('common')
|
||||||
|
|
||||||
|
local last_modified_date = '2023-31-05' -- Should be the last modified date
|
||||||
|
local script_version = 4
|
||||||
|
|
||||||
|
local bizhawk_version = client.getversion()
|
||||||
|
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
|
||||||
|
bizhawk_major = tonumber(bizhawk_major)
|
||||||
|
bizhawk_minor = tonumber(bizhawk_minor)
|
||||||
|
if bizhawk_patch == "" then
|
||||||
|
bizhawk_patch = 0
|
||||||
|
else
|
||||||
|
bizhawk_patch = tonumber(bizhawk_patch)
|
||||||
|
end
|
||||||
|
|
||||||
|
local STATE_OK = "Ok"
|
||||||
|
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||||
|
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||||
|
local STATE_UNINITIALIZED = "Uninitialized"
|
||||||
|
|
||||||
|
local prevstate = ""
|
||||||
|
local curstate = STATE_UNINITIALIZED
|
||||||
|
local mmbn3Socket = nil
|
||||||
|
local frame = 0
|
||||||
|
|
||||||
|
-- States
|
||||||
|
local ITEMSTATE_NONINITIALIZED = "Game Not Yet Started" -- Game has not yet started
|
||||||
|
local ITEMSTATE_NONITEM = "Non-Itemable State" -- Do not send item now. RAM is not capable of holding
|
||||||
|
local ITEMSTATE_IDLE = "Item State Ready" -- Ready for the next item if there are any
|
||||||
|
local ITEMSTATE_SENT = "Item Sent Not Claimed" -- The ItemBit is set, but the dialog has not been closed yet
|
||||||
|
local itemState = ITEMSTATE_NONINITIALIZED
|
||||||
|
|
||||||
|
local itemQueued = nil
|
||||||
|
local itemQueueCounter = 120
|
||||||
|
|
||||||
|
local debugEnabled = false
|
||||||
|
local game_complete = false
|
||||||
|
|
||||||
|
local backup_bytes = nil
|
||||||
|
|
||||||
|
local itemsReceived = {}
|
||||||
|
local previousMessageBit = 0x00
|
||||||
|
|
||||||
|
local key_item_start_address = 0x20019C0
|
||||||
|
|
||||||
|
-- The Canary Byte is a flag byte that is intentionally left unused. If this byte is FF, then we know the flag
|
||||||
|
-- data cannot be trusted, so we don't send checks.
|
||||||
|
local canary_byte = 0x20001A9
|
||||||
|
|
||||||
|
local charDict = {
|
||||||
|
[' ']=0x00,['0']=0x01,['1']=0x02,['2']=0x03,['3']=0x04,['4']=0x05,['5']=0x06,['6']=0x07,['7']=0x08,['8']=0x09,['9']=0x0A,
|
||||||
|
['A']=0x0B,['B']=0x0C,['C']=0x0D,['D']=0x0E,['E']=0x0F,['F']=0x10,['G']=0x11,['H']=0x12,['I']=0x13,['J']=0x14,['K']=0x15,
|
||||||
|
['L']=0x16,['M']=0x17,['N']=0x18,['O']=0x19,['P']=0x1A,['Q']=0x1B,['R']=0x1C,['S']=0x1D,['T']=0x1E,['U']=0x1F,['V']=0x20,
|
||||||
|
['W']=0x21,['X']=0x22,['Y']=0x23,['Z']=0x24,['a']=0x25,['b']=0x26,['c']=0x27,['d']=0x28,['e']=0x29,['f']=0x2A,['g']=0x2B,
|
||||||
|
['h']=0x2C,['i']=0x2D,['j']=0x2E,['k']=0x2F,['l']=0x30,['m']=0x31,['n']=0x32,['o']=0x33,['p']=0x34,['q']=0x35,['r']=0x36,
|
||||||
|
['s']=0x37,['t']=0x38,['u']=0x39,['v']=0x3A,['w']=0x3B,['x']=0x3C,['y']=0x3D,['z']=0x3E,['-']=0x3F,['×']=0x40,[']=']=0x41,
|
||||||
|
[':']=0x42,['+']=0x43,['÷']=0x44,['※']=0x45,['*']=0x46,['!']=0x47,['?']=0x48,['%']=0x49,['&']=0x4A,[',']=0x4B,['⋯']=0x4C,
|
||||||
|
['.']=0x4D,['・']=0x4E,[';']=0x4F,['\'']=0x50,['\"']=0x51,['~']=0x52,['/']=0x53,['(']=0x54,[')']=0x55,['「']=0x56,['」']=0x57,
|
||||||
|
["[V2]"]=0x58,["[V3]"]=0x59,["[V4]"]=0x5A,["[V5]"]=0x5B,['@']=0x5C,['♥']=0x5D,['♪']=0x5E,["[MB]"]=0x5F,['■']=0x60,['_']=0x61,
|
||||||
|
["[circle1]"]=0x62,["[circle2]"]=0x63,["[cross1]"]=0x64,["[cross2]"]=0x65,["[bracket1]"]=0x66,["[bracket2]"]=0x67,["[ModTools1]"]=0x68,
|
||||||
|
["[ModTools2]"]=0x69,["[ModTools3]"]=0x6A,['Σ']=0x6B,['Ω']=0x6C,['α']=0x6D,['β']=0x6E,['#']=0x6F,['…']=0x70,['>']=0x71,
|
||||||
|
['<']=0x72,['エ']=0x73,["[BowneGlobal1]"]=0x74,["[BowneGlobal2]"]=0x75,["[BowneGlobal3]"]=0x76,["[BowneGlobal4]"]=0x77,
|
||||||
|
["[BowneGlobal5]"]=0x78,["[BowneGlobal6]"]=0x79,["[BowneGlobal7]"]=0x7A,["[BowneGlobal8]"]=0x7B,["[BowneGlobal9]"]=0x7C,
|
||||||
|
["[BowneGlobal10]"]=0x7D,["[BowneGlobal11]"]=0x7E,['\n']=0xE8
|
||||||
|
}
|
||||||
|
|
||||||
|
local TableConcat = function(t1,t2)
|
||||||
|
for i=1,#t2 do
|
||||||
|
t1[#t1+1] = t2[i]
|
||||||
|
end
|
||||||
|
return t1
|
||||||
|
end
|
||||||
|
local int32ToByteList_le = function(x)
|
||||||
|
bytes = {}
|
||||||
|
hexString = string.format("%08x", x)
|
||||||
|
for i=#hexString, 1, -2 do
|
||||||
|
hbyte = hexString:sub(i-1, i)
|
||||||
|
table.insert(bytes,tonumber(hbyte,16))
|
||||||
|
end
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
local int16ToByteList_le = function(x)
|
||||||
|
bytes = {}
|
||||||
|
hexString = string.format("%04x", x)
|
||||||
|
for i=#hexString, 1, -2 do
|
||||||
|
hbyte = hexString:sub(i-1, i)
|
||||||
|
table.insert(bytes,tonumber(hbyte,16))
|
||||||
|
end
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
|
||||||
|
local IsInMenu = function()
|
||||||
|
return bit.band(memory.read_u8(0x0200027A),0x10) ~= 0
|
||||||
|
end
|
||||||
|
local IsInTransition = function()
|
||||||
|
return bit.band(memory.read_u8(0x02001880), 0x10) ~= 0
|
||||||
|
end
|
||||||
|
local IsInDialog = function()
|
||||||
|
return bit.band(memory.read_u8(0x02009480),0x01) ~= 0
|
||||||
|
end
|
||||||
|
local IsInBattle = function()
|
||||||
|
return memory.read_u8(0x020097F8) == 0x08
|
||||||
|
end
|
||||||
|
local IsItemQueued = function()
|
||||||
|
return memory.read_u8(0x2000224) == 0x01
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This function actually determines when you're on ANY full-screen menu (navi cust, link battle, etc.) but we
|
||||||
|
-- don't want to check any locations there either so it's fine.
|
||||||
|
local IsOnTitle = function()
|
||||||
|
return bit.band(memory.read_u8(0x020097F8),0x04) == 0
|
||||||
|
end
|
||||||
|
local IsItemable = function()
|
||||||
|
return not IsInMenu() and not IsInTransition() and not IsInDialog() and not IsInBattle() and not IsOnTitle() and not IsItemQueued()
|
||||||
|
end
|
||||||
|
|
||||||
|
local is_game_complete = function()
|
||||||
|
if IsOnTitle() or itemState == ITEMSTATE_NONINITIALIZED then return game_complete end
|
||||||
|
|
||||||
|
-- If the game is already marked complete, do not read memory
|
||||||
|
if game_complete then return true end
|
||||||
|
local is_alpha_defeated = bit.band(memory.read_u8(0x2000433), 0x01) ~= 0
|
||||||
|
|
||||||
|
if (is_alpha_defeated) then
|
||||||
|
game_complete = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Game is still ongoing
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local saveItemIndexToRAM = function(newIndex)
|
||||||
|
memory.write_s16_le(0x20000AE,newIndex)
|
||||||
|
end
|
||||||
|
|
||||||
|
local loadItemIndexFromRAM = function()
|
||||||
|
last_index = memory.read_s16_le(0x20000AE)
|
||||||
|
if (last_index < 0) then
|
||||||
|
last_index = 0
|
||||||
|
saveItemIndexToRAM(0)
|
||||||
|
end
|
||||||
|
return last_index
|
||||||
|
end
|
||||||
|
|
||||||
|
local loadPlayerNameFromROM = function()
|
||||||
|
return memory.read_bytes_as_array(0x7FFFC0,63,"ROM")
|
||||||
|
end
|
||||||
|
|
||||||
|
local check_all_locations = function()
|
||||||
|
local location_checks = {}
|
||||||
|
-- Title Screen should not check items
|
||||||
|
if itemState == ITEMSTATE_NONINITIALIZED or IsInTransition() then
|
||||||
|
return location_checks
|
||||||
|
end
|
||||||
|
if memory.read_u8(canary_byte) == 0xFF then
|
||||||
|
return location_checks
|
||||||
|
end
|
||||||
|
for k,v in pairs(memory.read_bytes_as_dict(0x02000000, 0x434)) do
|
||||||
|
str_k = string.format("%x", k)
|
||||||
|
location_checks[str_k] = v
|
||||||
|
end
|
||||||
|
return location_checks
|
||||||
|
end
|
||||||
|
|
||||||
|
local Check_Progressive_Undernet_ID = function()
|
||||||
|
ordered_offsets = { 0x020019DB,0x020019DC,0x020019DD,0x020019DE,0x020019DF,0x020019E0,0x020019FA,0x020019E2 }
|
||||||
|
for i=1,#ordered_offsets do
|
||||||
|
offset=ordered_offsets[i]
|
||||||
|
|
||||||
|
if memory.read_u8(offset) == 0 then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 9
|
||||||
|
end
|
||||||
|
local GenerateTextBytes = function(message)
|
||||||
|
bytes = {}
|
||||||
|
for i = 1, #message do
|
||||||
|
local c = message:sub(i,i)
|
||||||
|
table.insert(bytes, charDict[c])
|
||||||
|
end
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Item Message Generation functions
|
||||||
|
local Next_Progressive_Undernet_ID = function(index)
|
||||||
|
ordered_IDs = { 27,28,29,30,31,32,58,34}
|
||||||
|
if index > #ordered_IDs then
|
||||||
|
--It shouldn't reach this point, but if it does, just give another GigFreez I guess
|
||||||
|
return 34
|
||||||
|
end
|
||||||
|
item_index=ordered_IDs[index]
|
||||||
|
return item_index
|
||||||
|
end
|
||||||
|
local Extra_Progressive_Undernet = function()
|
||||||
|
fragBytes = int32ToByteList_le(20)
|
||||||
|
bytes = {
|
||||||
|
0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF
|
||||||
|
}
|
||||||
|
bytes = TableConcat(bytes, GenerateTextBytes("The extra data\ndecompiles into:\n\"20 BugFrags\"!!"))
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
|
||||||
|
local GenerateChipGet = function(chip, code, amt)
|
||||||
|
chipBytes = int16ToByteList_le(chip)
|
||||||
|
bytes = {
|
||||||
|
0xF6, 0x10, chipBytes[1], chipBytes[2], code, amt,
|
||||||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['c'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
|
||||||
|
|
||||||
|
}
|
||||||
|
if chip < 256 then
|
||||||
|
bytes = TableConcat(bytes, {
|
||||||
|
charDict['\"'], 0xF9,0x00,chipBytes[1],0x01,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
|
||||||
|
})
|
||||||
|
else
|
||||||
|
bytes = TableConcat(bytes, {
|
||||||
|
charDict['\"'], 0xF9,0x00,chipBytes[1],0x02,0x00,0xF9,0x00,code,0x03, charDict['\"'],charDict['!'],charDict['!']
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
local GenerateKeyItemGet = function(item, amt)
|
||||||
|
bytes = {
|
||||||
|
0xF6, 0x00, item, amt,
|
||||||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
|
||||||
|
charDict['\"'], 0xF9, 0x00, item, 0x00, charDict['\"'],charDict['!'],charDict['!']
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
local GenerateSubChipGet = function(subchip, amt)
|
||||||
|
-- SubChips have an extra bit of trouble. If you have too many, they're supposed to skip to another text bank that doesn't give you the item
|
||||||
|
-- Instead, I'm going to just let it get eaten
|
||||||
|
bytes = {
|
||||||
|
0xF6, 0x20, subchip, amt, 0xFF, 0xFF, 0xFF,
|
||||||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'],
|
||||||
|
charDict['S'], charDict['u'], charDict['b'], charDict['C'], charDict['h'], charDict['i'], charDict['p'], charDict[' '], charDict['f'], charDict['o'], charDict['r'], charDict['\n'],
|
||||||
|
charDict['\"'], 0xF9, 0x00, subchip, 0x00, charDict['\"'],charDict['!'],charDict['!']
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
local GenerateZennyGet = function(amt)
|
||||||
|
zennyBytes = int32ToByteList_le(amt)
|
||||||
|
bytes = {
|
||||||
|
0xF6, 0x30, zennyBytes[1], zennyBytes[2], zennyBytes[3], zennyBytes[4], 0xFF, 0xFF, 0xFF,
|
||||||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict['\n'], charDict['\"']
|
||||||
|
}
|
||||||
|
-- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
|
||||||
|
zennyStr = tostring(amt)
|
||||||
|
for i = 1, #zennyStr do
|
||||||
|
local c = zennyStr:sub(i,i)
|
||||||
|
table.insert(bytes, charDict[c])
|
||||||
|
end
|
||||||
|
bytes = TableConcat(bytes, {
|
||||||
|
charDict[' '], charDict['Z'], charDict['e'], charDict['n'], charDict['n'], charDict['y'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
|
||||||
|
})
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
local GenerateProgramGet = function(program, color, amt)
|
||||||
|
bytes = {
|
||||||
|
0xF6, 0x40, (program * 4), amt, color,
|
||||||
|
charDict['G'], charDict['o'], charDict['t'], charDict[' '], charDict['a'], charDict[' '], charDict['N'], charDict['a'], charDict['v'], charDict['i'], charDict['\n'],
|
||||||
|
charDict['C'], charDict['u'], charDict['s'], charDict['t'], charDict['o'], charDict['m'], charDict['i'], charDict['z'], charDict['e'], charDict['r'], charDict[' '], charDict['P'], charDict['r'], charDict['o'], charDict['g'], charDict['r'], charDict['a'], charDict['m'], charDict[':'], charDict['\n'],
|
||||||
|
charDict['\"'], 0xF9, 0x00, program, 0x05, charDict['\"'],charDict['!'],charDict['!']
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
local GenerateBugfragGet = function(amt)
|
||||||
|
fragBytes = int32ToByteList_le(amt)
|
||||||
|
bytes = {
|
||||||
|
0xF6, 0x50, fragBytes[1], fragBytes[2], fragBytes[3], fragBytes[4], 0xFF, 0xFF, 0xFF,
|
||||||
|
charDict['G'], charDict['o'], charDict['t'], charDict[':'], charDict['\n'], charDict['\"']
|
||||||
|
}
|
||||||
|
-- The text needs to be added one char at a time, so we need to convert the number to a string then iterate through it
|
||||||
|
bugFragStr = tostring(amt)
|
||||||
|
for i = 1, #bugFragStr do
|
||||||
|
local c = bugFragStr:sub(i,i)
|
||||||
|
table.insert(bytes, charDict[c])
|
||||||
|
end
|
||||||
|
bytes = TableConcat(bytes, {
|
||||||
|
charDict[' '], charDict['B'], charDict['u'], charDict['g'], charDict['F'], charDict['r'], charDict['a'], charDict['g'], charDict['s'], charDict['\"'],charDict['!'],charDict['!']
|
||||||
|
})
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
local GenerateGetMessageFromItem = function(item)
|
||||||
|
--Special case for progressive undernet
|
||||||
|
if item["type"] == "undernet" then
|
||||||
|
undernet_id = Check_Progressive_Undernet_ID()
|
||||||
|
if undernet_id > 8 then
|
||||||
|
return Extra_Progressive_Undernet()
|
||||||
|
end
|
||||||
|
return GenerateKeyItemGet(Next_Progressive_Undernet_ID(undernet_id),1)
|
||||||
|
elseif item["type"] == "chip" then
|
||||||
|
return GenerateChipGet(item["itemID"], item["subItemID"], item["count"])
|
||||||
|
elseif item["type"] == "key" then
|
||||||
|
return GenerateKeyItemGet(item["itemID"], item["count"])
|
||||||
|
elseif item["type"] == "subchip" then
|
||||||
|
return GenerateSubChipGet(item["itemID"], item["count"])
|
||||||
|
elseif item["type"] == "zenny" then
|
||||||
|
return GenerateZennyGet(item["count"])
|
||||||
|
elseif item["type"] == "program" then
|
||||||
|
return GenerateProgramGet(item["itemID"], item["subItemID"], item["count"])
|
||||||
|
elseif item["type"] == "bugfrag" then
|
||||||
|
return GenerateBugfragGet(item["count"])
|
||||||
|
end
|
||||||
|
|
||||||
|
return GenerateTextBytes("Empty Message")
|
||||||
|
end
|
||||||
|
|
||||||
|
local GetMessage = function(item)
|
||||||
|
startBytes = {0x02, 0x00}
|
||||||
|
playerLockBytes = {0xF8,0x00, 0xF8, 0x10}
|
||||||
|
msgOpenBytes = {0xF1, 0x02}
|
||||||
|
textBytes = GenerateTextBytes("Receiving\ndata from\n"..item["sender"]..".")
|
||||||
|
dotdotWaitBytes = {0xEA,0x00,0x0A,0x00,0x4D,0xEA,0x00,0x0A,0x00,0x4D}
|
||||||
|
continueBytes = {0xEB, 0xE9}
|
||||||
|
-- continueBytes = {0xE9}
|
||||||
|
playReceiveAnimationBytes = {0xF8,0x04,0x18}
|
||||||
|
chipGiveBytes = GenerateGetMessageFromItem(item)
|
||||||
|
playerFinishBytes = {0xF8, 0x0C}
|
||||||
|
playerUnlockBytes={0xEB, 0xF8, 0x08}
|
||||||
|
-- playerUnlockBytes={0xF8, 0x08}
|
||||||
|
endMessageBytes = {0xF8, 0x10, 0xE7}
|
||||||
|
|
||||||
|
bytes = {}
|
||||||
|
bytes = TableConcat(bytes,startBytes)
|
||||||
|
bytes = TableConcat(bytes,playerLockBytes)
|
||||||
|
bytes = TableConcat(bytes,msgOpenBytes)
|
||||||
|
bytes = TableConcat(bytes,textBytes)
|
||||||
|
bytes = TableConcat(bytes,dotdotWaitBytes)
|
||||||
|
bytes = TableConcat(bytes,continueBytes)
|
||||||
|
bytes = TableConcat(bytes,playReceiveAnimationBytes)
|
||||||
|
bytes = TableConcat(bytes,chipGiveBytes)
|
||||||
|
bytes = TableConcat(bytes,playerFinishBytes)
|
||||||
|
bytes = TableConcat(bytes,playerUnlockBytes)
|
||||||
|
bytes = TableConcat(bytes,endMessageBytes)
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
|
||||||
|
local getChipCodeIndex = function(chip_id, chip_code)
|
||||||
|
chipCodeArrayStartAddress = 0x8011510 + (0x20 * chip_id)
|
||||||
|
for i=1,6 do
|
||||||
|
currentCode = memory.read_u8(chipCodeArrayStartAddress + (i-1))
|
||||||
|
if currentCode == chip_code then
|
||||||
|
return i-1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local getProgramColorIndex = function(program_id, program_color)
|
||||||
|
-- The general case, most programs use white pink or yellow. This is the values the enums already have
|
||||||
|
if program_id >= 20 and program_id <= 47 then
|
||||||
|
return program_color-1
|
||||||
|
end
|
||||||
|
--The final three programs only have a color index 0, so just return those
|
||||||
|
if program_id > 47 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
--BrakChrg as an AP item only comes in orange, index 0
|
||||||
|
if program_id == 3 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
-- every other AP obtainable program returns only color index 3
|
||||||
|
return 3
|
||||||
|
end
|
||||||
|
|
||||||
|
local addChip = function(chip_id, chip_code, amount)
|
||||||
|
chipStartAddress = 0x02001F60
|
||||||
|
chipOffset = 0x12 * chip_id
|
||||||
|
chip_code_index = getChipCodeIndex(chip_id, chip_code)
|
||||||
|
currentChipAddress = chipStartAddress + chipOffset + chip_code_index
|
||||||
|
currentChipCount = memory.read_u8(currentChipAddress)
|
||||||
|
memory.write_u8(currentChipAddress,currentChipCount+amount)
|
||||||
|
end
|
||||||
|
|
||||||
|
local addProgram = function(program_id, program_color, amount)
|
||||||
|
programStartAddress = 0x02001A80
|
||||||
|
programOffset = 0x04 * program_id
|
||||||
|
program_code_index = getProgramColorIndex(program_id, program_color)
|
||||||
|
currentProgramAddress = programStartAddress + programOffset + program_code_index
|
||||||
|
currentProgramCount = memory.read_u8(currentProgramAddress)
|
||||||
|
memory.write_u8(currentProgramAddress, currentProgramCount+amount)
|
||||||
|
end
|
||||||
|
|
||||||
|
local addSubChip = function(subchip_id, amount)
|
||||||
|
subChipStartAddress = 0x02001A30
|
||||||
|
--SubChip indices start after the key items, so subtract 112 from the index to get the actual subchip index
|
||||||
|
currentSubChipAddress = subChipStartAddress + (subchip_id - 112)
|
||||||
|
currentSubChipCount = memory.read_u8(currentSubChipAddress)
|
||||||
|
--TODO check submem, reject if number too big
|
||||||
|
memory.write_u8(currentSubChipAddress, currentSubChipCount+amount)
|
||||||
|
end
|
||||||
|
|
||||||
|
local changeZenny = function(val)
|
||||||
|
if val == nil then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
if memory.read_u32_le(0x20018F4) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
|
||||||
|
memory.write_u32_le(0x20018f4, 0)
|
||||||
|
val = 0
|
||||||
|
return "empty"
|
||||||
|
end
|
||||||
|
memory.write_u32_le(0x20018f4, memory.read_u32_le(0x20018F4) + tonumber(val))
|
||||||
|
if memory.read_u32_le(0x20018F4) > 999999 then
|
||||||
|
memory.write_u32_le(0x20018F4, 999999)
|
||||||
|
end
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
|
||||||
|
local changeFrags = function(val)
|
||||||
|
if val == nil then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
if memory.read_u16_le(0x20018F8) <= math.abs(tonumber(val)) and tonumber(val) < 0 then
|
||||||
|
memory.write_u16_le(0x20018f8, 0)
|
||||||
|
val = 0
|
||||||
|
return "empty"
|
||||||
|
end
|
||||||
|
memory.write_u16_le(0x20018f8, memory.read_u16_le(0x20018F8) + tonumber(val))
|
||||||
|
if memory.read_u16_le(0x20018F8) > 9999 then
|
||||||
|
memory.write_u16_le(0x20018F8, 9999)
|
||||||
|
end
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fix Health Pools
|
||||||
|
local fix_hp = function()
|
||||||
|
-- Current Health fix
|
||||||
|
if IsInBattle() and not (memory.read_u16_le(0x20018A0) == memory.read_u16_le(0x2037294)) then
|
||||||
|
memory.write_u16_le(0x20018A0, memory.read_u16_le(0x2037294))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Max Health Fix
|
||||||
|
if IsInBattle() and not (memory.read_u16_le(0x20018A2) == memory.read_u16_le(0x2037296)) then
|
||||||
|
memory.write_u16_le(0x20018A2, memory.read_u16_le(0x2037296))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local changeRegMemory = function(amt)
|
||||||
|
regMemoryAddress = 0x02001897
|
||||||
|
currentRegMem = memory.read_u8(regMemoryAddress)
|
||||||
|
memory.write_u8(regMemoryAddress, currentRegMem + amt)
|
||||||
|
end
|
||||||
|
|
||||||
|
local changeMaxHealth = function(val)
|
||||||
|
fix_hp()
|
||||||
|
if val == nil then
|
||||||
|
fix_hp()
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
if math.abs(tonumber(val)) >= memory.read_u16_le(0x20018A2) and tonumber(val) < 0 then
|
||||||
|
memory.write_u16_le(0x20018A2, 0)
|
||||||
|
if IsInBattle() then
|
||||||
|
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
||||||
|
if memory.read_u16_le(0x2037296) >= memory.read_u16_le(0x20018A2) then
|
||||||
|
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fix_hp()
|
||||||
|
return "lethal"
|
||||||
|
end
|
||||||
|
memory.write_u16_le(0x20018A2, memory.read_u16_le(0x20018A2) + tonumber(val))
|
||||||
|
if memory.read_u16_le(0x20018A2) > 9999 then
|
||||||
|
memory.write_u16_le(0x20018A2, 9999)
|
||||||
|
end
|
||||||
|
if IsInBattle() then
|
||||||
|
memory.write_u16_le(0x2037296, memory.read_u16_le(0x20018A2))
|
||||||
|
end
|
||||||
|
fix_hp()
|
||||||
|
return val
|
||||||
|
end
|
||||||
|
|
||||||
|
local SendItem = function(item)
|
||||||
|
if item["type"] == "undernet" then
|
||||||
|
undernet_id = Check_Progressive_Undernet_ID()
|
||||||
|
if undernet_id > 8 then
|
||||||
|
-- Generate Extra BugFrags
|
||||||
|
changeFrags(20)
|
||||||
|
gui.addmessage("Receiving extra Undernet Rank from "..item["sender"]..", +20 BugFrags")
|
||||||
|
-- print("Receiving extra Undernet Rank from "..item["sender"]..", +20 BugFrags")
|
||||||
|
else
|
||||||
|
itemAddress = key_item_start_address + Next_Progressive_Undernet_ID(undernet_id)
|
||||||
|
|
||||||
|
itemCount = memory.read_u8(itemAddress)
|
||||||
|
itemCount = itemCount + item["count"]
|
||||||
|
memory.write_u8(itemAddress, itemCount)
|
||||||
|
gui.addmessage("Received Undernet Rank from player "..item["sender"])
|
||||||
|
-- print("Received Undernet Rank from player "..item["sender"])
|
||||||
|
end
|
||||||
|
elseif item["type"] == "chip" then
|
||||||
|
addChip(item["itemID"], item["subItemID"], item["count"])
|
||||||
|
gui.addmessage("Received Chip "..item["itemName"].." from player "..item["sender"])
|
||||||
|
-- print("Received Chip "..item["itemName"].." from player "..item["sender"])
|
||||||
|
elseif item["type"] == "key" then
|
||||||
|
itemAddress = key_item_start_address + item["itemID"]
|
||||||
|
itemCount = memory.read_u8(itemAddress)
|
||||||
|
itemCount = itemCount + item["count"]
|
||||||
|
memory.write_u8(itemAddress, itemCount)
|
||||||
|
-- HPMemory will increase the internal counter but not actually increase the HP. If the item is one of those, do that
|
||||||
|
if item["itemID"] == 96 then
|
||||||
|
changeMaxHealth(20)
|
||||||
|
end
|
||||||
|
-- Same for the RegUps, but there's three of those
|
||||||
|
if item["itemID"] == 98 then
|
||||||
|
changeRegMemory(1)
|
||||||
|
end
|
||||||
|
if item["itemID"] == 99 then
|
||||||
|
changeRegMemory(2)
|
||||||
|
end
|
||||||
|
if item["itemID"] == 100 then
|
||||||
|
changeRegMemory(3)
|
||||||
|
end
|
||||||
|
gui.addmessage("Received Key Item "..item["itemName"].." from player "..item["sender"])
|
||||||
|
-- print("Received Key Item "..item["itemName"].." from player "..item["sender"])
|
||||||
|
elseif item["type"] == "subchip" then
|
||||||
|
addSubChip(item["itemID"], item["count"])
|
||||||
|
gui.addmessage("Received SubChip "..item["itemName"].." from player "..item["sender"])
|
||||||
|
-- print("Received SubChip "..item["itemName"].." from player "..item["sender"])
|
||||||
|
elseif item["type"] == "zenny" then
|
||||||
|
changeZenny(item["count"])
|
||||||
|
gui.addmessage("Received "..item["count"].."z from "..item["sender"])
|
||||||
|
-- print("Received "..item["count"].."z from "..item["sender"])
|
||||||
|
elseif item["type"] == "program" then
|
||||||
|
addProgram(item["itemID"], item["subItemID"], item["count"])
|
||||||
|
gui.addmessage("Received Program "..item["itemName"].." from player "..item["sender"])
|
||||||
|
-- print("Received Program "..item["itemName"].." from player "..item["sender"])
|
||||||
|
elseif item["type"] == "bugfrag" then
|
||||||
|
changeFrags(item["count"])
|
||||||
|
gui.addmessage("Received "..item["count"].." BugFrag(s) from "..item["sender"])
|
||||||
|
-- print("Received "..item["count"].." BugFrag(s) from "..item["sender"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set the flags for opening the shortcuts as soon as the Cybermetro passes are received to save having to check email
|
||||||
|
local OpenShortcuts = function()
|
||||||
|
if (memory.read_u8(key_item_start_address + 92) > 0) then
|
||||||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x10))
|
||||||
|
end
|
||||||
|
-- if CSciPass
|
||||||
|
if (memory.read_u8(key_item_start_address + 93) > 0) then
|
||||||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x08))
|
||||||
|
end
|
||||||
|
if (memory.read_u8(key_item_start_address + 94) > 0) then
|
||||||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x20))
|
||||||
|
end
|
||||||
|
if (memory.read_u8(key_item_start_address + 95) > 0) then
|
||||||
|
memory.write_u8(0x2000032, bit.bor(memory.read_u8(0x2000032),0x40))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local RestoreItemRam = function()
|
||||||
|
if backup_bytes ~= nil then
|
||||||
|
memory.write_bytes_as_array(0x203fe10, backup_bytes)
|
||||||
|
end
|
||||||
|
backup_bytes = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local process_block = function(block)
|
||||||
|
-- Sometimes the block is nothing, if this is the case then quietly stop processing
|
||||||
|
if block == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
debugEnabled = block['debug']
|
||||||
|
-- Queue item for receiving, if one exists
|
||||||
|
if (itemsReceived ~= block['items']) then
|
||||||
|
itemsReceived = block['items']
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local itemStateMachineProcess = function()
|
||||||
|
if itemState == ITEMSTATE_NONINITIALIZED then
|
||||||
|
itemQueueCounter = 120
|
||||||
|
-- Only exit this state the first time a dialog window pops up. This way we know for sure that we're ready to receive
|
||||||
|
if not IsInMenu() and (IsInDialog() or IsInTransition()) then
|
||||||
|
itemState = ITEMSTATE_NONITEM
|
||||||
|
end
|
||||||
|
elseif itemState == ITEMSTATE_NONITEM then
|
||||||
|
itemQueueCounter = 120
|
||||||
|
-- Always attempt to restore the previously stored memory in this state
|
||||||
|
-- Exit this state whenever the game is in an itemable status
|
||||||
|
if IsItemable() then
|
||||||
|
itemState = ITEMSTATE_IDLE
|
||||||
|
end
|
||||||
|
elseif itemState == ITEMSTATE_IDLE then
|
||||||
|
-- Remain Idle until an item is sent or we enter a non itemable status
|
||||||
|
if not IsItemable() then
|
||||||
|
itemState = ITEMSTATE_NONITEM
|
||||||
|
end
|
||||||
|
if itemQueueCounter == 0 then
|
||||||
|
if #itemsReceived > loadItemIndexFromRAM() and not IsItemQueued() then
|
||||||
|
itemQueued = itemsReceived[loadItemIndexFromRAM()+1]
|
||||||
|
SendItem(itemQueued)
|
||||||
|
itemState = ITEMSTATE_SENT
|
||||||
|
end
|
||||||
|
else
|
||||||
|
itemQueueCounter = itemQueueCounter - 1
|
||||||
|
end
|
||||||
|
elseif itemState == ITEMSTATE_SENT then
|
||||||
|
-- Once the item is sent, wait for the dialog to close. Then clear the item bit and be ready for the next item.
|
||||||
|
if IsInTransition() or IsInMenu() or IsOnTitle() then
|
||||||
|
itemState = ITEMSTATE_NONITEM
|
||||||
|
itemQueued = nil
|
||||||
|
RestoreItemRam()
|
||||||
|
elseif not IsInDialog() then
|
||||||
|
itemState = ITEMSTATE_IDLE
|
||||||
|
saveItemIndexToRAM(itemQueued["itemIndex"])
|
||||||
|
itemQueued = nil
|
||||||
|
RestoreItemRam()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local receive = function()
|
||||||
|
l, e = mmbn3Socket:receive()
|
||||||
|
|
||||||
|
-- Handle incoming message
|
||||||
|
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
|
||||||
|
process_block(json.decode(l))
|
||||||
|
end
|
||||||
|
|
||||||
|
local send = function()
|
||||||
|
-- Determine message to send back
|
||||||
|
local retTable = {}
|
||||||
|
retTable["playerName"] = loadPlayerNameFromROM()
|
||||||
|
retTable["scriptVersion"] = script_version
|
||||||
|
retTable["locations"] = check_all_locations()
|
||||||
|
retTable["gameComplete"] = is_game_complete()
|
||||||
|
|
||||||
|
-- Send the message
|
||||||
|
msg = json.encode(retTable).."\n"
|
||||||
|
local ret, error = mmbn3Socket: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 main()
|
||||||
|
if (bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor >= 7)==false) then
|
||||||
|
print("Must use a version of bizhawk 2.7.0 or higher")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
server, error = socket.bind('localhost', 28922)
|
||||||
|
|
||||||
|
while true do
|
||||||
|
frame = frame + 1
|
||||||
|
|
||||||
|
if not (curstate == prevstate) then
|
||||||
|
prevstate = curstate
|
||||||
|
end
|
||||||
|
|
||||||
|
itemStateMachineProcess()
|
||||||
|
|
||||||
|
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||||
|
-- If we're connected and everything's fine, receive and send data from the network
|
||||||
|
if (frame % 60 == 0) then
|
||||||
|
receive()
|
||||||
|
send()
|
||||||
|
-- Perform utility functions which read and write data but aren't directly related to checks
|
||||||
|
OpenShortcuts()
|
||||||
|
end
|
||||||
|
elseif (curstate == STATE_UNINITIALIZED) then
|
||||||
|
-- If we're uninitialized, attempt to make the connection.
|
||||||
|
if (frame % 120 == 0) then
|
||||||
|
server:settimeout(2)
|
||||||
|
local client, timeout = server:accept()
|
||||||
|
if timeout == nil then
|
||||||
|
print('Initial Connection Made')
|
||||||
|
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||||
|
mmbn3Socket = client
|
||||||
|
mmbn3Socket:settimeout(0)
|
||||||
|
else
|
||||||
|
print('Connection failed, ensure MMBN3Client is running and rerun connector_mmbn3.lua')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle the debug data display
|
||||||
|
gui.cleartext()
|
||||||
|
if debugEnabled then
|
||||||
|
-- gui.text(0,0,"Item Queued: "..tostring(IsItemQueued()))
|
||||||
|
-- gui.text(0,16,"In Battle: "..tostring(IsInBattle()))
|
||||||
|
-- gui.text(0,32,"In Dialog: "..tostring(IsInDialog()))
|
||||||
|
-- gui.text(0,48,"In Menu: "..tostring(IsInMenu()))
|
||||||
|
gui.text(0,48,"Item Wait Time: "..tostring(itemQueueCounter))
|
||||||
|
gui.text(0,64,itemState)
|
||||||
|
if itemQueued == nil then
|
||||||
|
gui.text(0,80,"No item queued")
|
||||||
|
else
|
||||||
|
gui.text(0,80,itemQueued["type"].." "..itemQueued["itemID"])
|
||||||
|
end
|
||||||
|
gui.text(0,96,"Item Index: "..loadItemIndexFromRAM())
|
||||||
|
end
|
||||||
|
|
||||||
|
emu.frameadvance()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
local socket = require("socket")
|
local socket = require("socket")
|
||||||
local json = require('json')
|
local json = require('json')
|
||||||
local math = require('math')
|
local math = require('math')
|
||||||
|
require('common')
|
||||||
|
|
||||||
local last_modified_date = '2022-11-27' -- Should be the last modified date
|
local last_modified_date = '2022-4-15' -- Should be the last modified date
|
||||||
local script_version = 3
|
local script_version = 3
|
||||||
|
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
@@ -1861,8 +1862,7 @@ function receive()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function main()
|
function main()
|
||||||
if (is23Or24Or25 or is26To27) == false then
|
if not checkBizHawkVersion() then
|
||||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
server, error = socket.bind('localhost', 28921)
|
server, error = socket.bind('localhost', 28921)
|
||||||
@@ -1886,7 +1886,7 @@ function main()
|
|||||||
ootSocket = client
|
ootSocket = client
|
||||||
ootSocket:settimeout(0)
|
ootSocket:settimeout(0)
|
||||||
else
|
else
|
||||||
print('Connection failed, ensure OoTClient is running and rerun oot_connector.lua')
|
print('Connection failed, ensure OoTClient is running and rerun connector_oot.lua')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1895,4 +1895,4 @@ function main()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
main()
|
main()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
local socket = require("socket")
|
local socket = require("socket")
|
||||||
local json = require('json')
|
local json = require('json')
|
||||||
local math = require('math')
|
local math = require('math')
|
||||||
|
require("common")
|
||||||
local STATE_OK = "Ok"
|
local STATE_OK = "Ok"
|
||||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||||
@@ -32,9 +32,6 @@ local curstate = STATE_UNINITIALIZED
|
|||||||
local gbSocket = nil
|
local gbSocket = nil
|
||||||
local frame = 0
|
local frame = 0
|
||||||
|
|
||||||
local u8 = nil
|
|
||||||
local wU8 = nil
|
|
||||||
local u16
|
|
||||||
local compat = nil
|
local compat = nil
|
||||||
|
|
||||||
local function defineMemoryFunctions()
|
local function defineMemoryFunctions()
|
||||||
@@ -55,68 +52,42 @@ function uRange(address, bytes)
|
|||||||
return data
|
return data
|
||||||
end
|
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
|
|
||||||
|
|
||||||
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 generateLocationsChecked()
|
function generateLocationsChecked()
|
||||||
memDomain.wram()
|
memDomain.wram()
|
||||||
events = uRange(EventFlagAddress, 0x140)
|
events = uRange(EventFlagAddress, 0x140)
|
||||||
missables = uRange(MissableAddress, 0x20)
|
missables = uRange(MissableAddress, 0x20)
|
||||||
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
|
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
|
||||||
|
rod = {u8(RodAddress)}
|
||||||
dexsanity = uRange(DexSanityAddress, 19)
|
dexsanity = uRange(DexSanityAddress, 19)
|
||||||
rod = u8(RodAddress)
|
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
table.foreach(events, function(k, v) table.insert(data, v) end)
|
categories = {events, missables, hiddenitems, rod}
|
||||||
table.foreach(missables, function(k, v) table.insert(data, v) end)
|
if compat > 1 then
|
||||||
table.foreach(hiddenitems, function(k, v) table.insert(data, v) end)
|
table.insert(categories, dexsanity)
|
||||||
table.insert(data, rod)
|
end
|
||||||
|
for _, category in ipairs(categories) do
|
||||||
|
for _, v in ipairs(category) do
|
||||||
|
table.insert(data, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if compat > 1 then
|
|
||||||
table.foreach(dexsanity, function(k, v) table.insert(data, v) end)
|
|
||||||
end
|
|
||||||
return data
|
return data
|
||||||
end
|
end
|
||||||
|
|
||||||
local function arrayEqual(a1, a2)
|
local function arrayEqual(a1, a2)
|
||||||
if #a1 ~= #a2 then
|
if #a1 ~= #a2 then
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
for i, v in ipairs(a1) do
|
|
||||||
if v ~= a2[i] then
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
for i, v in ipairs(a1) do
|
||||||
return true
|
if v ~= a2[i] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function receive()
|
function receive()
|
||||||
@@ -196,8 +167,7 @@ function receive()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function main()
|
function main()
|
||||||
if (is23Or24Or25 or is26To28) == false then
|
if not checkBizHawkVersion() then
|
||||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
server, error = socket.bind('localhost', 17242)
|
server, error = socket.bind('localhost', 17242)
|
||||||
@@ -3,13 +3,12 @@
|
|||||||
local socket = require("socket")
|
local socket = require("socket")
|
||||||
local json = require('json')
|
local json = require('json')
|
||||||
local math = require('math')
|
local math = require('math')
|
||||||
|
require("common")
|
||||||
local STATE_OK = "Ok"
|
local STATE_OK = "Ok"
|
||||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
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 itemMessages = {}
|
|
||||||
local consumableStacks = nil
|
local consumableStacks = nil
|
||||||
local prevstate = ""
|
local prevstate = ""
|
||||||
local curstate = STATE_UNINITIALIZED
|
local curstate = STATE_UNINITIALIZED
|
||||||
@@ -21,8 +20,6 @@ local cave_index
|
|||||||
local triforce_byte
|
local triforce_byte
|
||||||
local game_state
|
local game_state
|
||||||
|
|
||||||
local u8 = nil
|
|
||||||
local wU8 = nil
|
|
||||||
local isNesHawk = false
|
local isNesHawk = false
|
||||||
|
|
||||||
local shopsChecked = {}
|
local shopsChecked = {}
|
||||||
@@ -420,83 +417,6 @@ local function checkCaveItemObtained()
|
|||||||
return returnTable
|
return returnTable
|
||||||
end
|
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()
|
function generateOverworldLocationChecked()
|
||||||
memDomain.ram()
|
memDomain.ram()
|
||||||
data = uRange(0x067E, 0x81)
|
data = uRange(0x067E, 0x81)
|
||||||
@@ -589,18 +509,6 @@ function processBlock(block)
|
|||||||
end
|
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()
|
function receive()
|
||||||
l, e = zeldaSocket:receive()
|
l, e = zeldaSocket:receive()
|
||||||
if e == 'closed' then
|
if e == 'closed' then
|
||||||
@@ -653,8 +561,7 @@ function receive()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function main()
|
function main()
|
||||||
if (is23Or24Or25 or is26To28) == false then
|
if not checkBizHawkVersion() then
|
||||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
server, error = socket.bind('localhost', 52980)
|
server, error = socket.bind('localhost', 52980)
|
||||||
12
data/lua/lua_5_3_compat.lua
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
function bit.rshift(a, b)
|
||||||
|
return a >> b
|
||||||
|
end
|
||||||
|
function bit.lshift(a, b)
|
||||||
|
return a << b
|
||||||
|
end
|
||||||
|
function bit.bor(a, b)
|
||||||
|
return a | b
|
||||||
|
end
|
||||||
|
function bit.band(a, b)
|
||||||
|
return a & b
|
||||||
|
end
|
||||||
@@ -10,8 +10,55 @@
|
|||||||
local base = _G
|
local base = _G
|
||||||
local string = require("string")
|
local string = require("string")
|
||||||
local math = require("math")
|
local math = require("math")
|
||||||
local socket = require("socket.core")
|
|
||||||
module("socket")
|
function get_lua_version()
|
||||||
|
local major, minor = _VERSION:match("Lua (%d+)%.(%d+)")
|
||||||
|
assert(tonumber(major) == 5)
|
||||||
|
if tonumber(minor) >= 4 then
|
||||||
|
return "5-4"
|
||||||
|
end
|
||||||
|
return "5-1"
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_os()
|
||||||
|
local the_os, ext, arch
|
||||||
|
if package.config:sub(1,1) == "\\" then
|
||||||
|
the_os, ext = "windows", "dll"
|
||||||
|
arch = os.getenv"PROCESSOR_ARCHITECTURE"
|
||||||
|
else
|
||||||
|
-- TODO: macos?
|
||||||
|
the_os, ext = "linux", "so"
|
||||||
|
arch = "x86_64" -- TODO: read ELF header from /proc/$PID/exe to get arch
|
||||||
|
end
|
||||||
|
|
||||||
|
if arch:find("64") ~= nil then
|
||||||
|
arch = "x64"
|
||||||
|
else
|
||||||
|
arch = "x86"
|
||||||
|
end
|
||||||
|
|
||||||
|
return the_os, ext, arch
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_socket_path()
|
||||||
|
local the_os, ext, arch = get_os()
|
||||||
|
-- for some reason ./ isn't working, so use a horrible hack to get the pwd
|
||||||
|
local pwd = (io.popen and io.popen("cd"):read'*l') or "."
|
||||||
|
return pwd .. "/" .. arch .. "/socket-" .. the_os .. "-" .. get_lua_version() .. "." .. ext
|
||||||
|
end
|
||||||
|
|
||||||
|
local socket_path = get_socket_path()
|
||||||
|
local socket = assert(package.loadlib(socket_path, "luaopen_socket_core"))()
|
||||||
|
|
||||||
|
-- http://lua-users.org/wiki/ModulesTutorial
|
||||||
|
local M = {}
|
||||||
|
if setfenv then
|
||||||
|
setfenv(1, M) -- for 5.1
|
||||||
|
else
|
||||||
|
_ENV = M -- for 5.2
|
||||||
|
end
|
||||||
|
|
||||||
|
M.socket = socket
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
-----------------------------------------------------------------------------
|
||||||
-- Exported auxiliar functions
|
-- Exported auxiliar functions
|
||||||
@@ -39,7 +86,7 @@ function bind(host, port, backlog)
|
|||||||
return sock
|
return sock
|
||||||
end
|
end
|
||||||
|
|
||||||
try = newtry()
|
try = socket.newtry()
|
||||||
|
|
||||||
function choose(table)
|
function choose(table)
|
||||||
return function(name, opt1, opt2)
|
return function(name, opt1, opt2)
|
||||||
@@ -130,3 +177,5 @@ end
|
|||||||
sourcet["default"] = sourcet["until-closed"]
|
sourcet["default"] = sourcet["until-closed"]
|
||||||
|
|
||||||
source = choose(sourcet)
|
source = choose(sourcet)
|
||||||
|
|
||||||
|
return M
|
||||||
|
|||||||
20
data/lua/x64/luasocket.LICENSE.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
LuaSocket 3.0 license
|
||||||
|
Copyright <20> 2004-2013 Diego Nehab
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
BIN
data/lua/x64/socket-linux-5-1.so
Normal file
BIN
data/lua/x64/socket-linux-5-4.so
Normal file
BIN
data/lua/x64/socket-windows-5-1.dll
Normal file
BIN
data/lua/x64/socket-windows-5-4.dll
Normal file
20
data/lua/x86/luasocket.LICENSE.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
LuaSocket 3.0 license
|
||||||
|
Copyright <20> 2004-2013 Diego Nehab
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
BIN
data/lua/x86/socket-windows-5-1.dll
Normal file
BIN
data/mcicon.png
Normal file
|
After Width: | Height: | Size: 594 B |
@@ -341,3 +341,4 @@ The various methods and attributes are documented in `/worlds/AutoWorld.py[World
|
|||||||
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
|
[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md),
|
||||||
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
though it is also recommended to look at existing implementations to see how all this works first-hand.
|
||||||
Once you get all that, all that remains to do is test the game and publish your work.
|
Once you get all that, all that remains to do is test the game and publish your work.
|
||||||
|
Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing.
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ See [world api.md](world%20api.md) for details.
|
|||||||
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
|
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
|
||||||
file into the worlds folder.
|
file into the worlds folder.
|
||||||
|
|
||||||
|
**Warning:** apworlds have to be all lower case, otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
|
||||||
|
|
||||||
|
|
||||||
## File Format
|
## File Format
|
||||||
|
|
||||||
apworld files are zip archives with the case-sensitive file ending `.apworld`.
|
apworld files are zip archives, all lower case, with the file ending `.apworld`.
|
||||||
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
|
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
|
||||||
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
|
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ Otherwise, we tend to judge code on a case to case basis.
|
|||||||
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see
|
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see
|
||||||
[the docs folder](/docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev
|
[the docs folder](/docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev
|
||||||
channel in our [Discord](https://archipelago.gg/discord).
|
channel in our [Discord](https://archipelago.gg/discord).
|
||||||
|
If you want to merge a new game, please make sure to read the responsibilities as
|
||||||
|
[world maintainer](/docs/world%20maintainer.md).
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ flowchart LR
|
|||||||
subgraph Final Fantasy 1
|
subgraph Final Fantasy 1
|
||||||
FF1[FF1Client]
|
FF1[FF1Client]
|
||||||
FFLUA[Lua Connector]
|
FFLUA[Lua Connector]
|
||||||
BZFF[BizHawk with Final Fantasy Loaded]
|
BZFF[EmuHawk with Final Fantasy Loaded]
|
||||||
FF1 <-- LuaSockets --> FFLUA
|
FF1 <-- LuaSockets --> FFLUA
|
||||||
FFLUA <--> BZFF
|
FFLUA <--> BZFF
|
||||||
end
|
end
|
||||||
@@ -45,7 +45,7 @@ flowchart LR
|
|||||||
subgraph Ocarina of Time
|
subgraph Ocarina of Time
|
||||||
OC[OoTClient]
|
OC[OoTClient]
|
||||||
LC[Lua Connector]
|
LC[Lua Connector]
|
||||||
OCB[BizHawk with Ocarina of Time Loaded]
|
OCB[EmuHawk with Ocarina of Time Loaded]
|
||||||
OC <-- LuaSockets --> LC
|
OC <-- LuaSockets --> LC
|
||||||
LC <--> OCB
|
LC <--> OCB
|
||||||
end
|
end
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
@@ -20,12 +20,13 @@ There are also a number of community-supported libraries available that implemen
|
|||||||
| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | |
|
| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | |
|
||||||
| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). |
|
| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). |
|
||||||
| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | |
|
| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | |
|
||||||
| .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | |
|
| .NET (C# / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | |
|
||||||
| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | header-only |
|
| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | header-only |
|
||||||
| | [APCpp](https://github.com/N00byKing/APCpp) | CMake |
|
| | [APCpp](https://github.com/N00byKing/APCpp) | CMake |
|
||||||
| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported |
|
| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported |
|
||||||
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
|
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
|
||||||
| Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | |
|
| Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | |
|
||||||
|
| Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | |
|
||||||
|
|
||||||
## Synchronizing Items
|
## Synchronizing Items
|
||||||
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
|
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
|
||||||
@@ -67,10 +68,11 @@ Sent to clients when they connect to an Archipelago server.
|
|||||||
| 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. |
|
||||||
|
| generator_version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which generated the multiworld. |
|
||||||
| 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 percentage of total locations that need to be checked 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). **Deprecated. Use `datapackage_checksums` instead.** |
|
| 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.** |
|
||||||
@@ -128,7 +130,8 @@ Sent to clients when the connection handshake is successfully completed.
|
|||||||
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
|
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
|
||||||
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 2<sup>53</sup>-1. |
|
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 2<sup>53</sup>-1. |
|
||||||
| slot_data | dict\[str, any\] | Contains a json object for slot related data, differs per game. Empty if not required. Not present if slot_data in [Connect](#Connect) is false. |
|
| slot_data | dict\[str, any\] | Contains a json object for slot related data, differs per game. Empty if not required. Not present if slot_data in [Connect](#Connect) is false. |
|
||||||
| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information |
|
| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information. |
|
||||||
|
| hint_points | int | Number of hint points that the current player has. |
|
||||||
|
|
||||||
### ReceivedItems
|
### ReceivedItems
|
||||||
Sent to clients when they receive an item.
|
Sent to clients when they receive an item.
|
||||||
@@ -146,17 +149,16 @@ Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) pack
|
|||||||
| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. |
|
| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. |
|
||||||
|
|
||||||
### RoomUpdate
|
### RoomUpdate
|
||||||
Sent when there is a need to update information about the present game session. Generally useful for async games.
|
Sent when there is a need to update information about the present game session.
|
||||||
Once authenticated (received Connected), this may also contain data from Connected.
|
|
||||||
#### Arguments
|
#### Arguments
|
||||||
The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
|
RoomUpdate may contain the same arguments from [RoomInfo](#RoomInfo) and, once authenticated, arguments from
|
||||||
|
[Connected](#Connected) with the following exceptions:
|
||||||
|
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
|-------------------|-----------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
||||||
| hint_points | int | New argument. The client's current hint points. |
|
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Sent in the event of an alias rename. Always sends all players, whether connected or not. |
|
||||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. |
|
| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
|
||||||
| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
|
| missing_locations | - | Never sent in this packet. If needed, it is the inverse of `checked_locations`. |
|
||||||
| missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. |
|
|
||||||
|
|
||||||
All arguments for this packet are optional, only changes are sent.
|
All arguments for this packet are optional, only changes are sent.
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,20 @@ need to create:
|
|||||||
- A new option class with a docstring detailing what the option will do to your user.
|
- 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 `display_name` to be displayed on the webhost.
|
||||||
- A new entry in the `option_definitions` dict for your World.
|
- 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
|
By style and convention, the internal names should be snake_case.
|
||||||
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
|
### Option Creation
|
||||||
|
- If the option supports having multiple sub_options, such as Choice options, these can be defined with
|
||||||
|
`option_value1`. Any attributes of the class with a preceding `option_` is added to the class's `options` lookup. The
|
||||||
|
`option_` is then stripped for users, so will show as `value1` in yaml files. If `auto_display_name` is True, it will
|
||||||
|
display as `Value1` on the webhost.
|
||||||
|
- An alternative name can be set for any specific option by setting an alias attribute
|
||||||
|
(i.e. `alias_value_1 = option_value1`) which will allow users to use either `value_1` or `value1` in their yaml
|
||||||
|
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
||||||
|
Choice, and defining `alias_true = option_full`.
|
||||||
|
- 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 `option_random`.
|
||||||
|
|
||||||
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
|
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
|
create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -32,13 +32,11 @@ 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**
|
* **Python 3.11 does not work currently**
|
||||||
|
|
||||||
* Download and install full Visual Studio from
|
* **Optional**: Download and install Visual Studio Build Tools from
|
||||||
[Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/)
|
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
|
||||||
or an older "Build Tools for Visual Studio" from
|
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
||||||
[Visual Studio Older Downloads](https://visualstudio.microsoft.com/vs/older-downloads/).
|
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
||||||
|
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
||||||
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details
|
|
||||||
* This step is optional. Pre-compiled modules are pinned on
|
|
||||||
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||||
|
|
||||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||||
@@ -71,6 +69,19 @@ It should be dropped as "SNI" into the root folder of the project. Alternatively
|
|||||||
host.yaml at your SNI folder.
|
host.yaml at your SNI folder.
|
||||||
|
|
||||||
|
|
||||||
|
## Optional: Git
|
||||||
|
|
||||||
|
[Git](https://git-scm.com) is required to install some of the packages that Archipelago depends on.
|
||||||
|
It may be possible to run Archipelago from source without it, at your own risk.
|
||||||
|
|
||||||
|
It is also generally recommended to have Git installed and understand how to use it, especially if you're thinking about contributing.
|
||||||
|
|
||||||
|
You can download the latest release of Git at [The downloads page on the Git website](https://git-scm.com/downloads).
|
||||||
|
|
||||||
|
Beyond that, there are also graphical interfaces for Git that make it more accessible.
|
||||||
|
For repositories on Github (such as this one), [Github Desktop](https://desktop.github.com) is one such option.
|
||||||
|
PyCharm has a built-in version control integration that supports Git.
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ World-specific IDs are 1 to 2<sup>53</sup>-1, IDs ≤ 0 are global and reserved.
|
|||||||
Special locations with ID `None` can hold events.
|
Special locations with ID `None` can hold events.
|
||||||
|
|
||||||
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
||||||
The Fill algorithm will fill priority first, giving higher chance of it being
|
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
||||||
required, and not place progression or useful items in excluded locations.
|
required, and will prevent progression and useful items from being placed at excluded locations.
|
||||||
|
|
||||||
### Items
|
### Items
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ on a single item. It can be used to reject placement of an item there.
|
|||||||
### Your World
|
### Your World
|
||||||
|
|
||||||
All code for your world implementation should be placed in a python package in
|
All code for your world implementation should be placed in a python package in
|
||||||
the `/worlds` directory. The starting point for the package is `__init.py__`.
|
the `/worlds` directory. The starting point for the package is `__init__.py`.
|
||||||
Conventionally, your world class is placed in that file.
|
Conventionally, your world class is placed in that file.
|
||||||
|
|
||||||
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
|
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
|
||||||
|
|||||||
60
docs/world maintainer.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# World Maintainer
|
||||||
|
|
||||||
|
A world maintainer is a person responsible for a world or part of a world in Archipelago.
|
||||||
|
|
||||||
|
If a world author does not want to take on the responsibilities of a world maintainer, they can release their world as
|
||||||
|
an unofficial [APWorld](/docs/apworld%20specification.md) or maintain their own fork instead.
|
||||||
|
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
Unless these are shared between multiple people, we expect the following from each world maintainer
|
||||||
|
|
||||||
|
* Be on our Discord to get updates on problems with and suggestions for the world.
|
||||||
|
* Decide if a feature (pull request) should be merged.
|
||||||
|
* Review contents of such pull requests or organize peer reviews or post that you did not review the content.
|
||||||
|
* Fix or point out issues when core changes break your code.
|
||||||
|
* Use the watch function on GitHub, the #github-updates channel on Discord or check manually from time to time for new
|
||||||
|
pull requests. Core maintainers may also ping you if a pull request concerns your world.
|
||||||
|
* Test (or have tested) the world on the main branch from time to time, especially during RC (release candidate) phases
|
||||||
|
of development.
|
||||||
|
* Let us know of long unavailabilities.
|
||||||
|
|
||||||
|
|
||||||
|
## Becoming a World Maintainer
|
||||||
|
|
||||||
|
### Adding a World
|
||||||
|
|
||||||
|
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||||
|
nominate someone else (i.e. there are multiple devs).
|
||||||
|
|
||||||
|
### Getting Voted
|
||||||
|
|
||||||
|
When a world is unmaintained, the [core maintainers](https://github.com/orgs/ArchipelagoMW/people)
|
||||||
|
can vote for a new maintainer if there is a candidate.
|
||||||
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
|
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||||
|
Voting shall be conducted on Discord in #archipelago-dev.
|
||||||
|
|
||||||
|
|
||||||
|
## Dropping out
|
||||||
|
|
||||||
|
### Resigning
|
||||||
|
|
||||||
|
A world maintainer can resign. If no new maintainer steps up and gets voted, the world becomes unmaintained.
|
||||||
|
|
||||||
|
### Getting Voted out
|
||||||
|
|
||||||
|
A world maintainer can be voted out by the [core maintainers](https://github.com/orgs/ArchipelagoMW/people),
|
||||||
|
for example when they become unreachable.
|
||||||
|
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||||
|
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
||||||
|
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
||||||
|
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
||||||
|
date, voting members and final result in the commit message.
|
||||||
|
|
||||||
|
|
||||||
|
## Handling of Unmaintained Worlds
|
||||||
|
|
||||||
|
As long as worlds are known to work for the most part, they can stay included. Once a world becomes broken it shall be
|
||||||
|
moved from `worlds/` to `worlds_disabled/`.
|
||||||
17
host.yaml
@@ -136,7 +136,7 @@ tloz_options:
|
|||||||
# true for operating system default program
|
# true for operating system default program
|
||||||
# Alternatively, a path to a program to open the .nes file with
|
# Alternatively, a path to a program to open the .nes file with
|
||||||
rom_start: true
|
rom_start: true
|
||||||
# Display message inside of Bizhawk
|
# Display message inside of EmuHawk
|
||||||
display_msgs: true
|
display_msgs: true
|
||||||
dkc3_options:
|
dkc3_options:
|
||||||
# File name of the DKC3 US rom
|
# File name of the DKC3 US rom
|
||||||
@@ -167,7 +167,10 @@ 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"
|
||||||
|
mmbn3_options:
|
||||||
|
# File name of the MMBN3 Blue US rom
|
||||||
|
rom_file: "Mega Man Battle Network 3 - Blue Version (USA).gba"
|
||||||
|
rom_start: true
|
||||||
adventure_options:
|
adventure_options:
|
||||||
# File name of the standard NTSC Adventure rom.
|
# File name of the standard NTSC Adventure rom.
|
||||||
# The licensed "The 80 Classic Games" CD-ROM contains this.
|
# The licensed "The 80 Classic Games" CD-ROM contains this.
|
||||||
@@ -178,12 +181,10 @@ adventure_options:
|
|||||||
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
||||||
rom_start: true
|
rom_start: true
|
||||||
# Optional, additional args passed into rom_start before the .bin file
|
# Optional, additional args passed into rom_start before the .bin file
|
||||||
# For example, this can be used to autoload the connector script in BizHawk
|
# For example, this can be used to autoload the connector script in EmuHawk
|
||||||
# (see BizHawk --lua= option)
|
# (see EmuHawk --lua= option)
|
||||||
|
# Windows example:
|
||||||
|
# rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"
|
||||||
rom_args: " "
|
rom_args: " "
|
||||||
# Set this to true to display item received messages in Emuhawk
|
# Set this to true to display item received messages in Emuhawk
|
||||||
display_msgs: true
|
display_msgs: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ 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/mmbn3"; Description: "MegaMan Battle Network 3"; Types: full hosting; ExtraDiskSpaceRequired: 8388608; Flags: disablenouninstallwarning
|
||||||
Name: "generator/ladx"; Description: "Link's Awakening DX 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: "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
|
||||||
@@ -81,6 +82,7 @@ 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/mmbn3"; Description: "MegaMan Battle Network 3 Client"; Types: full playing;
|
||||||
Name: "client/ladx"; Description: "Link's Awakening Client"; 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
|
||||||
@@ -88,6 +90,7 @@ 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/tloz"; Description: "The Legend of Zelda"; Types: full playing
|
||||||
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
||||||
|
Name: "client/ut"; Description: "Undertale"; 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]
|
||||||
@@ -104,6 +107,7 @@ 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:GetBN3ROMPath}"; DestDir: "{app}"; DestName: "Mega Man Battle Network 3 - Blue Version (USA).gba"; Flags: external; Components: client/mmbn3
|
||||||
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: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: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: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn
|
||||||
@@ -127,10 +131,12 @@ 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}\ArchipelagoMMBN3Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/mmbn3
|
||||||
Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz
|
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}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
|
||||||
Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
|
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: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
|
||||||
|
Source: "{#source_path}\ArchipelagoUndertaleClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ut
|
||||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
@@ -146,9 +152,12 @@ 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} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Components: client/mmbn3
|
||||||
Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
|
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} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
||||||
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
||||||
|
Name: "{group}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Components: client/wargroove
|
||||||
|
Name: "{group}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Components: client/ut
|
||||||
|
|
||||||
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
|
||||||
@@ -161,10 +170,12 @@ 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} MegaMan Battle Network 3 Client"; Filename: "{app}\ArchipelagoMMBN3Client.exe"; Tasks: desktopicon; Components: client/mmbn3
|
||||||
Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
|
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} 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} 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
|
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
||||||
|
Name: "{commondesktop}\{#MyAppName} Undertale Client"; Filename: "{app}\ArchipelagoUndertaleClient.exe"; Tasks: desktopicon; Components: client/ut
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
|
|
||||||
@@ -178,6 +189,8 @@ Type: dirifempty; Name: "{app}"
|
|||||||
[InstallDelete]
|
[InstallDelete]
|
||||||
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
|
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
|
||||||
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
|
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
|
||||||
|
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
||||||
|
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||||
#include "installdelete.iss"
|
#include "installdelete.iss"
|
||||||
|
|
||||||
[Registry]
|
[Registry]
|
||||||
@@ -242,6 +255,11 @@ 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: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/mmbn3
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
|
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"; 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\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
|
||||||
@@ -324,6 +342,9 @@ var RedROMFilePage: TInputFileWizardPage;
|
|||||||
var bluerom: string;
|
var bluerom: string;
|
||||||
var BlueROMFilePage: TInputFileWizardPage;
|
var BlueROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
|
var bn3rom: string;
|
||||||
|
var BN3ROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
var ladxrom: string;
|
var ladxrom: string;
|
||||||
var LADXROMFilePage: TInputFileWizardPage;
|
var LADXROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
@@ -443,6 +464,20 @@ begin
|
|||||||
'.gb');
|
'.gb');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function AddGBARomPage(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:',
|
||||||
|
'GBA ROM files|*.gba|All files|*.*',
|
||||||
|
'.gba');
|
||||||
|
end;
|
||||||
|
|
||||||
function AddSMSRomPage(name: string): TInputFileWizardPage;
|
function AddSMSRomPage(name: string): TInputFileWizardPage;
|
||||||
begin
|
begin
|
||||||
Result :=
|
Result :=
|
||||||
@@ -451,7 +486,6 @@ begin
|
|||||||
'Select ROM File',
|
'Select ROM File',
|
||||||
'Where is your ' + name + ' located?',
|
'Where is your ' + name + ' located?',
|
||||||
'Select the file, then click Next.');
|
'Select the file, then click Next.');
|
||||||
|
|
||||||
Result.Add(
|
Result.Add(
|
||||||
'Location of ROM file:',
|
'Location of ROM file:',
|
||||||
'SMS ROM files|*.sms|All files|*.*',
|
'SMS ROM files|*.sms|All files|*.*',
|
||||||
@@ -534,6 +568,8 @@ begin
|
|||||||
Result := not (L2ACROMFilePage.Values[0] = '')
|
Result := not (L2ACROMFilePage.Values[0] = '')
|
||||||
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
||||||
Result := not (OoTROMFilePage.Values[0] = '')
|
Result := not (OoTROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(BN3ROMFilePage)) and (CurPageID = BN3ROMFilePage.ID) then
|
||||||
|
Result := not (BN3ROMFilePage.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
|
else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
|
||||||
@@ -758,6 +794,22 @@ begin
|
|||||||
Result := '';
|
Result := '';
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function GetBN3ROMPath(Param: string): string;
|
||||||
|
begin
|
||||||
|
if Length(bn3rom) > 0 then
|
||||||
|
Result := bn3rom
|
||||||
|
else if Assigned(BN3ROMFilePage) then
|
||||||
|
begin
|
||||||
|
R := CompareStr(GetMD5OfFile(BN3ROMFilePage.Values[0]), '6fe31df0144759b34ad666badaacc442')
|
||||||
|
if R <> 0 then
|
||||||
|
MsgBox('MegaMan Battle Network 3 Blue ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
Result := BN3ROMFilePage.Values[0]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
procedure InitializeWizard();
|
procedure InitializeWizard();
|
||||||
begin
|
begin
|
||||||
AddOoTRomPage();
|
AddOoTRomPage();
|
||||||
@@ -794,6 +846,10 @@ 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');
|
||||||
|
|
||||||
|
bn3rom := CheckRom('Mega Man Battle Network 3 - Blue Version (USA).gba','6fe31df0144759b34ad666badaacc442');
|
||||||
|
if Length(bn3rom) = 0 then
|
||||||
|
BN3ROMFilePage:= AddGBARomPage('Mega Man Battle Network 3 - Blue Version (USA).gba');
|
||||||
|
|
||||||
ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
|
ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
|
||||||
if Length(ladxrom) = 0 then
|
if Length(ladxrom) = 0 then
|
||||||
LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
|
LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
|
||||||
@@ -835,6 +891,8 @@ 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(BN3ROMFilePage)) and (PageID = BN3ROMFilePage.ID) then
|
||||||
|
Result := not (WizardIsComponentSelected('generator/mmbn3') or WizardIsComponentSelected('client/mmbn3'));
|
||||||
if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
|
if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
|
||||||
Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
|
Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
|
||||||
if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
|
if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
|
||||||
|
|||||||