mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 13:03:22 -07:00
Compare commits
149 Commits
0.4.0
...
linux-py31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eb2d3ea3e | ||
|
|
f04054a177 | ||
|
|
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 |
8
.github/workflows/build.yml
vendored
8
.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
|
||||||
@@ -65,10 +65,10 @@ jobs:
|
|||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.10'
|
||||||
- name: Install build-time dependencies
|
- name: Install build-time dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
echo "PYTHON=python3.10" >> $GITHUB_ENV
|
||||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||||
chmod a+rx appimagetool-x86_64.AppImage
|
chmod a+rx appimagetool-x86_64.AppImage
|
||||||
./appimagetool-x86_64.AppImage --appimage-extract
|
./appimagetool-x86_64.AppImage --appimage-extract
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.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
|
||||||
@@ -44,10 +44,10 @@ jobs:
|
|||||||
- name: Get a recent python
|
- name: Get a recent python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.10'
|
||||||
- name: Install build-time dependencies
|
- name: Install build-time dependencies
|
||||||
run: |
|
run: |
|
||||||
echo "PYTHON=python3.9" >> $GITHUB_ENV
|
echo "PYTHON=python3.10" >> $GITHUB_ENV
|
||||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||||
chmod a+rx appimagetool-x86_64.AppImage
|
chmod a+rx appimagetool-x86_64.AppImage
|
||||||
./appimagetool-x86_64.AppImage --appimage-extract
|
./appimagetool-x86_64.AppImage --appimage-extract
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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, OrderedDict, 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, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
import Options
|
import Options
|
||||||
@@ -113,7 +113,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 +121,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 +135,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")
|
||||||
@@ -445,7 +444,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 +740,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:
|
||||||
@@ -836,6 +836,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__()
|
||||||
|
|
||||||
|
|||||||
@@ -68,14 +68,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 +157,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 +167,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
|
||||||
@@ -256,6 +261,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
|
||||||
@@ -642,11 +648,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 +719,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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
22
Fill.py
22
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
|
||||||
|
|
||||||
@@ -526,16 +525,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 +551,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 +828,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']:
|
||||||
|
|||||||
262
KH2Client.py
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"))
|
||||||
|
|||||||
76
Launcher.py
76
Launcher.py
@@ -11,14 +11,17 @@ Scroll down to components= to add components to the launcher as well as setup.py
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import itertools
|
import itertools
|
||||||
|
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 +41,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -58,24 +60,37 @@ def open_patch():
|
|||||||
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),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -132,6 +147,8 @@ 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"
|
||||||
@@ -153,24 +170,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._funcs.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())
|
||||||
|
|
||||||
@@ -209,6 +246,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_logging('Launcher')
|
init_logging('Launcher')
|
||||||
|
multiprocessing.freeze_support()
|
||||||
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.")
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ 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.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -126,6 +132,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 +178,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)
|
||||||
@@ -227,6 +240,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 +279,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 +496,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 +567,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 +644,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 +1140,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
|
||||||
|
|||||||
58
Main.py
58
Main.py
@@ -1,23 +1,24 @@
|
|||||||
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
|
||||||
|
|
||||||
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 +117,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 +154,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[
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
25
OoTClient.py
25
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"
|
||||||
@@ -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
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
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ 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
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
14
Utils.py
14
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.1"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -505,6 +508,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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -48,9 +48,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)
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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["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"]["spoiler"]
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,18 +40,6 @@ 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 = {
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -20,6 +20,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 +64,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 +115,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 +156,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"
|
||||||
|
|||||||
BIN
data/discord-mark-blue.png
Normal file
BIN
data/discord-mark-blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
data/icon.ico
BIN
data/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 255 KiB |
BIN
data/icon.png
BIN
data/icon.png
Binary file not shown.
|
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)
|
|
||||||
Binary file not shown.
@@ -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)
|
|
||||||
Binary file not shown.
@@ -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)
|
|
||||||
Binary file not shown.
@@ -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)
|
|
||||||
Binary file not shown.
@@ -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
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)
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
Binary file not shown.
12
data/lua/lua_5_3_compat.lua
Normal file
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
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-1.so
Normal file
Binary file not shown.
BIN
data/lua/x64/socket-linux-5-4.so
Normal file
BIN
data/lua/x64/socket-linux-5-4.so
Normal file
Binary file not shown.
BIN
data/lua/x64/socket-windows-5-1.dll
Normal file
BIN
data/lua/x64/socket-windows-5-1.dll
Normal file
Binary file not shown.
BIN
data/lua/x64/socket-windows-5-4.dll
Normal file
BIN
data/lua/x64/socket-windows-5-4.dll
Normal file
Binary file not shown.
20
data/lua/x86/luasocket.LICENSE.txt
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/lua/x86/socket-windows-5-1.dll
Normal file
Binary file not shown.
BIN
data/mcicon.png
Normal file
BIN
data/mcicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 594 B |
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ adventure_options:
|
|||||||
# 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 BizHawk
|
||||||
# (see BizHawk --lua= option)
|
# (see BizHawk --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
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoSta
|
|||||||
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: "{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
|
||||||
|
|||||||
19
kvui.py
19
kvui.py
@@ -25,6 +25,7 @@ from kivy.base import ExceptionHandler, ExceptionManager
|
|||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty
|
from kivy.properties import BooleanProperty, ObjectProperty
|
||||||
|
from kivy.metrics import dp
|
||||||
from kivy.uix.widget import Widget
|
from kivy.uix.widget import Widget
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
@@ -151,11 +152,11 @@ class ServerLabel(HovererableLabel):
|
|||||||
min_cost = int(ctx.server_version >= (0, 3, 9))
|
min_cost = int(ctx.server_version >= (0, 3, 9))
|
||||||
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
||||||
f"For you this means every " \
|
f"For you this means every " \
|
||||||
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))}" \
|
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
||||||
f" location checks."
|
"location checks." \
|
||||||
|
f"\nYou currently have {ctx.hint_points} points."
|
||||||
elif ctx.hint_cost == 0:
|
elif ctx.hint_cost == 0:
|
||||||
text += "\n!hint is free to use."
|
text += "\n!hint is free to use."
|
||||||
|
|
||||||
else:
|
else:
|
||||||
text += f"\nYou are not authenticated yet."
|
text += f"\nYou are not authenticated yet."
|
||||||
|
|
||||||
@@ -343,18 +344,18 @@ class GameManager(App):
|
|||||||
|
|
||||||
self.grid = MainLayout()
|
self.grid = MainLayout()
|
||||||
self.grid.cols = 1
|
self.grid.cols = 1
|
||||||
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30)
|
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||||
# top part
|
# top part
|
||||||
server_label = ServerLabel()
|
server_label = ServerLabel()
|
||||||
self.connect_layout.add_widget(server_label)
|
self.connect_layout.add_widget(server_label)
|
||||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None,
|
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None,
|
||||||
height=30, multiline=False, write_tab=False)
|
height=dp(30), multiline=False, write_tab=False)
|
||||||
def connect_bar_validate(sender):
|
def connect_bar_validate(sender):
|
||||||
if not self.ctx.server:
|
if not self.ctx.server:
|
||||||
self.connect_button_action(sender)
|
self.connect_button_action(sender)
|
||||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||||
self.connect_layout.add_widget(self.server_connect_bar)
|
self.connect_layout.add_widget(self.server_connect_bar)
|
||||||
self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None)
|
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
|
||||||
self.server_connect_button.bind(on_press=self.connect_button_action)
|
self.server_connect_button.bind(on_press=self.connect_button_action)
|
||||||
self.connect_layout.add_widget(self.server_connect_button)
|
self.connect_layout.add_widget(self.server_connect_button)
|
||||||
self.grid.add_widget(self.connect_layout)
|
self.grid.add_widget(self.connect_layout)
|
||||||
@@ -387,11 +388,11 @@ class GameManager(App):
|
|||||||
self.tabs.tab_height = 0
|
self.tabs.tab_height = 0
|
||||||
|
|
||||||
# bottom part
|
# bottom part
|
||||||
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30)
|
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||||
info_button = Button(height=30, text="Command:", size_hint_x=None)
|
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
|
||||||
info_button.bind(on_release=self.command_button_action)
|
info_button.bind(on_release=self.command_button_action)
|
||||||
bottom_layout.add_widget(info_button)
|
bottom_layout.add_widget(info_button)
|
||||||
self.textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False)
|
self.textinput = TextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
||||||
self.textinput.bind(on_text_validate=self.on_message)
|
self.textinput.bind(on_text_validate=self.on_message)
|
||||||
self.textinput.text_validate_unfocus = False
|
self.textinput.text_validate_unfocus = False
|
||||||
bottom_layout.add_widget(self.textinput)
|
bottom_layout.add_widget(self.textinput)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
colorama>=0.4.5
|
colorama>=0.4.5
|
||||||
websockets>=10.3
|
websockets>=11.0.1
|
||||||
PyYAML>=6.0
|
PyYAML>=6.0
|
||||||
jellyfish>=0.11.0
|
jellyfish>=0.11.2
|
||||||
jinja2>=3.1.2
|
jinja2>=3.1.2
|
||||||
schema>=0.7.5
|
schema>=0.7.5
|
||||||
kivy>=2.1.0
|
kivy>=2.1.0
|
||||||
|
|||||||
26
setup.py
26
setup.py
@@ -20,7 +20,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||||
try:
|
try:
|
||||||
requirement = 'cx-Freeze>=6.14.7'
|
requirement = 'cx-Freeze==6.14.7'
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
try:
|
try:
|
||||||
pkg_resources.require(requirement)
|
pkg_resources.require(requirement)
|
||||||
@@ -71,6 +71,9 @@ apworlds: set = {
|
|||||||
"Timespinner",
|
"Timespinner",
|
||||||
"Minecraft",
|
"Minecraft",
|
||||||
"The Messenger",
|
"The Messenger",
|
||||||
|
"Links Awakening DX",
|
||||||
|
"Super Metroid",
|
||||||
|
"SMZ3",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -154,11 +157,22 @@ build_arch = build_platform.split('-')[-1] if '-' in build_platform else platfor
|
|||||||
|
|
||||||
|
|
||||||
# see Launcher.py on how to add scripts to setup.py
|
# see Launcher.py on how to add scripts to setup.py
|
||||||
|
def resolve_icon(icon_name: str):
|
||||||
|
base_path = icon_paths[icon_name]
|
||||||
|
if is_windows:
|
||||||
|
path, extension = os.path.splitext(base_path)
|
||||||
|
ico_file = path + ".ico"
|
||||||
|
assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist."
|
||||||
|
return ico_file
|
||||||
|
else:
|
||||||
|
return base_path
|
||||||
|
|
||||||
|
|
||||||
exes = [
|
exes = [
|
||||||
cx_Freeze.Executable(
|
cx_Freeze.Executable(
|
||||||
script=f'{c.script_name}.py',
|
script=f'{c.script_name}.py',
|
||||||
target_name=c.frozen_name + (".exe" if is_windows else ""),
|
target_name=c.frozen_name + (".exe" if is_windows else ""),
|
||||||
icon=icon_paths[c.icon],
|
icon=resolve_icon(c.icon),
|
||||||
base="Win32GUI" if is_windows and not c.cli else None
|
base="Win32GUI" if is_windows and not c.cli else None
|
||||||
) for c in components if c.script_name and c.frozen_name
|
) for c in components if c.script_name and c.frozen_name
|
||||||
]
|
]
|
||||||
@@ -306,16 +320,12 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
|||||||
dirs_exist_ok=True)
|
dirs_exist_ok=True)
|
||||||
|
|
||||||
os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True)
|
os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True)
|
||||||
from WebHostLib.options import create
|
from Options import generate_yaml_templates
|
||||||
create()
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
assert not apworlds - set(AutoWorldRegister.world_types), "Unknown world designated for .apworld"
|
assert not apworlds - set(AutoWorldRegister.world_types), "Unknown world designated for .apworld"
|
||||||
folders_to_remove: typing.List[str] = []
|
folders_to_remove: typing.List[str] = []
|
||||||
|
generate_yaml_templates(self.buildfolder / "Players" / "Templates", False)
|
||||||
for worldname, worldtype in AutoWorldRegister.world_types.items():
|
for worldname, worldtype in AutoWorldRegister.world_types.items():
|
||||||
if not worldtype.hidden:
|
|
||||||
file_name = worldname+".yaml"
|
|
||||||
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
|
|
||||||
self.buildfolder / "Players" / "Templates" / file_name)
|
|
||||||
if worldname in apworlds:
|
if worldname in apworlds:
|
||||||
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
|
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
|
||||||
world_directory = self.libfolder / "worlds" / file_name
|
world_directory = self.libfolder / "worlds" / file_name
|
||||||
|
|||||||
70
test/general/TestHelpers.py
Normal file
70
test/general/TestHelpers.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from typing import Dict, Optional, Callable
|
||||||
|
|
||||||
|
from BaseClasses import MultiWorld, CollectionState, Region
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelpers(unittest.TestCase):
|
||||||
|
multiworld: MultiWorld
|
||||||
|
player: int = 1
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.multiworld = MultiWorld(self.player)
|
||||||
|
self.multiworld.game[self.player] = "helper_test_game"
|
||||||
|
self.multiworld.player_name = {1: "Tester"}
|
||||||
|
self.multiworld.set_seed()
|
||||||
|
self.multiworld.set_default_common_options()
|
||||||
|
|
||||||
|
def testRegionHelpers(self) -> None:
|
||||||
|
regions: Dict[str, str] = {
|
||||||
|
"TestRegion1": "I'm an apple",
|
||||||
|
"TestRegion2": "I'm a banana",
|
||||||
|
}
|
||||||
|
|
||||||
|
locations: Dict[str, Dict[str, Optional[int]]] = {
|
||||||
|
"TestRegion1": {
|
||||||
|
"loc_1": 123,
|
||||||
|
"loc_2": 456,
|
||||||
|
"event_loc": None,
|
||||||
|
},
|
||||||
|
"TestRegion2": {
|
||||||
|
"loc_1": 321,
|
||||||
|
"loc_2": 654,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reg_exits: Dict[str, Dict[str, Optional[str]]] = {
|
||||||
|
"TestRegion1": {"TestRegion2": "connection"},
|
||||||
|
"TestRegion2": {"TestRegion1": None},
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||||
|
"TestRegion1": lambda state: state.has("test_item", self.player)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
|
||||||
|
|
||||||
|
with self.subTest("Test Location Creation Helper"):
|
||||||
|
for region, loc_pair in locations.items():
|
||||||
|
self.multiworld.get_region(region, self.player).add_locations(loc_pair)
|
||||||
|
|
||||||
|
created_location_names = [loc.name for loc in self.multiworld.get_locations()]
|
||||||
|
for loc_pair in locations.values():
|
||||||
|
for loc_name in loc_pair:
|
||||||
|
self.assertTrue(loc_name in created_location_names)
|
||||||
|
|
||||||
|
with self.subTest("Test Exit Creation Helper"):
|
||||||
|
for region, exit_dict in reg_exits.items():
|
||||||
|
self.multiworld.get_region(region, self.player).add_exits(exit_dict, exit_rules)
|
||||||
|
|
||||||
|
created_exit_names = [exit.name for region in self.multiworld.get_regions() for exit in region.exits]
|
||||||
|
for parent, exit_pair in reg_exits.items():
|
||||||
|
for exit_reg, exit_name in exit_pair.items():
|
||||||
|
if exit_name:
|
||||||
|
self.assertTrue(exit_name in created_exit_names)
|
||||||
|
else:
|
||||||
|
self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names)
|
||||||
|
if exit_reg in exit_rules:
|
||||||
|
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
|
||||||
|
self.assertEqual(exit_rules[exit_reg],
|
||||||
|
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||||
@@ -59,3 +59,13 @@ class TestBase(unittest.TestCase):
|
|||||||
f"{game_name} modified region count during pre_fill")
|
f"{game_name} modified region count during pre_fill")
|
||||||
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
||||||
f"{game_name} modified locations count during pre_fill")
|
f"{game_name} modified locations count during pre_fill")
|
||||||
|
|
||||||
|
def testLocationGroup(self):
|
||||||
|
"""Test that all location name groups contain valid locations and don't share names."""
|
||||||
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
with self.subTest(game_name, game_name=game_name):
|
||||||
|
for group_name, locations in world_type.location_name_groups.items():
|
||||||
|
with self.subTest(group_name, group_name=group_name):
|
||||||
|
for location in locations:
|
||||||
|
self.assertIn(location, world_type.location_name_to_id)
|
||||||
|
self.assertNotIn(group_name, world_type.location_name_to_id)
|
||||||
|
|||||||
11
test/general/TestOptions.py
Normal file
11
test/general/TestOptions.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import unittest
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
|
||||||
|
class TestOptions(unittest.TestCase):
|
||||||
|
def testOptionsHaveDocString(self):
|
||||||
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
if not world_type.hidden:
|
||||||
|
for option_key, option in world_type.option_definitions.items():
|
||||||
|
with self.subTest(game=gamename, option=option_key):
|
||||||
|
self.assertTrue(option.__doc__)
|
||||||
@@ -25,7 +25,7 @@ class TestFileGeneration(unittest.TestCase):
|
|||||||
for file in os.scandir(target):
|
for file in os.scandir(target):
|
||||||
if file.is_file() and file.name.endswith(".yaml"):
|
if file.is_file() and file.name.endswith(".yaml"):
|
||||||
with self.subTest(file=file.name):
|
with self.subTest(file=file.name):
|
||||||
with open(file) as f:
|
with open(file, encoding="utf-8-sig") as f:
|
||||||
for value in roll_options({file.name: f.read()})[0].values():
|
for value in roll_options({file.name: f.read()})[0].values():
|
||||||
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
||||||
|
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
|||||||
f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" "
|
f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" "
|
||||||
f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.")
|
f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.")
|
||||||
|
|
||||||
# TODO: investigate: Iterating through a set is not a deterministic order.
|
for world_type in sorted(world_types, key=lambda world: world.__name__):
|
||||||
# If any random is used, this could make unreproducible seed.
|
|
||||||
for world_type in world_types:
|
|
||||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||||
if stage_callable:
|
if stage_callable:
|
||||||
stage_callable(multiworld, *args)
|
stage_callable(multiworld, *args)
|
||||||
@@ -159,7 +157,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
location_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
location_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||||
"""maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}"""
|
"""maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}"""
|
||||||
|
|
||||||
data_version: ClassVar[int] = 1
|
data_version: ClassVar[int] = 0
|
||||||
"""
|
"""
|
||||||
Increment this every time something in your world's names/id mappings changes.
|
Increment this every time something in your world's names/id mappings changes.
|
||||||
|
|
||||||
|
|||||||
@@ -95,12 +95,13 @@ components: List[Component] = [
|
|||||||
# Zillion
|
# Zillion
|
||||||
Component('Zillion Client', 'ZillionClient',
|
Component('Zillion Client', 'ZillionClient',
|
||||||
file_identifier=SuffixIdentifier('.apzl')),
|
file_identifier=SuffixIdentifier('.apzl')),
|
||||||
#Kingdom Hearts 2
|
# Kingdom Hearts 2
|
||||||
Component('KH2 Client', "KH2Client"),
|
Component('KH2 Client', "KH2Client"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
icon_paths = {
|
icon_paths = {
|
||||||
'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
|
'icon': local_path('data', 'icon.png'),
|
||||||
'mcicon': local_path('data', 'mcicon.ico')
|
'mcicon': local_path('data', 'mcicon.png'),
|
||||||
|
'discord': local_path('data', 'discord-mark-blue.png'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,15 +32,28 @@ components.append(Component('Adventure Client', 'AdventureClient', file_identifi
|
|||||||
|
|
||||||
|
|
||||||
class AdventureWeb(WebWorld):
|
class AdventureWeb(WebWorld):
|
||||||
tutorials = [Tutorial(
|
theme = "dirt"
|
||||||
|
|
||||||
|
setup = Tutorial(
|
||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
"A guide to setting up Adventure for MultiWorld.",
|
"A guide to setting up Adventure for MultiWorld.",
|
||||||
"English",
|
"English",
|
||||||
"setup_en.md",
|
"setup_en.md",
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["JusticePS"]
|
["JusticePS"]
|
||||||
)]
|
)
|
||||||
theme = "dirt"
|
|
||||||
|
setup_fr = Tutorial(
|
||||||
|
"Guide de configuration Multimonde",
|
||||||
|
"Un guide pour configurer Adventure MultiWorld",
|
||||||
|
"Français",
|
||||||
|
"setup_fr.md",
|
||||||
|
"setup/fr",
|
||||||
|
["TheLynk"]
|
||||||
|
)
|
||||||
|
|
||||||
|
tutorials = [setup, setup_fr]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_position_data_start(table_index: int):
|
def get_item_position_data_start(table_index: int):
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ Once Bizhawk has been installed, open Bizhawk and change the following settings:
|
|||||||
BizHawk is running in the background.
|
BizHawk is running in the background.
|
||||||
|
|
||||||
- It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically
|
- It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically
|
||||||
|
- At the same time, you can set an option to automatically load the connector_adventure.lua script when launching BizHawk
|
||||||
|
from AdventureClient.
|
||||||
|
Default Windows install example:
|
||||||
|
```rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"```
|
||||||
|
|
||||||
## Configuring your YAML file
|
## Configuring your YAML file
|
||||||
|
|
||||||
@@ -62,7 +66,8 @@ path as recommended).
|
|||||||
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
|
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
|
||||||
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
|
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
|
||||||
|
|
||||||
Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`.
|
Navigate to your Archipelago install folder and open `data/lua/connector_adventure.lua`, if it is not
|
||||||
|
configured to do this automatically.
|
||||||
|
|
||||||
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
|
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
|
||||||
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
|
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
|
||||||
|
|||||||
75
worlds/adventure/docs/setup_fr.md
Normal file
75
worlds/adventure/docs/setup_fr.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Guide d'installation pour Aventure : Archipelago
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
Comme nous utilisons Bizhawk, ce guide ne s'applique qu'aux systèmes Windows et Linux.
|
||||||
|
|
||||||
|
## Logiciel requis
|
||||||
|
|
||||||
|
- Bizhawk : [Bizhawk sort de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||||
|
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité.
|
||||||
|
- Des instructions d'installation détaillées pour Bizhawk peuvent être trouvées sur le lien ci-dessus.
|
||||||
|
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus.
|
||||||
|
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
|
(sélectionnez `Adventure Client` lors de l'installation).
|
||||||
|
- Un fichier ROM Adventure NTSC. La communauté Archipelago ne peut pas les fournir.
|
||||||
|
|
||||||
|
## Configuration de Bizhawk
|
||||||
|
|
||||||
|
Une fois Bizhawk installé, ouvrez Bizhawk et modifiez les paramètres suivants :
|
||||||
|
|
||||||
|
- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers
|
||||||
|
"Interface Lua+Lua". Redémarrez ensuite Bizhawk. Ceci est nécessaire pour que le script Lua fonctionne correctement.
|
||||||
|
**REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations**
|
||||||
|
**des versions plus récentes de Bizhawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours**
|
||||||
|
**"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.**
|
||||||
|
- Sous Config > Personnaliser, cochez la case "Exécuter en arrière-plan". Cela empêchera la déconnexion du client pendant
|
||||||
|
BizHawk s'exécute en arrière-plan.
|
||||||
|
|
||||||
|
- Il est recommandé de fournir un chemin vers BizHawk dans votre host.yaml pour Adventure afin que le client puisse le démarrer automatiquement
|
||||||
|
- En même temps, vous pouvez définir une option pour charger automatiquement le script connector_adventure.lua lors du lancement de BizHawk
|
||||||
|
d'AdventureClient.
|
||||||
|
Exemple d'installation Windows par défaut :
|
||||||
|
```rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"```
|
||||||
|
|
||||||
|
## Configuration de votre fichier YAML
|
||||||
|
|
||||||
|
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
|
||||||
|
|
||||||
|
Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit
|
||||||
|
générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter
|
||||||
|
une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes.
|
||||||
|
|
||||||
|
### Où puis-je obtenir un fichier YAML ?
|
||||||
|
|
||||||
|
Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-settings)
|
||||||
|
|
||||||
|
### Quels sont les paramètres recommandés pour s'initier à la rando ?
|
||||||
|
Régler la difficulty_switch_a et réduire la vitesse des dragons rend les dragons plus faciles à éviter. Ajouter Calice à
|
||||||
|
local_items garantit que vous visiterez au moins un des châteaux intéressants, car il ne peut être placé que dans un château ou
|
||||||
|
la salle des crédits.
|
||||||
|
|
||||||
|
## Rejoindre une partie MultiWorld
|
||||||
|
|
||||||
|
### Obtenez votre fichier de correctif Adventure
|
||||||
|
|
||||||
|
Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois cela fait,
|
||||||
|
l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun
|
||||||
|
des dossiers. Votre fichier de données doit avoir une extension `.apadvn`.
|
||||||
|
|
||||||
|
Faites glisser votre fichier de correctif vers AdventureClient.exe pour démarrer votre client et démarrer le processus de correctif ROM. Une fois le processus
|
||||||
|
est terminé (cela peut prendre un certain temps), le client et l'émulateur seront démarrés automatiquement (si vous configurez l'émulateur
|
||||||
|
chemin recommandé).
|
||||||
|
|
||||||
|
### Connectez-vous au multiserveur
|
||||||
|
|
||||||
|
Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils"
|
||||||
|
menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua.
|
||||||
|
|
||||||
|
Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/connector_adventure.lua`, si ce n'est pas le cas
|
||||||
|
configuré pour le faire automatiquement.
|
||||||
|
|
||||||
|
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le
|
||||||
|
le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect <adresse> :<port> [mot de passe]`)
|
||||||
|
|
||||||
|
Appuyez sur Réinitialiser et commencez à jouer
|
||||||
@@ -6,6 +6,7 @@ from Fill import FillError
|
|||||||
from .Options import LTTPBosses as Bosses
|
from .Options import LTTPBosses as Bosses
|
||||||
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source
|
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source
|
||||||
|
|
||||||
|
|
||||||
def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||||
if boss in boss_table:
|
if boss in boss_table:
|
||||||
enemizer_name, defeat_rule = boss_table[boss]
|
enemizer_name, defeat_rule = boss_table[boss]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Utils
|
|||||||
from NetUtils import ClientStatus, color
|
from NetUtils import ClientStatus, color
|
||||||
from worlds.AutoSNIClient import SNIClient
|
from worlds.AutoSNIClient import SNIClient
|
||||||
|
|
||||||
from worlds.alttp import Shops, Regions
|
from . import Shops, Regions
|
||||||
from .Rom import ROM_PLAYER_LIMIT
|
from .Rom import ROM_PLAYER_LIMIT
|
||||||
|
|
||||||
snes_logger = logging.getLogger("SNES")
|
snes_logger = logging.getLogger("SNES")
|
||||||
@@ -270,17 +270,20 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
|||||||
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
||||||
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
||||||
|
|
||||||
boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss',
|
collect_ignore_locations = {Regions.lookup_name_to_id[name] for name in {
|
||||||
'Desert Palace - Boss',
|
'Eastern Palace - Boss',
|
||||||
'Tower of Hera - Boss',
|
'Desert Palace - Boss',
|
||||||
'Palace of Darkness - Boss',
|
'Tower of Hera - Boss',
|
||||||
'Swamp Palace - Boss',
|
'Palace of Darkness - Boss',
|
||||||
'Skull Woods - Boss',
|
'Swamp Palace - Boss',
|
||||||
"Thieves' Town - Boss",
|
'Skull Woods - Boss',
|
||||||
'Ice Palace - Boss',
|
"Thieves' Town - Boss",
|
||||||
'Misery Mire - Boss',
|
'Ice Palace - Boss',
|
||||||
'Turtle Rock - Boss',
|
'Misery Mire - Boss',
|
||||||
'Sahasrahla'}}
|
'Turtle Rock - Boss',
|
||||||
|
'Sahasrahla',
|
||||||
|
'Master Sword Pedestal', # can circumvent ganon pedestal's goal's pendant collection
|
||||||
|
}}
|
||||||
|
|
||||||
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
|
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
|
||||||
|
|
||||||
@@ -322,8 +325,15 @@ location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
|
|||||||
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
|
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def should_collect(ctx, location_id: int) -> bool:
|
||||||
|
return ctx.allow_collect and location_id not in collect_ignore_locations and location_id in ctx.checked_locations \
|
||||||
|
and location_id not in ctx.locations_checked and location_id in ctx.locations_info \
|
||||||
|
and ctx.locations_info[location_id].player != ctx.slot
|
||||||
|
|
||||||
|
|
||||||
async def track_locations(ctx, roomid, roomdata) -> bool:
|
async def track_locations(ctx, roomid, roomdata) -> bool:
|
||||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||||
|
location_id: int
|
||||||
new_locations = []
|
new_locations = []
|
||||||
|
|
||||||
def new_check(location_id):
|
def new_check(location_id):
|
||||||
@@ -331,18 +341,19 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
|||||||
ctx.locations_checked.add(location_id)
|
ctx.locations_checked.add(location_id)
|
||||||
location = ctx.location_names[location_id]
|
location = ctx.location_names[location_id]
|
||||||
snes_logger.info(
|
snes_logger.info(
|
||||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
f'New Check: {location} ' +
|
||||||
|
f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' +
|
||||||
|
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN)
|
shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN)
|
||||||
shop_data_changed = False
|
shop_data_changed = False
|
||||||
shop_data = list(shop_data)
|
shop_data = list(shop_data)
|
||||||
for cnt, b in enumerate(shop_data):
|
for cnt, b in enumerate(shop_data):
|
||||||
location = Shops.SHOP_ID_START + cnt
|
location_id = Shops.SHOP_ID_START + cnt
|
||||||
if int(b) and location not in ctx.locations_checked:
|
if int(b) and location_id not in ctx.locations_checked:
|
||||||
new_check(location)
|
new_check(location_id)
|
||||||
if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \
|
if should_collect(ctx, location_id):
|
||||||
and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot:
|
|
||||||
if not int(b):
|
if not int(b):
|
||||||
shop_data[cnt] += 1
|
shop_data[cnt] += 1
|
||||||
shop_data_changed = True
|
shop_data_changed = True
|
||||||
@@ -369,9 +380,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
|||||||
uw_unchecked[location_id] = (roomid, mask)
|
uw_unchecked[location_id] = (roomid, mask)
|
||||||
uw_begin = min(uw_begin, roomid)
|
uw_begin = min(uw_begin, roomid)
|
||||||
uw_end = max(uw_end, roomid + 1)
|
uw_end = max(uw_end, roomid + 1)
|
||||||
if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \
|
if should_collect(ctx, location_id):
|
||||||
and location_id not in ctx.locations_checked and location_id in ctx.locations_info \
|
|
||||||
and ctx.locations_info[location_id].player != ctx.slot:
|
|
||||||
uw_begin = min(uw_begin, roomid)
|
uw_begin = min(uw_begin, roomid)
|
||||||
uw_end = max(uw_end, roomid + 1)
|
uw_end = max(uw_end, roomid + 1)
|
||||||
uw_checked[location_id] = (roomid, mask)
|
uw_checked[location_id] = (roomid, mask)
|
||||||
@@ -402,8 +411,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
|||||||
ow_unchecked[location_id] = screenid
|
ow_unchecked[location_id] = screenid
|
||||||
ow_begin = min(ow_begin, screenid)
|
ow_begin = min(ow_begin, screenid)
|
||||||
ow_end = max(ow_end, screenid + 1)
|
ow_end = max(ow_end, screenid + 1)
|
||||||
if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \
|
if should_collect(ctx, location_id):
|
||||||
and ctx.locations_info[location_id].player != ctx.slot:
|
|
||||||
ow_checked[location_id] = screenid
|
ow_checked[location_id] = screenid
|
||||||
|
|
||||||
if ow_begin < ow_end:
|
if ow_begin < ow_end:
|
||||||
@@ -426,9 +434,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
|||||||
for location_id, mask in location_table_npc_id.items():
|
for location_id, mask in location_table_npc_id.items():
|
||||||
if npc_value & mask != 0 and location_id not in ctx.locations_checked:
|
if npc_value & mask != 0 and location_id not in ctx.locations_checked:
|
||||||
new_check(location_id)
|
new_check(location_id)
|
||||||
if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \
|
if should_collect(ctx, location_id):
|
||||||
and location_id not in ctx.locations_checked and location_id in ctx.locations_info \
|
|
||||||
and ctx.locations_info[location_id].player != ctx.slot:
|
|
||||||
npc_value |= mask
|
npc_value |= mask
|
||||||
npc_value_changed = True
|
npc_value_changed = True
|
||||||
if npc_value_changed:
|
if npc_value_changed:
|
||||||
@@ -444,8 +450,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
|
|||||||
assert (0x3c6 <= offset <= 0x3c9)
|
assert (0x3c6 <= offset <= 0x3c9)
|
||||||
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
|
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
|
||||||
new_check(location_id)
|
new_check(location_id)
|
||||||
if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \
|
if should_collect(ctx, location_id):
|
||||||
and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot:
|
|
||||||
misc_data_changed = True
|
misc_data_changed = True
|
||||||
misc_data[offset - 0x3c6] |= mask
|
misc_data[offset - 0x3c6] |= mask
|
||||||
if misc_data_changed:
|
if misc_data_changed:
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import Dungeon
|
from BaseClasses import CollectionState, Dungeon
|
||||||
from worlds.alttp.Bosses import BossFactory
|
|
||||||
from Fill import fill_restrictive
|
from Fill import fill_restrictive
|
||||||
from worlds.alttp.Items import ItemFactory
|
|
||||||
from worlds.alttp.Regions import lookup_boss_drops
|
from .Bosses import BossFactory
|
||||||
from worlds.alttp.Options import smallkey_shuffle
|
from .Items import ItemFactory
|
||||||
|
from .Regions import lookup_boss_drops
|
||||||
|
from .Options import smallkey_shuffle
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from .SubClasses import ALttPLocation
|
from .SubClasses import ALttPLocation
|
||||||
|
|
||||||
|
|
||||||
def create_dungeons(world, player):
|
def create_dungeons(world, player):
|
||||||
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
||||||
dungeon = Dungeon(name, dungeon_regions, big_key,
|
dungeon = Dungeon(name, dungeon_regions, big_key,
|
||||||
@@ -152,7 +154,6 @@ def fill_dungeons_restrictive(world):
|
|||||||
(not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item)
|
(not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item)
|
||||||
|
|
||||||
world.random.shuffle(locations)
|
world.random.shuffle(locations)
|
||||||
all_state_base = world.get_all_state(use_cache=True)
|
|
||||||
# Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items
|
# Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items
|
||||||
# subsort in the order Big Key, Small Key, Other before placing dungeon items
|
# subsort in the order Big Key, Small Key, Other before placing dungeon items
|
||||||
|
|
||||||
@@ -160,8 +161,31 @@ def fill_dungeons_restrictive(world):
|
|||||||
in_dungeon_items.sort(
|
in_dungeon_items.sort(
|
||||||
key=lambda item: sort_order.get(item.type, 1) +
|
key=lambda item: sort_order.get(item.type, 1) +
|
||||||
(5 if (item.player, item.name) in dungeon_specific else 0))
|
(5 if (item.player, item.name) in dungeon_specific else 0))
|
||||||
|
|
||||||
|
# Construct a partial all_state which contains only the items from get_pre_fill_items which aren't in_dungeon
|
||||||
|
in_dungeon_player_ids = {item.player for item in in_dungeon_items}
|
||||||
|
all_state_base = CollectionState(world)
|
||||||
|
for item in world.itempool:
|
||||||
|
world.worlds[item.player].collect(all_state_base, item)
|
||||||
|
pre_fill_items = []
|
||||||
|
for player in in_dungeon_player_ids:
|
||||||
|
pre_fill_items += world.worlds[player].get_pre_fill_items()
|
||||||
for item in in_dungeon_items:
|
for item in in_dungeon_items:
|
||||||
all_state_base.remove(item)
|
try:
|
||||||
|
pre_fill_items.remove(item)
|
||||||
|
except ValueError:
|
||||||
|
# pre_fill_items should be a subset of in_dungeon_items, but just in case
|
||||||
|
pass
|
||||||
|
for item in pre_fill_items:
|
||||||
|
world.worlds[item.player].collect(all_state_base, item)
|
||||||
|
all_state_base.sweep_for_events()
|
||||||
|
|
||||||
|
|
||||||
|
# Remove completion condition so that minimal-accessibility worlds place keys properly
|
||||||
|
for player in {item.player for item in in_dungeon_items}:
|
||||||
|
if all_state_base.has("Triforce", player):
|
||||||
|
all_state_base.remove(world.worlds[player].create_item("Triforce"))
|
||||||
|
|
||||||
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True)
|
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave.
|
# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave.
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from worlds.alttp.OverworldGlitchRules import overworld_glitch_connections
|
|
||||||
from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections
|
from .OverworldGlitchRules import overworld_glitch_connections
|
||||||
|
from .UnderworldGlitchRules import underworld_glitch_connections
|
||||||
|
|
||||||
|
|
||||||
def link_entrances(world, player):
|
def link_entrances(world, player):
|
||||||
connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now
|
connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import collections
|
import collections
|
||||||
from worlds.alttp.Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
|
|
||||||
from worlds.alttp.SubClasses import LTTPRegionType
|
from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
|
||||||
|
from .SubClasses import LTTPRegionType
|
||||||
|
|
||||||
|
|
||||||
def create_inverted_regions(world, player):
|
def create_inverted_regions(world, player):
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ from collections import namedtuple
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from BaseClasses import ItemClassification
|
from BaseClasses import ItemClassification
|
||||||
from worlds.alttp.SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
|
|
||||||
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations
|
|
||||||
from worlds.alttp.Bosses import place_bosses
|
|
||||||
from worlds.alttp.Dungeons import get_dungeon_item_pool_player
|
|
||||||
from worlds.alttp.EntranceShuffle import connect_entrance
|
|
||||||
from Fill import FillError
|
from Fill import FillError
|
||||||
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
|
|
||||||
from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses
|
from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
|
||||||
|
from .Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations
|
||||||
|
from .Bosses import place_bosses
|
||||||
|
from .Dungeons import get_dungeon_item_pool_player
|
||||||
|
from .EntranceShuffle import connect_entrance
|
||||||
|
from .Items import ItemFactory, GetBeemizerItem
|
||||||
|
from .Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses
|
||||||
from .StateHelpers import has_triforce_pieces, has_melee_weapon
|
from .StateHelpers import has_triforce_pieces, has_melee_weapon
|
||||||
|
|
||||||
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import typing
|
|||||||
|
|
||||||
from BaseClasses import ItemClassification as IC
|
from BaseClasses import ItemClassification as IC
|
||||||
|
|
||||||
|
|
||||||
def GetBeemizerItem(world, player: int, item):
|
def GetBeemizerItem(world, player: int, item):
|
||||||
item_name = item if isinstance(item, str) else item.name
|
item_name = item if isinstance(item, str) else item.name
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice, PlandoBosses
|
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses
|
||||||
|
|
||||||
|
|
||||||
class Logic(Choice):
|
class Logic(Choice):
|
||||||
@@ -466,5 +466,6 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
|||||||
"beemizer_total_chance": BeemizerTotalChance,
|
"beemizer_total_chance": BeemizerTotalChance,
|
||||||
"beemizer_trap_chance": BeemizerTrapChance,
|
"beemizer_trap_chance": BeemizerTrapChance,
|
||||||
"death_link": DeathLink,
|
"death_link": DeathLink,
|
||||||
"allow_collect": AllowCollect
|
"allow_collect": AllowCollect,
|
||||||
|
"start_inventory_from_pool": StartInventoryPool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,21 @@ from BaseClasses import Entrance
|
|||||||
|
|
||||||
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
|
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
|
||||||
|
|
||||||
|
|
||||||
def get_sword_required_superbunny_mirror_regions():
|
def get_sword_required_superbunny_mirror_regions():
|
||||||
"""
|
"""
|
||||||
Cave regions that superbunny can get through - but only with a sword.
|
Cave regions that superbunny can get through - but only with a sword.
|
||||||
"""
|
"""
|
||||||
yield 'Spiral Cave (Top)'
|
yield 'Spiral Cave (Top)'
|
||||||
|
|
||||||
|
|
||||||
def get_boots_required_superbunny_mirror_regions():
|
def get_boots_required_superbunny_mirror_regions():
|
||||||
"""
|
"""
|
||||||
Cave regions that superbunny can get through - but only with boots.
|
Cave regions that superbunny can get through - but only with boots.
|
||||||
"""
|
"""
|
||||||
yield 'Two Brothers House'
|
yield 'Two Brothers House'
|
||||||
|
|
||||||
|
|
||||||
def get_boots_required_superbunny_mirror_locations():
|
def get_boots_required_superbunny_mirror_locations():
|
||||||
"""
|
"""
|
||||||
Cave locations that superbunny can access - but only with boots.
|
Cave locations that superbunny can access - but only with boots.
|
||||||
@@ -207,7 +210,6 @@ def get_mirror_offset_spots_lw(player):
|
|||||||
yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player))
|
yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_invalid_bunny_revival_dungeons():
|
def get_invalid_bunny_revival_dungeons():
|
||||||
"""
|
"""
|
||||||
Dungeon regions that can't be bunny revived from without superbunny state.
|
Dungeon regions that can't be bunny revived from without superbunny state.
|
||||||
@@ -300,6 +302,7 @@ def create_no_logic_connections(player, world, connections):
|
|||||||
parent.exits.append(connection)
|
parent.exits.append(connection)
|
||||||
connection.connect(target)
|
connection.connect(target)
|
||||||
|
|
||||||
|
|
||||||
def create_owg_connections(player, world, connections):
|
def create_owg_connections(player, world, connections):
|
||||||
for entrance, parent_region, target_region, *rule_override in connections:
|
for entrance, parent_region, target_region, *rule_override in connections:
|
||||||
parent = world.get_region(parent_region, player)
|
parent = world.get_region(parent_region, player)
|
||||||
@@ -308,6 +311,7 @@ def create_owg_connections(player, world, connections):
|
|||||||
parent.exits.append(connection)
|
parent.exits.append(connection)
|
||||||
connection.connect(target)
|
connection.connect(target)
|
||||||
|
|
||||||
|
|
||||||
def set_owg_connection_rules(player, world, connections, default_rule):
|
def set_owg_connection_rules(player, world, connections, default_rule):
|
||||||
for entrance, _, _, *rule_override in connections:
|
for entrance, _, _, *rule_override in connections:
|
||||||
connection = world.get_entrance(entrance, player)
|
connection = world.get_entrance(entrance, player)
|
||||||
|
|||||||
@@ -21,22 +21,22 @@ import bsdiff4
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||||
from worlds.alttp.Shops import ShopType, ShopPriceType
|
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||||
from worlds.alttp.Dungeons import dungeon_music_addresses
|
|
||||||
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
|
from .Shops import ShopType, ShopPriceType
|
||||||
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
|
from .Dungeons import dungeon_music_addresses
|
||||||
from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \
|
from .Regions import old_location_address_to_new_location_address
|
||||||
|
from .Text import MultiByteTextMapper, text_addresses, Credits, TextTable
|
||||||
|
from .Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \
|
||||||
Blind_texts, \
|
Blind_texts, \
|
||||||
BombShop2_texts, junk_texts
|
BombShop2_texts, junk_texts
|
||||||
|
from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \
|
||||||
from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \
|
|
||||||
DeathMountain_texts, \
|
DeathMountain_texts, \
|
||||||
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
||||||
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
from .Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
|
from .EntranceShuffle import door_addresses
|
||||||
from worlds.alttp.EntranceShuffle import door_addresses
|
from .Options import smallkey_shuffle
|
||||||
from worlds.alttp.Options import smallkey_shuffle
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from maseya import z3pr
|
from maseya import z3pr
|
||||||
@@ -189,7 +189,7 @@ def check_enemizer(enemizercli):
|
|||||||
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
|
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
|
||||||
if getattr(check_enemizer, "done", None):
|
if getattr(check_enemizer, "done", None):
|
||||||
return
|
return
|
||||||
wanted_version = (7, 0, 1)
|
wanted_version = (7, 1, 0)
|
||||||
# version info is saved on the lib, for some reason
|
# version info is saved on the lib, for some reason
|
||||||
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
|
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
|
||||||
with open(library_info) as f:
|
with open(library_info) as f:
|
||||||
@@ -1247,8 +1247,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
# assorted fixes
|
# assorted fixes
|
||||||
rom.write_byte(0x1800A2, 0x01 if world.fix_fake_world[
|
rom.write_byte(0x1800A2, 0x01 if world.fix_fake_world[
|
||||||
player] else 0x00) # Toggle whether to be in real/fake dark world when dying in a DW dungeon before killing aga1
|
player] else 0x00) # Toggle whether to be in real/fake dark world when dying in a DW dungeon before killing aga1
|
||||||
rom.write_byte(0x180169,
|
# Lock or unlock aga tower door during escape sequence.
|
||||||
0x01 if world.lock_aga_door_in_escape else 0x00) # Lock or unlock aga tower door during escape sequence.
|
rom.write_byte(0x180169, 0x00)
|
||||||
if world.mode[player] == 'inverted':
|
if world.mode[player] == 'inverted':
|
||||||
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
|
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
|
||||||
rom.write_byte(0x180171,
|
rom.write_byte(0x180171,
|
||||||
@@ -1775,8 +1775,57 @@ def hud_format_text(text):
|
|||||||
output += b'\x7f\x00'
|
output += b'\x7f\x00'
|
||||||
return output[:32]
|
return output[:32]
|
||||||
|
|
||||||
|
def apply_oof_sfx(rom, oof: str):
|
||||||
|
with open(oof, 'rb') as stream:
|
||||||
|
oof_bytes = bytearray(stream.read())
|
||||||
|
|
||||||
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
|
oof_len_bytes = len(oof_bytes).to_bytes(2, byteorder='little')
|
||||||
|
|
||||||
|
# Credit to kan for this method, and Nyx for initial C# implementation
|
||||||
|
# this is ported from, with both of their permission for use by AP
|
||||||
|
# Original C# implementation:
|
||||||
|
# https://github.com/Nyx-Edelstein/The-Unachievable-Ideal-of-Chibi-Elf-Grunting-Noises-When-They-Get-Punched-A-Z3-Rom-Patcher
|
||||||
|
|
||||||
|
# Jump execution from the SPC load routine to new code
|
||||||
|
rom.write_bytes(0x8CF, [0x5C, 0x00, 0x80, 0x25])
|
||||||
|
|
||||||
|
# Change the pointer for instrument 9 in SPC memory to point to the new data we'll be inserting:
|
||||||
|
rom.write_bytes(0x1A006C, [0x88, 0x31, 0x00, 0x00])
|
||||||
|
|
||||||
|
# Insert a sigil so we can branch on it later
|
||||||
|
# We will recover the value it overwrites after we're done with insertion
|
||||||
|
rom.write_bytes(0x1AD38C, [0xBE, 0xBE])
|
||||||
|
|
||||||
|
# Change the "oof" sound effect to use instrument 9:
|
||||||
|
rom.write_byte(0x1A9C4E, 0x09)
|
||||||
|
|
||||||
|
# Correct the pitch shift value:
|
||||||
|
rom.write_byte(0x1A9C51, 0xB6)
|
||||||
|
|
||||||
|
# Modify parameters of instrument 9
|
||||||
|
# (I don't actually understand this part, they're just magic values to me)
|
||||||
|
rom.write_bytes(0x1A9CAE, [0x7F, 0x7F, 0x00, 0x10, 0x1A, 0x00, 0x00, 0x7F, 0x01])
|
||||||
|
|
||||||
|
# Hook from SPC load routine:
|
||||||
|
# * Check for the read of the sigil
|
||||||
|
# * Once we find it, change the SPC load routine's data pointer to read from the location containing the new sample
|
||||||
|
# * Note: XXXX in the string below is a placeholder for the number of bytes in the .brr sample (little endian)
|
||||||
|
# * Another sigil "$EBEB" is inserted at the end of the data
|
||||||
|
# * When the second sigil is read, we know we're done inserting our data so we can change the data pointer back
|
||||||
|
# * Effect: The new data gets loaded into SPC memory without having to relocate the SPC load routine
|
||||||
|
# Slight variation from VT-compatible algorithm: We need to change the data pointer to $00 00 35 and load 538E into Y to pick back up where we left off
|
||||||
|
rom.write_bytes(0x128000, [0xB7, 0x00, 0xC8, 0xC8, 0xC9, 0xBE, 0xBE, 0xF0, 0x09, 0xC9, 0xEB, 0xEB, 0xF0, 0x1B, 0x5C, 0xD3, 0x88, 0x00, 0xA2, oof_len_bytes[0], oof_len_bytes[1], 0xA9, 0x80, 0x25, 0x85, 0x01, 0xA9, 0x3A, 0x80, 0x85, 0x00, 0xA0, 0x00, 0x00, 0xA9, 0x88, 0x31, 0x5C, 0xD8, 0x88, 0x00, 0xA9, 0x80, 0x35, 0x64, 0x00, 0x85, 0x01, 0xA2, 0x00, 0x00, 0xA0, 0x8E, 0x53, 0x5C, 0xD4, 0x88, 0x00])
|
||||||
|
|
||||||
|
# The new sample data
|
||||||
|
# (We need to insert the second sigil at the end)
|
||||||
|
rom.write_bytes(0x12803A, oof_bytes)
|
||||||
|
rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB])
|
||||||
|
|
||||||
|
#Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT")
|
||||||
|
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
|
||||||
|
|
||||||
|
|
||||||
|
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
|
||||||
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
|
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
|
||||||
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
|
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
|
||||||
local_random = random if not world else world.per_slot_randoms[player]
|
local_random = random if not world else world.per_slot_randoms[player]
|
||||||
@@ -1918,6 +1967,10 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri
|
|||||||
|
|
||||||
apply_random_sprite_on_event(rom, sprite, local_random, allow_random_on_event,
|
apply_random_sprite_on_event(rom, sprite, local_random, allow_random_on_event,
|
||||||
world.sprite_pool[player] if world else [])
|
world.sprite_pool[player] if world else [])
|
||||||
|
|
||||||
|
if oof is not None:
|
||||||
|
apply_oof_sfx(rom, oof)
|
||||||
|
|
||||||
if isinstance(rom, LocalRom):
|
if isinstance(rom, LocalRom):
|
||||||
rom.write_crc()
|
rom.write_crc()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Iterator, Set
|
|||||||
|
|
||||||
from BaseClasses import Entrance, MultiWorld
|
from BaseClasses import Entrance, MultiWorld
|
||||||
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
||||||
item_in_locations, location_item_name, set_rule, allow_self_locking_items)
|
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
|
||||||
|
|
||||||
from . import OverworldGlitchRules
|
from . import OverworldGlitchRules
|
||||||
from .Bosses import GanonDefeatRule
|
from .Bosses import GanonDefeatRule
|
||||||
@@ -305,7 +305,7 @@ def global_rules(world, player):
|
|||||||
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
|
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
|
||||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
|
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
|
||||||
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
|
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
|
||||||
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||||
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
|
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
|
||||||
|
|
||||||
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
|
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
|
||||||
@@ -381,17 +381,17 @@ def global_rules(world, player):
|
|||||||
|
|
||||||
#The actual requirements for these rooms to avoid key-lock
|
#The actual requirements for these rooms to avoid key-lock
|
||||||
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
|
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
|
||||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
|
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
|
||||||
for location in randomizer_room_chests:
|
for location in randomizer_room_chests:
|
||||||
set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
||||||
|
|
||||||
# Once again it is possible to need more than 3 keys...
|
# Once again it is possible to need more than 3 keys...
|
||||||
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
|
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
|
||||||
# Actual requirements
|
# Actual requirements
|
||||||
for location in compass_room_chests:
|
for location in compass_room_chests:
|
||||||
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||||
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
|
item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
|
||||||
|
|
||||||
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
|
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||||
|
|
||||||
@@ -919,7 +919,7 @@ def set_trock_key_rules(world, player):
|
|||||||
else:
|
else:
|
||||||
# Middle to front requires 2 keys if the back is locked, otherwise 4
|
# Middle to front requires 2 keys if the back is locked, otherwise 4
|
||||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
|
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
|
||||||
if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
|
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
|
||||||
else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
|
else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
|
||||||
|
|
||||||
# Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
|
# Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ from enum import unique, IntEnum
|
|||||||
from typing import List, Optional, Set, NamedTuple, Dict
|
from typing import List, Optional, Set, NamedTuple, Dict
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from worlds.alttp.SubClasses import ALttPLocation
|
|
||||||
from worlds.alttp.EntranceShuffle import door_addresses
|
|
||||||
from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
|
|
||||||
from worlds.alttp.Options import smallkey_shuffle
|
|
||||||
from Utils import int16_as_bytes
|
from Utils import int16_as_bytes
|
||||||
|
|
||||||
|
from .SubClasses import ALttPLocation
|
||||||
|
from .EntranceShuffle import door_addresses
|
||||||
|
from .Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
|
||||||
|
from .Options import smallkey_shuffle
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("Shops")
|
logger = logging.getLogger("Shops")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user