mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-21 06:45:49 -07:00
Compare commits
85 Commits
allow_coll
...
0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8971340a66 | ||
|
|
30cfd3186c | ||
|
|
1dc4e2b44b | ||
|
|
d5b4a91a13 | ||
|
|
bf5282dfa8 | ||
|
|
4eea91daab | ||
|
|
20e80d06cf | ||
|
|
59b78528a9 | ||
|
|
cd4fd18706 | ||
|
|
af44c1ba3d | ||
|
|
3ef0a56ec2 | ||
|
|
4ff282a384 | ||
|
|
f3dad894ec | ||
|
|
a5373e3672 | ||
|
|
639606e0be | ||
|
|
bb79073ce7 | ||
|
|
53b3cd029e | ||
|
|
99bd525c8e | ||
|
|
d14ab97849 | ||
|
|
f50e85b401 | ||
|
|
b64565594a | ||
|
|
ae7dad8bf9 | ||
|
|
b7c74919b7 | ||
|
|
a7f7f91aaf | ||
|
|
e62f989ce8 | ||
|
|
21c6c28755 | ||
|
|
f0403b9c9d | ||
|
|
f09f3663d6 | ||
|
|
b5bd93c420 | ||
|
|
90813c0f4b | ||
|
|
e2c4293a6d | ||
|
|
963c33c02a | ||
|
|
7d603e7d8d | ||
|
|
f2e1495d39 | ||
|
|
7927b2ee25 | ||
|
|
4f2b13a674 | ||
|
|
ffd7d5da74 | ||
|
|
67eb370200 | ||
|
|
4456e36fbb | ||
|
|
7fd9e71b3c | ||
|
|
f4a68f1c3d | ||
|
|
754a57cf69 | ||
|
|
384577e421 | ||
|
|
0ed3865c30 | ||
|
|
77b2ed54a6 | ||
|
|
0386d9f6d2 | ||
|
|
7e52b6d8bb | ||
|
|
03cf525b2c | ||
|
|
e1f46d623c | ||
|
|
5bb6ff0ce0 | ||
|
|
256f493ada | ||
|
|
3ec2d45f4f | ||
|
|
b3895750ab | ||
|
|
7591404151 | ||
|
|
d48e1e447f | ||
|
|
206f8cf5ed | ||
|
|
0c6b1827fe | ||
|
|
017f91c1b5 | ||
|
|
95b01def6b | ||
|
|
5977e401d5 | ||
|
|
21a3c74783 | ||
|
|
2fb9176511 | ||
|
|
1c69fb3c3c | ||
|
|
91502505a1 | ||
|
|
01c13ca243 | ||
|
|
c2a8b842de | ||
|
|
856efebc39 | ||
|
|
5a4203649d | ||
|
|
ddb764a9b6 | ||
|
|
9f65f22fac | ||
|
|
b7ff9b69ba | ||
|
|
012e6ba24c | ||
|
|
cd9d0bebc8 | ||
|
|
3fa6588637 | ||
|
|
e6d16c905c | ||
|
|
958829d491 | ||
|
|
9ee37b0ec5 | ||
|
|
81a239325d | ||
|
|
67bf12369a | ||
|
|
d4b793902f | ||
|
|
6671b21a86 | ||
|
|
6d13dc4944 | ||
|
|
ff9f563d4a | ||
|
|
d825576f12 | ||
|
|
5d6184f1fd |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -36,8 +36,7 @@ jobs:
|
||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||
- name: Build
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools
|
||||
pip install -r requirements.txt
|
||||
python -m pip install --upgrade pip
|
||||
python setup.py build_exe --yes
|
||||
$NAME="$(ls build)".Split('.',2)[1]
|
||||
$ZIP_NAME="Archipelago_$NAME.7z"
|
||||
@@ -85,8 +84,7 @@ jobs:
|
||||
# charset-normalizer was somehow incomplete in the github runner
|
||||
"${{ env.PYTHON }}" -m venv venv
|
||||
source venv/bin/activate
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
|
||||
pip install -r requirements.txt
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
|
||||
python setup.py build_exe --yes bdist_appimage --yes
|
||||
echo -e "setup.py build output:\n `ls build`"
|
||||
echo -e "setup.py dist output:\n `ls dist`"
|
||||
@@ -96,6 +94,10 @@ jobs:
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - copy code above to release.yml -
|
||||
- name: Build Again
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python setup.py build_exe --yes
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '**.py'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
flake8:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8 pytest pytest-subtests
|
||||
pip install flake8
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -63,9 +63,8 @@ jobs:
|
||||
# charset-normalizer was somehow incomplete in the github runner
|
||||
"${{ env.PYTHON }}" -m venv venv
|
||||
source venv/bin/activate
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
|
||||
pip install -r requirements.txt
|
||||
python setup.py build --yes bdist_appimage --yes
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
|
||||
python setup.py build_exe --yes bdist_appimage --yes
|
||||
echo -e "setup.py build output:\n `ls build`"
|
||||
echo -e "setup.py dist output:\n `ls dist`"
|
||||
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
|
||||
|
||||
4
.github/workflows/unittests.yml
vendored
4
.github/workflows/unittests.yml
vendored
@@ -52,8 +52,8 @@ jobs:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8 pytest pytest-subtests
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-subtests
|
||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||
- name: Unittests
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,6 +26,7 @@
|
||||
*multisave
|
||||
*.archipelago
|
||||
*.apsave
|
||||
*.BIN
|
||||
|
||||
build
|
||||
bundle/components.wxs
|
||||
@@ -52,6 +53,7 @@ Output Logs/
|
||||
/setup.ini
|
||||
/installdelete.iss
|
||||
/data/user.kv
|
||||
/datapackage
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
516
AdventureClient.py
Normal file
516
AdventureClient.py
Normal file
@@ -0,0 +1,516 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import bsdiff4
|
||||
import subprocess
|
||||
import zipfile
|
||||
from asyncio import StreamReader, StreamWriter, CancelledError
|
||||
from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from NetUtils import ClientStatus
|
||||
from Utils import async_start
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
from worlds.adventure import AdventureDeltaPatch
|
||||
|
||||
from worlds.adventure.Locations import base_location_id
|
||||
from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation
|
||||
from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table
|
||||
from worlds.adventure.Offsets import static_item_element_size, connector_port_offset
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
|
||||
CONNECTION_TIMING_OUT_STATUS = \
|
||||
"Connection timing out. Please restart your emulator, then restart adventure_connector.lua"
|
||||
CONNECTION_REFUSED_STATUS = \
|
||||
"Connection Refused. Please start your emulator and make sure adventure_connector.lua is running"
|
||||
CONNECTION_RESET_STATUS = \
|
||||
"Connection was reset. Please restart your emulator, then restart adventure_connector.lua"
|
||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||
|
||||
SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class AdventureCommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
super().__init__(ctx)
|
||||
|
||||
def _cmd_2600(self):
|
||||
"""Check 2600 Connection State"""
|
||||
if isinstance(self.ctx, AdventureContext):
|
||||
logger.info(f"2600 Status: {self.ctx.atari_status}")
|
||||
|
||||
def _cmd_aconnect(self):
|
||||
"""Discard current atari 2600 connection state"""
|
||||
if isinstance(self.ctx, AdventureContext):
|
||||
self.ctx.atari_sync_task.cancel()
|
||||
|
||||
|
||||
class AdventureContext(CommonContext):
|
||||
command_processor = AdventureCommandProcessor
|
||||
game = 'Adventure'
|
||||
lua_connector_port: int = 17242
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.freeincarnates_used: int = -1
|
||||
self.freeincarnate_pending: int = 0
|
||||
self.foreign_items: [AdventureForeignItemInfo] = []
|
||||
self.autocollect_items: [AdventureAutoCollectLocation] = []
|
||||
self.atari_streams: (StreamReader, StreamWriter) = None
|
||||
self.atari_sync_task = None
|
||||
self.messages = {}
|
||||
self.locations_array = None
|
||||
self.atari_status = CONNECTION_INITIAL_STATUS
|
||||
self.awaiting_rom = False
|
||||
self.display_msgs = True
|
||||
self.deathlink_pending = False
|
||||
self.set_deathlink = False
|
||||
self.client_compatibility_mode = 0
|
||||
self.items_handling = 0b111
|
||||
self.checked_locations_sent: bool = False
|
||||
self.port_offset = 0
|
||||
self.bat_no_touch_locations: [BatNoTouchLocation] = []
|
||||
self.local_item_locations = {}
|
||||
self.dragon_speed_info = {}
|
||||
|
||||
options = Utils.get_options()
|
||||
self.display_msgs = options["adventure_options"]["display_msgs"]
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(AdventureContext, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
self.auth = self.player_name
|
||||
if not self.auth:
|
||||
self.awaiting_rom = True
|
||||
logger.info('Awaiting connection to adventure_connector to get Player information')
|
||||
return
|
||||
|
||||
await self.send_connect()
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if self.display_msgs:
|
||||
self.messages[(time.time(), msg_id)] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
self.locations_array = None
|
||||
if Utils.get_options()["adventure_options"].get("death_link", False):
|
||||
self.set_deathlink = True
|
||||
async_start(self.get_freeincarnates_used())
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args['seed_name']
|
||||
elif cmd == 'Print':
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "ReceivedItems":
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "Retrieved":
|
||||
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
elif cmd == "SetReply":
|
||||
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
|
||||
self.freeincarnates_used = args["value"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
self.deathlink_pending = True
|
||||
super().on_deathlink(data)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class AdventureManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Adventure Client"
|
||||
|
||||
self.ui = AdventureManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def get_freeincarnates_used(self):
|
||||
if self.server and not self.server.socket.closed:
|
||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
|
||||
await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
|
||||
|
||||
def send_pending_freeincarnates(self):
|
||||
if self.freeincarnate_pending > 0:
|
||||
async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending))
|
||||
self.freeincarnate_pending = 0
|
||||
|
||||
async def send_pending_freeincarnates_impl(self, send_val: int) -> None:
|
||||
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
|
||||
"default": 0, "want_reply": False,
|
||||
"operations": [{"operation": "add", "value": send_val}]}])
|
||||
|
||||
async def used_freeincarnate(self) -> None:
|
||||
if self.server and not self.server.socket.closed:
|
||||
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
|
||||
"default": 0, "want_reply": True,
|
||||
"operations": [{"operation": "add", "value": 1}]}])
|
||||
else:
|
||||
self.freeincarnate_pending = self.freeincarnate_pending + 1
|
||||
|
||||
|
||||
def convert_item_id(ap_item_id: int):
|
||||
static_item_index = ap_item_id - base_adventure_item_id
|
||||
return static_item_index * static_item_element_size
|
||||
|
||||
|
||||
def get_payload(ctx: AdventureContext):
|
||||
current_time = time.time()
|
||||
items = []
|
||||
dragon_speed_update = {}
|
||||
diff_a_locked = ctx.diff_a_mode > 0
|
||||
diff_b_locked = ctx.diff_b_mode > 0
|
||||
freeincarnate_count = 0
|
||||
for item in ctx.items_received:
|
||||
item_id_str = str(item.item)
|
||||
if base_adventure_item_id < item.item <= standard_item_max:
|
||||
items.append(convert_item_id(item.item))
|
||||
elif item_id_str in ctx.dragon_speed_info:
|
||||
if item.item in dragon_speed_update:
|
||||
last_index = len(ctx.dragon_speed_info[item_id_str]) - 1
|
||||
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index]
|
||||
else:
|
||||
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0]
|
||||
elif item.item == item_table["Left Difficulty Switch"].id:
|
||||
diff_a_locked = False
|
||||
elif item.item == item_table["Right Difficulty Switch"].id:
|
||||
diff_b_locked = False
|
||||
elif item.item == item_table["Freeincarnate"].id:
|
||||
freeincarnate_count = freeincarnate_count + 1
|
||||
freeincarnates_available = 0
|
||||
|
||||
if ctx.freeincarnates_used >= 0:
|
||||
freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending)
|
||||
ret = json.dumps(
|
||||
{
|
||||
"items": items,
|
||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||
if key[0] > current_time - 10},
|
||||
"deathlink": ctx.deathlink_pending,
|
||||
"dragon_speeds": dragon_speed_update,
|
||||
"difficulty_a_locked": diff_a_locked,
|
||||
"difficulty_b_locked": diff_b_locked,
|
||||
"freeincarnates_available": freeincarnates_available,
|
||||
"bat_logic": ctx.bat_logic
|
||||
}
|
||||
)
|
||||
ctx.deathlink_pending = False
|
||||
return ret
|
||||
|
||||
|
||||
async def parse_locations(data: List, ctx: AdventureContext):
|
||||
locations = data
|
||||
|
||||
# for loc_name, loc_data in location_table.items():
|
||||
|
||||
# if flags["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||
# await ctx.send_msgs([
|
||||
# {"cmd": "StatusUpdate",
|
||||
# "status": 30}
|
||||
# ])
|
||||
# ctx.finished_game = True
|
||||
if locations == ctx.locations_array:
|
||||
return
|
||||
ctx.locations_array = locations
|
||||
if locations is not None:
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
|
||||
|
||||
|
||||
def send_ap_foreign_items(adventure_context):
|
||||
foreign_item_json_list = []
|
||||
autocollect_item_json_list = []
|
||||
bat_no_touch_locations_json_list = []
|
||||
for fi in adventure_context.foreign_items:
|
||||
foreign_item_json_list.append(fi.get_dict())
|
||||
for fi in adventure_context.autocollect_items:
|
||||
autocollect_item_json_list.append(fi.get_dict())
|
||||
for ntl in adventure_context.bat_no_touch_locations:
|
||||
bat_no_touch_locations_json_list.append(ntl.get_dict())
|
||||
payload = json.dumps(
|
||||
{
|
||||
"foreign_items": foreign_item_json_list,
|
||||
"autocollect_items": autocollect_item_json_list,
|
||||
"local_item_locations": adventure_context.local_item_locations,
|
||||
"bat_no_touch_locations": bat_no_touch_locations_json_list
|
||||
}
|
||||
)
|
||||
print("sending foreign items")
|
||||
msg = payload.encode()
|
||||
(reader, writer) = adventure_context.atari_streams
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
|
||||
|
||||
def send_checked_locations_if_needed(adventure_context):
|
||||
if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None:
|
||||
if len(adventure_context.checked_locations) == 0:
|
||||
return
|
||||
checked_short_ids = []
|
||||
for location in adventure_context.checked_locations:
|
||||
checked_short_ids.append(location - base_location_id)
|
||||
print("Sending checked locations")
|
||||
payload = json.dumps(
|
||||
{
|
||||
"checked_locations": checked_short_ids,
|
||||
}
|
||||
)
|
||||
msg = payload.encode()
|
||||
(reader, writer) = adventure_context.atari_streams
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
adventure_context.checked_locations_sent = True
|
||||
|
||||
|
||||
async def atari_sync_task(ctx: AdventureContext):
|
||||
logger.info("Starting Atari 2600 connector. Use /2600 for status information")
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
error_status = None
|
||||
if ctx.atari_streams:
|
||||
(reader, writer) = ctx.atari_streams
|
||||
msg = get_payload(ctx).encode()
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
try:
|
||||
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
||||
try:
|
||||
# Data will return a dict with 1+ fields
|
||||
# 1. A keepalive response of the Players Name (always)
|
||||
# 2. romhash field with sha256 hash of the ROM memory region
|
||||
# 3. locations, messages, and deathLink
|
||||
# 4. freeincarnate, to indicate a freeincarnate was used
|
||||
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
data_decoded = json.loads(data.decode())
|
||||
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
|
||||
msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \
|
||||
"Lua and AdventureClient are from the same Archipelago installation."
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data:
|
||||
msg = "The server is running a different multiworld than your client is. " \
|
||||
"(invalid seed_name)"
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
if 'romhash' in data_decoded:
|
||||
if ctx.rom_hash.upper() != data_decoded['romhash'].upper():
|
||||
msg = "The rom hash does not match the client rom hash data"
|
||||
print("got " + data_decoded['romhash'])
|
||||
print("expected " + str(ctx.rom_hash))
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
if ctx.auth is None:
|
||||
ctx.auth = ctx.player_name
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \
|
||||
and not error_status and ctx.auth:
|
||||
# Not just a keep alive ping, parse
|
||||
async_start(parse_locations(data_decoded['locations'], ctx))
|
||||
if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags:
|
||||
dragon_name = "a dragon"
|
||||
if data_decoded['deathLink'] == 1:
|
||||
dragon_name = "Rhindle"
|
||||
elif data_decoded['deathLink'] == 2:
|
||||
dragon_name = "Yorgle"
|
||||
elif data_decoded['deathLink'] == 3:
|
||||
dragon_name = "Grundle"
|
||||
print (ctx.auth + " has been eaten by " + dragon_name )
|
||||
await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name)
|
||||
# TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by '
|
||||
if 'victory' in data_decoded and not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
if 'freeincarnate' in data_decoded:
|
||||
await ctx.used_freeincarnate()
|
||||
if ctx.set_deathlink:
|
||||
await ctx.update_death_link(True)
|
||||
send_checked_locations_if_needed(ctx)
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("Read Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except ConnectionResetError as e:
|
||||
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except ConnectionResetError:
|
||||
logger.debug("Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except CancelledError:
|
||||
logger.debug("Connection Cancelled, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
pass
|
||||
except Exception as e:
|
||||
print("unknown exception " + e)
|
||||
raise
|
||||
if ctx.atari_status == CONNECTION_TENTATIVE_STATUS:
|
||||
if not error_status:
|
||||
logger.info("Successfully Connected to 2600")
|
||||
ctx.atari_status = CONNECTION_CONNECTED_STATUS
|
||||
ctx.checked_locations_sent = False
|
||||
send_ap_foreign_items(ctx)
|
||||
send_checked_locations_if_needed(ctx)
|
||||
else:
|
||||
ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}"
|
||||
elif error_status:
|
||||
ctx.atari_status = error_status
|
||||
logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates")
|
||||
else:
|
||||
try:
|
||||
port = ctx.lua_connector_port + ctx.port_offset
|
||||
logger.debug(f"Attempting to connect to 2600 on port {port}")
|
||||
print(f"Attempting to connect to 2600 on port {port}")
|
||||
ctx.atari_streams = await asyncio.wait_for(
|
||||
asyncio.open_connection("localhost",
|
||||
port),
|
||||
timeout=10)
|
||||
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Trying Again")
|
||||
ctx.atari_status = CONNECTION_TIMING_OUT_STATUS
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
logger.debug("Connection Refused, Trying Again")
|
||||
ctx.atari_status = CONNECTION_REFUSED_STATUS
|
||||
continue
|
||||
except CancelledError:
|
||||
pass
|
||||
except CancelledError:
|
||||
pass
|
||||
print("exiting atari sync task")
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
|
||||
rom_args = Utils.get_options()["adventure_options"].get("rom_args")
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
elif os.path.isfile(auto_start):
|
||||
open_args = [auto_start, romfile]
|
||||
if rom_args is not None:
|
||||
open_args.insert(1, rom_args)
|
||||
subprocess.Popen(open_args,
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
async def patch_and_run_game(patch_file, ctx):
|
||||
base_name = os.path.splitext(patch_file)[0]
|
||||
comp_path = base_name + '.a26'
|
||||
try:
|
||||
base_rom = AdventureDeltaPatch.get_source_data()
|
||||
except Exception as msg:
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
|
||||
with open(Utils.user_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
|
||||
basepatch = bytes(file.read())
|
||||
|
||||
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)
|
||||
|
||||
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
|
||||
if not AdventureDeltaPatch.check_version(patch_archive):
|
||||
logger.error("apadvn version doesn't match this client. Make sure your generator and client are the same")
|
||||
raise Exception("apadvn version doesn't match this client.")
|
||||
|
||||
ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive)
|
||||
ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive)
|
||||
ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive)
|
||||
ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive)
|
||||
ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive)
|
||||
ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive)
|
||||
ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive)
|
||||
ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive)
|
||||
ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive)
|
||||
ctx.auth = ctx.player_name
|
||||
|
||||
patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas)
|
||||
rom_hash = hashlib.sha256()
|
||||
rom_hash.update(patched_rom_data)
|
||||
ctx.rom_hash = rom_hash.hexdigest()
|
||||
ctx.port_offset = patched_rom_data[connector_port_offset]
|
||||
|
||||
with open(comp_path, "wb") as patched_rom_file:
|
||||
patched_rom_file.write(patched_rom_data)
|
||||
|
||||
async_start(run_game(comp_path))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Utils.init_logging("AdventureClient")
|
||||
|
||||
async def main():
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('patch_file', default="", type=str, nargs="?",
|
||||
help='Path to an ADVNTURE.BIN rom file')
|
||||
parser.add_argument('port', default=17242, type=int, nargs="?",
|
||||
help='port for adventure_connector connection')
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = AdventureContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
ctx.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync")
|
||||
|
||||
if args.patch_file:
|
||||
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
|
||||
if ext == "apadvn":
|
||||
logger.info("apadvn file supplied, beginning patching process...")
|
||||
async_start(patch_and_run_game(args.patch_file, ctx))
|
||||
else:
|
||||
logger.warning(f"Unknown patch file extension {ext}")
|
||||
if args.port is int:
|
||||
ctx.lua_connector_port = args.port
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
if ctx.atari_sync_task:
|
||||
await ctx.atari_sync_task
|
||||
print("finished atari_sync_task (main)")
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
@@ -67,7 +67,6 @@ class MultiWorld():
|
||||
local_early_items: Dict[int, Dict[str, int]]
|
||||
local_items: Dict[int, Options.LocalItems]
|
||||
non_local_items: Dict[int, Options.NonLocalItems]
|
||||
allow_collect: Dict[int, Options.AllowCollect]
|
||||
progression_balancing: Dict[int, Options.ProgressionBalancing]
|
||||
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
||||
indirect_connections: Dict[Region, Set[Entrance]]
|
||||
@@ -337,7 +336,7 @@ class MultiWorld():
|
||||
return self.player_name[player]
|
||||
|
||||
def get_file_safe_player_name(self, player: int) -> str:
|
||||
return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*')
|
||||
return Utils.get_file_safe_name(self.get_player_name(player))
|
||||
|
||||
def get_out_file_name_base(self, player: int) -> str:
|
||||
""" the base name (without file extension) for each player's output file for a seed """
|
||||
@@ -1209,7 +1208,7 @@ class Spoiler():
|
||||
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
||||
|
||||
# we can finally output our playthrough
|
||||
self.playthrough = {"0": sorted([str(item) for item in
|
||||
self.playthrough = {"0": sorted([self.multiworld.get_name_string_for_object(item) for item in
|
||||
chain.from_iterable(multiworld.precollected_items.values())
|
||||
if item.advancement])}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class CommonContext:
|
||||
items_handling: typing.Optional[int] = None
|
||||
want_slot_data: bool = True # should slot_data be retrieved via Connect
|
||||
|
||||
# datapackage
|
||||
# data package
|
||||
# Contents in flux until connection to server is made, to download correct data for this multiworld.
|
||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
@@ -223,7 +223,7 @@ class CommonContext:
|
||||
self.watcher_event = asyncio.Event()
|
||||
|
||||
self.jsontotextparser = JSONtoTextParser(self)
|
||||
self.update_datapackage(network_data_package)
|
||||
self.update_data_package(network_data_package)
|
||||
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||
@@ -399,32 +399,40 @@ class CommonContext:
|
||||
self.input_task.cancel()
|
||||
|
||||
# DataPackage
|
||||
async def prepare_datapackage(self, relevant_games: typing.Set[str],
|
||||
remote_datepackage_versions: typing.Dict[str, int]):
|
||||
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
||||
remote_date_package_versions: typing.Dict[str, int],
|
||||
remote_data_package_checksums: typing.Dict[str, str]):
|
||||
"""Validate that all data is present for the current multiworld.
|
||||
Download, assimilate and cache missing data from the server."""
|
||||
# by documentation any game can use Archipelago locations/items -> always relevant
|
||||
relevant_games.add("Archipelago")
|
||||
|
||||
cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
needed_updates: typing.Set[str] = set()
|
||||
for game in relevant_games:
|
||||
if game not in remote_datepackage_versions:
|
||||
if game not in remote_date_package_versions and game not in remote_data_package_checksums:
|
||||
continue
|
||||
remote_version: int = remote_datepackage_versions[game]
|
||||
|
||||
if remote_version == 0: # custom datapackage for this game
|
||||
remote_version: int = remote_date_package_versions.get(game, 0)
|
||||
remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game)
|
||||
|
||||
if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game
|
||||
needed_updates.add(game)
|
||||
continue
|
||||
|
||||
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
|
||||
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
||||
# no action required if local version is new enough
|
||||
if remote_version > local_version:
|
||||
cache_version: int = cache_package.get(game, {}).get("version", 0)
|
||||
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
|
||||
or remote_checksum != local_checksum:
|
||||
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
|
||||
cache_version: int = cached_game.get("version", 0)
|
||||
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
|
||||
# download remote version if cache is not new enough
|
||||
if remote_version > cache_version:
|
||||
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
|
||||
or remote_checksum != cache_checksum:
|
||||
needed_updates.add(game)
|
||||
else:
|
||||
self.update_game(cache_package[game])
|
||||
self.update_game(cached_game)
|
||||
if needed_updates:
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
|
||||
|
||||
@@ -434,15 +442,17 @@ class CommonContext:
|
||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||
self.location_names[location_id] = location_name
|
||||
|
||||
def update_datapackage(self, data_package: dict):
|
||||
for game, gamedata in data_package["games"].items():
|
||||
self.update_game(gamedata)
|
||||
def update_data_package(self, data_package: dict):
|
||||
for game, game_data in data_package["games"].items():
|
||||
self.update_game(game_data)
|
||||
|
||||
def consume_network_datapackage(self, data_package: dict):
|
||||
self.update_datapackage(data_package)
|
||||
def consume_network_data_package(self, data_package: dict):
|
||||
self.update_data_package(data_package)
|
||||
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
current_cache.update(data_package["games"])
|
||||
Utils.persistent_store("datapackage", "games", current_cache)
|
||||
for game, game_data in data_package["games"].items():
|
||||
Utils.store_data_package_for_checksum(game, game_data)
|
||||
|
||||
# DeathLink hooks
|
||||
|
||||
@@ -661,14 +671,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||
current_team = network_player.team
|
||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||
|
||||
# update datapackage
|
||||
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
|
||||
# update data package
|
||||
data_package_versions = args.get("datapackage_versions", {})
|
||||
data_package_checksums = args.get("datapackage_checksums", {})
|
||||
await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums)
|
||||
|
||||
await ctx.server_auth(args['password'])
|
||||
|
||||
elif cmd == 'DataPackage':
|
||||
logger.info("Got new ID/Name DataPackage")
|
||||
ctx.consume_network_datapackage(args['data'])
|
||||
ctx.consume_network_data_package(args['data'])
|
||||
|
||||
elif cmd == 'ConnectionRefused':
|
||||
errors = args["errors"]
|
||||
|
||||
41
Fill.py
41
Fill.py
@@ -23,15 +23,27 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
|
||||
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||
allow_partial: bool = False) -> None:
|
||||
allow_partial: bool = False, allow_excluded: bool = False) -> None:
|
||||
"""
|
||||
:param world: Multiworld to be filled.
|
||||
:param base_state: State assumed before fill.
|
||||
:param locations: Locations to be filled with item_pool
|
||||
:param item_pool: Items to fill into the locations
|
||||
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
|
||||
:param lock: locations are set to locked as they are filled
|
||||
:param swap: if true, swaps of already place items are done in the event of a dead end
|
||||
:param on_place: callback that is called when a placement happens
|
||||
:param allow_partial: only place what is possible. Remaining items will be in the item_pool list.
|
||||
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
|
||||
"""
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
|
||||
for item in itempool:
|
||||
for item in item_pool:
|
||||
reachable_items.setdefault(item.player, deque()).append(item)
|
||||
|
||||
while any(reachable_items.values()) and locations:
|
||||
@@ -39,9 +51,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
items_to_place = [items.pop()
|
||||
for items in reachable_items.values() if items]
|
||||
for item in items_to_place:
|
||||
itempool.remove(item)
|
||||
item_pool.remove(item)
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, itempool + unplaced_items)
|
||||
base_state, item_pool + unplaced_items)
|
||||
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
@@ -111,7 +123,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
reachable_items[placed_item.player].appendleft(
|
||||
placed_item)
|
||||
itempool.append(placed_item)
|
||||
item_pool.append(placed_item)
|
||||
|
||||
break
|
||||
|
||||
@@ -133,6 +145,21 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
if on_place:
|
||||
on_place(spot_to_fill)
|
||||
|
||||
if allow_excluded:
|
||||
# check if partial fill is the result of excluded locations, in which case retry
|
||||
excluded_locations = [
|
||||
location for location in locations
|
||||
if location.progress_type == location.progress_type.EXCLUDED and not location.item
|
||||
]
|
||||
if excluded_locations:
|
||||
for location in excluded_locations:
|
||||
location.progress_type = location.progress_type.DEFAULT
|
||||
fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
|
||||
swap, on_place, allow_partial, False)
|
||||
for location in excluded_locations:
|
||||
if not location.item:
|
||||
location.progress_type = location.progress_type.EXCLUDED
|
||||
|
||||
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
if world.can_beat_game():
|
||||
@@ -142,7 +169,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
item_pool.extend(unplaced_items)
|
||||
|
||||
|
||||
def remaining_fill(world: MultiWorld,
|
||||
|
||||
906
KH2Client.py
Normal file
906
KH2Client.py
Normal file
@@ -0,0 +1,906 @@
|
||||
import os
|
||||
import asyncio
|
||||
import ModuleUpdate
|
||||
import json
|
||||
import Utils
|
||||
from pymem import pymem
|
||||
from worlds.kh2.Items import exclusionItem_table, CheckDupingItems
|
||||
from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table
|
||||
|
||||
from worlds.kh2.WorldLocations import *
|
||||
|
||||
from worlds import network_data_package
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("KH2Client", exception_logger="Client")
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
|
||||
CommonContext, server_loop
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"]
|
||||
|
||||
|
||||
# class KH2CommandProcessor(ClientCommandProcessor):
|
||||
|
||||
|
||||
class KH2Context(CommonContext):
|
||||
# command_processor: int = KH2CommandProcessor
|
||||
game = "Kingdom Hearts 2"
|
||||
items_handling = 0b101 # Indicates you get items sent from other worlds.
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(KH2Context, self).__init__(server_address, password)
|
||||
self.kh2LocalItems = None
|
||||
self.ability = None
|
||||
self.growthlevel = None
|
||||
self.KH2_sync_task = None
|
||||
self.syncing = False
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||
self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in
|
||||
item_dictionary_table.items() if data.code}
|
||||
self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in
|
||||
all_locations.items() if data.code}
|
||||
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
|
||||
|
||||
self.location_table = {}
|
||||
self.collectible_table = {}
|
||||
self.collectible_override_flags_address = 0
|
||||
self.collectible_offsets = {}
|
||||
self.sending = []
|
||||
# flag for if the player has gotten their starting inventory from the server
|
||||
self.hasStartingInvo = False
|
||||
# list used to keep track of locations+items player has. Used for disoneccting
|
||||
self.kh2seedsave = {"checked_locations": {"0": []},
|
||||
"starting_inventory": self.hasStartingInvo,
|
||||
|
||||
# Character: [back of invo, front of invo]
|
||||
"SoraInvo": [0x25CC, 0x2546],
|
||||
"DonaldInvo": [0x2678, 0x2658],
|
||||
"GoofyInvo": [0x278E, 0x276C],
|
||||
"AmountInvo": {
|
||||
"ServerItems": {
|
||||
"Ability": {},
|
||||
"Amount": {},
|
||||
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, "Aerial Dodge": 0,
|
||||
"Glide": 0},
|
||||
"Bitmask": [],
|
||||
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
|
||||
"Equipment": [],
|
||||
"Magic": {},
|
||||
"StatIncrease": {},
|
||||
"Boost": {},
|
||||
},
|
||||
"LocalItems": {
|
||||
"Ability": {},
|
||||
"Amount": {},
|
||||
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
|
||||
"Aerial Dodge": 0, "Glide": 0},
|
||||
"Bitmask": [],
|
||||
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
|
||||
"Equipment": [],
|
||||
"Magic": {},
|
||||
"StatIncrease": {},
|
||||
"Boost": {},
|
||||
}},
|
||||
# 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked
|
||||
"worldIdChecks": {
|
||||
"1": [], # world of darkness (story cutscenes)
|
||||
"2": [],
|
||||
"3": [], # destiny island doesn't have checks to ima put tt checks here
|
||||
"4": [],
|
||||
"5": [],
|
||||
"6": [],
|
||||
"7": [],
|
||||
"8": [],
|
||||
"9": [],
|
||||
"10": [],
|
||||
"11": [],
|
||||
# atlantica isn't a supported world. if you go in atlantica it will check dc
|
||||
"12": [],
|
||||
"13": [],
|
||||
"14": [],
|
||||
"15": [],
|
||||
# world map, but you only go to the world map while on the way to goa so checking hb
|
||||
"16": [],
|
||||
"17": [],
|
||||
"18": [],
|
||||
"255": [], # starting screen
|
||||
},
|
||||
"Levels": {
|
||||
"SoraLevel": 0,
|
||||
"ValorLevel": 0,
|
||||
"WisdomLevel": 0,
|
||||
"LimitLevel": 0,
|
||||
"MasterLevel": 0,
|
||||
"FinalLevel": 0,
|
||||
},
|
||||
"SoldEquipment": [],
|
||||
"SoldBoosts": {"Power Boost": 0,
|
||||
"Magic Boost": 0,
|
||||
"Defense Boost": 0,
|
||||
"AP Boost": 0}
|
||||
}
|
||||
self.slotDataProgressionNames = {}
|
||||
self.kh2seedname = None
|
||||
self.kh2slotdata = None
|
||||
self.itemamount = {}
|
||||
# sora equipped, valor equipped, master equipped, final equipped
|
||||
self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4)
|
||||
if "localappdata" in os.environ:
|
||||
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
|
||||
self.amountOfPieces = 0
|
||||
# hooked object
|
||||
self.kh2 = None
|
||||
self.ItemIsSafe = False
|
||||
self.game_connected = False
|
||||
self.finalxemnas = False
|
||||
self.worldid = {
|
||||
# 1: {}, # world of darkness (story cutscenes)
|
||||
2: TT_Checks,
|
||||
# 3: {}, # destiny island doesn't have checks to ima put tt checks here
|
||||
4: HB_Checks,
|
||||
5: BC_Checks,
|
||||
6: Oc_Checks,
|
||||
7: AG_Checks,
|
||||
8: LoD_Checks,
|
||||
9: HundredAcreChecks,
|
||||
10: PL_Checks,
|
||||
11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc
|
||||
12: DC_Checks,
|
||||
13: TR_Checks,
|
||||
14: HT_Checks,
|
||||
15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
|
||||
16: PR_Checks,
|
||||
17: SP_Checks,
|
||||
18: TWTNW_Checks,
|
||||
# 255: {}, # starting screen
|
||||
}
|
||||
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
|
||||
self.sveroom = 0x2A09C00 + 0x41
|
||||
# 0 not in battle 1 in yellow battle 2 red battle #short
|
||||
self.inBattle = 0x2A0EAC4 + 0x40
|
||||
self.onDeath = 0xAB9078
|
||||
# PC Address anchors
|
||||
self.Now = 0x0714DB8
|
||||
self.Save = 0x09A70B0
|
||||
self.Sys3 = 0x2A59DF0
|
||||
self.Bt10 = 0x2A74880
|
||||
self.BtlEnd = 0x2A0D3E0
|
||||
self.Slot1 = 0x2A20C98
|
||||
|
||||
self.chest_set = set(exclusion_table["Chests"])
|
||||
|
||||
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
|
||||
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
|
||||
self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
|
||||
|
||||
self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
|
||||
|
||||
self.equipment_categories = CheckDupingItems["Equipment"]
|
||||
self.armor_set = set(self.equipment_categories["Armor"])
|
||||
self.accessories_set = set(self.equipment_categories["Accessories"])
|
||||
self.all_equipment = self.armor_set.union(self.accessories_set)
|
||||
|
||||
self.Equipment_Anchor_Dict = {
|
||||
"Armor": [0x2504, 0x2506, 0x2508, 0x250A],
|
||||
"Accessories": [0x2514, 0x2516, 0x2518, 0x251A]}
|
||||
|
||||
self.AbilityQuantityDict = {}
|
||||
self.ability_categories = CheckDupingItems["Abilities"]
|
||||
|
||||
self.sora_ability_set = set(self.ability_categories["Sora"])
|
||||
self.donald_ability_set = set(self.ability_categories["Donald"])
|
||||
self.goofy_ability_set = set(self.ability_categories["Goofy"])
|
||||
|
||||
self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
|
||||
|
||||
self.boost_set = set(CheckDupingItems["Boosts"])
|
||||
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
|
||||
|
||||
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
|
||||
# Growth:[level 1,level 4,slot]
|
||||
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE],
|
||||
"Quick Run": [0x62, 0x65, 0x25D0],
|
||||
"Dodge Roll": [0x234, 0x237, 0x25D2],
|
||||
"Aerial Dodge": [0x066, 0x069, 0x25D4],
|
||||
"Glide": [0x6A, 0x6D, 0x25D6]}
|
||||
self.boost_to_anchor_dict = {
|
||||
"Power Boost": 0x24F9,
|
||||
"Magic Boost": 0x24FA,
|
||||
"Defense Boost": 0x24FB,
|
||||
"AP Boost": 0x24F8}
|
||||
|
||||
self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]]
|
||||
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
|
||||
|
||||
self.bitmask_item_code = [
|
||||
0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007
|
||||
, 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C
|
||||
, 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023
|
||||
, 0x13002A, 0x13002B, 0x13002C, 0x13002D]
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(KH2Context, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
async def connection_closed(self):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname is not None and self.auth is not None:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||
await super(KH2Context, self).connection_closed()
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool = False):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||
await super(KH2Context, self).disconnect()
|
||||
|
||||
@property
|
||||
def endpoints(self):
|
||||
if self.server:
|
||||
return [self.server]
|
||||
else:
|
||||
return []
|
||||
|
||||
async def shutdown(self):
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2seedsave, indent=4))
|
||||
await super(KH2Context, self).shutdown()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"RoomInfo"}:
|
||||
self.kh2seedname = args['seed_name']
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
|
||||
'wt') as f:
|
||||
pass
|
||||
elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
|
||||
with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f:
|
||||
self.kh2seedsave = json.load(f)
|
||||
|
||||
if cmd in {"Connected"}:
|
||||
for player in args['players']:
|
||||
if str(player.slot) not in self.kh2seedsave["checked_locations"]:
|
||||
self.kh2seedsave["checked_locations"].update({str(player.slot): []})
|
||||
self.kh2slotdata = args['slot_data']
|
||||
self.serverconneced = True
|
||||
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
|
||||
try:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
logger.info("You are now auto-tracking")
|
||||
self.kh2connected = True
|
||||
except Exception as e:
|
||||
logger.info("Line 247")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
if cmd in {"ReceivedItems"}:
|
||||
start_index = args["index"]
|
||||
if start_index != len(self.items_received):
|
||||
for item in args['items']:
|
||||
# starting invo from server
|
||||
if item.location in {-2}:
|
||||
if not self.kh2seedsave["starting_inventory"]:
|
||||
asyncio.create_task(self.give_item(item.item))
|
||||
# if location is not already given or is !getitem
|
||||
elif item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
|
||||
or item.location in {-1}:
|
||||
asyncio.create_task(self.give_item(item.item))
|
||||
if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
|
||||
and item.location not in {-1, -2}:
|
||||
self.kh2seedsave["checked_locations"][str(item.player)].append(item.location)
|
||||
if not self.kh2seedsave["starting_inventory"]:
|
||||
self.kh2seedsave["starting_inventory"] = True
|
||||
|
||||
if cmd in {"RoomUpdate"}:
|
||||
if "checked_locations" in args:
|
||||
new_locations = set(args["checked_locations"])
|
||||
# TODO: make this take locations from other players on the same slot so proper coop happens
|
||||
# items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if
|
||||
# location_id in self.kh2LocalItems.keys()]
|
||||
self.checked_locations |= new_locations
|
||||
|
||||
async def checkWorldLocations(self):
|
||||
try:
|
||||
currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")
|
||||
if currentworldint in self.worldid:
|
||||
curworldid = self.worldid[currentworldint]
|
||||
for location, data in curworldid.items():
|
||||
if location not in self.locations_checked \
|
||||
and (int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") & 0x1 << data.bitIndex) > 0:
|
||||
self.locations_checked.add(location)
|
||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||
except Exception as e:
|
||||
logger.info("Line 285")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def checkLevels(self):
|
||||
try:
|
||||
for location, data in SoraLevels.items():
|
||||
currentLevel = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
|
||||
if location not in self.locations_checked \
|
||||
and currentLevel >= data.bitIndex:
|
||||
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
|
||||
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
|
||||
self.locations_checked.add(location)
|
||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||
formDict = {
|
||||
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
|
||||
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
|
||||
for i in range(5):
|
||||
for location, data in formDict[i][1].items():
|
||||
formlevel = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
|
||||
if location not in self.locations_checked \
|
||||
and formlevel >= data.bitIndex:
|
||||
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
|
||||
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
|
||||
self.locations_checked.add(location)
|
||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||
except Exception as e:
|
||||
logger.info("Line 312")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def checkSlots(self):
|
||||
try:
|
||||
for location, data in weaponSlots.items():
|
||||
if location not in self.locations_checked:
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") > 0:
|
||||
self.locations_checked.add(location)
|
||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||
|
||||
for location, data in formSlots.items():
|
||||
if location not in self.locations_checked:
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") & 0x1 << data.bitIndex > 0:
|
||||
self.locations_checked.add(location)
|
||||
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
logger.info("Line 333")
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def verifyChests(self):
|
||||
try:
|
||||
currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big"))
|
||||
for location in self.kh2seedsave["worldIdChecks"][currentworld]:
|
||||
locationName = self.lookup_id_to_Location[location]
|
||||
if locationName in self.chest_set:
|
||||
if locationName in self.location_name_to_worlddata.keys():
|
||||
locationData = self.location_name_to_worlddata[locationName]
|
||||
if int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
|
||||
"big") & 0x1 << locationData.bitIndex == 0:
|
||||
roomData = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
|
||||
1), "big")
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
|
||||
(roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1)
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
logger.info("Line 350")
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
async def verifyLevel(self):
|
||||
for leveltype, anchor in {"SoraLevel": 0x24FF,
|
||||
"ValorLevel": 0x32F6,
|
||||
"WisdomLevel": 0x332E,
|
||||
"LimitLevel": 0x3366,
|
||||
"MasterLevel": 0x339E,
|
||||
"FinalLevel": 0x33D6}.items():
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \
|
||||
self.kh2seedsave["Levels"][leveltype]:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
|
||||
(self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1)
|
||||
|
||||
def verifyLocation(self, location):
|
||||
locationData = self.location_name_to_worlddata[location]
|
||||
locationName = self.lookup_id_to_Location[location]
|
||||
isChecked = True
|
||||
|
||||
if locationName not in levels_locations:
|
||||
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
|
||||
"big") & 0x1 << locationData.bitIndex) == 0:
|
||||
isChecked = False
|
||||
elif locationName in SoraLevels:
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1),
|
||||
"big") < locationData.bitIndex:
|
||||
isChecked = False
|
||||
elif int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
|
||||
"big") < locationData.bitIndex:
|
||||
isChecked = False
|
||||
return isChecked
|
||||
|
||||
async def give_item(self, item, ItemType="ServerItems"):
|
||||
try:
|
||||
itemname = self.lookup_id_to_item[item]
|
||||
itemcode = self.item_name_to_data[itemname]
|
||||
if itemcode.ability:
|
||||
abilityInvoType = 0
|
||||
TwilightZone = 2
|
||||
if ItemType == "LocalItems":
|
||||
abilityInvoType = 1
|
||||
TwilightZone = -2
|
||||
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1
|
||||
return
|
||||
|
||||
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = []
|
||||
# appending the slot that the ability should be in
|
||||
|
||||
if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \
|
||||
self.AbilityQuantityDict[itemname]:
|
||||
if itemname in self.sora_ability_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||
self.kh2seedsave["SoraInvo"][abilityInvoType])
|
||||
self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone
|
||||
elif itemname in self.donald_ability_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||
self.kh2seedsave["DonaldInvo"][abilityInvoType])
|
||||
self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
|
||||
self.kh2seedsave["GoofyInvo"][abilityInvoType])
|
||||
self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone
|
||||
|
||||
elif itemcode.code in self.bitmask_item_code:
|
||||
|
||||
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname)
|
||||
|
||||
elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
|
||||
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1
|
||||
elif itemname in self.all_equipment:
|
||||
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname)
|
||||
|
||||
elif itemname in self.all_weapons:
|
||||
if itemname in self.keyblade_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname)
|
||||
elif itemname in self.staff_set:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname)
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname)
|
||||
|
||||
elif itemname in self.boost_set:
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1
|
||||
|
||||
elif itemname in self.stat_increase_set:
|
||||
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1
|
||||
|
||||
else:
|
||||
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1
|
||||
else:
|
||||
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
logger.info("Line 398")
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
from kvui import GameManager
|
||||
|
||||
class KH2Manager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago KH2 Client"
|
||||
|
||||
self.ui = KH2Manager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def IsInShop(self, sellable, master_boost):
|
||||
# journal = 0x741230 shop = 0x741320
|
||||
# if journal=-1 and shop = 5 then in shop
|
||||
# if journam !=-1 and shop = 10 then journal
|
||||
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
|
||||
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
|
||||
if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||
# print("your in the shop")
|
||||
sellable_dict = {}
|
||||
for itemName in sellable:
|
||||
itemdata = self.item_name_to_data[itemName]
|
||||
amount = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
|
||||
sellable_dict[itemName] = amount
|
||||
while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
|
||||
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
|
||||
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
|
||||
await asyncio.sleep(0.5)
|
||||
for item, amount in sellable_dict.items():
|
||||
itemdata = self.item_name_to_data[item]
|
||||
afterShop = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
|
||||
if afterShop < amount:
|
||||
if item in master_boost:
|
||||
self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop)
|
||||
else:
|
||||
self.kh2seedsave["SoldEquipment"].append(item)
|
||||
|
||||
async def verifyItems(self):
|
||||
try:
|
||||
local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys())
|
||||
server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys())
|
||||
master_amount = local_amount | server_amount
|
||||
|
||||
local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys())
|
||||
server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys())
|
||||
master_ability = local_ability | server_ability
|
||||
|
||||
local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"])
|
||||
server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"])
|
||||
master_bitmask = local_bitmask | server_bitmask
|
||||
|
||||
local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"])
|
||||
local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"])
|
||||
local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"])
|
||||
|
||||
server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"])
|
||||
server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"])
|
||||
server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"])
|
||||
|
||||
master_keyblade = local_keyblade | server_keyblade
|
||||
master_staff = local_staff | server_staff
|
||||
master_shield = local_shield | server_shield
|
||||
|
||||
local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"])
|
||||
server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"])
|
||||
master_equipment = local_equipment | server_equipment
|
||||
|
||||
local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys())
|
||||
server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys())
|
||||
master_magic = local_magic | server_magic
|
||||
|
||||
local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys())
|
||||
server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys())
|
||||
master_stat = local_stat | server_stat
|
||||
|
||||
local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys())
|
||||
server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys())
|
||||
master_boost = local_boost | server_boost
|
||||
|
||||
master_sell = master_equipment | master_staff | master_shield | master_boost
|
||||
await asyncio.create_task(self.IsInShop(master_sell, master_boost))
|
||||
for itemName in master_amount:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_amount:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName]
|
||||
if itemName in server_amount:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName]
|
||||
|
||||
if itemName == "Torn Page":
|
||||
# Torn Pages are handled differently because they can be consumed.
|
||||
# Will check the progression in 100 acre and - the amount of visits
|
||||
# amountofitems-amount of visits done
|
||||
for location, data in tornPageLocks.items():
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
|
||||
"big") & 0x1 << data.bitIndex > 0:
|
||||
amountOfItems -= 1
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != amountOfItems and amountOfItems >= 0:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
amountOfItems.to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_keyblade:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
# if the inventory slot for that keyblade is less than the amount they should have
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1),
|
||||
"big") != 13:
|
||||
# Checking form anchors for the keyblade
|
||||
if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \
|
||||
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \
|
||||
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \
|
||||
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(0).to_bytes(1, 'big'), 1)
|
||||
else:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
for itemName in master_staff:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1 \
|
||||
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \
|
||||
and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_shield:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1 \
|
||||
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \
|
||||
and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_ability:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
ability_slot = []
|
||||
if itemName in local_ability:
|
||||
ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName]
|
||||
if itemName in server_ability:
|
||||
ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName]
|
||||
for slot in ability_slot:
|
||||
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
||||
ability = current & 0x0FFF
|
||||
if ability | 0x8000 != (0x8000 + itemData.memaddr):
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
|
||||
|
||||
for itemName in self.master_growth:
|
||||
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
|
||||
+ self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName]
|
||||
if growthLevel > 0:
|
||||
slot = self.growth_values_dict[itemName][2]
|
||||
min_growth = self.growth_values_dict[itemName][0]
|
||||
max_growth = self.growth_values_dict[itemName][1]
|
||||
if growthLevel > 4:
|
||||
growthLevel = 4
|
||||
current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
|
||||
ability = current_growth_level & 0x0FFF
|
||||
# if the player should be getting a growth ability
|
||||
if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel:
|
||||
# if it should be level one of that growth
|
||||
if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth:
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth)
|
||||
# if it is already in the inventory
|
||||
elif ability | 0x8000 < (0x8000 + max_growth):
|
||||
self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1)
|
||||
|
||||
for itemName in master_bitmask:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
itemMemory = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big")
|
||||
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") & 0x1 << itemData.bitmask) == 0:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_equipment:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
isThere = False
|
||||
if itemName in self.accessories_set:
|
||||
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"]
|
||||
else:
|
||||
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"]
|
||||
# Checking form anchors for the equipment
|
||||
for slot in Equipment_Anchor_List:
|
||||
if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id:
|
||||
isThere = True
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 0:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(0).to_bytes(1, 'big'), 1)
|
||||
break
|
||||
if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]:
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != 1:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(1).to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_magic:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_magic:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName]
|
||||
if itemName in server_magic:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName]
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != amountOfItems \
|
||||
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
amountOfItems.to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_stat:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_stat:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName]
|
||||
if itemName in server_stat:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
|
||||
|
||||
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big") != amountOfItems \
|
||||
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
|
||||
"big") >= 5:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
amountOfItems.to_bytes(1, 'big'), 1)
|
||||
|
||||
for itemName in master_boost:
|
||||
itemData = self.item_name_to_data[itemName]
|
||||
amountOfItems = 0
|
||||
if itemName in local_boost:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName]
|
||||
if itemName in server_boost:
|
||||
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName]
|
||||
amountOfBoostsInInvo = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
|
||||
"big")
|
||||
amountOfUsedBoosts = int.from_bytes(
|
||||
self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1),
|
||||
"big")
|
||||
# Ap Boots start at +50 for some reason
|
||||
if itemName == "AP Boost":
|
||||
amountOfUsedBoosts -= 50
|
||||
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
|
||||
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][itemName] and amountOfBoostsInInvo < 255:
|
||||
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
|
||||
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
|
||||
|
||||
except Exception as e:
|
||||
logger.info("Line 573")
|
||||
if self.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
self.kh2connected = False
|
||||
logger.info(e)
|
||||
|
||||
|
||||
def finishedGame(ctx: KH2Context, message):
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if 0x1301ED in message[0]["locations"]:
|
||||
ctx.finalxemnas = True
|
||||
# three proofs
|
||||
if ctx.kh2slotdata['Goal'] == 0:
|
||||
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \
|
||||
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \
|
||||
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0:
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if ctx.finalxemnas:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif ctx.kh2slotdata['Goal'] == 1:
|
||||
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \
|
||||
ctx.kh2slotdata['LuckyEmblemsRequired']:
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if ctx.finalxemnas:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif ctx.kh2slotdata['Goal'] == 2:
|
||||
for boss in ctx.kh2slotdata["hitlist"]:
|
||||
if boss in message[0]["locations"]:
|
||||
ctx.amountOfPieces += 1
|
||||
if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]:
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
|
||||
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
|
||||
if ctx.kh2slotdata['FinalXemnas'] == 1:
|
||||
if ctx.finalxemnas:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
async def kh2_watcher(ctx: KH2Context):
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
if ctx.kh2connected and ctx.serverconneced:
|
||||
ctx.sending = []
|
||||
await asyncio.create_task(ctx.checkWorldLocations())
|
||||
await asyncio.create_task(ctx.checkLevels())
|
||||
await asyncio.create_task(ctx.checkSlots())
|
||||
await asyncio.create_task(ctx.verifyChests())
|
||||
await asyncio.create_task(ctx.verifyItems())
|
||||
await asyncio.create_task(ctx.verifyLevel())
|
||||
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
|
||||
if finishedGame(ctx, message):
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
location_ids = []
|
||||
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
|
||||
for location in location_ids:
|
||||
currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big")
|
||||
if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]:
|
||||
ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location)
|
||||
if location in ctx.kh2LocalItems:
|
||||
item = ctx.kh2slotdata["LocalItems"][str(location)]
|
||||
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
|
||||
await ctx.send_msgs(message)
|
||||
elif not ctx.kh2connected and ctx.serverconneced:
|
||||
logger.info("Game is not open. Disconnecting from Server.")
|
||||
await ctx.disconnect()
|
||||
except Exception as e:
|
||||
logger.info("Line 661")
|
||||
if ctx.kh2connected:
|
||||
logger.info("Connection Lost.")
|
||||
ctx.kh2connected = False
|
||||
logger.info(e)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
async def main(args):
|
||||
ctx = KH2Context(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
progression_watcher = asyncio.create_task(
|
||||
kh2_watcher(ctx), name="KH2ProgressionWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await progression_watcher
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser(description="KH2 Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
101
Launcher.py
101
Launcher.py
@@ -14,10 +14,11 @@ import itertools
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from enum import Enum, auto
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Iterable, Sequence, Callable, Union, Optional
|
||||
from typing import Sequence, Union, Optional
|
||||
|
||||
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
@@ -70,104 +71,12 @@ def browse_files():
|
||||
webbrowser.open(file)
|
||||
|
||||
|
||||
# noinspection PyArgumentList
|
||||
class Type(Enum):
|
||||
TOOL = auto()
|
||||
FUNC = auto() # not a real component
|
||||
CLIENT = auto()
|
||||
ADJUSTER = auto()
|
||||
|
||||
|
||||
class SuffixIdentifier:
|
||||
suffixes: Iterable[str]
|
||||
|
||||
def __init__(self, *args: str):
|
||||
self.suffixes = args
|
||||
|
||||
def __call__(self, path: str):
|
||||
if isinstance(path, str):
|
||||
for suffix in self.suffixes:
|
||||
if path.endswith(suffix):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Component:
|
||||
display_name: str
|
||||
type: Optional[Type]
|
||||
script_name: Optional[str]
|
||||
frozen_name: Optional[str]
|
||||
icon: str # just the name, no suffix
|
||||
cli: bool
|
||||
func: Optional[Callable]
|
||||
file_identifier: Optional[Callable[[str], bool]]
|
||||
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||
cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None,
|
||||
file_identifier: Optional[Callable[[str], bool]] = None):
|
||||
self.display_name = display_name
|
||||
self.script_name = script_name
|
||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||
self.icon = icon
|
||||
self.cli = cli
|
||||
self.type = component_type or \
|
||||
None if not display_name else \
|
||||
Type.FUNC if func else \
|
||||
Type.CLIENT if 'Client' in display_name else \
|
||||
Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL
|
||||
self.func = func
|
||||
self.file_identifier = file_identifier
|
||||
|
||||
def handles_file(self, path: str):
|
||||
return self.file_identifier(path) if self.file_identifier else False
|
||||
|
||||
|
||||
components: Iterable[Component] = (
|
||||
# Launcher
|
||||
Component('', 'Launcher'),
|
||||
# Core
|
||||
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
||||
Component('Generate', 'Generate', cli=True),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
|
||||
# SNI
|
||||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Factorio
|
||||
Component('Factorio Client', 'FactorioClient'),
|
||||
# Minecraft
|
||||
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
||||
file_identifier=SuffixIdentifier('.apmc')),
|
||||
# Ocarina of Time
|
||||
Component('OoT Client', 'OoTClient',
|
||||
file_identifier=SuffixIdentifier('.apz5')),
|
||||
Component('OoT Adjuster', 'OoTAdjuster'),
|
||||
# FF1
|
||||
Component('FF1 Client', 'FF1Client'),
|
||||
# Pokémon
|
||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
||||
# TLoZ
|
||||
Component('Zelda 1 Client', 'Zelda1Client'),
|
||||
# ChecksFinder
|
||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||
# Starcraft 2
|
||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
||||
# Wargroove
|
||||
Component('Wargroove Client', 'WargrooveClient'),
|
||||
# Zillion
|
||||
Component('Zillion Client', 'ZillionClient',
|
||||
file_identifier=SuffixIdentifier('.apzl')),
|
||||
components.extend([
|
||||
# Functions
|
||||
Component('Open host.yaml', func=open_host_yaml),
|
||||
Component('Open Patch', func=open_patch),
|
||||
Component('Browse Files', func=browse_files),
|
||||
)
|
||||
icon_paths = {
|
||||
'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
|
||||
'mcicon': local_path('data', 'mcicon.ico')
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
def identify(path: Union[None, str]):
|
||||
|
||||
609
LinksAwakeningClient.py
Normal file
609
LinksAwakeningClient.py
Normal file
@@ -0,0 +1,609 @@
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import io
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
import typing
|
||||
import urllib
|
||||
|
||||
import colorama
|
||||
|
||||
|
||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
server_loop)
|
||||
from NetUtils import ClientStatus
|
||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from worlds.ladx.ItemTracker import ItemTracker
|
||||
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
||||
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
||||
|
||||
class GameboyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RetroArchDisconnectError(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidEmulatorStateError(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
class BadRetroArchResponse(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
def magpie_logo():
|
||||
from kivy.uix.image import CoreImage
|
||||
binary_data = """
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
|
||||
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
|
||||
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
|
||||
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
|
||||
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
|
||||
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
|
||||
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
|
||||
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
|
||||
binary_data = base64.b64decode(binary_data)
|
||||
data = io.BytesIO(binary_data)
|
||||
return CoreImage(data, ext="png").texture
|
||||
|
||||
|
||||
class LAClientConstants:
|
||||
# Connector version
|
||||
VERSION = 0x01
|
||||
#
|
||||
# Memory locations of LADXR
|
||||
ROMGameID = 0x0051 # 4 bytes
|
||||
SlotName = 0x0134
|
||||
# Unused
|
||||
# ROMWorldID = 0x0055
|
||||
# ROMConnectorVersion = 0x0056
|
||||
# RO: We should only act if this is higher then 6, as it indicates that the game is running normally
|
||||
wGameplayType = 0xDB95
|
||||
# RO: Starts at 0, increases every time an item is received from the server and processed
|
||||
wLinkSyncSequenceNumber = 0xDDF6
|
||||
wLinkStatusBits = 0xDDF7 # RW:
|
||||
# Bit0: wLinkGive* contains valid data, set from script cleared from ROM.
|
||||
wLinkHealth = 0xDB5A
|
||||
wLinkGiveItem = 0xDDF8 # RW
|
||||
wLinkGiveItemFrom = 0xDDF9 # RW
|
||||
# All of these six bytes are unused, we can repurpose
|
||||
# wLinkSendItemRoomHigh = 0xDDFA # RO
|
||||
# wLinkSendItemRoomLow = 0xDDFB # RO
|
||||
# wLinkSendItemTarget = 0xDDFC # RO
|
||||
# wLinkSendItemItem = 0xDDFD # RO
|
||||
# wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items)
|
||||
# RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0
|
||||
# wLinkSendShopTarget = 0xDDFF
|
||||
|
||||
|
||||
wRecvIndex = 0xDDFE # 0xDB58
|
||||
wCheckAddress = 0xC0FF - 0x4
|
||||
WRamCheckSize = 0x4
|
||||
WRamSafetyValue = bytearray([0]*WRamCheckSize)
|
||||
|
||||
MinGameplayValue = 0x06
|
||||
MaxGameplayValue = 0x1A
|
||||
VictoryGameplayAndSub = 0x0102
|
||||
|
||||
|
||||
class RAGameboy():
|
||||
cache = []
|
||||
cache_start = 0
|
||||
cache_size = 0
|
||||
last_cache_read = None
|
||||
socket = None
|
||||
|
||||
def __init__(self, address, port) -> None:
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
assert (self.socket)
|
||||
self.socket.setblocking(False)
|
||||
|
||||
def get_retroarch_version(self):
|
||||
self.send(b'VERSION\n')
|
||||
select.select([self.socket], [], [])
|
||||
response_str, addr = self.socket.recvfrom(16)
|
||||
return response_str.rstrip()
|
||||
|
||||
def get_retroarch_status(self, timeout):
|
||||
self.send(b'GET_STATUS\n')
|
||||
select.select([self.socket], [], [], timeout)
|
||||
response_str, addr = self.socket.recvfrom(1000, )
|
||||
return response_str.rstrip()
|
||||
|
||||
def set_cache_limits(self, cache_start, cache_size):
|
||||
self.cache_start = cache_start
|
||||
self.cache_size = cache_size
|
||||
|
||||
def send(self, b):
|
||||
if type(b) is str:
|
||||
b = b.encode('ascii')
|
||||
self.socket.sendto(b, (self.address, self.port))
|
||||
|
||||
def recv(self):
|
||||
select.select([self.socket], [], [])
|
||||
response, _ = self.socket.recvfrom(4096)
|
||||
return response
|
||||
|
||||
async def async_recv(self):
|
||||
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
|
||||
return response
|
||||
|
||||
async def check_safe_gameplay(self, throw=True):
|
||||
async def check_wram():
|
||||
check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize)
|
||||
|
||||
if check_values != LAClientConstants.WRamSafetyValue:
|
||||
if throw:
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
return True
|
||||
|
||||
if not await check_wram():
|
||||
if throw:
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
|
||||
gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType)
|
||||
gameplay_value = gameplay_value[0]
|
||||
# In gameplay or credits
|
||||
if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1:
|
||||
if throw:
|
||||
logger.info("invalid emu state")
|
||||
raise InvalidEmulatorStateError()
|
||||
return False
|
||||
if not await check_wram():
|
||||
return False
|
||||
return True
|
||||
|
||||
# We're sadly unable to update the whole cache at once
|
||||
# as RetroArch only gives back some number of bytes at a time
|
||||
# So instead read as big as chunks at a time as we can manage
|
||||
async def update_cache(self):
|
||||
# First read the safety address - if it's invalid, bail
|
||||
self.cache = []
|
||||
|
||||
if not await self.check_safe_gameplay():
|
||||
return
|
||||
|
||||
cache = []
|
||||
remaining_size = self.cache_size
|
||||
while remaining_size:
|
||||
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
|
||||
remaining_size -= len(block)
|
||||
cache += block
|
||||
|
||||
if not await self.check_safe_gameplay():
|
||||
return
|
||||
|
||||
self.cache = cache
|
||||
self.last_cache_read = time.time()
|
||||
|
||||
async def read_memory_cache(self, addresses):
|
||||
# TODO: can we just update once per frame?
|
||||
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
|
||||
await self.update_cache()
|
||||
if not self.cache:
|
||||
return None
|
||||
assert (len(self.cache) == self.cache_size)
|
||||
for address in addresses:
|
||||
assert self.cache_start <= address <= self.cache_start + self.cache_size
|
||||
r = {address: self.cache[address - self.cache_start]
|
||||
for address in addresses}
|
||||
return r
|
||||
|
||||
async def async_read_memory_safe(self, address, size=1):
|
||||
# whenever we do a read for a check, we need to make sure that we aren't reading
|
||||
# garbage memory values - we also need to protect against reading a value, then the emulator resetting
|
||||
#
|
||||
# ...actually, we probably _only_ need the post check
|
||||
|
||||
# Check before read
|
||||
if not await self.check_safe_gameplay():
|
||||
return None
|
||||
|
||||
# Do read
|
||||
r = await self.async_read_memory(address, size)
|
||||
|
||||
# Check after read
|
||||
if not await self.check_safe_gameplay():
|
||||
return None
|
||||
|
||||
return r
|
||||
|
||||
def read_memory(self, address, size=1):
|
||||
command = "READ_CORE_MEMORY"
|
||||
|
||||
self.send(f'{command} {hex(address)} {size}\n')
|
||||
response = self.recv()
|
||||
|
||||
splits = response.decode().split(" ", 2)
|
||||
|
||||
assert (splits[0] == command)
|
||||
# Ignore the address for now
|
||||
|
||||
# TODO: transform to bytes
|
||||
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
|
||||
raise BadRetroArchResponse()
|
||||
return bytearray.fromhex(splits[2])
|
||||
|
||||
async def async_read_memory(self, address, size=1):
|
||||
command = "READ_CORE_MEMORY"
|
||||
|
||||
self.send(f'{command} {hex(address)} {size}\n')
|
||||
response = await self.async_recv()
|
||||
response = response[:-1]
|
||||
splits = response.decode().split(" ", 2)
|
||||
|
||||
assert (splits[0] == command)
|
||||
# Ignore the address for now
|
||||
|
||||
# TODO: transform to bytes
|
||||
return bytearray.fromhex(splits[2])
|
||||
|
||||
def write_memory(self, address, bytes):
|
||||
command = "WRITE_CORE_MEMORY"
|
||||
|
||||
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
|
||||
select.select([self.socket], [], [])
|
||||
response, _ = self.socket.recvfrom(4096)
|
||||
|
||||
splits = response.decode().split(" ", 3)
|
||||
|
||||
assert (splits[0] == command)
|
||||
|
||||
if splits[2] == "-1":
|
||||
logger.info(splits[3])
|
||||
|
||||
|
||||
class LinksAwakeningClient():
|
||||
socket = None
|
||||
gameboy = None
|
||||
tracker = None
|
||||
auth = None
|
||||
game_crc = None
|
||||
pending_deathlink = False
|
||||
deathlink_debounce = True
|
||||
recvd_checks = {}
|
||||
|
||||
def msg(self, m):
|
||||
logger.info(m)
|
||||
s = f"SHOW_MSG {m}\n"
|
||||
self.gameboy.send(s)
|
||||
|
||||
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
|
||||
self.gameboy = RAGameboy(retroarch_address, retroarch_port)
|
||||
|
||||
async def wait_for_retroarch_connection(self):
|
||||
logger.info("Waiting on connection to Retroarch...")
|
||||
while True:
|
||||
try:
|
||||
version = self.gameboy.get_retroarch_version()
|
||||
NO_CONTENT = b"GET_STATUS CONTENTLESS"
|
||||
status = NO_CONTENT
|
||||
core_type = None
|
||||
GAME_BOY = b"game_boy"
|
||||
while status == NO_CONTENT or core_type != GAME_BOY:
|
||||
try:
|
||||
status = self.gameboy.get_retroarch_status(0.1)
|
||||
if status.count(b" ") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
|
||||
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
||||
if status.count(b",") < 2:
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
core_type, rom_name, self.game_crc = info.split(b",", 2)
|
||||
if core_type != GAME_BOY:
|
||||
logger.info(
|
||||
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
except (BlockingIOError, TimeoutError) as e:
|
||||
await asyncio.sleep(0.1)
|
||||
pass
|
||||
logger.info(f"Connected to Retroarch {version} {info}")
|
||||
self.gameboy.read_memory(0x1000)
|
||||
return
|
||||
except ConnectionResetError:
|
||||
await asyncio.sleep(1.0)
|
||||
pass
|
||||
|
||||
def reset_auth(self):
|
||||
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
|
||||
|
||||
if self.auth:
|
||||
assert (auth == self.auth)
|
||||
|
||||
self.auth = auth
|
||||
|
||||
async def wait_and_init_tracker(self):
|
||||
await self.wait_for_game_ready()
|
||||
self.tracker = LocationTracker(self.gameboy)
|
||||
self.item_tracker = ItemTracker(self.gameboy)
|
||||
self.gps_tracker = GpsTracker(self.gameboy)
|
||||
|
||||
async def recved_item_from_ap(self, item_id, from_player, next_index):
|
||||
# Don't allow getting an item until you've got your first check
|
||||
if not self.tracker.has_start_item():
|
||||
return
|
||||
|
||||
# Spin until we either:
|
||||
# get an exception from a bad read (emu shut down or reset)
|
||||
# beat the game
|
||||
# the client handles the last pending item
|
||||
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
||||
while not (await self.is_victory()) and status & 1 == 1:
|
||||
time.sleep(0.1)
|
||||
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
||||
|
||||
item_id -= LABaseID
|
||||
# The player name table only goes up to 100, so don't go past that
|
||||
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
|
||||
if from_player > 100:
|
||||
from_player = 100
|
||||
|
||||
next_index += 1
|
||||
self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [
|
||||
item_id, from_player])
|
||||
status |= 1
|
||||
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
||||
self.gameboy.write_memory(LAClientConstants.wRecvIndex, [next_index])
|
||||
|
||||
async def wait_for_game_ready(self):
|
||||
logger.info("Waiting on game to be in valid state...")
|
||||
while not await self.gameboy.check_safe_gameplay(throw=False):
|
||||
pass
|
||||
logger.info("Ready!")
|
||||
last_index = 0
|
||||
|
||||
async def is_victory(self):
|
||||
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
||||
|
||||
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
|
||||
await self.tracker.readChecks(item_get_cb)
|
||||
await self.item_tracker.readItems()
|
||||
await self.gps_tracker.read_location()
|
||||
|
||||
next_index = self.gameboy.read_memory(LAClientConstants.wRecvIndex)[0]
|
||||
if next_index != self.last_index:
|
||||
self.last_index = next_index
|
||||
# logger.info(f"Got new index {next_index}")
|
||||
|
||||
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
|
||||
if self.deathlink_debounce and current_health != 0:
|
||||
self.deathlink_debounce = False
|
||||
elif not self.deathlink_debounce and current_health == 0:
|
||||
# logger.info("YOU DIED.")
|
||||
await deathlink_cb()
|
||||
self.deathlink_debounce = True
|
||||
|
||||
if self.pending_deathlink:
|
||||
logger.info("Got a deathlink")
|
||||
self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0])
|
||||
self.pending_deathlink = False
|
||||
self.deathlink_debounce = True
|
||||
|
||||
if await self.is_victory():
|
||||
await win_cb()
|
||||
|
||||
recv_index = (await self.gameboy.async_read_memory_safe(LAClientConstants.wRecvIndex))[0]
|
||||
|
||||
# Play back one at a time
|
||||
if recv_index in self.recvd_checks:
|
||||
item = self.recvd_checks[recv_index]
|
||||
await self.recved_item_from_ap(item.item, item.player, recv_index)
|
||||
|
||||
|
||||
all_tasks = set()
|
||||
|
||||
def create_task_log_exception(awaitable) -> asyncio.Task:
|
||||
async def _log_exception(awaitable):
|
||||
try:
|
||||
return await awaitable
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
pass
|
||||
finally:
|
||||
all_tasks.remove(task)
|
||||
task = asyncio.create_task(_log_exception(awaitable))
|
||||
all_tasks.add(task)
|
||||
|
||||
|
||||
class LinksAwakeningContext(CommonContext):
|
||||
tags = {"AP"}
|
||||
game = "Links Awakening DX"
|
||||
items_handling = 0b101
|
||||
want_slot_data = True
|
||||
la_task = None
|
||||
client = None
|
||||
# TODO: does this need to re-read on reset?
|
||||
found_checks = []
|
||||
last_resend = time.time()
|
||||
|
||||
magpie = MagpieBridge()
|
||||
magpie_task = None
|
||||
won = False
|
||||
|
||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
|
||||
self.client = LinksAwakeningClient()
|
||||
super().__init__(server_address, password)
|
||||
|
||||
def run_gui(self) -> None:
|
||||
import webbrowser
|
||||
import kvui
|
||||
from kvui import Button, GameManager
|
||||
from kivy.uix.image import Image
|
||||
|
||||
class LADXManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("Tracker", "Tracker"),
|
||||
]
|
||||
base_title = "Archipelago Links Awakening DX Client"
|
||||
|
||||
def build(self):
|
||||
b = super().build()
|
||||
|
||||
button = Button(text="", size=(30, 30), size_hint_x=None,
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
image = Image(size=(16, 16), texture=magpie_logo())
|
||||
button.add_widget(image)
|
||||
|
||||
def set_center(_, center):
|
||||
image.center = center
|
||||
button.bind(center=set_center)
|
||||
|
||||
self.connect_layout.add_widget(button)
|
||||
return b
|
||||
|
||||
self.ui = LADXManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def send_checks(self):
|
||||
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
ENABLE_DEATHLINK = False
|
||||
async def send_deathlink(self):
|
||||
if self.ENABLE_DEATHLINK:
|
||||
message = [{"cmd": 'Deathlink',
|
||||
'time': time.time(),
|
||||
'cause': 'Had a nightmare',
|
||||
# 'source': self.slot_info[self.slot].name,
|
||||
}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
async def send_victory(self):
|
||||
if not self.won:
|
||||
message = [{"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL}]
|
||||
logger.info("victory!")
|
||||
await self.send_msgs(message)
|
||||
self.won = True
|
||||
|
||||
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||
if self.ENABLE_DEATHLINK:
|
||||
self.client.pending_deathlink = True
|
||||
|
||||
def new_checks(self, item_ids, ladxr_ids):
|
||||
self.found_checks += item_ids
|
||||
create_task_log_exception(self.send_checks())
|
||||
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(LinksAwakeningContext, self).server_auth(password_requested)
|
||||
self.auth = self.client.auth
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
self.game = self.slot_info[self.slot].game
|
||||
# TODO - use watcher_event
|
||||
if cmd == "ReceivedItems":
|
||||
for index, item in enumerate(args["items"], args["index"]):
|
||||
self.client.recvd_checks[index] = item
|
||||
|
||||
item_id_lookup = get_locations_to_id()
|
||||
|
||||
async def run_game_loop(self):
|
||||
def on_item_get(ladxr_checks):
|
||||
checks = [self.item_id_lookup[meta_to_name(
|
||||
checkMetadataTable[check.id])] for check in ladxr_checks]
|
||||
self.new_checks(checks, [check.id for check in ladxr_checks])
|
||||
|
||||
async def victory():
|
||||
await self.send_victory()
|
||||
|
||||
async def deathlink():
|
||||
await self.send_deathlink()
|
||||
|
||||
self.magpie_task = asyncio.create_task(self.magpie.serve())
|
||||
|
||||
# yield to allow UI to start
|
||||
await asyncio.sleep(0)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# TODO: cancel all client tasks
|
||||
logger.info("(Re)Starting game loop")
|
||||
self.found_checks.clear()
|
||||
await self.client.wait_for_retroarch_connection()
|
||||
self.client.reset_auth()
|
||||
await self.client.wait_and_init_tracker()
|
||||
|
||||
while True:
|
||||
await self.client.main_tick(on_item_get, victory, deathlink)
|
||||
await asyncio.sleep(0.1)
|
||||
now = time.time()
|
||||
if self.last_resend + 5.0 < now:
|
||||
self.last_resend = now
|
||||
await self.send_checks()
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||
await self.magpie.send_gps(self.client.gps_tracker)
|
||||
|
||||
except GameboyException:
|
||||
time.sleep(1.0)
|
||||
pass
|
||||
|
||||
|
||||
async def main():
|
||||
parser = get_base_parser(description="Link's Awakening Client.")
|
||||
parser.add_argument("--url", help="Archipelago connection url")
|
||||
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apladx Archipelago Binary Patch file')
|
||||
args = parser.parse_args()
|
||||
logger.info(args)
|
||||
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logger.info("patch file was supplied - creating rom...")
|
||||
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta:
|
||||
args.url = meta["server"]
|
||||
logger.info(f"wrote rom file to {rom_file}")
|
||||
|
||||
if args.url:
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
args.connect = url.netloc
|
||||
if url.password:
|
||||
args.password = urllib.parse.unquote(url.password)
|
||||
|
||||
ctx = LinksAwakeningContext(args.connect, args.password)
|
||||
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
|
||||
# TODO: nothing about the lambda about has to be in a lambda
|
||||
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
16
Main.py
16
Main.py
@@ -316,7 +316,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
client_versions[slot] = player_world.required_client_version
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
|
||||
world.player_types[slot], bool(world.allow_collect[slot].value))
|
||||
world.player_types[slot])
|
||||
for slot, group in world.groups.items():
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
|
||||
@@ -355,13 +355,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
||||
precollect_hint(location)
|
||||
|
||||
# custom datapackage
|
||||
datapackage = {}
|
||||
for game_world in world.worlds.values():
|
||||
if game_world.data_version == 0 and game_world.game not in datapackage:
|
||||
datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game]
|
||||
datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups
|
||||
datapackage[game_world.game]["location_name_groups"] = game_world.location_name_groups
|
||||
# embedded data package
|
||||
data_package = {
|
||||
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||
for game_world in world.worlds.values()
|
||||
}
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
@@ -378,7 +376,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": world.seed_name,
|
||||
"datapackage": datapackage,
|
||||
"datapackage": data_package,
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
|
||||
|
||||
@@ -77,49 +77,34 @@ def read_apmc_file(apmc_file):
|
||||
return json.loads(b64decode(f.read()))
|
||||
|
||||
|
||||
def update_mod(forge_dir, minecraft_version: str, get_prereleases=False):
|
||||
def update_mod(forge_dir, url: str):
|
||||
"""Check mod version, download new mod from GitHub releases page if needed. """
|
||||
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
||||
|
||||
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
|
||||
resp = requests.get(client_releases_endpoint)
|
||||
if resp.status_code == 200: # OK
|
||||
try:
|
||||
latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and
|
||||
(minecraft_version in release['assets'][0]['name']),
|
||||
resp.json()))
|
||||
if ap_randomizer != latest_release['assets'][0]['name']:
|
||||
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
||||
f"{latest_release['assets'][0]['name']}")
|
||||
if ap_randomizer is not None:
|
||||
logging.info(f"Your current mod is {ap_randomizer}.")
|
||||
else:
|
||||
logging.info(f"You do not have the AP randomizer mod installed.")
|
||||
if prompt_yes_no("Would you like to update?"):
|
||||
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
||||
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
|
||||
logging.info("Downloading AP randomizer mod. This may take a moment...")
|
||||
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
|
||||
if apmod_resp.status_code == 200:
|
||||
with open(new_ap_mod, 'wb') as f:
|
||||
f.write(apmod_resp.content)
|
||||
logging.info(f"Wrote new mod file to {new_ap_mod}")
|
||||
if old_ap_mod is not None:
|
||||
os.remove(old_ap_mod)
|
||||
logging.info(f"Removed old mod file from {old_ap_mod}")
|
||||
else:
|
||||
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||
logging.error(f"Please report this issue on the Archipelago Discord server.")
|
||||
sys.exit(1)
|
||||
except StopIteration:
|
||||
logging.warning(f"No compatible mod version found for {minecraft_version}.")
|
||||
if not prompt_yes_no("Run server anyway?"):
|
||||
sys.exit(0)
|
||||
os.path.basename(url)
|
||||
if ap_randomizer is not None:
|
||||
logging.info(f"Your current mod is {ap_randomizer}.")
|
||||
else:
|
||||
logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
|
||||
logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.")
|
||||
if not prompt_yes_no("Continue anyways?"):
|
||||
sys.exit(0)
|
||||
logging.info(f"You do not have the AP randomizer mod installed.")
|
||||
|
||||
if ap_randomizer != os.path.basename(url):
|
||||
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
|
||||
f"{os.path.basename(url)}")
|
||||
if prompt_yes_no("Would you like to update?"):
|
||||
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
|
||||
new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url))
|
||||
logging.info("Downloading AP randomizer mod. This may take a moment...")
|
||||
apmod_resp = requests.get(url)
|
||||
if apmod_resp.status_code == 200:
|
||||
with open(new_ap_mod, 'wb') as f:
|
||||
f.write(apmod_resp.content)
|
||||
logging.info(f"Wrote new mod file to {new_ap_mod}")
|
||||
if old_ap_mod is not None:
|
||||
os.remove(old_ap_mod)
|
||||
logging.info(f"Removed old mod file from {old_ap_mod}")
|
||||
else:
|
||||
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
|
||||
logging.error(f"Please report this issue on the Archipelago Discord server.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_eula(forge_dir):
|
||||
@@ -264,8 +249,13 @@ def get_minecraft_versions(version, release_channel="release"):
|
||||
return next(filter(lambda entry: entry["version"] == version, data[release_channel]))
|
||||
else:
|
||||
return resp.json()[release_channel][0]
|
||||
except StopIteration:
|
||||
logging.error(f"No compatible mod version found for client version {version}.")
|
||||
except (StopIteration, KeyError):
|
||||
logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.")
|
||||
if release_channel != "release":
|
||||
logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file")
|
||||
else:
|
||||
logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def is_correct_forge(forge_dir) -> bool:
|
||||
@@ -286,6 +276,8 @@ if __name__ == '__main__':
|
||||
help="specify java version.")
|
||||
parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store',
|
||||
help="specify forge version. (Minecraft Version-Forge Version)")
|
||||
parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store',
|
||||
help="specify Mod data version to download.")
|
||||
|
||||
args = parser.parse_args()
|
||||
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
|
||||
@@ -296,12 +288,12 @@ if __name__ == '__main__':
|
||||
options = Utils.get_options()
|
||||
channel = args.channel or options["minecraft_options"]["release_channel"]
|
||||
apmc_data = None
|
||||
data_version = None
|
||||
data_version = args.data_version or None
|
||||
|
||||
if apmc_file is None and not args.install:
|
||||
apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),))
|
||||
|
||||
if apmc_file is not None:
|
||||
if apmc_file is not None and data_version is None:
|
||||
apmc_data = read_apmc_file(apmc_file)
|
||||
data_version = apmc_data.get('client_version', '')
|
||||
|
||||
@@ -311,6 +303,7 @@ if __name__ == '__main__':
|
||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||
forge_version = args.forge or versions["forge"]
|
||||
java_version = args.java or versions["java"]
|
||||
mod_url = versions["url"]
|
||||
java_dir = find_jdk_dir(java_version)
|
||||
|
||||
if args.install:
|
||||
@@ -344,7 +337,7 @@ if __name__ == '__main__':
|
||||
if not max_heap_re.match(max_heap):
|
||||
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
|
||||
|
||||
update_mod(forge_dir, f"MC{forge_version.split('-')[0]}", channel != "release")
|
||||
update_mod(forge_dir, mod_url)
|
||||
replace_apmc_files(forge_dir, apmc_file)
|
||||
check_eula(forge_dir)
|
||||
server_process = run_forge_server(forge_dir, java_version, max_heap)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import pkg_resources
|
||||
import warnings
|
||||
|
||||
local_dir = os.path.dirname(__file__)
|
||||
@@ -22,18 +21,50 @@ if not update_ran:
|
||||
requirements_files.add(req_file)
|
||||
|
||||
|
||||
def check_pip():
|
||||
# detect if pip is available
|
||||
try:
|
||||
import pip # noqa: F401
|
||||
except ImportError:
|
||||
raise RuntimeError("pip not available. Please install pip.")
|
||||
|
||||
|
||||
def confirm(msg: str):
|
||||
try:
|
||||
input(f"\n{msg}")
|
||||
except KeyboardInterrupt:
|
||||
print("\nAborting")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def update_command():
|
||||
check_pip()
|
||||
for file in requirements_files:
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"])
|
||||
|
||||
|
||||
def install_pkg_resources(yes=False):
|
||||
try:
|
||||
import pkg_resources # noqa: F401
|
||||
except ImportError:
|
||||
check_pip()
|
||||
if not yes:
|
||||
confirm("pkg_resources not found, press enter to install it")
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
|
||||
|
||||
|
||||
def update(yes=False, force=False):
|
||||
global update_ran
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
|
||||
if force:
|
||||
update_command()
|
||||
return
|
||||
|
||||
install_pkg_resources(yes=yes)
|
||||
import pkg_resources
|
||||
|
||||
for req_file in requirements_files:
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||
if not os.path.exists(path):
|
||||
@@ -52,7 +83,7 @@ def update(yes=False, force=False):
|
||||
egg = egg.split(";", 1)[0].rstrip()
|
||||
if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")):
|
||||
warnings.warn(f"Specifying version as #egg={egg} will become unavailable in pip 25.0. "
|
||||
"Use name @ url#version instead.", DeprecationWarning)
|
||||
"Use name @ url#version instead.", DeprecationWarning)
|
||||
line = egg
|
||||
else:
|
||||
egg = ""
|
||||
@@ -79,11 +110,7 @@ def update(yes=False, force=False):
|
||||
if not yes:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
try:
|
||||
input(f"\nRequirement {requirement} is not satisfied, press enter to install it")
|
||||
except KeyboardInterrupt:
|
||||
print("\nAborting")
|
||||
sys.exit(1)
|
||||
confirm(f"Requirement {requirement} is not satisfied, press enter to install it")
|
||||
update_command()
|
||||
return
|
||||
|
||||
|
||||
@@ -7,17 +7,20 @@ import functools
|
||||
import logging
|
||||
import zlib
|
||||
import collections
|
||||
import typing
|
||||
import inspect
|
||||
import weakref
|
||||
import datetime
|
||||
import threading
|
||||
import random
|
||||
import pickle
|
||||
import itertools
|
||||
import time
|
||||
import operator
|
||||
import functools
|
||||
import hashlib
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import operator
|
||||
import pickle
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
import weakref
|
||||
import zlib
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -160,6 +163,7 @@ class Context:
|
||||
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
|
||||
slot_info: typing.Dict[int, NetworkSlot]
|
||||
|
||||
checksums: typing.Dict[str, str]
|
||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
@@ -222,7 +226,6 @@ class Context:
|
||||
self.save_dirty = False
|
||||
self.tags = ['AP']
|
||||
self.games: typing.Dict[int, str] = {}
|
||||
self.allow_collect: typing.Dict[int, bool] = {}
|
||||
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
|
||||
self.seed_name = ""
|
||||
self.groups = {}
|
||||
@@ -234,6 +237,7 @@ class Context:
|
||||
|
||||
# init empty to satisfy linter, I suppose
|
||||
self.gamespackage = {}
|
||||
self.checksums = {}
|
||||
self.item_name_groups = {}
|
||||
self.location_name_groups = {}
|
||||
self.all_item_and_group_names = {}
|
||||
@@ -242,7 +246,7 @@ class Context:
|
||||
|
||||
self._load_game_data()
|
||||
|
||||
# Datapackage retrieval
|
||||
# Data package retrieval
|
||||
def _load_game_data(self):
|
||||
import worlds
|
||||
self.gamespackage = worlds.network_data_package["games"]
|
||||
@@ -256,6 +260,7 @@ class Context:
|
||||
|
||||
def _init_game_data(self):
|
||||
for game_name, game_package in self.gamespackage.items():
|
||||
self.checksums[game_name] = game_package["checksum"]
|
||||
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||
self.item_names[item_id] = item_name
|
||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||
@@ -351,6 +356,7 @@ class Context:
|
||||
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
|
||||
for text in texts]))
|
||||
|
||||
|
||||
# loading
|
||||
|
||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||
@@ -367,7 +373,7 @@ class Context:
|
||||
with open(multidatapath, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
self._load(self.decompress(data), use_embedded_server_options)
|
||||
self._load(self.decompress(data), {}, use_embedded_server_options)
|
||||
self.data_filename = multidatapath
|
||||
|
||||
@staticmethod
|
||||
@@ -377,7 +383,8 @@ class Context:
|
||||
raise Utils.VersionException("Incompatible multidata.")
|
||||
return restricted_loads(zlib.decompress(data[1:]))
|
||||
|
||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||
def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any],
|
||||
use_embedded_server_options: bool):
|
||||
self.read_data = {}
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > Utils.version_tuple:
|
||||
@@ -392,9 +399,6 @@ class Context:
|
||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
|
||||
if slot_info.type == SlotType.group}
|
||||
# TODO: around 0.4.2 or so, remove the if/else backwards compatibility check.
|
||||
self.allow_collect = {slot: slot_info.allow_collect if type(slot_info.allow_collect) is bool else True
|
||||
for slot, slot_info in self.slot_info.items()}
|
||||
|
||||
self.clients = {0: {}}
|
||||
slot_info: NetworkSlot
|
||||
@@ -435,13 +439,15 @@ class Context:
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
# custom datapackage
|
||||
# embedded data package
|
||||
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
||||
logging.info(f"Loading custom datapackage for game {game_name}")
|
||||
if game_name in game_data_packages:
|
||||
data = game_data_packages[game_name]
|
||||
logging.info(f"Loading embedded data package for game {game_name}")
|
||||
self.gamespackage[game_name] = data
|
||||
self.item_name_groups[game_name] = data["item_name_groups"]
|
||||
self.location_name_groups[game_name] = data["location_name_groups"]
|
||||
del data["item_name_groups"] # remove from datapackage, 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()
|
||||
for game_name, data in self.item_name_groups.items():
|
||||
@@ -591,7 +597,7 @@ class Context:
|
||||
|
||||
def get_hint_cost(self, slot):
|
||||
if self.hint_cost:
|
||||
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return 0
|
||||
|
||||
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
|
||||
@@ -739,10 +745,12 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
NetworkPlayer(team, slot,
|
||||
ctx.name_aliases.get((team, slot), name), name)
|
||||
)
|
||||
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
||||
games.add("Archipelago")
|
||||
await ctx.send_msgs(client, [{
|
||||
'cmd': 'RoomInfo',
|
||||
'password': bool(ctx.password),
|
||||
'games': {ctx.games[x] for x in range(1, len(ctx.games) + 1)},
|
||||
'games': games,
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
'tags': ctx.tags,
|
||||
@@ -751,7 +759,9 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_versions': {game: game_data["version"] for game, game_data
|
||||
in ctx.gamespackage.items()},
|
||||
in ctx.gamespackage.items() if game in games},
|
||||
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
|
||||
in ctx.gamespackage.items() if game in games},
|
||||
'seed_name': ctx.seed_name,
|
||||
'time': time.time(),
|
||||
}])
|
||||
@@ -772,7 +782,8 @@ async def on_client_disconnected(ctx: Context, client: Client):
|
||||
|
||||
|
||||
async def on_client_joined(ctx: Context, client: Client):
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
||||
if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN:
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
||||
version_str = '.'.join(str(x) for x in client.version)
|
||||
verb = "tracking" if "Tracker" in client.tags else "playing"
|
||||
ctx.broadcast_text_all(
|
||||
@@ -789,11 +800,12 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||
|
||||
|
||||
async def on_client_left(ctx: Context, client: Client):
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
||||
if len(ctx.clients[client.team][client.slot]) < 1:
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
ctx.broadcast_text_all(
|
||||
"%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1),
|
||||
{"type": "Part", "team": client.team, "slot": client.slot})
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
async def countdown(ctx: Context, timer: int):
|
||||
@@ -889,8 +901,6 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
|
||||
"""register any locations that are in the multidata, pointing towards this player"""
|
||||
all_locations = collections.defaultdict(set)
|
||||
for source_slot, location_data in ctx.locations.items():
|
||||
if not ctx.allow_collect[source_slot]:
|
||||
continue
|
||||
for location_id, values in location_data.items():
|
||||
if values[1] == slot:
|
||||
all_locations[source_slot].add(location_id)
|
||||
|
||||
@@ -35,7 +35,7 @@ class SlotType(enum.IntFlag):
|
||||
|
||||
@property
|
||||
def always_goal(self) -> bool:
|
||||
"""Mark this slot has having reached its goal instantly."""
|
||||
"""Mark this slot as having reached its goal instantly."""
|
||||
return self.value != 0b01
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ class NetworkSlot(typing.NamedTuple):
|
||||
name: str
|
||||
game: str
|
||||
type: SlotType
|
||||
allow_collect: bool = True
|
||||
group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group
|
||||
|
||||
|
||||
|
||||
@@ -289,7 +289,10 @@ async def patch_and_run_game(apz5_file):
|
||||
decomp_path = base_name + '-decomp.z64'
|
||||
comp_path = base_name + '.z64'
|
||||
# Load vanilla ROM, patch file, compress ROM
|
||||
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
|
||||
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
|
||||
if not os.path.exists(rom_file_name):
|
||||
rom_file_name = Utils.user_path(rom_file_name)
|
||||
rom = Rom(rom_file_name)
|
||||
apply_patch_file(rom, apz5_file,
|
||||
sub_file=(os.path.basename(base_name) + '.zpf'
|
||||
if zipfile.is_zipfile(apz5_file)
|
||||
|
||||
10
Options.py
10
Options.py
@@ -875,17 +875,9 @@ class ProgressionBalancing(SpecialRange):
|
||||
}
|
||||
|
||||
|
||||
class AllowCollect(DefaultOnToggle):
|
||||
"""Controls whether items are collected from the slot when a player does a !collect or not.
|
||||
The impact for the collecting player is that the collector might not get all of their items, until
|
||||
the player(s) that has disallowed collection actually completes or releases their location checks."""
|
||||
display_name = "Allow Collect"
|
||||
|
||||
|
||||
common_options = {
|
||||
"progression_balancing": ProgressionBalancing,
|
||||
"accessibility": Accessibility,
|
||||
"allow_collect": AllowCollect
|
||||
"accessibility": Accessibility
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ Currently, the following games are supported:
|
||||
* Stardew Valley
|
||||
* The Legend of Zelda
|
||||
* The Messenger
|
||||
* Kingdom Hearts 2
|
||||
* The Legend of Zelda: Link's Awakening DX
|
||||
* Clique
|
||||
* Adventure
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -115,8 +115,8 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
class SNIContext(CommonContext):
|
||||
command_processor: typing.Type[SNIClientCommandProcessor] = SNIClientCommandProcessor
|
||||
game = None # set in validate_rom
|
||||
items_handling = None # set in game_watcher
|
||||
game: typing.Optional[str] = None # set in validate_rom
|
||||
items_handling: typing.Optional[int] = None # set in game_watcher
|
||||
snes_connect_task: "typing.Optional[asyncio.Task[None]]" = None
|
||||
snes_autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None
|
||||
|
||||
|
||||
69
Utils.py
69
Utils.py
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import typing
|
||||
import builtins
|
||||
import os
|
||||
@@ -38,7 +39,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.3.9"
|
||||
__version__ = "0.4.0"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -87,7 +88,10 @@ def is_frozen() -> bool:
|
||||
|
||||
|
||||
def local_path(*path: str) -> str:
|
||||
"""Returns path to a file in the local Archipelago installation or source."""
|
||||
"""
|
||||
Returns path to a file in the local Archipelago installation or source.
|
||||
This might be read-only and user_path should be used instead for ROMs, configuration, etc.
|
||||
"""
|
||||
if hasattr(local_path, 'cached_path'):
|
||||
pass
|
||||
elif is_frozen():
|
||||
@@ -142,6 +146,17 @@ def user_path(*path: str) -> str:
|
||||
return os.path.join(user_path.cached_path, *path)
|
||||
|
||||
|
||||
def cache_path(*path: str) -> str:
|
||||
"""Returns path to a file in the user's Archipelago cache directory."""
|
||||
if hasattr(cache_path, "cached_path"):
|
||||
pass
|
||||
else:
|
||||
import platformdirs
|
||||
cache_path.cached_path = platformdirs.user_cache_dir("Archipelago", False)
|
||||
|
||||
return os.path.join(cache_path.cached_path, *path)
|
||||
|
||||
|
||||
def output_path(*path: str) -> str:
|
||||
if hasattr(output_path, 'cached_path'):
|
||||
return os.path.join(output_path.cached_path, *path)
|
||||
@@ -248,6 +263,9 @@ def get_default_options() -> OptionsType:
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
},
|
||||
"ladx_options": {
|
||||
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
|
||||
},
|
||||
"server_options": {
|
||||
"host": None,
|
||||
"port": 38281,
|
||||
@@ -317,7 +335,13 @@ def get_default_options() -> OptionsType:
|
||||
},
|
||||
"wargroove_options": {
|
||||
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||
}
|
||||
},
|
||||
"adventure_options": {
|
||||
"rom_file": "ADVNTURE.BIN",
|
||||
"display_msgs": True,
|
||||
"rom_start": True,
|
||||
"rom_args": ""
|
||||
},
|
||||
}
|
||||
return options
|
||||
|
||||
@@ -385,6 +409,45 @@ def persistent_load() -> typing.Dict[str, dict]:
|
||||
return storage
|
||||
|
||||
|
||||
def get_file_safe_name(name: str) -> str:
|
||||
return "".join(c for c in name if c not in '<>:"/\\|?*')
|
||||
|
||||
|
||||
def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) -> Dict[str, Any]:
|
||||
if checksum and game:
|
||||
if checksum != get_file_safe_name(checksum):
|
||||
raise ValueError(f"Bad symbols in checksum: {checksum}")
|
||||
path = cache_path("datapackage", get_file_safe_name(game), f"{checksum}.json")
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not load data package: {e}")
|
||||
|
||||
# fall back to old cache
|
||||
cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
|
||||
if cache.get("checksum") == checksum:
|
||||
return cache
|
||||
|
||||
# cache does not match
|
||||
return {}
|
||||
|
||||
|
||||
def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> None:
|
||||
checksum = data.get("checksum")
|
||||
if checksum and game:
|
||||
if checksum != get_file_safe_name(checksum):
|
||||
raise ValueError(f"Bad symbols in checksum: {checksum}")
|
||||
game_folder = cache_path("datapackage", get_file_safe_name(game))
|
||||
os.makedirs(game_folder, exist_ok=True)
|
||||
try:
|
||||
with open(os.path.join(game_folder, f"{checksum}.json"), "w", encoding="utf-8-sig") as f:
|
||||
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
||||
except Exception as e:
|
||||
logging.debug(f"Could not store data package: {e}")
|
||||
|
||||
|
||||
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
|
||||
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
|
||||
return adjuster_settings
|
||||
|
||||
@@ -39,12 +39,21 @@ def get_datapackage():
|
||||
|
||||
@api_endpoints.route('/datapackage_version')
|
||||
@cache.cached()
|
||||
|
||||
def get_datapackage_versions():
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
from worlds import AutoWorldRegister
|
||||
|
||||
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
||||
return version_package
|
||||
|
||||
|
||||
@api_endpoints.route('/datapackage_checksum')
|
||||
@cache.cached()
|
||||
def get_datapackage_checksums():
|
||||
from worlds import network_data_package
|
||||
version_package = {
|
||||
game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
|
||||
}
|
||||
return version_package
|
||||
|
||||
|
||||
from . import generate, user # trigger registration
|
||||
|
||||
@@ -19,7 +19,7 @@ import Utils
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
||||
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
|
||||
from .models import Room, Command, db
|
||||
from .models import Command, GameDataPackage, Room, db
|
||||
|
||||
|
||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
@@ -92,7 +92,20 @@ class WebHostContext(Context):
|
||||
else:
|
||||
self.port = get_random_port()
|
||||
|
||||
return self._load(self.decompress(room.seed.multidata), True)
|
||||
multidata = self.decompress(room.seed.multidata)
|
||||
game_data_packages = {}
|
||||
for game in list(multidata["datapackage"]):
|
||||
game_data = multidata["datapackage"][game]
|
||||
if "checksum" in game_data:
|
||||
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
|
||||
# non-custom. remove from multidata
|
||||
# games package could be dropped from static data once all rooms embed data package
|
||||
del multidata["datapackage"][game]
|
||||
else:
|
||||
data = Utils.restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
|
||||
game_data_packages[game] = data
|
||||
|
||||
return self._load(multidata, game_data_packages, True)
|
||||
|
||||
@db_session
|
||||
def init_save(self, enabled: bool = True):
|
||||
@@ -190,6 +203,11 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
with Locker(room_id):
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
except:
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
|
||||
@@ -88,6 +88,8 @@ def download_slot_file(room_id, player_id: int):
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex"
|
||||
elif slot_data.game == "Dark Souls III":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
||||
elif slot_data.game == "Kingdom Hearts 2":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
|
||||
else:
|
||||
return "Game download not supported."
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
|
||||
|
||||
@@ -56,3 +56,8 @@ class Generation(db.Entity):
|
||||
options = Required(buffer, lazy=True)
|
||||
meta = Required(LongStr, default=lambda: "{\"race\": false}")
|
||||
state = Required(int, default=0, index=True)
|
||||
|
||||
|
||||
class GameDataPackage(db.Entity):
|
||||
checksum = PrimaryKey(str)
|
||||
data = Required(bytes)
|
||||
|
||||
@@ -11,7 +11,7 @@ from Utils import __version__, local_path
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||
"exclude_locations"}
|
||||
"exclude_locations", "priority_locations"}
|
||||
|
||||
|
||||
def create():
|
||||
@@ -88,7 +88,7 @@ def create():
|
||||
if option_name in handled_in_js:
|
||||
pass
|
||||
|
||||
elif option.options:
|
||||
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
|
||||
game_options[option_name] = this_option = {
|
||||
"type": "select",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
@@ -98,15 +98,15 @@ def create():
|
||||
}
|
||||
|
||||
for sub_option_id, sub_option_name in option.name_lookup.items():
|
||||
this_option["options"].append({
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
})
|
||||
|
||||
if sub_option_name != "random":
|
||||
this_option["options"].append({
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
})
|
||||
if sub_option_id == option.default:
|
||||
this_option["defaultValue"] = sub_option_name
|
||||
|
||||
if option.default == "random":
|
||||
if not this_option["defaultValue"]:
|
||||
this_option["defaultValue"] = "random"
|
||||
|
||||
elif issubclass(option, Options.Range):
|
||||
@@ -126,21 +126,21 @@ def create():
|
||||
for key, val in option.special_range_names.items():
|
||||
game_options[option_name]["value_names"][key] = val
|
||||
|
||||
elif getattr(option, "verify_item_name", False):
|
||||
elif issubclass(option, Options.ItemSet):
|
||||
game_options[option_name] = {
|
||||
"type": "items-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
}
|
||||
|
||||
elif getattr(option, "verify_location_name", False):
|
||||
elif issubclass(option, Options.LocationSet):
|
||||
game_options[option_name] = {
|
||||
"type": "locations-list",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
"description": get_html_doc(option),
|
||||
}
|
||||
|
||||
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
|
||||
elif issubclass(option, Options.VerifyKeys):
|
||||
if option.valid_keys:
|
||||
game_options[option_name] = {
|
||||
"type": "custom-list",
|
||||
@@ -160,6 +160,14 @@ def create():
|
||||
json.dump(player_settings, f, indent=2, separators=(',', ': '))
|
||||
|
||||
if not world.hidden and world.web.settings_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle settings
|
||||
for option in game_options.values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
|
||||
if not option["defaultValue"]:
|
||||
option["defaultValue"] = "random"
|
||||
|
||||
weighted_settings["baseOptions"]["game"][game_name] = 0
|
||||
weighted_settings["games"][game_name] = {}
|
||||
weighted_settings["games"][game_name]["gameSettings"] = game_options
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Mobile menu handling
|
||||
const menuButton = document.getElementById('base-header-mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('base-header-mobile-menu');
|
||||
|
||||
menuButton.addEventListener('click', (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (!mobileMenu.style.display || mobileMenu.style.display === 'none') {
|
||||
return mobileMenu.style.display = 'flex';
|
||||
@@ -15,4 +17,24 @@ window.addEventListener('load', () => {
|
||||
window.addEventListener('resize', () => {
|
||||
mobileMenu.style.display = 'none';
|
||||
});
|
||||
|
||||
// Popover handling
|
||||
const popoverText = document.getElementById('base-header-popover-text');
|
||||
const popoverMenu = document.getElementById('base-header-popover-menu');
|
||||
|
||||
popoverText.addEventListener('click', (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (!popoverMenu.style.display || popoverMenu.style.display === 'none') {
|
||||
return popoverMenu.style.display = 'flex';
|
||||
}
|
||||
|
||||
popoverMenu.style.display = 'none';
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', () => {
|
||||
mobileMenu.style.display = 'none';
|
||||
popoverMenu.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,8 +78,6 @@ const createDefaultSettings = (settingData) => {
|
||||
break;
|
||||
case 'range':
|
||||
case 'special_range':
|
||||
newSettings[game][gameSetting][setting.min] = 0;
|
||||
newSettings[game][gameSetting][setting.max] = 0;
|
||||
newSettings[game][gameSetting]['random'] = 0;
|
||||
newSettings[game][gameSetting]['random-low'] = 0;
|
||||
newSettings[game][gameSetting]['random-high'] = 0;
|
||||
@@ -103,6 +101,7 @@ const createDefaultSettings = (settingData) => {
|
||||
|
||||
newSettings[game].start_inventory = {};
|
||||
newSettings[game].exclude_locations = [];
|
||||
newSettings[game].priority_locations = [];
|
||||
newSettings[game].local_items = [];
|
||||
newSettings[game].non_local_items = [];
|
||||
newSettings[game].start_hints = [];
|
||||
@@ -138,21 +137,28 @@ const buildUI = (settingData) => {
|
||||
expandButton.classList.add('invisible');
|
||||
gameDiv.appendChild(expandButton);
|
||||
|
||||
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings);
|
||||
settingData.games[game].gameItems.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
|
||||
settingData.games[game].gameLocations.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
|
||||
|
||||
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings,
|
||||
settingData.games[game].gameItems, settingData.games[game].gameLocations);
|
||||
gameDiv.appendChild(weightedSettingsDiv);
|
||||
|
||||
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
||||
gameDiv.appendChild(itemsDiv);
|
||||
const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
||||
gameDiv.appendChild(itemPoolDiv);
|
||||
|
||||
const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
|
||||
gameDiv.appendChild(hintsDiv);
|
||||
|
||||
const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations);
|
||||
gameDiv.appendChild(locationsDiv);
|
||||
|
||||
gamesWrapper.appendChild(gameDiv);
|
||||
|
||||
collapseButton.addEventListener('click', () => {
|
||||
collapseButton.classList.add('invisible');
|
||||
weightedSettingsDiv.classList.add('invisible');
|
||||
itemsDiv.classList.add('invisible');
|
||||
itemPoolDiv.classList.add('invisible');
|
||||
hintsDiv.classList.add('invisible');
|
||||
expandButton.classList.remove('invisible');
|
||||
});
|
||||
@@ -160,7 +166,7 @@ const buildUI = (settingData) => {
|
||||
expandButton.addEventListener('click', () => {
|
||||
collapseButton.classList.remove('invisible');
|
||||
weightedSettingsDiv.classList.remove('invisible');
|
||||
itemsDiv.classList.remove('invisible');
|
||||
itemPoolDiv.classList.remove('invisible');
|
||||
hintsDiv.classList.remove('invisible');
|
||||
expandButton.classList.add('invisible');
|
||||
});
|
||||
@@ -228,7 +234,7 @@ const buildGameChoice = (games) => {
|
||||
gameChoiceDiv.appendChild(table);
|
||||
};
|
||||
|
||||
const buildWeightedSettingsDiv = (game, settings) => {
|
||||
const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const settingsWrapper = document.createElement('div');
|
||||
settingsWrapper.classList.add('settings-wrapper');
|
||||
@@ -270,7 +276,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||
range.setAttribute('data-type', setting.type);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.value = currentSettings[game][settingName][option.value];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -296,33 +302,33 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||
if (((setting.max - setting.min) + 1) < 11) {
|
||||
for (let i=setting.min; i <= setting.max; ++i) {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = i;
|
||||
tr.appendChild(tdLeft);
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = i;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
|
||||
range.setAttribute('data-game', game);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', i);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][i];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
|
||||
range.setAttribute('data-game', game);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', i);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.value = currentSettings[game][settingName][i] || 0;
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
rangeTbody.appendChild(tr);
|
||||
}
|
||||
} else {
|
||||
const hintText = document.createElement('p');
|
||||
@@ -379,7 +385,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -430,7 +436,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -464,7 +470,17 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
switch(option){
|
||||
case 'random':
|
||||
tdLeft.innerText = 'Random';
|
||||
break;
|
||||
case 'random-low':
|
||||
tdLeft.innerText = "Random (Low)";
|
||||
break;
|
||||
case 'random-high':
|
||||
tdLeft.innerText = "Random (High)";
|
||||
break;
|
||||
}
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
@@ -477,7 +493,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.value = currentSettings[game][settingName][option];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -495,15 +511,108 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||
break;
|
||||
|
||||
case 'items-list':
|
||||
// TODO
|
||||
const itemsList = document.createElement('div');
|
||||
itemsList.classList.add('simple-list');
|
||||
|
||||
Object.values(gameItems).forEach((item) => {
|
||||
const itemRow = document.createElement('div');
|
||||
itemRow.classList.add('list-row');
|
||||
|
||||
const itemLabel = document.createElement('label');
|
||||
itemLabel.setAttribute('for', `${game}-${settingName}-${item}`)
|
||||
|
||||
const itemCheckbox = document.createElement('input');
|
||||
itemCheckbox.setAttribute('id', `${game}-${settingName}-${item}`);
|
||||
itemCheckbox.setAttribute('type', 'checkbox');
|
||||
itemCheckbox.setAttribute('data-game', game);
|
||||
itemCheckbox.setAttribute('data-setting', settingName);
|
||||
itemCheckbox.setAttribute('data-option', item.toString());
|
||||
itemCheckbox.addEventListener('change', updateListSetting);
|
||||
if (currentSettings[game][settingName].includes(item)) {
|
||||
itemCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
|
||||
const itemName = document.createElement('span');
|
||||
itemName.innerText = item.toString();
|
||||
|
||||
itemLabel.appendChild(itemCheckbox);
|
||||
itemLabel.appendChild(itemName);
|
||||
|
||||
itemRow.appendChild(itemLabel);
|
||||
itemsList.appendChild((itemRow));
|
||||
});
|
||||
|
||||
settingWrapper.appendChild(itemsList);
|
||||
break;
|
||||
|
||||
case 'locations-list':
|
||||
// TODO
|
||||
const locationsList = document.createElement('div');
|
||||
locationsList.classList.add('simple-list');
|
||||
|
||||
Object.values(gameLocations).forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${game}-${settingName}-${location}`)
|
||||
|
||||
const locationCheckbox = document.createElement('input');
|
||||
locationCheckbox.setAttribute('id', `${game}-${settingName}-${location}`);
|
||||
locationCheckbox.setAttribute('type', 'checkbox');
|
||||
locationCheckbox.setAttribute('data-game', game);
|
||||
locationCheckbox.setAttribute('data-setting', settingName);
|
||||
locationCheckbox.setAttribute('data-option', location.toString());
|
||||
locationCheckbox.addEventListener('change', updateListSetting);
|
||||
if (currentSettings[game][settingName].includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location.toString();
|
||||
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
locationsList.appendChild((locationRow));
|
||||
});
|
||||
|
||||
settingWrapper.appendChild(locationsList);
|
||||
break;
|
||||
|
||||
case 'custom-list':
|
||||
// TODO
|
||||
const customList = document.createElement('div');
|
||||
customList.classList.add('simple-list');
|
||||
|
||||
Object.values(settings[settingName].options).forEach((listItem) => {
|
||||
const customListRow = document.createElement('div');
|
||||
customListRow.classList.add('list-row');
|
||||
|
||||
const customItemLabel = document.createElement('label');
|
||||
customItemLabel.setAttribute('for', `${game}-${settingName}-${listItem}`)
|
||||
|
||||
const customItemCheckbox = document.createElement('input');
|
||||
customItemCheckbox.setAttribute('id', `${game}-${settingName}-${listItem}`);
|
||||
customItemCheckbox.setAttribute('type', 'checkbox');
|
||||
customItemCheckbox.setAttribute('data-game', game);
|
||||
customItemCheckbox.setAttribute('data-setting', settingName);
|
||||
customItemCheckbox.setAttribute('data-option', listItem.toString());
|
||||
customItemCheckbox.addEventListener('change', updateListSetting);
|
||||
if (currentSettings[game][settingName].includes(listItem)) {
|
||||
customItemCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
|
||||
const customItemName = document.createElement('span');
|
||||
customItemName.innerText = listItem.toString();
|
||||
|
||||
customItemLabel.appendChild(customItemCheckbox);
|
||||
customItemLabel.appendChild(customItemName);
|
||||
|
||||
customListRow.appendChild(customItemLabel);
|
||||
customList.appendChild((customListRow));
|
||||
});
|
||||
|
||||
settingWrapper.appendChild(customList);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -729,21 +838,22 @@ const buildHintsDiv = (game, items, locations) => {
|
||||
const hintsDescription = document.createElement('p');
|
||||
hintsDescription.classList.add('setting-description');
|
||||
hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' +
|
||||
' items are, or what those locations contain. Excluded locations will not contain progression items.';
|
||||
' items are, or what those locations contain.';
|
||||
hintsDiv.appendChild(hintsDescription);
|
||||
|
||||
const itemHintsContainer = document.createElement('div');
|
||||
itemHintsContainer.classList.add('hints-container');
|
||||
|
||||
// Item Hints
|
||||
const itemHintsWrapper = document.createElement('div');
|
||||
itemHintsWrapper.classList.add('hints-wrapper');
|
||||
itemHintsWrapper.innerText = 'Starting Item Hints';
|
||||
|
||||
const itemHintsDiv = document.createElement('div');
|
||||
itemHintsDiv.classList.add('item-container');
|
||||
itemHintsDiv.classList.add('simple-list');
|
||||
items.forEach((item) => {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.classList.add('hint-div');
|
||||
const itemRow = document.createElement('div');
|
||||
itemRow.classList.add('list-row');
|
||||
|
||||
const itemLabel = document.createElement('label');
|
||||
itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
|
||||
@@ -757,29 +867,30 @@ const buildHintsDiv = (game, items, locations) => {
|
||||
if (currentSettings[game].start_hints.includes(item)) {
|
||||
itemCheckbox.setAttribute('checked', 'true');
|
||||
}
|
||||
itemCheckbox.addEventListener('change', hintChangeHandler);
|
||||
itemCheckbox.addEventListener('change', updateListSetting);
|
||||
itemLabel.appendChild(itemCheckbox);
|
||||
|
||||
const itemName = document.createElement('span');
|
||||
itemName.innerText = item;
|
||||
itemLabel.appendChild(itemName);
|
||||
|
||||
itemDiv.appendChild(itemLabel);
|
||||
itemHintsDiv.appendChild(itemDiv);
|
||||
itemRow.appendChild(itemLabel);
|
||||
itemHintsDiv.appendChild(itemRow);
|
||||
});
|
||||
|
||||
itemHintsWrapper.appendChild(itemHintsDiv);
|
||||
itemHintsContainer.appendChild(itemHintsWrapper);
|
||||
|
||||
// Starting Location Hints
|
||||
const locationHintsWrapper = document.createElement('div');
|
||||
locationHintsWrapper.classList.add('hints-wrapper');
|
||||
locationHintsWrapper.innerText = 'Starting Location Hints';
|
||||
|
||||
const locationHintsDiv = document.createElement('div');
|
||||
locationHintsDiv.classList.add('item-container');
|
||||
locationHintsDiv.classList.add('simple-list');
|
||||
locations.forEach((location) => {
|
||||
const locationDiv = document.createElement('div');
|
||||
locationDiv.classList.add('hint-div');
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
|
||||
@@ -793,29 +904,89 @@ const buildHintsDiv = (game, items, locations) => {
|
||||
if (currentSettings[game].start_location_hints.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', hintChangeHandler);
|
||||
locationCheckbox.addEventListener('change', updateListSetting);
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationDiv.appendChild(locationLabel);
|
||||
locationHintsDiv.appendChild(locationDiv);
|
||||
locationRow.appendChild(locationLabel);
|
||||
locationHintsDiv.appendChild(locationRow);
|
||||
});
|
||||
|
||||
locationHintsWrapper.appendChild(locationHintsDiv);
|
||||
itemHintsContainer.appendChild(locationHintsWrapper);
|
||||
|
||||
hintsDiv.appendChild(itemHintsContainer);
|
||||
return hintsDiv;
|
||||
};
|
||||
|
||||
const buildLocationsDiv = (game, locations) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
locations.sort(); // Sort alphabetical, in-place
|
||||
|
||||
const locationsDiv = document.createElement('div');
|
||||
locationsDiv.classList.add('locations-div');
|
||||
const locationsHeader = document.createElement('h3');
|
||||
locationsHeader.innerText = 'Priority & Exclusion Locations';
|
||||
locationsDiv.appendChild(locationsHeader);
|
||||
const locationsDescription = document.createElement('p');
|
||||
locationsDescription.classList.add('setting-description');
|
||||
locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' +
|
||||
'excluded locations will not contain progression or useful items.';
|
||||
locationsDiv.appendChild(locationsDescription);
|
||||
|
||||
const locationsContainer = document.createElement('div');
|
||||
locationsContainer.classList.add('locations-container');
|
||||
|
||||
// Priority Locations
|
||||
const priorityLocationsWrapper = document.createElement('div');
|
||||
priorityLocationsWrapper.classList.add('locations-wrapper');
|
||||
priorityLocationsWrapper.innerText = 'Priority Locations';
|
||||
|
||||
const priorityLocationsDiv = document.createElement('div');
|
||||
priorityLocationsDiv.classList.add('simple-list');
|
||||
locations.forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${game}-priority_locations-${location}`);
|
||||
|
||||
const locationCheckbox = document.createElement('input');
|
||||
locationCheckbox.setAttribute('type', 'checkbox');
|
||||
locationCheckbox.setAttribute('id', `${game}-priority_locations-${location}`);
|
||||
locationCheckbox.setAttribute('data-game', game);
|
||||
locationCheckbox.setAttribute('data-setting', 'priority_locations');
|
||||
locationCheckbox.setAttribute('data-option', location);
|
||||
if (currentSettings[game].priority_locations.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', updateListSetting);
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
priorityLocationsDiv.appendChild(locationRow);
|
||||
});
|
||||
|
||||
priorityLocationsWrapper.appendChild(priorityLocationsDiv);
|
||||
locationsContainer.appendChild(priorityLocationsWrapper);
|
||||
|
||||
// Exclude Locations
|
||||
const excludeLocationsWrapper = document.createElement('div');
|
||||
excludeLocationsWrapper.classList.add('hints-wrapper');
|
||||
excludeLocationsWrapper.classList.add('locations-wrapper');
|
||||
excludeLocationsWrapper.innerText = 'Exclude Locations';
|
||||
|
||||
const excludeLocationsDiv = document.createElement('div');
|
||||
excludeLocationsDiv.classList.add('item-container');
|
||||
excludeLocationsDiv.classList.add('simple-list');
|
||||
locations.forEach((location) => {
|
||||
const locationDiv = document.createElement('div');
|
||||
locationDiv.classList.add('hint-div');
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
|
||||
@@ -829,40 +1000,22 @@ const buildHintsDiv = (game, items, locations) => {
|
||||
if (currentSettings[game].exclude_locations.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', hintChangeHandler);
|
||||
locationCheckbox.addEventListener('change', updateListSetting);
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationDiv.appendChild(locationLabel);
|
||||
excludeLocationsDiv.appendChild(locationDiv);
|
||||
locationRow.appendChild(locationLabel);
|
||||
excludeLocationsDiv.appendChild(locationRow);
|
||||
});
|
||||
|
||||
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
|
||||
itemHintsContainer.appendChild(excludeLocationsWrapper);
|
||||
locationsContainer.appendChild(excludeLocationsWrapper);
|
||||
|
||||
hintsDiv.appendChild(itemHintsContainer);
|
||||
return hintsDiv;
|
||||
};
|
||||
|
||||
const hintChangeHandler = (evt) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const game = evt.target.getAttribute('data-game');
|
||||
const setting = evt.target.getAttribute('data-setting');
|
||||
const option = evt.target.getAttribute('data-option');
|
||||
|
||||
if (evt.target.checked) {
|
||||
if (!currentSettings[game][setting].includes(option)) {
|
||||
currentSettings[game][setting].push(option);
|
||||
}
|
||||
} else {
|
||||
if (currentSettings[game][setting].includes(option)) {
|
||||
currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1);
|
||||
}
|
||||
}
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
|
||||
locationsDiv.appendChild(locationsContainer);
|
||||
return locationsDiv;
|
||||
};
|
||||
|
||||
const updateVisibleGames = () => {
|
||||
@@ -908,13 +1061,12 @@ const updateBaseSetting = (event) => {
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(settings));
|
||||
};
|
||||
|
||||
const updateGameSetting = (evt) => {
|
||||
const updateRangeSetting = (evt) => {
|
||||
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const game = evt.target.getAttribute('data-game');
|
||||
const setting = evt.target.getAttribute('data-setting');
|
||||
const option = evt.target.getAttribute('data-option');
|
||||
document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value;
|
||||
console.log(event);
|
||||
if (evt.action && evt.action === 'rangeDelete') {
|
||||
delete options[game][setting][option];
|
||||
} else {
|
||||
@@ -923,6 +1075,26 @@ const updateGameSetting = (evt) => {
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateListSetting = (evt) => {
|
||||
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const game = evt.target.getAttribute('data-game');
|
||||
const setting = evt.target.getAttribute('data-setting');
|
||||
const option = evt.target.getAttribute('data-option');
|
||||
|
||||
if (evt.target.checked) {
|
||||
// If the option is to be enabled and it is already enabled, do nothing
|
||||
if (options[game][setting].includes(option)) { return; }
|
||||
|
||||
options[game][setting].push(option);
|
||||
} else {
|
||||
// If the option is to be disabled and it is already disabled, do nothing
|
||||
if (!options[game][setting].includes(option)) { return; }
|
||||
|
||||
options[game][setting].splice(options[game][setting].indexOf(option), 1);
|
||||
}
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateItemSetting = (evt) => {
|
||||
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const game = evt.target.getAttribute('data-game');
|
||||
|
||||
BIN
WebHostLib/static/static/button-images/popover.png
Normal file
BIN
WebHostLib/static/static/button-images/popover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
@@ -15,3 +15,33 @@
|
||||
padding-left: 0.5rem;
|
||||
color: #dfedc6;
|
||||
}
|
||||
@media all and (max-width: 900px) {
|
||||
#island-footer{
|
||||
font-size: 17px;
|
||||
font-size: 2vw;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 768px) {
|
||||
#island-footer{
|
||||
font-size: 15px;
|
||||
font-size: 2vw;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 650px) {
|
||||
#island-footer{
|
||||
font-size: 13px;
|
||||
font-size: 2vw;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 580px) {
|
||||
#island-footer{
|
||||
font-size: 11px;
|
||||
font-size: 2vw;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 512px) {
|
||||
#island-footer{
|
||||
font-size: 9px;
|
||||
font-size: 2vw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ html{
|
||||
margin-right: auto;
|
||||
margin-top: 10px;
|
||||
height: 140px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#landing-header h4{
|
||||
@@ -223,7 +222,7 @@ html{
|
||||
}
|
||||
|
||||
#landing{
|
||||
width: 700px;
|
||||
max-width: 700px;
|
||||
min-height: 280px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@@ -30,6 +30,8 @@ html{
|
||||
}
|
||||
|
||||
#base-header-right{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -42,7 +44,7 @@ html{
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#base-header a, #base-header-mobile-menu a{
|
||||
#base-header a, #base-header-mobile-menu a, #base-header-popover-text{
|
||||
color: #2f6b83;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
@@ -72,22 +74,92 @@ html{
|
||||
position: absolute;
|
||||
top: 7rem;
|
||||
right: 0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu a{
|
||||
padding: 4rem 2rem;
|
||||
font-size: 5rem;
|
||||
padding: 3rem 1.5rem;
|
||||
font-size: 4rem;
|
||||
line-height: 5rem;
|
||||
color: #699ca8;
|
||||
border-top: 1px solid #d3d3d3;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu :first-child, #base-header-popover-menu :first-child{
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
#base-header-right-mobile img{
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1580px){
|
||||
#base-header-popover-menu{
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
margin-left: -108px;
|
||||
margin-top: 2.25rem;
|
||||
border-radius: 10px;
|
||||
border-left: 2px solid #d0ebe6;
|
||||
border-bottom: 2px solid #d0ebe6;
|
||||
border-right: 1px solid #d0ebe6;
|
||||
filter: drop-shadow(-6px 6px 2px #2e3e83);
|
||||
}
|
||||
|
||||
#base-header-popover-menu a{
|
||||
color: #699ca8;
|
||||
border-top: 1px solid #d3d3d3;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
line-height: 3rem;
|
||||
margin-right: 2px;
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
|
||||
#base-header-popover-icon {
|
||||
width: 14px;
|
||||
margin-bottom: 3px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px), only screen and (max-device-width: 768px) {
|
||||
#base-header-right{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#base-header-right-mobile{
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px){
|
||||
#base-header-right-mobile{
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#base-header-right-mobile img{
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu{
|
||||
top: 3.3rem;
|
||||
width: unset;
|
||||
border-left: 2px solid #d0ebe6;
|
||||
border-bottom: 2px solid #d0ebe6;
|
||||
filter: drop-shadow(-6px 6px 2px #2e3e83);
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu a{
|
||||
font-size: 1.5rem;
|
||||
line-height: 3rem;
|
||||
margin: 0;
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 768px){
|
||||
html{
|
||||
padding-top: 260px;
|
||||
scroll-padding-top: 230px;
|
||||
@@ -103,12 +175,4 @@ html{
|
||||
margin-top: 30px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#base-header-right{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#base-header-right-mobile{
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,41 +157,29 @@ html{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div{
|
||||
#weighted-settings .hints-div, #weighted-settings .locations-div{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div h3{
|
||||
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div .hints-container{
|
||||
#weighted-settings .hints-container, #weighted-settings .locations-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
|
||||
width: calc(50% - 0.5rem);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div .hints-wrapper{
|
||||
width: 32.5%;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div .hints-wrapper .hint-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div .hints-wrapper .hint-div:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div .hints-wrapper .hint-div label{
|
||||
flex-grow: 1;
|
||||
padding: 0.125rem 0.5rem;
|
||||
cursor: pointer;
|
||||
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
|
||||
margin-top: 0.25rem;
|
||||
height: 300px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#weighted-settings #weighted-settings-button-row{
|
||||
@@ -280,6 +268,30 @@ html{
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label{
|
||||
display: block;
|
||||
width: calc(100% - 0.5rem);
|
||||
padding: 0.0625rem 0.25rem;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label:hover{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list .list-row label input[type=checkbox]{
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .invisible{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,18 @@
|
||||
</a>
|
||||
</div>
|
||||
<div id="base-header-right">
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="/start-playing">start playing</a>
|
||||
<a href="/faq/en">f.a.q.</a>
|
||||
<div id="base-header-popover-text">
|
||||
<img id="base-header-popover-icon" src="/static/static/button-images/popover.png" alt="Popover Menu" />
|
||||
get started
|
||||
</div>
|
||||
<div id="base-header-popover-menu">
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="/generate">generate game</a>
|
||||
<a href="/uploads">host game</a>
|
||||
<a href="/user-content">user content</a>
|
||||
</div>
|
||||
<a href="/faq/en">f.a.q</a>
|
||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
<div id="base-header-right-mobile">
|
||||
@@ -26,8 +34,10 @@
|
||||
<div id="base-header-mobile-menu">
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="/start-playing">start playing</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 %}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
{% elif patch.game == "Factorio" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download Factorio Mod...</a>
|
||||
{% elif patch.game == "Kingdom Hearts 2" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download Kingdom Hearts 2 Mod...</a>
|
||||
{% elif patch.game == "Ocarina of Time" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APZ5 File...</a>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
{% endblock %}
|
||||
<th class="center-column">Checks</th>
|
||||
<th class="center-column">%</th>
|
||||
<th class="center-column">Status</th>
|
||||
<th class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -51,8 +52,10 @@
|
||||
{% endblock %}
|
||||
<td class="center-column">{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}</td>
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
<td>{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing",
|
||||
30: "Goal Completed"}.get(states[team, player], "Unknown State") }}</td>
|
||||
{%- if activity_timers[team, player] -%}
|
||||
<td class="center-column">{{ activity_timers[team, player].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
|
||||
@@ -11,10 +11,10 @@ from werkzeug.exceptions import abort
|
||||
from MultiServer import Context, get_saving_second
|
||||
from NetUtils import SlotType
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package
|
||||
from worlds.alttp import Items
|
||||
from . import app, cache
|
||||
from .models import Room
|
||||
from .models import GameDataPackage, Room
|
||||
|
||||
alttp_icons = {
|
||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
@@ -229,14 +229,15 @@ def render_timedelta(delta: datetime.timedelta):
|
||||
|
||||
@pass_context
|
||||
def get_location_name(context: runtime.Context, loc: int) -> str:
|
||||
# once all rooms embed data package, the chain lookup can be dropped
|
||||
context_locations = context.get("custom_locations", {})
|
||||
return collections.ChainMap(lookup_any_location_id_to_name, context_locations).get(loc, loc)
|
||||
return collections.ChainMap(context_locations, lookup_any_location_id_to_name).get(loc, loc)
|
||||
|
||||
|
||||
@pass_context
|
||||
def get_item_name(context: runtime.Context, item: int) -> str:
|
||||
context_items = context.get("custom_items", {})
|
||||
return collections.ChainMap(lookup_any_item_id_to_name, context_items).get(item, item)
|
||||
return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item)
|
||||
|
||||
|
||||
app.jinja_env.filters["location_name"] = get_location_name
|
||||
@@ -274,11 +275,21 @@ def get_static_room_data(room: Room):
|
||||
if slot_info.type == SlotType.group}
|
||||
|
||||
for game in games.values():
|
||||
if game in multidata["datapackage"]:
|
||||
custom_locations.update(
|
||||
{id: name for name, id in multidata["datapackage"][game]["location_name_to_id"].items()})
|
||||
custom_items.update(
|
||||
{id: name for name, id in multidata["datapackage"][game]["item_name_to_id"].items()})
|
||||
if game not in multidata["datapackage"]:
|
||||
continue
|
||||
game_data = multidata["datapackage"][game]
|
||||
if "checksum" in game_data:
|
||||
if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]:
|
||||
# non-custom. remove from multidata
|
||||
# network_data_package import could be skipped once all rooms embed data package
|
||||
del multidata["datapackage"][game]
|
||||
continue
|
||||
else:
|
||||
game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
|
||||
custom_locations.update(
|
||||
{id_: name for name, id_ in game_data["location_name_to_id"].items()})
|
||||
custom_items.update(
|
||||
{id_: name for name, id_ in game_data["item_name_to_id"].items()})
|
||||
elif "games" in multidata:
|
||||
games = multidata["games"]
|
||||
seed_checks_in_area = checks_in_area.copy()
|
||||
@@ -1373,24 +1384,26 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
|
||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
player_names = {}
|
||||
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||
for team, names in enumerate(names):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[(team, player)] = name
|
||||
player_names[team, player] = name
|
||||
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
long_player_names = player_names.copy()
|
||||
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||
player_names[(team, player)] = alias
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
||||
player_names[team, player] = alias
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
|
||||
|
||||
video = {}
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
video[team, player] = data
|
||||
|
||||
return dict(player_names=player_names, room=room, checks_done=checks_done,
|
||||
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
|
||||
activity_timers=activity_timers, video=video, hints=hints,
|
||||
long_player_names=long_player_names,
|
||||
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
||||
locations=locations, games=games)
|
||||
locations=locations, games=games, states=states)
|
||||
|
||||
|
||||
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import base64
|
||||
import json
|
||||
import pickle
|
||||
import typing
|
||||
import uuid
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
import zlib
|
||||
|
||||
from io import BytesIO
|
||||
from flask import request, flash, redirect, url_for, session, render_template, Markup
|
||||
from pony.orm import flush, select
|
||||
from pony.orm import commit, flush, select, rollback
|
||||
from pony.orm.core import TransactionIntegrityError
|
||||
|
||||
import MultiServer
|
||||
from NetUtils import NetworkSlot, SlotType
|
||||
from Utils import VersionException, __version__
|
||||
from worlds.Files import AutoPatchRegister
|
||||
from . import app
|
||||
from .models import Seed, Room, Slot
|
||||
from .models import Seed, Room, Slot, GameDataPackage
|
||||
|
||||
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||
|
||||
@@ -78,6 +81,27 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
# Load multi data.
|
||||
if multidata:
|
||||
decompressed_multidata = MultiServer.Context.decompress(multidata)
|
||||
recompress = False
|
||||
|
||||
if "datapackage" in decompressed_multidata:
|
||||
# strip datapackage from multidata, leaving only the checksums
|
||||
game_data_packages: typing.List[GameDataPackage] = []
|
||||
for game, game_data in decompressed_multidata["datapackage"].items():
|
||||
if game_data.get("checksum"):
|
||||
game_data_package = GameDataPackage(checksum=game_data["checksum"],
|
||||
data=pickle.dumps(game_data))
|
||||
decompressed_multidata["datapackage"][game] = {
|
||||
"version": game_data.get("version", 0),
|
||||
"checksum": game_data["checksum"]
|
||||
}
|
||||
recompress = True
|
||||
try:
|
||||
commit() # commit game data package
|
||||
game_data_packages.append(game_data_package)
|
||||
except TransactionIntegrityError:
|
||||
del game_data_package
|
||||
rollback()
|
||||
|
||||
if "slot_info" in decompressed_multidata:
|
||||
for slot, slot_info in decompressed_multidata["slot_info"].items():
|
||||
# Ignore Player Groups (e.g. item links)
|
||||
@@ -90,6 +114,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
|
||||
flush() # commit slots
|
||||
|
||||
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),
|
||||
id=sid if sid else uuid.uuid4())
|
||||
flush() # create seed
|
||||
|
||||
BIN
data/adventure_basepatch.bsdiff4
Normal file
BIN
data/adventure_basepatch.bsdiff4
Normal file
Binary file not shown.
851
data/lua/ADVENTURE/adventure_connector.lua
Normal file
851
data/lua/ADVENTURE/adventure_connector.lua
Normal file
@@ -0,0 +1,851 @@
|
||||
local socket = require("socket")
|
||||
local json = require('json')
|
||||
local math = require('math')
|
||||
|
||||
local STATE_OK = "Ok"
|
||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||
local STATE_UNINITIALIZED = "Uninitialized"
|
||||
|
||||
local SCRIPT_VERSION = 1
|
||||
|
||||
local APItemValue = 0xA2
|
||||
local APItemRam = 0xE7
|
||||
local BatAPItemValue = 0xAB
|
||||
local BatAPItemRam = 0xEA
|
||||
local PlayerRoomAddr = 0x8A -- if in number room, we're not in play mode
|
||||
local WinAddr = 0xDE -- if not 0 (I think if 0xff specifically), we won (and should update once, immediately)
|
||||
|
||||
-- If any of these are 2, that dragon ate the player (should send update immediately
|
||||
-- once, and reset that when none of them are 2 again)
|
||||
|
||||
local DragonState = {0xA8, 0xAD, 0xB2}
|
||||
local last_dragon_state = {0, 0, 0}
|
||||
local carryAddress = 0x9D -- uses rom object table
|
||||
local batRoomAddr = 0xCB
|
||||
local batCarryAddress = 0xD0 -- uses ram object location
|
||||
local batInvalidCarryItem = 0x78
|
||||
local batItemCheckAddr = 0xf69f
|
||||
local batMatrixLen = 11 -- number of pairs
|
||||
local last_carry_item = 0xB4
|
||||
local frames_with_no_item = 0
|
||||
local ItemTableStart = 0xfe9d
|
||||
local PlayerSlotAddress = 0xfff9
|
||||
|
||||
local itemMessages = {}
|
||||
|
||||
local nullObjectId = 0xB4
|
||||
local ItemsReceived = nil
|
||||
local sha256hash = nil
|
||||
local foreign_items = nil
|
||||
local foreign_items_by_room = {}
|
||||
local bat_no_touch_locations_by_room = {}
|
||||
local bat_no_touch_items = {}
|
||||
local autocollect_items = {}
|
||||
local localItemLocations = {}
|
||||
|
||||
local prev_bat_room = 0xff
|
||||
local prev_player_room = 0
|
||||
local prev_ap_room_index = nil
|
||||
|
||||
local pending_foreign_items_collected = {}
|
||||
local pending_local_items_collected = {}
|
||||
local rendering_foreign_item = nil
|
||||
local skip_inventory_items = {}
|
||||
|
||||
local inventory = {}
|
||||
local next_inventory_item = nil
|
||||
|
||||
local input_button_address = 0xD7
|
||||
|
||||
local deathlink_rec = nil
|
||||
local deathlink_send = 0
|
||||
|
||||
local deathlink_sent = false
|
||||
|
||||
local prevstate = ""
|
||||
local curstate = STATE_UNINITIALIZED
|
||||
local atariSocket = nil
|
||||
local frame = 0
|
||||
|
||||
local ItemIndex = 0
|
||||
|
||||
local yorgle_speed_address = 0xf725
|
||||
local grundle_speed_address = 0xf740
|
||||
local rhindle_speed_address = 0xf70A
|
||||
|
||||
local read_switch_a = 0xf780
|
||||
local read_switch_b = 0xf764
|
||||
|
||||
local yorgle_speed = nil
|
||||
local grundle_speed = nil
|
||||
local rhindle_speed = nil
|
||||
|
||||
local slow_yorgle_id = tostring(118000000 + 0x103)
|
||||
local slow_grundle_id = tostring(118000000 + 0x104)
|
||||
local slow_rhindle_id = tostring(118000000 + 0x105)
|
||||
|
||||
local yorgle_dead = false
|
||||
local grundle_dead = false
|
||||
local rhindle_dead = false
|
||||
|
||||
local diff_a_locked = false
|
||||
local diff_b_locked = false
|
||||
|
||||
local bat_logic = 0
|
||||
|
||||
local is_dead = 0
|
||||
local freeincarnates_available = 0
|
||||
local send_freeincarnate_used = false
|
||||
local current_bat_ap_item = nil
|
||||
|
||||
local was_in_number_room = false
|
||||
|
||||
local u8 = nil
|
||||
local wU8 = nil
|
||||
local u16
|
||||
|
||||
local bizhawk_version = client.getversion()
|
||||
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
|
||||
local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8")
|
||||
|
||||
u8 = memory.read_u8
|
||||
wU8 = memory.write_u8
|
||||
u16 = memory.read_u16_le
|
||||
function uRangeRam(address, bytes)
|
||||
data = memory.read_bytes_as_array(address, bytes, "Main RAM")
|
||||
return data
|
||||
end
|
||||
function uRangeRom(address, bytes)
|
||||
data = memory.read_bytes_as_array(address+0xf000, bytes, "System Bus")
|
||||
return data
|
||||
end
|
||||
function uRangeAddress(address, bytes)
|
||||
data = memory.read_bytes_as_array(address, bytes, "System Bus")
|
||||
return data
|
||||
end
|
||||
|
||||
|
||||
function table.empty (self)
|
||||
for _, _ in pairs(self) do
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function slice (tbl, s, e)
|
||||
local pos, new = 1, {}
|
||||
for i = s + 1, e do
|
||||
new[pos] = tbl[i]
|
||||
pos = pos + 1
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
local function createForeignItemsByRoom()
|
||||
foreign_items_by_room = {}
|
||||
if foreign_items == nil then
|
||||
return
|
||||
end
|
||||
for _, foreign_item in pairs(foreign_items) do
|
||||
if foreign_items_by_room[foreign_item.room_id] == nil then
|
||||
foreign_items_by_room[foreign_item.room_id] = {}
|
||||
end
|
||||
new_foreign_item = {}
|
||||
new_foreign_item.room_id = foreign_item.room_id
|
||||
new_foreign_item.room_x = foreign_item.room_x
|
||||
new_foreign_item.room_y = foreign_item.room_y
|
||||
new_foreign_item.short_location_id = foreign_item.short_location_id
|
||||
|
||||
table.insert(foreign_items_by_room[foreign_item.room_id], new_foreign_item)
|
||||
end
|
||||
end
|
||||
|
||||
function debugPrintNoTouchLocations()
|
||||
for room_id, list in pairs(bat_no_touch_locations_by_room) do
|
||||
for index, notouch_location in ipairs(list) do
|
||||
print("ROOM "..tostring(room_id).. "["..tostring(index).."]: "..tostring(notouch_location.short_location_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function processBlock(block)
|
||||
if block == nil then
|
||||
return
|
||||
end
|
||||
local block_identified = 0
|
||||
local msgBlock = block['messages']
|
||||
if msgBlock ~= nil then
|
||||
block_identified = 1
|
||||
for i, v in pairs(msgBlock) do
|
||||
if itemMessages[i] == nil then
|
||||
local msg = {TTL=450, message=v, color=0xFFFF0000}
|
||||
itemMessages[i] = msg
|
||||
end
|
||||
end
|
||||
end
|
||||
local itemsBlock = block["items"]
|
||||
if itemsBlock ~= nil then
|
||||
block_identified = 1
|
||||
ItemsReceived = itemsBlock
|
||||
end
|
||||
local apItemsBlock = block["foreign_items"]
|
||||
if apItemsBlock ~= nil then
|
||||
block_identified = 1
|
||||
print("got foreign items block")
|
||||
foreign_items = apItemsBlock
|
||||
createForeignItemsByRoom()
|
||||
end
|
||||
local autocollectItems = block["autocollect_items"]
|
||||
if autocollectItems ~= nil then
|
||||
block_identified = 1
|
||||
autocollect_items = {}
|
||||
for _, acitem in pairs(autocollectItems) do
|
||||
if autocollect_items[acitem.room_id] == nil then
|
||||
autocollect_items[acitem.room_id] = {}
|
||||
end
|
||||
table.insert(autocollect_items[acitem.room_id], acitem)
|
||||
end
|
||||
end
|
||||
local localLocalItemLocations = block["local_item_locations"]
|
||||
if localLocalItemLocations ~= nil then
|
||||
block_identified = 1
|
||||
localItemLocations = localLocalItemLocations
|
||||
print("got local item locations")
|
||||
end
|
||||
local checkedLocationsBlock = block["checked_locations"]
|
||||
if checkedLocationsBlock ~= nil then
|
||||
block_identified = 1
|
||||
for room_id, foreign_item_list in pairs(foreign_items_by_room) do
|
||||
for i, foreign_item in pairs(foreign_item_list) do
|
||||
short_id = foreign_item.short_location_id
|
||||
for j, checked_id in pairs(checkedLocationsBlock) do
|
||||
if checked_id == short_id then
|
||||
table.remove(foreign_item_list, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if foreign_items ~= nil then
|
||||
for i, foreign_item in pairs(foreign_items) do
|
||||
short_id = foreign_item.short_location_id
|
||||
for j, checked_id in pairs(checkedLocationsBlock) do
|
||||
if checked_id == short_id then
|
||||
foreign_items[i] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local dragon_speeds_block = block["dragon_speeds"]
|
||||
if dragon_speeds_block ~= nil then
|
||||
block_identified = 1
|
||||
yorgle_speed = dragon_speeds_block[slow_yorgle_id]
|
||||
grundle_speed = dragon_speeds_block[slow_grundle_id]
|
||||
rhindle_speed = dragon_speeds_block[slow_rhindle_id]
|
||||
end
|
||||
local diff_a_block = block["difficulty_a_locked"]
|
||||
if diff_a_block ~= nil then
|
||||
block_identified = 1
|
||||
diff_a_locked = diff_a_block
|
||||
end
|
||||
local diff_b_block = block["difficulty_b_locked"]
|
||||
if diff_b_block ~= nil then
|
||||
block_identified = 1
|
||||
diff_b_locked = diff_b_block
|
||||
end
|
||||
local freeincarnates_available_block = block["freeincarnates_available"]
|
||||
if freeincarnates_available_block ~= nil then
|
||||
block_identified = 1
|
||||
if freeincarnates_available ~= freeincarnates_available_block then
|
||||
freeincarnates_available = freeincarnates_available_block
|
||||
local msg = {TTL=450, message="freeincarnates: "..tostring(freeincarnates_available), color=0xFFFF0000}
|
||||
itemMessages[-2] = msg
|
||||
end
|
||||
end
|
||||
local bat_logic_block = block["bat_logic"]
|
||||
if bat_logic_block ~= nil then
|
||||
block_identified = 1
|
||||
bat_logic = bat_logic_block
|
||||
end
|
||||
local bat_no_touch_locations_block = block["bat_no_touch_locations"]
|
||||
if bat_no_touch_locations_block ~= nil then
|
||||
block_identified = 1
|
||||
for _, notouch_location in pairs(bat_no_touch_locations_block) do
|
||||
local room_id = tonumber(notouch_location.room_id)
|
||||
if bat_no_touch_locations_by_room[room_id] == nil then
|
||||
bat_no_touch_locations_by_room[room_id] = {}
|
||||
end
|
||||
table.insert(bat_no_touch_locations_by_room[room_id], notouch_location)
|
||||
|
||||
if notouch_location.local_item ~= nil and notouch_location.local_item ~= 255 then
|
||||
bat_no_touch_items[tonumber(notouch_location.local_item)] = true
|
||||
-- print("no touch: "..tostring(notouch_location.local_item))
|
||||
end
|
||||
end
|
||||
-- debugPrintNoTouchLocations()
|
||||
end
|
||||
deathlink_rec = deathlink_rec or block["deathlink"]
|
||||
if( block_identified == 0 ) then
|
||||
print("unidentified block")
|
||||
print(block)
|
||||
end
|
||||
end
|
||||
|
||||
local function clearScreen()
|
||||
if is23Or24Or25 then
|
||||
return
|
||||
elseif is26To28 then
|
||||
drawText(0, 0, "", "black")
|
||||
end
|
||||
end
|
||||
|
||||
local function getMaxMessageLength()
|
||||
if is23Or24Or25 then
|
||||
return client.screenwidth()/11
|
||||
elseif is26To28 then
|
||||
return client.screenwidth()/12
|
||||
end
|
||||
end
|
||||
|
||||
function drawText(x, y, message, color)
|
||||
if is23Or24Or25 then
|
||||
gui.addmessage(message)
|
||||
elseif is26To28 then
|
||||
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client")
|
||||
end
|
||||
end
|
||||
|
||||
local function drawMessages()
|
||||
if table.empty(itemMessages) then
|
||||
clearScreen()
|
||||
return
|
||||
end
|
||||
local y = 10
|
||||
found = false
|
||||
maxMessageLength = getMaxMessageLength()
|
||||
for k, v in pairs(itemMessages) do
|
||||
if v["TTL"] > 0 then
|
||||
message = v["message"]
|
||||
while true do
|
||||
drawText(5, y, message:sub(1, maxMessageLength), v["color"])
|
||||
y = y + 16
|
||||
|
||||
message = message:sub(maxMessageLength + 1, message:len())
|
||||
if message:len() == 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
newTTL = 0
|
||||
if is26To28 then
|
||||
newTTL = itemMessages[k]["TTL"] - 1
|
||||
end
|
||||
itemMessages[k]["TTL"] = newTTL
|
||||
found = true
|
||||
end
|
||||
end
|
||||
if found == false then
|
||||
clearScreen()
|
||||
end
|
||||
end
|
||||
|
||||
function difference(a, b)
|
||||
local aa = {}
|
||||
for k,v in pairs(a) do aa[v]=true end
|
||||
for k,v in pairs(b) do aa[v]=nil end
|
||||
local ret = {}
|
||||
local n = 0
|
||||
for k,v in pairs(a) do
|
||||
if aa[v] then n=n+1 ret[n]=v end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function getAllRam()
|
||||
uRangeRAM(0,128);
|
||||
return data
|
||||
end
|
||||
|
||||
local function arrayEqual(a1, a2)
|
||||
if #a1 ~= #a2 then
|
||||
return false
|
||||
end
|
||||
|
||||
for i, v in ipairs(a1) do
|
||||
if v ~= a2[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function alive_mode()
|
||||
return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00)
|
||||
end
|
||||
|
||||
local function generateLocationsChecked()
|
||||
list_of_locations = {}
|
||||
for s, f in pairs(pending_foreign_items_collected) do
|
||||
table.insert(list_of_locations, f.short_location_id + 118000000)
|
||||
end
|
||||
for s, f in pairs(pending_local_items_collected) do
|
||||
table.insert(list_of_locations, f + 118000000)
|
||||
end
|
||||
return list_of_locations
|
||||
end
|
||||
|
||||
function receive()
|
||||
l, e = atariSocket:receive()
|
||||
if e == 'closed' then
|
||||
if curstate == STATE_OK then
|
||||
print("Connection closed")
|
||||
end
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
elseif e == 'timeout' then
|
||||
return
|
||||
elseif e ~= nil then
|
||||
print(e)
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
if l ~= nil then
|
||||
processBlock(json.decode(l))
|
||||
end
|
||||
-- Determine Message to send back
|
||||
|
||||
newSha256 = memory.hash_region(0xF000, 0x1000, "System Bus")
|
||||
if (sha256hash ~= nil and sha256hash ~= newSha256) then
|
||||
print("ROM changed, quitting")
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
sha256hash = newSha256
|
||||
local retTable = {}
|
||||
retTable["scriptVersion"] = SCRIPT_VERSION
|
||||
retTable["romhash"] = sha256hash
|
||||
if (alive_mode()) then
|
||||
retTable["locations"] = generateLocationsChecked()
|
||||
end
|
||||
if (u8(WinAddr) ~= 0x00) then
|
||||
retTable["victory"] = 1
|
||||
end
|
||||
if( deathlink_sent or deathlink_send == 0 ) then
|
||||
retTable["deathLink"] = 0
|
||||
else
|
||||
print("Sending deathlink "..tostring(deathlink_send))
|
||||
retTable["deathLink"] = deathlink_send
|
||||
deathlink_sent = true
|
||||
end
|
||||
deathlink_send = 0
|
||||
|
||||
if send_freeincarnate_used == true then
|
||||
print("Sending freeincarnate used")
|
||||
retTable["freeincarnate"] = true
|
||||
send_freeincarnate_used = false
|
||||
end
|
||||
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = atariSocket:send(msg)
|
||||
if ret == nil then
|
||||
print(error)
|
||||
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
|
||||
curstate = STATE_TENTATIVELY_CONNECTED
|
||||
elseif curstate == STATE_TENTATIVELY_CONNECTED then
|
||||
print("Connected!")
|
||||
curstate = STATE_OK
|
||||
end
|
||||
end
|
||||
|
||||
function AutocollectFromRoom()
|
||||
if autocollect_items ~= nil and autocollect_items[prev_player_room] ~= nil then
|
||||
for _, item in pairs(autocollect_items[prev_player_room]) do
|
||||
pending_foreign_items_collected[item.short_location_id] = item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SetYorgleSpeed()
|
||||
if yorgle_speed ~= nil then
|
||||
emu.setregister("A", yorgle_speed);
|
||||
end
|
||||
end
|
||||
|
||||
function SetGrundleSpeed()
|
||||
if grundle_speed ~= nil then
|
||||
emu.setregister("A", grundle_speed);
|
||||
end
|
||||
end
|
||||
|
||||
function SetRhindleSpeed()
|
||||
if rhindle_speed ~= nil then
|
||||
emu.setregister("A", rhindle_speed);
|
||||
end
|
||||
end
|
||||
|
||||
function SetDifficultySwitchB()
|
||||
if diff_b_locked then
|
||||
local a = emu.getregister("A")
|
||||
if a < 128 then
|
||||
emu.setregister("A", a + 128)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SetDifficultySwitchA()
|
||||
if diff_a_locked then
|
||||
local a = emu.getregister("A")
|
||||
if (a > 128 and a < 128 + 64) or (a < 64) then
|
||||
emu.setregister("A", a + 64)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TryFreeincarnate()
|
||||
if freeincarnates_available > 0 then
|
||||
freeincarnates_available = freeincarnates_available - 1
|
||||
for index, state_addr in pairs(DragonState) do
|
||||
if last_dragon_state[index] == 1 then
|
||||
send_freeincarnate_used = true
|
||||
memory.write_u8(state_addr, 1, "System Bus")
|
||||
local msg = {TTL=450, message="used freeincarnate", color=0xFF00FF00}
|
||||
itemMessages[-1] = msg
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
function GetLinkedObject()
|
||||
if emu.getregister("X") == batRoomAddr then
|
||||
bat_interest_item = emu.getregister("A")
|
||||
-- if the bat can't touch that item, we'll switch it to the number item, which should never be
|
||||
-- in the same room as the bat.
|
||||
if bat_no_touch_items[bat_interest_item] ~= nil then
|
||||
emu.setregister("A", 0xDD )
|
||||
emu.setregister("Y", 0xDD )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CheckCollectAPItem(carry_item, target_item_value, target_item_ram, rendering_foreign_item)
|
||||
if( carry_item == target_item_value and rendering_foreign_item ~= nil ) then
|
||||
memory.write_u8(carryAddress, nullObjectId, "System Bus")
|
||||
memory.write_u8(target_item_ram, 0xFF, "System Bus")
|
||||
pending_foreign_items_collected[rendering_foreign_item.short_location_id] = rendering_foreign_item
|
||||
for index, fi in pairs(foreign_items_by_room[rendering_foreign_item.room_id]) do
|
||||
if( fi.short_location_id == rendering_foreign_item.short_location_id ) then
|
||||
table.remove(foreign_items_by_room[rendering_foreign_item.room_id], index)
|
||||
break
|
||||
end
|
||||
end
|
||||
for index, fi in pairs(foreign_items) do
|
||||
if( fi.short_location_id == rendering_foreign_item.short_location_id ) then
|
||||
foreign_items[index] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
prev_ap_room_index = 0
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function BatCanTouchForeign(foreign_item, bat_room)
|
||||
if bat_no_touch_locations_by_room[bat_room] == nil or bat_no_touch_locations_by_room[bat_room][1] == nil then
|
||||
return true
|
||||
end
|
||||
|
||||
for index, location in ipairs(bat_no_touch_locations_by_room[bat_room]) do
|
||||
if location.short_location_id == foreign_item.short_location_id then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function main()
|
||||
memory.usememorydomain("System Bus")
|
||||
if (is23Or24Or25 or is26To28) == false then
|
||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
||||
return
|
||||
end
|
||||
local playerSlot = memory.read_u8(PlayerSlotAddress)
|
||||
local port = 17242 + playerSlot
|
||||
print("Using port"..tostring(port))
|
||||
server, error = socket.bind('localhost', port)
|
||||
if( error ~= nil ) then
|
||||
print(error)
|
||||
end
|
||||
event.onmemoryexecute(SetYorgleSpeed, yorgle_speed_address);
|
||||
event.onmemoryexecute(SetGrundleSpeed, grundle_speed_address);
|
||||
event.onmemoryexecute(SetRhindleSpeed, rhindle_speed_address);
|
||||
event.onmemoryexecute(SetDifficultySwitchA, read_switch_a)
|
||||
event.onmemoryexecute(SetDifficultySwitchB, read_switch_b)
|
||||
event.onmemoryexecute(GetLinkedObject, batItemCheckAddr)
|
||||
-- TODO: Add an onmemoryexecute event to intercept the bat reading item rooms, and don't 'see' an item in the
|
||||
-- room if it is in bat_no_touch_locations_by_room. Although realistically, I may have to handle this in the rom
|
||||
-- for it to be totally reliable, because it won't work before the script connects (I might have to reset them?)
|
||||
-- TODO: Also remove those items from the bat_no_touch_locations_by_room if they have been collected
|
||||
while true do
|
||||
frame = frame + 1
|
||||
drawMessages()
|
||||
if not (curstate == prevstate) then
|
||||
print("Current state: "..curstate)
|
||||
prevstate = curstate
|
||||
end
|
||||
|
||||
local current_player_room = u8(PlayerRoomAddr)
|
||||
local bat_room = u8(batRoomAddr)
|
||||
local bat_carrying_item = u8(batCarryAddress)
|
||||
local bat_carrying_ap_item = (BatAPItemRam == bat_carrying_item)
|
||||
|
||||
if current_player_room == 0x1E then
|
||||
if u8(PlayerRoomAddr + 1) > 0x4B then
|
||||
memory.write_u8(PlayerRoomAddr + 1, 0x4B)
|
||||
end
|
||||
end
|
||||
|
||||
if current_player_room == 0x00 then
|
||||
if not was_in_number_room then
|
||||
print("reset "..tostring(bat_carrying_ap_item).." "..tostring(bat_carrying_item))
|
||||
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||
memory.write_u8(batCarryAddress+ 1, 0)
|
||||
createForeignItemsByRoom()
|
||||
memory.write_u8(BatAPItemRam, 0xff)
|
||||
memory.write_u8(APItemRam, 0xff)
|
||||
prev_ap_room_index = 0
|
||||
prev_player_room = 0
|
||||
rendering_foreign_item = nil
|
||||
was_in_number_room = true
|
||||
end
|
||||
else
|
||||
was_in_number_room = false
|
||||
end
|
||||
|
||||
if bat_room ~= prev_bat_room then
|
||||
if bat_carrying_ap_item then
|
||||
if foreign_items_by_room[prev_bat_room] ~= nil then
|
||||
for r,f in pairs(foreign_items_by_room[prev_bat_room]) do
|
||||
if f.short_location_id == current_bat_ap_item.short_location_id then
|
||||
-- print("removing item from "..tostring(r).." in "..tostring(prev_bat_room))
|
||||
table.remove(foreign_items_by_room[prev_bat_room], r)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if foreign_items_by_room[bat_room] == nil then
|
||||
foreign_items_by_room[bat_room] = {}
|
||||
end
|
||||
-- print("adding item to "..tostring(bat_room))
|
||||
table.insert(foreign_items_by_room[bat_room], current_bat_ap_item)
|
||||
else
|
||||
-- set AP item room and position for new room, or to invalid room
|
||||
if foreign_items_by_room[bat_room] ~= nil and foreign_items_by_room[bat_room][1] ~= nil
|
||||
and BatCanTouchForeign(foreign_items_by_room[bat_room][1], bat_room) then
|
||||
if current_bat_ap_item ~= foreign_items_by_room[bat_room][1] then
|
||||
current_bat_ap_item = foreign_items_by_room[bat_room][1]
|
||||
-- print("Changing bat item to "..tostring(current_bat_ap_item.short_location_id))
|
||||
end
|
||||
memory.write_u8(BatAPItemRam, bat_room)
|
||||
memory.write_u8(BatAPItemRam + 1, current_bat_ap_item.room_x)
|
||||
memory.write_u8(BatAPItemRam + 2, current_bat_ap_item.room_y)
|
||||
else
|
||||
memory.write_u8(BatAPItemRam, 0xff)
|
||||
if current_bat_ap_item ~= nil then
|
||||
-- print("clearing bat item")
|
||||
end
|
||||
current_bat_ap_item = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
prev_bat_room = bat_room
|
||||
|
||||
-- update foreign_items_by_room position and room id for bat item if bat carrying an item
|
||||
if bat_carrying_ap_item then
|
||||
-- this is setting the item using the bat's position, which is somewhat wrong, but I think
|
||||
-- there will be more problems with the room not matching sometimes if I use the actual item position
|
||||
current_bat_ap_item.room_id = bat_room
|
||||
current_bat_ap_item.room_x = u8(batRoomAddr + 1)
|
||||
current_bat_ap_item.room_y = u8(batRoomAddr + 2)
|
||||
end
|
||||
|
||||
if (alive_mode()) then
|
||||
if (current_player_room ~= prev_player_room) then
|
||||
memory.write_u8(APItemRam, 0xFF, "System Bus")
|
||||
prev_ap_room_index = 0
|
||||
prev_player_room = current_player_room
|
||||
AutocollectFromRoom()
|
||||
end
|
||||
local carry_item = memory.read_u8(carryAddress, "System Bus")
|
||||
bat_no_touch_items[carry_item] = nil
|
||||
if (next_inventory_item ~= nil) then
|
||||
if ( carry_item == nullObjectId and last_carry_item == nullObjectId ) then
|
||||
frames_with_no_item = frames_with_no_item + 1
|
||||
if (frames_with_no_item > 10) then
|
||||
frames_with_no_item = 10
|
||||
local input_value = memory.read_u8(input_button_address, "System Bus")
|
||||
if( input_value >= 64 and input_value < 128 ) then -- high bit clear, second highest bit set
|
||||
memory.write_u8(carryAddress, next_inventory_item)
|
||||
local item_ram_location = memory.read_u8(ItemTableStart + next_inventory_item)
|
||||
if( memory.read_u8(batCarryAddress) ~= 0x78 and
|
||||
memory.read_u8(batCarryAddress) == item_ram_location) then
|
||||
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||
memory.write_u8(batCarryAddress+ 1, 0)
|
||||
memory.write_u8(item_ram_location, current_player_room)
|
||||
memory.write_u8(item_ram_location + 1, memory.read_u8(PlayerRoomAddr + 1))
|
||||
memory.write_u8(item_ram_location + 2, memory.read_u8(PlayerRoomAddr + 2))
|
||||
end
|
||||
ItemIndex = ItemIndex + 1
|
||||
next_inventory_item = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
frames_with_no_item = 0
|
||||
end
|
||||
end
|
||||
if( carry_item ~= last_carry_item ) then
|
||||
if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then
|
||||
pending_local_items_collected[localItemLocations[tostring(carry_item)]] =
|
||||
localItemLocations[tostring(carry_item)]
|
||||
table.remove(localItemLocations, tostring(carry_item))
|
||||
skip_inventory_items[carry_item] = carry_item
|
||||
end
|
||||
end
|
||||
last_carry_item = carry_item
|
||||
|
||||
CheckCollectAPItem(carry_item, APItemValue, APItemRam, rendering_foreign_item)
|
||||
if CheckCollectAPItem(carry_item, BatAPItemValue, BatAPItemRam, current_bat_ap_item) and bat_carrying_ap_item then
|
||||
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||
memory.write_u8(batCarryAddress+ 1, 0)
|
||||
end
|
||||
|
||||
|
||||
rendering_foreign_item = nil
|
||||
if( foreign_items_by_room[current_player_room] ~= nil ) then
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil ) and memory.read_u8(APItemRam) ~= 0xff then
|
||||
foreign_items_by_room[current_player_room][prev_ap_room_index].room_x = memory.read_u8(APItemRam + 1)
|
||||
foreign_items_by_room[current_player_room][prev_ap_room_index].room_y = memory.read_u8(APItemRam + 2)
|
||||
end
|
||||
prev_ap_room_index = prev_ap_room_index + 1
|
||||
local invalid_index = -1
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then
|
||||
prev_ap_room_index = 1
|
||||
end
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and current_bat_ap_item ~= nil and
|
||||
foreign_items_by_room[current_player_room][prev_ap_room_index].short_location_id == current_bat_ap_item.short_location_id) then
|
||||
invalid_index = prev_ap_room_index
|
||||
prev_ap_room_index = prev_ap_room_index + 1
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then
|
||||
prev_ap_room_index = 1
|
||||
end
|
||||
end
|
||||
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and prev_ap_room_index ~= invalid_index ) then
|
||||
memory.write_u8(APItemRam, current_player_room)
|
||||
rendering_foreign_item = foreign_items_by_room[current_player_room][prev_ap_room_index]
|
||||
memory.write_u8(APItemRam + 1, rendering_foreign_item.room_x)
|
||||
memory.write_u8(APItemRam + 2, rendering_foreign_item.room_y)
|
||||
else
|
||||
memory.write_u8(APItemRam, 0xFF, "System Bus")
|
||||
end
|
||||
end
|
||||
if is_dead == 0 then
|
||||
dragons_revived = false
|
||||
player_dead = false
|
||||
new_dragon_state = {0,0,0}
|
||||
for index, dragon_state_addr in pairs(DragonState) do
|
||||
new_dragon_state[index] = memory.read_u8(dragon_state_addr, "System Bus" )
|
||||
if last_dragon_state[index] == 1 and new_dragon_state[index] ~= 1 then
|
||||
dragons_revived = true
|
||||
elseif last_dragon_state[index] ~= 1 and new_dragon_state[index] == 1 then
|
||||
dragon_real_index = index - 1
|
||||
print("Killed dragon: "..tostring(dragon_real_index))
|
||||
local dragon_item = {}
|
||||
dragon_item["short_location_id"] = 0xD0 + dragon_real_index
|
||||
pending_foreign_items_collected[dragon_item.short_location_id] = dragon_item
|
||||
end
|
||||
if new_dragon_state[index] == 2 then
|
||||
player_dead = true
|
||||
end
|
||||
end
|
||||
if dragons_revived and player_dead == false then
|
||||
TryFreeincarnate()
|
||||
end
|
||||
last_dragon_state = new_dragon_state
|
||||
end
|
||||
elseif (u8(PlayerRoomAddr) == 0x00) then -- not alive mode, in number room
|
||||
ItemIndex = 0 -- reset our inventory
|
||||
next_inventory_item = nil
|
||||
skip_inventory_items = {}
|
||||
end
|
||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||
if (frame % 5 == 0) then
|
||||
receive()
|
||||
if alive_mode() then
|
||||
local was_dead = is_dead
|
||||
is_dead = 0
|
||||
for index, dragonStateAddr in pairs(DragonState) do
|
||||
local dragonstateval = memory.read_u8(dragonStateAddr, "System Bus")
|
||||
if ( dragonstateval == 2) then
|
||||
is_dead = index
|
||||
end
|
||||
end
|
||||
if was_dead ~= 0 and is_dead == 0 then
|
||||
TryFreeincarnate()
|
||||
end
|
||||
if deathlink_rec == true and is_dead == 0 then
|
||||
print("setting dead from deathlink")
|
||||
deathlink_rec = false
|
||||
deathlink_sent = true
|
||||
is_dead = 1
|
||||
memory.write_u8(carryAddress, nullObjectId, "System Bus")
|
||||
memory.write_u8(DragonState[1], 2, "System Bus")
|
||||
end
|
||||
if (is_dead > 0 and deathlink_send == 0 and not deathlink_sent) then
|
||||
deathlink_send = is_dead
|
||||
print("setting deathlink_send to "..tostring(is_dead))
|
||||
elseif (is_dead == 0) then
|
||||
deathlink_send = 0
|
||||
deathlink_sent = false
|
||||
end
|
||||
if ItemsReceived ~= nil and ItemsReceived[ItemIndex + 1] ~= nil then
|
||||
while ItemsReceived[ItemIndex + 1] ~= nil and skip_inventory_items[ItemsReceived[ItemIndex + 1]] ~= nil do
|
||||
print("skip")
|
||||
ItemIndex = ItemIndex + 1
|
||||
end
|
||||
local static_id = ItemsReceived[ItemIndex + 1]
|
||||
if static_id ~= nil then
|
||||
inventory[static_id] = 1
|
||||
if next_inventory_item == nil then
|
||||
next_inventory_item = static_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
if (frame % 60 == 0) then
|
||||
|
||||
print("Waiting for client.")
|
||||
|
||||
emu.frameadvance()
|
||||
server:settimeout(2)
|
||||
print("Attempting to connect")
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
print("Initial connection made")
|
||||
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||
atariSocket = client
|
||||
atariSocket:settimeout(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
emu.frameadvance()
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
||||
380
data/lua/ADVENTURE/json.lua
Normal file
380
data/lua/ADVENTURE/json.lua
Normal file
@@ -0,0 +1,380 @@
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2015 rxi
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
local json = { _version = "0.1.0" }
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\\\",
|
||||
[ "\"" ] = "\\\"",
|
||||
[ "\b" ] = "\\b",
|
||||
[ "\f" ] = "\\f",
|
||||
[ "\n" ] = "\\n",
|
||||
[ "\r" ] = "\\r",
|
||||
[ "\t" ] = "\\t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if val[1] ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
--local line_count = 1
|
||||
--local col_count = 1
|
||||
--for i = 1, idx - 1 do
|
||||
-- col_count = col_count + 1
|
||||
-- if str:sub(i, i) == "\n" then
|
||||
-- line_count = line_count + 1
|
||||
-- col_count = 1
|
||||
-- end
|
||||
-- end
|
||||
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local has_unicode_escape = false
|
||||
local has_surrogate_escape = false
|
||||
local has_escape = false
|
||||
local last
|
||||
for j = i + 1, #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
end
|
||||
|
||||
if last == 92 then -- "\\" (escape char)
|
||||
if x == 117 then -- "u" (unicode escape sequence)
|
||||
local hex = str:sub(j + 1, j + 5)
|
||||
if not hex:find("%x%x%x%x") then
|
||||
decode_error(str, j, "invalid unicode escape in string")
|
||||
end
|
||||
if hex:find("^[dD][89aAbB]") then
|
||||
has_surrogate_escape = true
|
||||
else
|
||||
has_unicode_escape = true
|
||||
end
|
||||
else
|
||||
local c = string.char(x)
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
has_escape = true
|
||||
end
|
||||
last = nil
|
||||
|
||||
elseif x == 34 then -- '"' (end of string)
|
||||
local s = str:sub(i + 1, j - 1)
|
||||
if has_surrogate_escape then
|
||||
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_unicode_escape then
|
||||
s = s:gsub("\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_escape then
|
||||
s = s:gsub("\\.", escape_char_map_inv)
|
||||
end
|
||||
return s, j + 1
|
||||
|
||||
else
|
||||
last = x
|
||||
end
|
||||
end
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
return ( parse(str, next_char(str, 1, space_chars, true)) )
|
||||
end
|
||||
|
||||
|
||||
return json
|
||||
132
data/lua/ADVENTURE/socket.lua
Normal file
132
data/lua/ADVENTURE/socket.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
-----------------------------------------------------------------------------
|
||||
-- LuaSocket helper module
|
||||
-- Author: Diego Nehab
|
||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Declare module and import dependencies
|
||||
-----------------------------------------------------------------------------
|
||||
local base = _G
|
||||
local string = require("string")
|
||||
local math = require("math")
|
||||
local socket = require("socket.core")
|
||||
module("socket")
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
-----------------------------------------------------------------------------
|
||||
function connect(address, port, laddress, lport)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
if laddress then
|
||||
local res, err = sock:bind(laddress, lport, -1)
|
||||
if not res then return nil, err end
|
||||
end
|
||||
local res, err = sock:connect(address, port)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
function bind(host, port, backlog)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
sock:setoption("reuseaddr", true)
|
||||
local res, err = sock:bind(host, port)
|
||||
if not res then return nil, err end
|
||||
res, err = sock:listen(backlog)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
try = newtry()
|
||||
|
||||
function choose(table)
|
||||
return function(name, opt1, opt2)
|
||||
if base.type(name) ~= "string" then
|
||||
name, opt1, opt2 = "default", name, opt1
|
||||
end
|
||||
local f = table[name or "nil"]
|
||||
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||
else return f(opt1, opt2) end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Socket sources and sinks, conforming to LTN12
|
||||
-----------------------------------------------------------------------------
|
||||
-- create namespaces inside LuaSocket namespace
|
||||
sourcet = {}
|
||||
sinkt = {}
|
||||
|
||||
BLOCKSIZE = 2048
|
||||
|
||||
sinkt["close-when-done"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if not chunk then
|
||||
sock:close()
|
||||
return 1
|
||||
else return sock:send(chunk) end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["keep-open"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if chunk then return sock:send(chunk)
|
||||
else return 1 end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["default"] = sinkt["keep-open"]
|
||||
|
||||
sink = choose(sinkt)
|
||||
|
||||
sourcet["by-length"] = function(sock, length)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if length <= 0 then return nil end
|
||||
local size = math.min(socket.BLOCKSIZE, length)
|
||||
local chunk, err = sock:receive(size)
|
||||
if err then return nil, err end
|
||||
length = length - string.len(chunk)
|
||||
return chunk
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sourcet["until-closed"] = function(sock)
|
||||
local done
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if done then return nil end
|
||||
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||
if not err then return chunk
|
||||
elseif err == "closed" then
|
||||
sock:close()
|
||||
done = 1
|
||||
return partial
|
||||
else return nil, err end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
sourcet["default"] = sourcet["until-closed"]
|
||||
|
||||
source = choose(sourcet)
|
||||
@@ -621,7 +621,7 @@ function receive()
|
||||
|
||||
-- Determine Message to send back
|
||||
memDomain.rom()
|
||||
local playerName = uRange(0x1F, 0x10)
|
||||
local playerName = uRange(0x1F, 0x11)
|
||||
playerName[0] = nil
|
||||
local retTable = {}
|
||||
retTable["playerName"] = playerName
|
||||
|
||||
137
data/lua/connector_ladx_bizhawk.lua
Normal file
137
data/lua/connector_ladx_bizhawk.lua
Normal file
@@ -0,0 +1,137 @@
|
||||
-- SPDX-FileCopyrightText: 2023 Wilhelm Schürmann <wimschuermann@googlemail.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- This script attempts to implement the basic functionality needed in order for
|
||||
-- the LADXR Archipelago client to be able to talk to BizHawk instead of RetroArch
|
||||
-- by reproducing the RetroArch API with BizHawk's Lua interface.
|
||||
--
|
||||
-- RetroArch UDP API: https://github.com/libretro/RetroArch/blob/master/command.c
|
||||
--
|
||||
-- Only
|
||||
-- VERSION
|
||||
-- GET_STATUS
|
||||
-- READ_CORE_MEMORY
|
||||
-- WRITE_CORE_MEMORY
|
||||
-- commands are supported right now.
|
||||
--
|
||||
-- USAGE:
|
||||
-- Load this script in BizHawk ("Tools" -> "Lua Console" -> "Script" -> "Open Script")
|
||||
--
|
||||
-- All inconsistencies (like missing newlines for some commands) of the RetroArch
|
||||
-- UDP API (network_cmd_enable) are reproduced as-is in order for clients written to work with
|
||||
-- RetroArch's current API to "just work"(tm).
|
||||
--
|
||||
-- This script has only been tested on GB(C). If you have made sure it works for N64 or other
|
||||
-- cores supported by BizHawk, please let me know. Note that GET_STATUS, at the very least, will
|
||||
-- have to be adjusted.
|
||||
--
|
||||
--
|
||||
-- NOTE:
|
||||
-- BizHawk's Lua API is very trigger-happy on throwing exceptions.
|
||||
-- Emulation will continue fine, but the RetroArch API layer will stop working. This
|
||||
-- is indicated only by an exception visible in the Lua console, which most players
|
||||
-- will probably not have in the foreground.
|
||||
--
|
||||
-- pcall(), the usual way to catch exceptions in Lua, doesn't appear to be supported at all,
|
||||
-- meaning that error/exception handling is not easily possible.
|
||||
--
|
||||
-- This means that a lot more error checking would need to happen before e.g. reading/writing
|
||||
-- memory. Since the end goal, according to AP's Discord, seems to be SNI integration of GB(C),
|
||||
-- no further fault-proofing has been done on this.
|
||||
--
|
||||
|
||||
|
||||
local socket = require("socket")
|
||||
local udp = socket.udp()
|
||||
|
||||
udp:setsockname('127.0.0.1', 55355)
|
||||
udp:settimeout(0)
|
||||
|
||||
|
||||
while true do
|
||||
-- Attempt to lessen the CPU load by only polling the UDP socket every x frames.
|
||||
-- x = 10 is entirely arbitrary, very little thought went into it.
|
||||
-- We could try to make use of client.get_approx_framerate() here, but the values returned
|
||||
-- seemed more or less arbitrary as well.
|
||||
--
|
||||
-- NOTE: Never mind the above, the LADXR Archipelago client appears to run into problems with
|
||||
-- interwoven GET_STATUS calls, leading to stopped communication.
|
||||
-- For GB(C), polling the socket on every frame is OK-ish, so we just do that.
|
||||
--
|
||||
--while emu.framecount() % 10 ~= 0 do
|
||||
-- emu.frameadvance()
|
||||
--end
|
||||
|
||||
local data, msg_or_ip, port_or_nil = udp:receivefrom()
|
||||
if data then
|
||||
-- "data" format is "COMMAND [PARAMETERS] [...]"
|
||||
local command = string.match(data, "%S+")
|
||||
if command == "VERSION" then
|
||||
-- 1.14 is the latest RetroArch release at the time of writing this, no other reason
|
||||
-- for choosing this here.
|
||||
udp:sendto("1.14.0\n", msg_or_ip, port_or_nil)
|
||||
elseif command == "GET_STATUS" then
|
||||
local status = "PLAYING"
|
||||
if client.ispaused() then
|
||||
status = "PAUSED"
|
||||
end
|
||||
|
||||
if emu.getsystemid() == "GBC" then
|
||||
-- Actual reply from RetroArch's API:
|
||||
-- "GET_STATUS PLAYING game_boy,AP_62468482466172374046_P1_Lonk,crc32=3ecb7b6f"
|
||||
-- CRC32 isn't readily available through the Lua API. We could calculate
|
||||
-- it ourselves, but since LADXR doesn't make use of this field it is
|
||||
-- simply replaced by the hash that BizHawk _does_ make available.
|
||||
|
||||
udp:sendto(
|
||||
"GET_STATUS " .. status .. " game_boy," ..
|
||||
string.gsub(gameinfo.getromname(), "[%s,]", "_") ..
|
||||
",romhash=" ..
|
||||
gameinfo.getromhash() .. "\n",
|
||||
msg_or_ip, port_or_nil
|
||||
)
|
||||
else -- No ROM loaded
|
||||
-- NOTE: No newline is intentional here for 1:1 RetroArch compatibility
|
||||
udp:sendto("GET_STATUS CONTENTLESS", msg_or_ip, port_or_nil)
|
||||
end
|
||||
elseif command == "READ_CORE_MEMORY" then
|
||||
local _, address, length = string.match(data, "(%S+) (%S+) (%S+)")
|
||||
address = tonumber(address, 16)
|
||||
length = tonumber(length)
|
||||
|
||||
-- NOTE: mainmemory.read_bytes_as_array() would seem to be the obvious choice
|
||||
-- here instead, but it isn't. At least for Sameboy and Gambatte, the "main"
|
||||
-- memory differs (ROM vs WRAM).
|
||||
-- Using memory.read_bytes_as_array() and explicitly using the System Bus
|
||||
-- as the active memory domain solves this incompatibility, allowing us
|
||||
-- to hopefully use whatever GB(C) emulator we want.
|
||||
local mem = memory.read_bytes_as_array(address, length, "System Bus")
|
||||
local hex_string = ""
|
||||
for _, v in ipairs(mem) do
|
||||
hex_string = hex_string .. string.format("%02X ", v)
|
||||
end
|
||||
hex_string = hex_string:sub(1, -2) -- Hang head in shame, remove last " "
|
||||
local reply = string.format("%s %02x %s\n", command, address, hex_string)
|
||||
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||
elseif command == "WRITE_CORE_MEMORY" then
|
||||
local _, address = string.match(data, "(%S+) (%S+)")
|
||||
address = tonumber(address, 16)
|
||||
|
||||
local to_write = {}
|
||||
local i = 1
|
||||
for byte_str in string.gmatch(data, "%S+") do
|
||||
if i > 2 then
|
||||
table.insert(to_write, tonumber(byte_str, 16))
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
memory.write_bytes_as_array(address, to_write, "System Bus")
|
||||
local reply = string.format("%s %02x %d\n", command, address, i - 3)
|
||||
udp:sendto(reply, msg_or_ip, port_or_nil)
|
||||
end
|
||||
end
|
||||
|
||||
emu.frameadvance()
|
||||
end
|
||||
BIN
data/lua/core.dll
Normal file
BIN
data/lua/core.dll
Normal file
Binary file not shown.
132
data/lua/socket.lua
Normal file
132
data/lua/socket.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
-----------------------------------------------------------------------------
|
||||
-- LuaSocket helper module
|
||||
-- Author: Diego Nehab
|
||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Declare module and import dependencies
|
||||
-----------------------------------------------------------------------------
|
||||
local base = _G
|
||||
local string = require("string")
|
||||
local math = require("math")
|
||||
local socket = require("socket.core")
|
||||
module("socket")
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
-----------------------------------------------------------------------------
|
||||
function connect(address, port, laddress, lport)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
if laddress then
|
||||
local res, err = sock:bind(laddress, lport, -1)
|
||||
if not res then return nil, err end
|
||||
end
|
||||
local res, err = sock:connect(address, port)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
function bind(host, port, backlog)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
sock:setoption("reuseaddr", true)
|
||||
local res, err = sock:bind(host, port)
|
||||
if not res then return nil, err end
|
||||
res, err = sock:listen(backlog)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
try = newtry()
|
||||
|
||||
function choose(table)
|
||||
return function(name, opt1, opt2)
|
||||
if base.type(name) ~= "string" then
|
||||
name, opt1, opt2 = "default", name, opt1
|
||||
end
|
||||
local f = table[name or "nil"]
|
||||
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||
else return f(opt1, opt2) end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Socket sources and sinks, conforming to LTN12
|
||||
-----------------------------------------------------------------------------
|
||||
-- create namespaces inside LuaSocket namespace
|
||||
sourcet = {}
|
||||
sinkt = {}
|
||||
|
||||
BLOCKSIZE = 2048
|
||||
|
||||
sinkt["close-when-done"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if not chunk then
|
||||
sock:close()
|
||||
return 1
|
||||
else return sock:send(chunk) end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["keep-open"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if chunk then return sock:send(chunk)
|
||||
else return 1 end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["default"] = sinkt["keep-open"]
|
||||
|
||||
sink = choose(sinkt)
|
||||
|
||||
sourcet["by-length"] = function(sock, length)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if length <= 0 then return nil end
|
||||
local size = math.min(socket.BLOCKSIZE, length)
|
||||
local chunk, err = sock:receive(size)
|
||||
if err then return nil, err end
|
||||
length = length - string.len(chunk)
|
||||
return chunk
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sourcet["until-closed"] = function(sock)
|
||||
local done
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if done then return nil end
|
||||
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||
if not err then return chunk
|
||||
elseif err == "closed" then
|
||||
sock:close()
|
||||
done = 1
|
||||
return partial
|
||||
else return nil, err end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
sourcet["default"] = sourcet["until-closed"]
|
||||
|
||||
source = choose(sourcet)
|
||||
BIN
data/sprites/ladx/Bowwow.bdiff
Normal file
BIN
data/sprites/ladx/Bowwow.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Bunny.bdiff
Normal file
BIN
data/sprites/ladx/Bunny.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Luigi.bdiff
Normal file
BIN
data/sprites/ladx/Luigi.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Mario.bdiff
Normal file
BIN
data/sprites/ladx/Mario.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Matty_LA.bdiff
Normal file
BIN
data/sprites/ladx/Matty_LA.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Richard.bdiff
Normal file
BIN
data/sprites/ladx/Richard.bdiff
Normal file
Binary file not shown.
BIN
data/sprites/ladx/Tarin.bdiff
Normal file
BIN
data/sprites/ladx/Tarin.bdiff
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 535 KiB |
@@ -75,6 +75,18 @@ flowchart LR
|
||||
end
|
||||
SNI <-- Various, depending on SNES device --> DK3
|
||||
|
||||
%% Super Mario World
|
||||
subgraph Super Mario World
|
||||
SMW[SNES]
|
||||
end
|
||||
SNI <-- Various, depending on SNES device --> SMW
|
||||
|
||||
%% Lufia II Ancient Cave
|
||||
subgraph Lufia II Ancient Cave
|
||||
L2AC[SNES]
|
||||
end
|
||||
SNI <-- Various, depending on SNES device --> L2AC
|
||||
|
||||
%% Native Clients or Games
|
||||
%% Games or clients which compile to native or which the client is integrated in the game.
|
||||
subgraph "Native"
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 91 KiB |
@@ -64,18 +64,19 @@ These packets are are sent from the multiworld server to the client. They are no
|
||||
### RoomInfo
|
||||
Sent to clients when they connect to an Archipelago server.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||
| 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". |
|
||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
||||
| 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. |
|
||||
| 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). |
|
||||
| seed_name | str | uniquely identifying name of this generation |
|
||||
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
||||
| Name | Type | Notes |
|
||||
|-----------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||
| 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". |
|
||||
| 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. |
|
||||
| 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_checksums | dict[str, str] | Checksum hash of the individual games' data packages the server will send. Used by newer clients to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents) for more information. |
|
||||
| seed_name | str | Uniquely identifying name of this generation |
|
||||
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
||||
|
||||
#### release
|
||||
Dictates what is allowed when it comes to a player releasing their run. A release is an action which distributes the rest of the items in a player's run to those other players awaiting them.
|
||||
@@ -106,8 +107,8 @@ Dictates what is allowed when it comes to a player querying the items remaining
|
||||
### ConnectionRefused
|
||||
Sent to clients when the server refuses connection. This is sent during the initial connection handshake.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| Name | Type | Notes |
|
||||
|--------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `IncompatibleVersion`, `InvalidPassword`, or `InvalidItemsHandling`. |
|
||||
|
||||
InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server.
|
||||
@@ -554,12 +555,16 @@ Color options:
|
||||
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
|
||||
|
||||
### Client States
|
||||
An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate).
|
||||
An enumeration containing the possible client states that may be used to inform
|
||||
the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets
|
||||
the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection
|
||||
to a slot.
|
||||
|
||||
```python
|
||||
import enum
|
||||
class ClientStatus(enum.IntEnum):
|
||||
CLIENT_UNKNOWN = 0
|
||||
CLIENT_CONNECTED = 5
|
||||
CLIENT_READY = 10
|
||||
CLIENT_PLAYING = 20
|
||||
CLIENT_GOAL = 30
|
||||
@@ -644,11 +649,12 @@ Note:
|
||||
#### GameData
|
||||
GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation.
|
||||
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
|
||||
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
|
||||
| version | int | Version number of this game's data |
|
||||
| Name | Type | Notes |
|
||||
|---------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
|
||||
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
|
||||
| version | int | Version number of this game's data. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. |
|
||||
| checksum | str | A checksum hash of this game's data. |
|
||||
|
||||
### Tags
|
||||
Tags are represented as a list of strings, the common Client tags follow:
|
||||
|
||||
@@ -7,10 +7,11 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* Python 3.8.7 or newer
|
||||
* pip (Depending on platform may come included)
|
||||
* A C compiler
|
||||
* possibly optional, read OS-specific sections
|
||||
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* **Python 3.11 does not work currently**
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
* possibly optional, read operating system specific sections
|
||||
|
||||
Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the
|
||||
required modules and after pressing enter proceed to install everything automatically.
|
||||
@@ -29,6 +30,8 @@ After this, you should be able to run the programs.
|
||||
|
||||
Recommended steps
|
||||
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
|
||||
* **Python 3.11 does not work currently**
|
||||
|
||||
* Download and install full Visual Studio from
|
||||
[Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/)
|
||||
or an older "Build Tools for Visual Studio" from
|
||||
@@ -40,6 +43,8 @@ Recommended steps
|
||||
|
||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
||||
* In PyCharm: right-click Generate.py and select `Run 'Generate'`
|
||||
* Without PyCharm: open a command prompt in the source folder and type `py Generate.py`
|
||||
|
||||
|
||||
## macOS
|
||||
@@ -59,7 +64,7 @@ setting in host.yaml at your Enemizer executable.
|
||||
|
||||
## Optional: SNI
|
||||
|
||||
SNI is required to use SNIClient. If not integrated into the project, it has to be started manually.
|
||||
[SNI](https://github.com/alttpo/sni/blob/main/README.md) is required to use SNIClient. If not integrated into the project, it has to be started manually.
|
||||
|
||||
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
|
||||
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
|
||||
|
||||
@@ -364,14 +364,9 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
|
||||
|
||||
class MyGameWorld(World):
|
||||
"""Insert description of the world/game here."""
|
||||
game: str = "My Game" # name of the game/world
|
||||
game = "My Game" # name of the game/world
|
||||
option_definitions = mygame_options # options the player can set
|
||||
topology_present: bool = True # show path to required location checks in spoiler
|
||||
|
||||
# data_version is used to signal that items, locations or their names
|
||||
# changed. Set this to 0 during development so other games' clients do not
|
||||
# cache any texts, then increase by 1 for each release that makes changes.
|
||||
data_version = 0
|
||||
topology_present = True # show path to required location checks in spoiler
|
||||
|
||||
# ID of first item and location, could be hard-coded but code may be easier
|
||||
# to read with this as a propery.
|
||||
|
||||
24
host.yaml
24
host.yaml
@@ -93,6 +93,10 @@ sni_options:
|
||||
lttp_options:
|
||||
# File name of the v1.0 J rom
|
||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
ladx_options:
|
||||
# File name of the Link's Awakening DX rom
|
||||
rom_file: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
|
||||
|
||||
lufia2ac_options:
|
||||
# File name of the US rom
|
||||
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
|
||||
@@ -163,3 +167,23 @@ zillion_options:
|
||||
# RetroArch doesn't make it easy to launch a game from the command line.
|
||||
# You have to know the path to the emulator core library on the user's computer.
|
||||
rom_start: "retroarch"
|
||||
|
||||
adventure_options:
|
||||
# File name of the standard NTSC Adventure rom.
|
||||
# The licensed "The 80 Classic Games" CD-ROM contains this.
|
||||
# It may also have a .a26 extension
|
||||
rom_file: "ADVNTURE.BIN"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program for '.a26'
|
||||
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
||||
rom_start: true
|
||||
# Optional, additional args passed into rom_start before the .bin file
|
||||
# For example, this can be used to autoload the connector script in BizHawk
|
||||
# (see BizHawk --lua= option)
|
||||
rom_args: " "
|
||||
# Set this to true to display item received messages in Emuhawk
|
||||
display_msgs: true
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
176
inno_setup.iss
176
inno_setup.iss
@@ -63,6 +63,8 @@ Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full
|
||||
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
||||
Name: "generator/pkmn_r"; Description: "Pokemon Red ROM Setup"; Types: full hosting
|
||||
Name: "generator/pkmn_b"; Description: "Pokemon Blue ROM Setup"; Types: full hosting
|
||||
Name: "generator/ladx"; Description: "Link's Awakening DX ROM Setup"; Types: full hosting
|
||||
Name: "generator/tloz"; Description: "The Legend of Zelda ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 135168; Flags: disablenouninstallwarning
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; Types: full playing
|
||||
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
||||
@@ -72,15 +74,20 @@ Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch
|
||||
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||
Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
|
||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
|
||||
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
|
||||
Name: "client/pkmn"; Description: "Pokemon Client"
|
||||
Name: "client/pkmn/red"; Description: "Pokemon Client - Pokemon Red Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/pkmn/blue"; Description: "Pokemon Client - Pokemon Blue Setup"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/ladx"; Description: "Link's Awakening Client"; Types: full playing; ExtraDiskSpaceRequired: 1048576
|
||||
Name: "client/cf"; Description: "ChecksFinder"; Types: full playing
|
||||
Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
||||
Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
|
||||
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
||||
Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
|
||||
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
|
||||
[Dirs]
|
||||
@@ -97,15 +104,20 @@ Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda
|
||||
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
||||
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
||||
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
|
||||
Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
|
||||
Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz
|
||||
Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||
|
||||
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
||||
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
|
||||
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
|
||||
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
|
||||
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||
Source: "{#source_path}\ArchipelagoOoTClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
|
||||
@@ -115,6 +127,10 @@ Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: igno
|
||||
Source: "{#source_path}\ArchipelagoPokemonClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/pkmn
|
||||
Source: "{#source_path}\ArchipelagoChecksFinderClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/cf
|
||||
Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sc2
|
||||
Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz
|
||||
Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
|
||||
Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
|
||||
Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
|
||||
[Icons]
|
||||
@@ -130,6 +146,9 @@ Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Archipelag
|
||||
Name: "{group}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Components: client/pkmn
|
||||
Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Components: client/cf
|
||||
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
|
||||
Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
|
||||
Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
||||
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
||||
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
||||
@@ -142,6 +161,10 @@ Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\Ar
|
||||
Name: "{commondesktop}\{#MyAppName} Pokemon Client"; Filename: "{app}\ArchipelagoPokemonClient.exe"; Tasks: desktopicon; Components: client/pkmn
|
||||
Name: "{commondesktop}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoChecksFinderClient.exe"; Tasks: desktopicon; Components: client/cf
|
||||
Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Tasks: desktopicon; Components: client/sc2
|
||||
Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
|
||||
Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
|
||||
Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
|
||||
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
||||
|
||||
[Run]
|
||||
|
||||
@@ -219,6 +242,21 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Ar
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/ladx
|
||||
|
||||
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
|
||||
Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
|
||||
@@ -286,6 +324,15 @@ var RedROMFilePage: TInputFileWizardPage;
|
||||
var bluerom: string;
|
||||
var BlueROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var ladxrom: string;
|
||||
var LADXROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var tlozrom: string;
|
||||
var TLoZROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var advnrom: string;
|
||||
var AdvnROMFilePage: TInputFileWizardPage;
|
||||
|
||||
function GetSNESMD5OfFile(const rom: string): string;
|
||||
var data: AnsiString;
|
||||
begin
|
||||
@@ -346,6 +393,25 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function CheckNESRom(name: string; hash: string): string;
|
||||
var rom: string;
|
||||
begin
|
||||
log('Handling ' + name)
|
||||
rom := FileSearch(name, WizardDirValue());
|
||||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetSMSMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetSMSMD5OfFile(rom), hash) = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
Result := rom;
|
||||
exit;
|
||||
end;
|
||||
log('existing ROM failed verification');
|
||||
end;
|
||||
end;
|
||||
|
||||
function AddRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
@@ -392,6 +458,21 @@ begin
|
||||
'.sms');
|
||||
end;
|
||||
|
||||
function AddNESRomPage(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'NES ROM files|*.nes|All files|*.*',
|
||||
'.nes');
|
||||
end;
|
||||
|
||||
procedure AddOoTRomPage();
|
||||
begin
|
||||
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
|
||||
@@ -422,6 +503,21 @@ begin
|
||||
'.z64');
|
||||
end;
|
||||
|
||||
function AddA26Page(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'A2600 ROM files|*.BIN;*.a26|All files|*.*',
|
||||
'.BIN');
|
||||
end;
|
||||
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
begin
|
||||
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
|
||||
@@ -440,6 +536,16 @@ begin
|
||||
Result := not (OoTROMFilePage.Values[0] = '')
|
||||
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
||||
Result := not (ZlROMFilePage.Values[0] = '')
|
||||
else if (assigned(RedROMFilePage)) and (CurPageID = RedROMFilePage.ID) then
|
||||
Result := not (RedROMFilePage.Values[0] = '')
|
||||
else if (assigned(BlueROMFilePage)) and (CurPageID = BlueROMFilePage.ID) then
|
||||
Result := not (BlueROMFilePage.Values[0] = '')
|
||||
else if (assigned(LADXROMFilePage)) and (CurPageID = LADXROMFilePage.ID) then
|
||||
Result := not (LADXROMFilePage.Values[0] = '')
|
||||
else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then
|
||||
Result := not (TLoZROMFilePage.Values[0] = '')
|
||||
else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then
|
||||
Result := not (AdvnROMFilePage.Values[0] = '')
|
||||
else
|
||||
Result := True;
|
||||
end;
|
||||
@@ -576,7 +682,7 @@ function GetRedROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(redrom) > 0 then
|
||||
Result := redrom
|
||||
else if Assigned(RedRomFilePage) then
|
||||
else if Assigned(RedROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(RedROMFilePage.Values[0]), '3d45c1ee9abd5738df46d2bdda8b57dc')
|
||||
if R <> 0 then
|
||||
@@ -592,7 +698,7 @@ function GetBlueROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(bluerom) > 0 then
|
||||
Result := bluerom
|
||||
else if Assigned(BlueRomFilePage) then
|
||||
else if Assigned(BlueROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(BlueROMFilePage.Values[0]), '50927e843568814f7ed45ec4f944bd8b')
|
||||
if R <> 0 then
|
||||
@@ -603,6 +709,54 @@ begin
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetTLoZROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(tlozrom) > 0 then
|
||||
Result := tlozrom
|
||||
else if Assigned(TLoZROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(TLoZROMFilePage.Values[0]), '337bd6f1a1163df31bf2633665589ab0');
|
||||
if R <> 0 then
|
||||
MsgBox('The Legend of Zelda ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := TLoZROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetLADXROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(ladxrom) > 0 then
|
||||
Result := ladxrom
|
||||
else if Assigned(LADXROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(LADXROMFilePage.Values[0]), '07c211479386825042efb4ad31bb525f')
|
||||
if R <> 0 then
|
||||
MsgBox('Link''s Awakening DX ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := LADXROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
function GetAdvnROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(advnrom) > 0 then
|
||||
Result := advnrom
|
||||
else if Assigned(AdvnROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284');
|
||||
if R <> 0 then
|
||||
MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := AdvnROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
@@ -640,9 +794,21 @@ begin
|
||||
if Length(bluerom) = 0 then
|
||||
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
|
||||
|
||||
ladxrom := CheckRom('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc','07c211479386825042efb4ad31bb525f');
|
||||
if Length(ladxrom) = 0 then
|
||||
LADXROMFilePage:= AddGBRomPage('Legend of Zelda, The - Link''s Awakening DX (USA, Europe) (SGB Enhanced).gbc');
|
||||
|
||||
l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
|
||||
if Length(l2acrom) = 0 then
|
||||
L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
|
||||
|
||||
tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0');
|
||||
if Length(tlozrom) = 0 then
|
||||
TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes');
|
||||
|
||||
advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284');
|
||||
if Length(advnrom) = 0 then
|
||||
AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN');
|
||||
end;
|
||||
|
||||
|
||||
@@ -669,4 +835,10 @@ begin
|
||||
Result := not (WizardIsComponentSelected('generator/pkmn_r') or WizardIsComponentSelected('client/pkmn/red'));
|
||||
if (assigned(BlueROMFilePage)) and (PageID = BlueROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/pkmn_b') or WizardIsComponentSelected('client/pkmn/blue'));
|
||||
if (assigned(LADXROMFilePage)) and (PageID = LADXROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
|
||||
if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz'));
|
||||
if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/advn'));
|
||||
end;
|
||||
|
||||
12
kvui.py
12
kvui.py
@@ -148,9 +148,11 @@ class ServerLabel(HovererableLabel):
|
||||
for permission_name, permission_data in ctx.permissions.items():
|
||||
text += f"\n {permission_name}: {permission_data}"
|
||||
if ctx.hint_cost is not None and ctx.total_locations:
|
||||
min_cost = int(ctx.server_version >= (0, 3, 9))
|
||||
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
|
||||
f"For you this means every {max(0, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
||||
"location checks."
|
||||
f"For you this means every " \
|
||||
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))}" \
|
||||
f" location checks."
|
||||
elif ctx.hint_cost == 0:
|
||||
text += "\n!hint is free to use."
|
||||
|
||||
@@ -486,6 +488,10 @@ class GameManager(App):
|
||||
if hasattr(self, "energy_link_label"):
|
||||
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
||||
|
||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||
def open_settings(self, *largs):
|
||||
pass
|
||||
|
||||
|
||||
class LogtoUI(logging.Handler):
|
||||
def __init__(self, on_log):
|
||||
@@ -606,7 +612,7 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
ExceptionManager.add_handler(E())
|
||||
|
||||
Builder.load_file(Utils.local_path("data", "client.kv"))
|
||||
user_file = Utils.local_path("data", "user.kv")
|
||||
user_file = Utils.user_path("data", "user.kv")
|
||||
if os.path.exists(user_file):
|
||||
logging.info("Loading user.kv into builder.")
|
||||
Builder.load_file(user_file)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
colorama>=0.4.5
|
||||
websockets>=10.3
|
||||
PyYAML>=6.0
|
||||
jellyfish>=0.9.0
|
||||
jellyfish>=0.11.0
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.1.0
|
||||
bsdiff4>=1.2.2
|
||||
bsdiff4>=1.2.3
|
||||
platformdirs>=3.2.0
|
||||
|
||||
53
setup.py
53
setup.py
@@ -12,7 +12,6 @@ import io
|
||||
import json
|
||||
import threading
|
||||
import subprocess
|
||||
import pkg_resources
|
||||
|
||||
from collections.abc import Iterable
|
||||
from hashlib import sha3_512
|
||||
@@ -22,13 +21,29 @@ 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
|
||||
try:
|
||||
requirement = 'cx-Freeze>=6.14.7'
|
||||
pkg_resources.require(requirement)
|
||||
import cx_Freeze
|
||||
except pkg_resources.ResolutionError:
|
||||
import pkg_resources
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
install_cx_freeze = False
|
||||
except pkg_resources.ResolutionError:
|
||||
install_cx_freeze = True
|
||||
except ImportError:
|
||||
install_cx_freeze = True
|
||||
pkg_resources = None # type: ignore [assignment]
|
||||
|
||||
if install_cx_freeze:
|
||||
# check if pip is available
|
||||
try:
|
||||
import pip # noqa: F401
|
||||
except ImportError:
|
||||
raise RuntimeError("pip not available. Please install pip.")
|
||||
# install and import cx_freeze
|
||||
if '--yes' not in sys.argv and '-y' not in sys.argv:
|
||||
input(f'Requirement {requirement} is not satisfied, press enter to install it')
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade'])
|
||||
import cx_Freeze
|
||||
import pkg_resources
|
||||
|
||||
import cx_Freeze
|
||||
|
||||
# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line
|
||||
import setuptools.command.build
|
||||
@@ -40,7 +55,7 @@ if __name__ == "__main__":
|
||||
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
|
||||
ModuleUpdate.update_ran = False # restore for later
|
||||
|
||||
from Launcher import components, icon_paths
|
||||
from worlds.LauncherComponents import components, icon_paths
|
||||
from Utils import version_tuple, is_windows, is_linux
|
||||
|
||||
|
||||
@@ -49,6 +64,7 @@ apworlds: set = {
|
||||
"Subnautica",
|
||||
"Factorio",
|
||||
"Rogue Legacy",
|
||||
"Sonic Adventure 2 Battle",
|
||||
"Donkey Kong Country 3",
|
||||
"Super Mario World",
|
||||
"Stardew Valley",
|
||||
@@ -119,6 +135,7 @@ def download_SNI():
|
||||
print(f"No SNI found for system spec {platform_name} {machine_name}")
|
||||
|
||||
|
||||
signtool: typing.Optional[str]
|
||||
if os.path.exists("X:/pw.txt"):
|
||||
print("Using signtool")
|
||||
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
||||
@@ -143,7 +160,7 @@ exes = [
|
||||
target_name=c.frozen_name + (".exe" if is_windows else ""),
|
||||
icon=icon_paths[c.icon],
|
||||
base="Win32GUI" if is_windows and not c.cli else None
|
||||
) for c in components if c.script_name
|
||||
) for c in components if c.script_name and c.frozen_name
|
||||
]
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
|
||||
@@ -306,7 +323,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
# which should be ok
|
||||
with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
entry: os.DirEntry
|
||||
for path in world_directory.rglob("*.*"):
|
||||
relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:])
|
||||
zf.write(path, relative_path)
|
||||
@@ -329,9 +345,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
for exe in self.distribution.executables:
|
||||
print(f"Signing {exe.target_name}")
|
||||
os.system(signtool + os.path.join(self.buildfolder, exe.target_name))
|
||||
print(f"Signing SNI")
|
||||
print("Signing SNI")
|
||||
os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe"))
|
||||
print(f"Signing OoT Utils")
|
||||
print("Signing OoT Utils")
|
||||
for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")):
|
||||
os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path))
|
||||
|
||||
@@ -385,7 +401,8 @@ class AppImageCommand(setuptools.Command):
|
||||
yes: bool
|
||||
|
||||
def write_desktop(self):
|
||||
desktop_filename = self.app_dir / f'{self.app_id}.desktop'
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
desktop_filename = self.app_dir / f"{self.app_id}.desktop"
|
||||
with open(desktop_filename, 'w', encoding="utf-8") as f:
|
||||
f.write("\n".join((
|
||||
"[Desktop Entry]",
|
||||
@@ -399,7 +416,8 @@ class AppImageCommand(setuptools.Command):
|
||||
desktop_filename.chmod(0o755)
|
||||
|
||||
def write_launcher(self, default_exe: Path):
|
||||
launcher_filename = self.app_dir / f'AppRun'
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
launcher_filename = self.app_dir / "AppRun"
|
||||
with open(launcher_filename, 'w', encoding="utf-8") as f:
|
||||
f.write(f"""#!/bin/sh
|
||||
exe="{default_exe}"
|
||||
@@ -421,11 +439,12 @@ $APPDIR/$exe "$@"
|
||||
launcher_filename.chmod(0o755)
|
||||
|
||||
def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None):
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
try:
|
||||
from PIL import Image
|
||||
except ModuleNotFoundError:
|
||||
if not self.yes:
|
||||
input(f'Requirement PIL is not satisfied, press enter to install it')
|
||||
input("Requirement PIL is not satisfied, press enter to install it")
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade'])
|
||||
from PIL import Image
|
||||
im = Image.open(src)
|
||||
@@ -502,8 +521,12 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
|
||||
return (lib, lib_arch, lib_libc), path
|
||||
|
||||
if not hasattr(find_libs, "cache"):
|
||||
data = subprocess.run([shutil.which('ldconfig'), '-p'], capture_output=True, text=True).stdout.split('\n')[1:]
|
||||
find_libs.cache = {k: v for k, v in (parse(line) for line in data if '=>' in line)}
|
||||
ldconfig = shutil.which("ldconfig")
|
||||
assert ldconfig, "Make sure ldconfig is in PATH"
|
||||
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
|
||||
find_libs.cache = { # type: ignore [attr-defined]
|
||||
k: v for k, v in (parse(line) for line in data if "=>" in line)
|
||||
}
|
||||
|
||||
def find_lib(lib, arch, libc):
|
||||
for k, v in find_libs.cache.items():
|
||||
|
||||
@@ -199,11 +199,15 @@ class WorldTestBase(unittest.TestCase):
|
||||
|
||||
self.collect_all_but(all_items)
|
||||
for location in self.multiworld.get_locations():
|
||||
self.assertEqual(self.multiworld.state.can_reach(location), location.name not in locations)
|
||||
loc_reachable = self.multiworld.state.can_reach(location)
|
||||
self.assertEqual(loc_reachable, location.name not in locations,
|
||||
f"{location.name} is reachable without {all_items}" if loc_reachable
|
||||
else f"{location.name} is not reachable without {all_items}")
|
||||
for item_names in possible_items:
|
||||
items = self.collect_by_name(item_names)
|
||||
for location in locations:
|
||||
self.assertTrue(self.can_reach_location(location))
|
||||
self.assertTrue(self.can_reach_location(location),
|
||||
f"{location} not reachable with {item_names}")
|
||||
self.remove(items)
|
||||
|
||||
def assertBeatable(self, beatable: bool):
|
||||
|
||||
@@ -6,13 +6,19 @@ from . import setup_solo_multiworld
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def testCreateDuplicateLocations(self):
|
||||
"""Tests that no two Locations share a name."""
|
||||
"""Tests that no two Locations share a name or ID."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
locations = Counter(multiworld.get_locations())
|
||||
locations = Counter(location.name for location in multiworld.get_locations())
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location {locations.most_common(1)}")
|
||||
f"{world_type.game} has duplicate of location name {locations.most_common(1)}")
|
||||
|
||||
locations = Counter(location.address for location in multiworld.get_locations()
|
||||
if type(location.address) is int)
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
||||
|
||||
def testLocationsInDatapackage(self):
|
||||
"""Tests that created locations not filled before fill starts exist in the datapackage."""
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \
|
||||
ClassVar
|
||||
import sys
|
||||
from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \
|
||||
Union
|
||||
|
||||
from Options import AssembleOptions
|
||||
from BaseClasses import CollectionState
|
||||
from Options import AssembleOptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial
|
||||
from . import GamesPackage
|
||||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
world_types: Dict[str, Type[World]] = {}
|
||||
__file__: str
|
||||
zip_path: Optional[str]
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
if "web" in dct:
|
||||
@@ -32,6 +36,9 @@ class AutoWorldRegister(type):
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("location_name_groups", {}).items()}
|
||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
|
||||
# move away from get_required_client_version function
|
||||
@@ -154,9 +161,14 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
data_version: ClassVar[int] = 1
|
||||
"""
|
||||
increment this every time something in your world's names/id mappings changes.
|
||||
While this is set to 0, this world's DataPackage is considered in testing mode and will be inserted to the multidata
|
||||
and retrieved by clients on every connection.
|
||||
Increment this every time something in your world's names/id mappings changes.
|
||||
|
||||
When this is set to 0, that world's DataPackage is considered in "testing mode", which signals to servers/clients
|
||||
that it should not be cached, and clients should request that world's DataPackage every connection. Not
|
||||
recommended for production-ready worlds.
|
||||
|
||||
Deprecated. Clients should utilize `checksum` to determine if DataPackage has changed since last connection and
|
||||
request a new DataPackage, if necessary.
|
||||
"""
|
||||
|
||||
required_client_version: Tuple[int, int, int] = (0, 1, 6)
|
||||
@@ -343,8 +355,35 @@ class World(metaclass=AutoWorldRegister):
|
||||
def create_filler(self) -> "Item":
|
||||
return self.create_item(self.get_filler_item_name())
|
||||
|
||||
@classmethod
|
||||
def get_data_package_data(cls) -> "GamesPackage":
|
||||
sorted_item_name_groups = {
|
||||
name: sorted(cls.item_name_groups[name]) for name in sorted(cls.item_name_groups)
|
||||
}
|
||||
sorted_location_name_groups = {
|
||||
name: sorted(cls.location_name_groups[name]) for name in sorted(cls.location_name_groups)
|
||||
}
|
||||
res: "GamesPackage" = {
|
||||
# sorted alphabetically
|
||||
"item_name_groups": sorted_item_name_groups,
|
||||
"item_name_to_id": cls.item_name_to_id,
|
||||
"location_name_groups": sorted_location_name_groups,
|
||||
"location_name_to_id": cls.location_name_to_id,
|
||||
"version": cls.data_version,
|
||||
}
|
||||
res["checksum"] = data_package_checksum(res)
|
||||
return res
|
||||
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
class LogicMixin(metaclass=AutoLogicRegister):
|
||||
pass
|
||||
|
||||
|
||||
def data_package_checksum(data: "GamesPackage") -> str:
|
||||
"""Calculates the data package checksum for a game from a dict"""
|
||||
assert "checksum" not in data, "Checksum already in data"
|
||||
assert sorted(data) == list(data), "Data not ordered"
|
||||
from NetUtils import encode
|
||||
return hashlib.sha1(encode(data).encode()).hexdigest()
|
||||
|
||||
106
worlds/LauncherComponents.py
Normal file
106
worlds/LauncherComponents.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Callable, List, Iterable
|
||||
|
||||
from Utils import local_path, is_windows
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
TOOL = auto()
|
||||
FUNC = auto() # not a real component
|
||||
CLIENT = auto()
|
||||
ADJUSTER = auto()
|
||||
|
||||
|
||||
class Component:
|
||||
display_name: str
|
||||
type: Optional[Type]
|
||||
script_name: Optional[str]
|
||||
frozen_name: Optional[str]
|
||||
icon: str # just the name, no suffix
|
||||
cli: bool
|
||||
func: Optional[Callable]
|
||||
file_identifier: Optional[Callable[[str], bool]]
|
||||
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||
cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None,
|
||||
file_identifier: Optional[Callable[[str], bool]] = None):
|
||||
self.display_name = display_name
|
||||
self.script_name = script_name
|
||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||
self.icon = icon
|
||||
self.cli = cli
|
||||
self.type = component_type or \
|
||||
None if not display_name else \
|
||||
Type.FUNC if func else \
|
||||
Type.CLIENT if 'Client' in display_name else \
|
||||
Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL
|
||||
self.func = func
|
||||
self.file_identifier = file_identifier
|
||||
|
||||
def handles_file(self, path: str):
|
||||
return self.file_identifier(path) if self.file_identifier else False
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.display_name})"
|
||||
|
||||
|
||||
class SuffixIdentifier:
|
||||
suffixes: Iterable[str]
|
||||
|
||||
def __init__(self, *args: str):
|
||||
self.suffixes = args
|
||||
|
||||
def __call__(self, path: str):
|
||||
if isinstance(path, str):
|
||||
for suffix in self.suffixes:
|
||||
if path.endswith(suffix):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
components: List[Component] = [
|
||||
# Launcher
|
||||
Component('', 'Launcher'),
|
||||
# Core
|
||||
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
|
||||
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
|
||||
Component('Generate', 'Generate', cli=True),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
|
||||
# SNI
|
||||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac')),
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Minecraft
|
||||
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
||||
file_identifier=SuffixIdentifier('.apmc')),
|
||||
# Ocarina of Time
|
||||
Component('OoT Client', 'OoTClient',
|
||||
file_identifier=SuffixIdentifier('.apz5')),
|
||||
Component('OoT Adjuster', 'OoTAdjuster'),
|
||||
# FF1
|
||||
Component('FF1 Client', 'FF1Client'),
|
||||
# Pokémon
|
||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
||||
# TLoZ
|
||||
Component('Zelda 1 Client', 'Zelda1Client'),
|
||||
# ChecksFinder
|
||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||
# Starcraft 2
|
||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
||||
# Wargroove
|
||||
Component('Wargroove Client', 'WargrooveClient'),
|
||||
# Zillion
|
||||
Component('Zillion Client', 'ZillionClient',
|
||||
file_identifier=SuffixIdentifier('.apzl')),
|
||||
#Kingdom Hearts 2
|
||||
Component('KH2 Client', "KH2Client"),
|
||||
]
|
||||
|
||||
|
||||
icon_paths = {
|
||||
'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
|
||||
'mcicon': local_path('data', 'mcicon.ico')
|
||||
}
|
||||
@@ -20,14 +20,19 @@ if typing.TYPE_CHECKING:
|
||||
from .AutoWorld import World
|
||||
|
||||
|
||||
class GamesPackage(typing.TypedDict):
|
||||
class GamesData(typing.TypedDict):
|
||||
item_name_groups: typing.Dict[str, typing.List[str]]
|
||||
item_name_to_id: typing.Dict[str, int]
|
||||
location_name_groups: typing.Dict[str, typing.List[str]]
|
||||
location_name_to_id: typing.Dict[str, int]
|
||||
version: int
|
||||
|
||||
|
||||
class GamesPackage(GamesData, total=False):
|
||||
checksum: str
|
||||
|
||||
|
||||
class DataPackage(typing.TypedDict):
|
||||
version: int
|
||||
games: typing.Dict[str, GamesPackage]
|
||||
|
||||
|
||||
@@ -35,6 +40,9 @@ class WorldSource(typing.NamedTuple):
|
||||
path: str # typically relative path from this module
|
||||
is_zip: bool = False
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip})"
|
||||
|
||||
|
||||
# find potential world containers, currently folders and zip-importable .apworld's
|
||||
world_sources: typing.List[WorldSource] = []
|
||||
@@ -50,24 +58,35 @@ for file in os.scandir(folder):
|
||||
# import all submodules to trigger AutoWorldRegister
|
||||
world_sources.sort()
|
||||
for world_source in world_sources:
|
||||
if world_source.is_zip:
|
||||
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
||||
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||
spec = importer.find_spec(world_source.path.split(".", 1)[0])
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
else: # TODO: remove with 3.8 support
|
||||
mod = importer.load_module(world_source.path.split(".", 1)[0])
|
||||
try:
|
||||
if world_source.is_zip:
|
||||
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
||||
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||
spec = importer.find_spec(world_source.path.split(".", 1)[0])
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
else: # TODO: remove with 3.8 support
|
||||
mod = importer.load_module(world_source.path.split(".", 1)[0])
|
||||
|
||||
mod.__package__ = f"worlds.{mod.__package__}"
|
||||
mod.__name__ = f"worlds.{mod.__name__}"
|
||||
sys.modules[mod.__name__] = mod
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
||||
# Found no equivalent for < 3.10
|
||||
if hasattr(importer, "exec_module"):
|
||||
importer.exec_module(mod)
|
||||
else:
|
||||
importlib.import_module(f".{world_source.path}", "worlds")
|
||||
mod.__package__ = f"worlds.{mod.__package__}"
|
||||
mod.__name__ = f"worlds.{mod.__name__}"
|
||||
sys.modules[mod.__name__] = mod
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
||||
# Found no equivalent for < 3.10
|
||||
if hasattr(importer, "exec_module"):
|
||||
importer.exec_module(mod)
|
||||
else:
|
||||
importlib.import_module(f".{world_source.path}", "worlds")
|
||||
except Exception as e:
|
||||
# A single world failing can still mean enough is working for the user, log and carry on
|
||||
import traceback
|
||||
import io
|
||||
file_like = io.StringIO()
|
||||
print(f"Could not load world {world_source}:", file=file_like)
|
||||
traceback.print_exc(file=file_like)
|
||||
file_like.seek(0)
|
||||
import logging
|
||||
logging.exception(file_like.read())
|
||||
|
||||
lookup_any_item_id_to_name = {}
|
||||
lookup_any_location_id_to_name = {}
|
||||
@@ -75,14 +94,9 @@ games: typing.Dict[str, GamesPackage] = {}
|
||||
|
||||
from .AutoWorld import AutoWorldRegister
|
||||
|
||||
# Build the data package for each game.
|
||||
for world_name, world in AutoWorldRegister.world_types.items():
|
||||
games[world_name] = {
|
||||
"item_name_to_id": world.item_name_to_id,
|
||||
"location_name_to_id": world.location_name_to_id,
|
||||
"version": world.data_version,
|
||||
# seems clients don't actually want this. Keeping it here in case someone changes their mind.
|
||||
# "item_name_groups": {name: tuple(items) for name, items in world.item_name_groups.items()}
|
||||
}
|
||||
games[world_name] = world.get_data_package_data()
|
||||
lookup_any_item_id_to_name.update(world.item_id_to_name)
|
||||
lookup_any_location_id_to_name.update(world.location_id_to_name)
|
||||
|
||||
|
||||
53
worlds/adventure/Items.py
Normal file
53
worlds/adventure/Items.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from typing import Optional
|
||||
from BaseClasses import ItemClassification, Item
|
||||
|
||||
base_adventure_item_id = 118000000
|
||||
|
||||
|
||||
class AdventureItem(Item):
|
||||
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
|
||||
super().__init__(name, classification, code, player)
|
||||
|
||||
|
||||
class ItemData:
|
||||
def __init__(self, id: int, classification: ItemClassification):
|
||||
self.classification = classification
|
||||
self.id = None if id is None else id + base_adventure_item_id
|
||||
self.table_index = id
|
||||
|
||||
|
||||
nothing_item_id = base_adventure_item_id
|
||||
|
||||
# base IDs are the index in the static item data table, which is
|
||||
# not the same order as the items in RAM (but offset 0 is a 16-bit address of
|
||||
# location of room and position data)
|
||||
item_table = {
|
||||
"Yellow Key": ItemData(0xB, ItemClassification.progression_skip_balancing),
|
||||
"White Key": ItemData(0xC, ItemClassification.progression),
|
||||
"Black Key": ItemData(0xD, ItemClassification.progression),
|
||||
"Bridge": ItemData(0xA, ItemClassification.progression),
|
||||
"Magnet": ItemData(0x11, ItemClassification.progression),
|
||||
"Sword": ItemData(0x9, ItemClassification.progression),
|
||||
"Chalice": ItemData(0x10, ItemClassification.progression_skip_balancing),
|
||||
# Non-ROM Adventure items, managed by lua
|
||||
"Left Difficulty Switch": ItemData(0x100, ItemClassification.filler),
|
||||
"Right Difficulty Switch": ItemData(0x101, ItemClassification.filler),
|
||||
# Can use these instead of 'nothing'
|
||||
"Freeincarnate": ItemData(0x102, ItemClassification.filler),
|
||||
# These should only be enabled if fast dragons is on?
|
||||
"Slow Yorgle": ItemData(0x103, ItemClassification.filler),
|
||||
"Slow Grundle": ItemData(0x104, ItemClassification.filler),
|
||||
"Slow Rhindle": ItemData(0x105, ItemClassification.filler),
|
||||
# this should only be enabled if opted into? For now, I'll just exclude them
|
||||
"Revive Dragons": ItemData(0x106, ItemClassification.trap),
|
||||
"nothing": ItemData(0x0, ItemClassification.filler)
|
||||
# Bat Trap
|
||||
# Bat Time Out
|
||||
# "Revive Dragons": ItemData(0x110, ItemClassification.trap)
|
||||
}
|
||||
|
||||
standard_item_max = item_table["Magnet"].id
|
||||
|
||||
|
||||
event_table = {
|
||||
}
|
||||
214
worlds/adventure/Locations.py
Normal file
214
worlds/adventure/Locations.py
Normal file
@@ -0,0 +1,214 @@
|
||||
from BaseClasses import Location
|
||||
|
||||
base_location_id = 118000000
|
||||
|
||||
|
||||
class AdventureLocation(Location):
|
||||
game: str = "Adventure"
|
||||
|
||||
|
||||
class WorldPosition:
|
||||
room_id: int
|
||||
room_x: int
|
||||
room_y: int
|
||||
|
||||
def __init__(self, room_id: int, room_x: int = None, room_y: int = None):
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
|
||||
def get_position(self, random):
|
||||
if self.room_x is None or self.room_y is None:
|
||||
return random.choice(standard_positions)
|
||||
else:
|
||||
return self.room_x, self.room_y
|
||||
|
||||
|
||||
class LocationData:
|
||||
def __init__(self, region, name, location_id, world_positions: [WorldPosition] = None, event=False,
|
||||
needs_bat_logic: bool = False):
|
||||
self.region: str = region
|
||||
self.name: str = name
|
||||
self.world_positions: [WorldPosition] = world_positions
|
||||
self.room_id: int = None
|
||||
self.room_x: int = None
|
||||
self.room_y: int = None
|
||||
self.location_id: int = location_id
|
||||
if location_id is None:
|
||||
self.short_location_id: int = None
|
||||
self.location_id: int = None
|
||||
else:
|
||||
self.short_location_id: int = location_id
|
||||
self.location_id: int = location_id + base_location_id
|
||||
self.event: bool = event
|
||||
if world_positions is None and not event:
|
||||
self.room_id: int = self.short_location_id
|
||||
self.needs_bat_logic: int = needs_bat_logic
|
||||
self.local_item: int = None
|
||||
|
||||
def get_position(self, random):
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
if self.room_id is None:
|
||||
return None
|
||||
self.room_x, self.room_y = random.choice(standard_positions)
|
||||
if self.room_id is None:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return self.room_x, self.room_y
|
||||
|
||||
def get_room_id(self, random):
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
return None
|
||||
if self.room_id is None:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return self.room_id
|
||||
|
||||
|
||||
standard_positions = [
|
||||
(0x80, 0x20),
|
||||
(0x20, 0x20),
|
||||
(0x20, 0x40),
|
||||
(0x20, 0x40),
|
||||
(0x30, 0x20)
|
||||
]
|
||||
|
||||
|
||||
# Gives the most difficult region the dragon can reach and get stuck in from the provided room without the
|
||||
# player unlocking something for it
|
||||
def dragon_room_to_region(room: int) -> str:
|
||||
if room <= 0x11:
|
||||
return "Overworld"
|
||||
elif room <= 0x12:
|
||||
return "YellowCastle"
|
||||
elif room <= 0x16 or room == 0x1B:
|
||||
return "BlackCastle"
|
||||
elif room <= 0x1A:
|
||||
return "WhiteCastleVault"
|
||||
elif room <= 0x1D:
|
||||
return "Overworld"
|
||||
elif room <= 0x1E:
|
||||
return "CreditsRoom"
|
||||
|
||||
|
||||
def get_random_room_in_regions(regions: [str], random) -> int:
|
||||
possible_rooms = {}
|
||||
for locname in location_table:
|
||||
if location_table[locname].region in regions:
|
||||
room = location_table[locname].get_room_id(random)
|
||||
if room is not None:
|
||||
possible_rooms[room] = location_table[locname].room_id
|
||||
return random.choice(list(possible_rooms.keys()))
|
||||
|
||||
|
||||
location_table = {
|
||||
"Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4,
|
||||
[WorldPosition(0x4, 0x83, 0x47), # exit upper right
|
||||
WorldPosition(0x4, 0x12, 0x47), # exit upper left
|
||||
WorldPosition(0x4, 0x65, 0x20), # exit bottom right
|
||||
WorldPosition(0x4, 0x2A, 0x20), # exit bottom left
|
||||
WorldPosition(0x5, 0x4B, 0x60), # T room, top
|
||||
WorldPosition(0x5, 0x28, 0x1F), # T room, bottom left
|
||||
WorldPosition(0x5, 0x70, 0x1F), # T room, bottom right
|
||||
]),
|
||||
"Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x6,
|
||||
[WorldPosition(0x6, 0x8C, 0x20), # final turn bottom right
|
||||
WorldPosition(0x6, 0x03, 0x20), # final turn bottom left
|
||||
WorldPosition(0x6, 0x4B, 0x30), # final turn center
|
||||
WorldPosition(0x7, 0x4B, 0x40), # straightaway center
|
||||
WorldPosition(0x8, 0x40, 0x40), # entrance middle loop
|
||||
WorldPosition(0x8, 0x4B, 0x60), # entrance upper loop
|
||||
WorldPosition(0x8, 0x8C, 0x5E), # entrance right loop
|
||||
]),
|
||||
"Catacombs": LocationData("Overworld", "Catacombs", 0x9,
|
||||
[WorldPosition(0x9, 0x49, 0x40),
|
||||
WorldPosition(0x9, 0x4b, 0x20),
|
||||
WorldPosition(0xA),
|
||||
WorldPosition(0xA),
|
||||
WorldPosition(0xB, 0x40, 0x40),
|
||||
WorldPosition(0xB, 0x22, 0x1f),
|
||||
WorldPosition(0xB, 0x70, 0x1f)]),
|
||||
"Adjacent to Catacombs": LocationData("Overworld", "Adjacent to Catacombs", 0xC,
|
||||
[WorldPosition(0xC),
|
||||
WorldPosition(0xD)]),
|
||||
"Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE),
|
||||
"White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF),
|
||||
"Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10),
|
||||
"Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11),
|
||||
"Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12),
|
||||
"Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13,
|
||||
[WorldPosition(0x13),
|
||||
WorldPosition(0x14)]),
|
||||
"Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0xB5,
|
||||
[WorldPosition(0x15, 0x46, 0x1B)],
|
||||
needs_bat_logic=True),
|
||||
"Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x15,
|
||||
[WorldPosition(0x15),
|
||||
WorldPosition(0x16)]),
|
||||
"RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17,
|
||||
[WorldPosition(0x17, 0x70, 0x40), # right side third room
|
||||
WorldPosition(0x17, 0x18, 0x40), # left side third room
|
||||
WorldPosition(0x18, 0x20, 0x40),
|
||||
WorldPosition(0x18, 0x1A, 0x3F), # left side second room
|
||||
WorldPosition(0x18, 0x70, 0x3F), # right side second room
|
||||
]),
|
||||
"Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance", 0xB7,
|
||||
[WorldPosition(0x17, 0x50, 0x60)],
|
||||
needs_bat_logic=True),
|
||||
"Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19,
|
||||
[WorldPosition(0x19, 0x4E, 0x35)],
|
||||
needs_bat_logic=True),
|
||||
"RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x1A), # entrance
|
||||
"Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B),
|
||||
"Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C),
|
||||
"Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D),
|
||||
"Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E,
|
||||
[WorldPosition(0x1E, 0x25, 0x50)]),
|
||||
"Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0xBE,
|
||||
[WorldPosition(0x1E, 0x70, 0x40)],
|
||||
needs_bat_logic=True),
|
||||
"Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True),
|
||||
"Slay Yorgle": LocationData("Varies", "Slay Yorgle", 0xD1, event=False),
|
||||
"Slay Grundle": LocationData("Varies", "Slay Grundle", 0xD2, event=False),
|
||||
"Slay Rhindle": LocationData("Varies", "Slay Rhindle", 0xD0, event=False),
|
||||
}
|
||||
|
||||
# the old location table, for reference
|
||||
location_table_old = {
|
||||
"Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4),
|
||||
"Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x5),
|
||||
"Blue Labyrinth 2": LocationData("Overworld", "Blue Labyrinth 2", 0x6),
|
||||
"Blue Labyrinth 3": LocationData("Overworld", "Blue Labyrinth 3", 0x7),
|
||||
"Blue Labyrinth 4": LocationData("Overworld", "Blue Labyrinth 4", 0x8),
|
||||
"Catacombs0": LocationData("Overworld", "Catacombs0", 0x9),
|
||||
"Catacombs1": LocationData("Overworld", "Catacombs1", 0xA),
|
||||
"Catacombs2": LocationData("Overworld", "Catacombs2", 0xB),
|
||||
"East of Catacombs": LocationData("Overworld", "East of Catacombs", 0xC),
|
||||
"West of Catacombs": LocationData("Overworld", "West of Catacombs", 0xD),
|
||||
"Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE),
|
||||
"White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF),
|
||||
"Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10),
|
||||
"Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11),
|
||||
"Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12),
|
||||
"Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13),
|
||||
"Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x14),
|
||||
"Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0x15,
|
||||
[WorldPosition(0xB5, 0x46, 0x1B)]),
|
||||
"Dungeon2": LocationData("BlackCastle", "Dungeon2", 0x15),
|
||||
"Dungeon3": LocationData("BlackCastle", "Dungeon3", 0x16),
|
||||
"RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17, [WorldPosition(0x17, 0x70, 0x40)]),
|
||||
"RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x18, [WorldPosition(0x18, 0x20, 0x40)]),
|
||||
"Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance",
|
||||
0x17, [WorldPosition(0xB7, 0x50, 0x60)]),
|
||||
"Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19, [WorldPosition(0x19, 0x4E, 0x35)]),
|
||||
"RedMaze3": LocationData("WhiteCastle", "RedMaze3", 0x1A),
|
||||
"Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B),
|
||||
"Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C),
|
||||
"Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D),
|
||||
"Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E, [WorldPosition(0x1E, 0x25, 0x50)]),
|
||||
"Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0x1E,
|
||||
[WorldPosition(0xBE, 0x70, 0x40)]),
|
||||
"Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True)
|
||||
}
|
||||
46
worlds/adventure/Offsets.py
Normal file
46
worlds/adventure/Offsets.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# probably I should generate this from the list file
|
||||
|
||||
static_item_data_location = 0xe9d
|
||||
static_item_element_size = 9
|
||||
static_first_dragon_index = 6
|
||||
item_position_table = 0x402
|
||||
items_ram_start = 0xa1
|
||||
connector_port_offset = 0xff9
|
||||
# dragon speeds are hardcoded directly in their respective movement subroutines, not in their item table or state data
|
||||
# so this is the second byte of an LDA immediate instruction
|
||||
yorgle_speed_data_location = 0x724
|
||||
grundle_speed_data_location = 0x73f
|
||||
rhindle_speed_data_location = 0x709
|
||||
|
||||
|
||||
# in case I need to place a rom address in the rom
|
||||
rom_address_space_start = 0xf000
|
||||
|
||||
start_castle_offset = 0x39c
|
||||
start_castle_values = [0x11, 0x10, 0x0F]
|
||||
"""yellow, black, white castle gate rooms"""
|
||||
|
||||
# indexed by static item table index. 0x00 indicates the position data is in ROM and is irrelevant to the randomizer
|
||||
item_ram_addresses = [
|
||||
0xD9, # lamp
|
||||
0x00, # portcullis 1
|
||||
0x00, # portcullis 2
|
||||
0x00, # portcullis 3
|
||||
0x00, # author name
|
||||
0x00, # GO object
|
||||
0xA4, # Rhindle
|
||||
0xA9, # Yorgle
|
||||
0xAE, # Grundle
|
||||
0xB6, # Sword
|
||||
0xBC, # Bridge
|
||||
0xBF, # Yellow Key
|
||||
0xC2, # White key
|
||||
0xC5, # Black key
|
||||
0xCB, # Bat
|
||||
0xA1, # Dot
|
||||
0xB9, # Chalice
|
||||
0xB3, # Magnet
|
||||
0xE7, # AP object 1
|
||||
0xEA, # AP bat object
|
||||
0xBC, # NULL object (end of table)
|
||||
]
|
||||
244
worlds/adventure/Options.py
Normal file
244
worlds/adventure/Options.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
|
||||
|
||||
|
||||
class FreeincarnateMax(Range):
|
||||
"""How many maximum freeincarnate items to allow
|
||||
|
||||
When done generating items, any remaining item slots will be filled
|
||||
with freeincarnates, up to this maximum amount. Any remaining item
|
||||
slots after that will be 'nothing' items placed locally, so in multigame
|
||||
multiworlds, keeping this value high will allow more items from other games
|
||||
into Adventure.
|
||||
"""
|
||||
display_name = "Freeincarnate Maximum"
|
||||
range_start = 0
|
||||
range_end = 17
|
||||
default = 17
|
||||
|
||||
|
||||
class ItemRandoType(Choice):
|
||||
"""Choose how items are placed in the game
|
||||
|
||||
Not yet implemented. Currently only traditional supported
|
||||
Traditional: Adventure items are not in the map until
|
||||
they are collected (except local items) and are dropped
|
||||
on the player when collected. Adventure items are not checks.
|
||||
Inactive: Every item is placed, but is inactive until collected.
|
||||
Each item touched is a check. The bat ignores inactive items.
|
||||
|
||||
Supported values: traditional, inactive
|
||||
Default value: traditional
|
||||
"""
|
||||
|
||||
display_name = "Item type"
|
||||
option_traditional = 0x00
|
||||
option_inactive = 0x01
|
||||
default = option_traditional
|
||||
|
||||
|
||||
class DragonSlayCheck(DefaultOnToggle):
|
||||
"""If true, slaying each dragon for the first time is a check
|
||||
"""
|
||||
display_name = "Slay Dragon Checks"
|
||||
|
||||
|
||||
class TrapBatCheck(Choice):
|
||||
"""
|
||||
Locking the bat inside a castle may be a check
|
||||
|
||||
Not yet implemented
|
||||
If set to yes, the bat will not start inside a castle.
|
||||
Setting with_key requires the matching castle key to also be
|
||||
in the castle with the bat, achieved by dropping the key in the
|
||||
path of the portcullis as it falls. This setting is not recommended with the bat use_logic setting
|
||||
|
||||
Supported values: no, yes, with_key
|
||||
Default value: yes
|
||||
"""
|
||||
display_name = "Trap bat check"
|
||||
option_no_check = 0x0
|
||||
option_yes_key_optional = 0x1
|
||||
option_with_key = 0x2
|
||||
default = option_yes_key_optional
|
||||
|
||||
|
||||
class DragonRandoType(Choice):
|
||||
"""
|
||||
How to randomize the dragon starting locations
|
||||
|
||||
normal: Grundle is in the overworld, Yorgle in the white castle, and Rhindle in the black castle
|
||||
shuffle: A random dragon is placed in the overworld, one in the white castle, and one in the black castle
|
||||
overworldplus: Dragons can be placed anywhere, but at least one will be in the overworld
|
||||
randomized: Dragons can be anywhere except the credits room
|
||||
|
||||
|
||||
Supported values: normal, shuffle, overworldplus, randomized
|
||||
Default value: shuffle
|
||||
"""
|
||||
display_name = "Dragon Randomization"
|
||||
option_normal = 0x0
|
||||
option_shuffle = 0x1
|
||||
option_overworldplus = 0x2
|
||||
option_randomized = 0x3
|
||||
default = option_shuffle
|
||||
|
||||
|
||||
class BatLogic(Choice):
|
||||
"""How the bat is considered for logic
|
||||
|
||||
With cannot_break, the bat cannot pick up an item that starts out-of-logic until the player touches it
|
||||
With can_break, the bat is free to pick up any items, even if they are out-of-logic
|
||||
With use_logic, the bat can pick up anything just like can_break, and locations are no longer considered to require
|
||||
the magnet or bridge to collect, since the bat can retrieve these.
|
||||
A future option may allow the bat itself to be placed as an item.
|
||||
|
||||
Supported values: cannot_break, can_break, use_logic
|
||||
Default value: can_break
|
||||
"""
|
||||
display_name = "Bat Logic"
|
||||
option_cannot_break = 0x0
|
||||
option_can_break = 0x1
|
||||
option_use_logic = 0x2
|
||||
default = option_can_break
|
||||
|
||||
|
||||
class YorgleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Yorgle's initial speed. Yorgle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Yorgle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class YorgleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Yorgle's speed when all speed reducers are found. Yorgle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Yorgle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 1
|
||||
|
||||
|
||||
class GrundleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Grundle's initial speed. Grundle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Grundle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class GrundleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Grundle's speed when all speed reducers are found. Grundle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Grundle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 1
|
||||
|
||||
|
||||
class RhindleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Rhindle's initial speed. Rhindle has a speed of 3 in the original game
|
||||
Default value: 3
|
||||
"""
|
||||
display_name = "Rhindle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 3
|
||||
|
||||
|
||||
class RhindleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Rhindle's speed when all speed reducers are found. Rhindle has a speed of 3 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Rhindle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class ConnectorMultiSlot(Toggle):
|
||||
"""If true, the client and lua connector will add lowest 8 bits of the player slot
|
||||
to the port number used to connect to each other, to simplify connecting multiple local
|
||||
clients to local BizHawks.
|
||||
Set in the yaml, since the connector has to read this out of the rom file before connecting.
|
||||
"""
|
||||
display_name = "Connector Multi-Slot"
|
||||
|
||||
|
||||
class DifficultySwitchA(Choice):
|
||||
"""Set availability of left difficulty switch
|
||||
This controls the speed of the dragons' bite animation
|
||||
|
||||
"""
|
||||
display_name = "Left Difficulty Switch"
|
||||
option_normal = 0x0
|
||||
option_locked_hard = 0x1
|
||||
option_hard_with_unlock_item = 0x2
|
||||
default = option_hard_with_unlock_item
|
||||
|
||||
|
||||
class DifficultySwitchB(Choice):
|
||||
"""Set availability of right difficulty switch
|
||||
On hard, dragons will run away from the sword
|
||||
|
||||
"""
|
||||
display_name = "Right Difficulty Switch"
|
||||
option_normal = 0x0
|
||||
option_locked_hard = 0x1
|
||||
option_hard_with_unlock_item = 0x2
|
||||
default = option_hard_with_unlock_item
|
||||
|
||||
|
||||
class StartCastle(Choice):
|
||||
"""Choose or randomize which castle to start in front of.
|
||||
|
||||
This affects both normal start and reincarnation. Starting
|
||||
at the black castle may give easy dot runs, while starting
|
||||
at the white castle may make them more dangerous! Also, not
|
||||
starting at the yellow castle can make delivering the chalice
|
||||
with a full inventory slightly less trivial.
|
||||
|
||||
This doesn't affect logic since all the castles are reachable
|
||||
from each other.
|
||||
"""
|
||||
display_name = "Start Castle"
|
||||
option_yellow = 0
|
||||
option_black = 1
|
||||
option_white = 2
|
||||
default = option_yellow
|
||||
|
||||
|
||||
adventure_option_definitions: Dict[str, type(Option)] = {
|
||||
"dragon_slay_check": DragonSlayCheck,
|
||||
"death_link": DeathLink,
|
||||
"bat_logic": BatLogic,
|
||||
"freeincarnate_max": FreeincarnateMax,
|
||||
"dragon_rando_type": DragonRandoType,
|
||||
"connector_multi_slot": ConnectorMultiSlot,
|
||||
"yorgle_speed": YorgleStartingSpeed,
|
||||
"yorgle_min_speed": YorgleMinimumSpeed,
|
||||
"grundle_speed": GrundleStartingSpeed,
|
||||
"grundle_min_speed": GrundleMinimumSpeed,
|
||||
"rhindle_speed": RhindleStartingSpeed,
|
||||
"rhindle_min_speed": RhindleMinimumSpeed,
|
||||
"difficulty_switch_a": DifficultySwitchA,
|
||||
"difficulty_switch_b": DifficultySwitchB,
|
||||
"start_castle": StartCastle,
|
||||
|
||||
}
|
||||
160
worlds/adventure/Regions.py
Normal file
160
worlds/adventure/Regions.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
|
||||
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
||||
one_way=False, name=None):
|
||||
source_region = world.get_region(source, player)
|
||||
target_region = world.get_region(target, player)
|
||||
|
||||
if name is None:
|
||||
name = source + " to " + target
|
||||
|
||||
connection = Entrance(
|
||||
player,
|
||||
name,
|
||||
source_region
|
||||
)
|
||||
|
||||
connection.access_rule = rule
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
if not one_way:
|
||||
connect(world, player, target, source, rule, True)
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
for name, locdata in location_table.items():
|
||||
locdata.get_position(multiworld.random)
|
||||
|
||||
menu = Region("Menu", player, multiworld)
|
||||
|
||||
menu.exits.append(Entrance(player, "GameStart", menu))
|
||||
multiworld.regions.append(menu)
|
||||
|
||||
overworld = Region("Overworld", player, multiworld)
|
||||
overworld.exits.append(Entrance(player, "YellowCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "WhiteCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "BlackCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "CreditsWall", overworld))
|
||||
multiworld.regions.append(overworld)
|
||||
|
||||
yellow_castle = Region("YellowCastle", player, multiworld, "Yellow Castle")
|
||||
yellow_castle.exits.append(Entrance(player, "YellowCastleExit", yellow_castle))
|
||||
multiworld.regions.append(yellow_castle)
|
||||
|
||||
white_castle = Region("WhiteCastle", player, multiworld, "White Castle")
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastleExit", white_castle))
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastleSecretPassage", white_castle))
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastlePeekPassage", white_castle))
|
||||
multiworld.regions.append(white_castle)
|
||||
|
||||
white_castle_pre_vault_peek = Region("WhiteCastlePreVaultPeek", player, multiworld, "White Castle Secret Peek")
|
||||
white_castle_pre_vault_peek.exits.append(Entrance(player, "WhiteCastleFromPeek", white_castle_pre_vault_peek))
|
||||
multiworld.regions.append(white_castle_pre_vault_peek)
|
||||
|
||||
white_castle_secret_room = Region("WhiteCastleVault", player, multiworld, "White Castle Vault",)
|
||||
white_castle_secret_room.exits.append(Entrance(player, "WhiteCastleReturnPassage", white_castle_secret_room))
|
||||
multiworld.regions.append(white_castle_secret_room)
|
||||
|
||||
black_castle = Region("BlackCastle", player, multiworld, "Black Castle")
|
||||
black_castle.exits.append(Entrance(player, "BlackCastleExit", black_castle))
|
||||
black_castle.exits.append(Entrance(player, "BlackCastleVaultEntrance", black_castle))
|
||||
multiworld.regions.append(black_castle)
|
||||
|
||||
black_castle_secret_room = Region("BlackCastleVault", player, multiworld, "Black Castle Vault")
|
||||
black_castle_secret_room.exits.append(Entrance(player, "BlackCastleReturnPassage", black_castle_secret_room))
|
||||
multiworld.regions.append(black_castle_secret_room)
|
||||
|
||||
credits_room = Region("CreditsRoom", player, multiworld, "Credits Room")
|
||||
credits_room.exits.append(Entrance(player, "CreditsExit", credits_room))
|
||||
credits_room.exits.append(Entrance(player, "CreditsToFarSide", credits_room))
|
||||
multiworld.regions.append(credits_room)
|
||||
|
||||
credits_room_far_side = Region("CreditsRoomFarSide", player, multiworld, "Credits Far Side")
|
||||
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
|
||||
multiworld.regions.append(credits_room_far_side)
|
||||
|
||||
dragon_slay_check = multiworld.dragon_slay_check[player].value
|
||||
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
|
||||
|
||||
for name, location_data in location_table.items():
|
||||
require_sword = False
|
||||
if location_data.region == "Varies":
|
||||
if location_data.name == "Slay Yorgle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[0])
|
||||
elif location_data.name == "Slay Grundle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[1])
|
||||
elif location_data.name == "Slay Rhindle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[2])
|
||||
else:
|
||||
raise Exception(f"Unknown location region for {location_data.name}")
|
||||
r = multiworld.get_region(region_name, player)
|
||||
else:
|
||||
r = multiworld.get_region(location_data.region, player)
|
||||
|
||||
adventure_loc = AdventureLocation(player, location_data.name, location_data.location_id, r)
|
||||
if adventure_loc.name in priority_locations:
|
||||
adventure_loc.progress_type = LocationProgressType.PRIORITY
|
||||
r.locations.append(adventure_loc)
|
||||
|
||||
# In a tracker and plando-free world, I'd determine unused locations here and not add them.
|
||||
# But that would cause problems with both plandos and trackers. So I guess I'll stick
|
||||
# with filling in with 'nothing' in pre_fill.
|
||||
|
||||
# in the future, I may randomize the map some, and that will require moving
|
||||
# connections to later, probably
|
||||
|
||||
multiworld.get_entrance("GameStart", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("YellowCastlePort", player) \
|
||||
.connect(multiworld.get_region("YellowCastle", player))
|
||||
multiworld.get_entrance("YellowCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("WhiteCastlePort", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
multiworld.get_entrance("WhiteCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("WhiteCastleSecretPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastleVault", player))
|
||||
multiworld.get_entrance("WhiteCastleReturnPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
multiworld.get_entrance("WhiteCastlePeekPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastlePreVaultPeek", player))
|
||||
multiworld.get_entrance("WhiteCastleFromPeek", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
|
||||
multiworld.get_entrance("BlackCastlePort", player) \
|
||||
.connect(multiworld.get_region("BlackCastle", player))
|
||||
multiworld.get_entrance("BlackCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
multiworld.get_entrance("BlackCastleVaultEntrance", player) \
|
||||
.connect(multiworld.get_region("BlackCastleVault", player))
|
||||
multiworld.get_entrance("BlackCastleReturnPassage", player) \
|
||||
.connect(multiworld.get_region("BlackCastle", player))
|
||||
|
||||
multiworld.get_entrance("CreditsWall", player) \
|
||||
.connect(multiworld.get_region("CreditsRoom", player))
|
||||
multiworld.get_entrance("CreditsExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("CreditsToFarSide", player) \
|
||||
.connect(multiworld.get_region("CreditsRoomFarSide", player))
|
||||
multiworld.get_entrance("CreditsFromFarSide", player) \
|
||||
.connect(multiworld.get_region("CreditsRoom", player))
|
||||
|
||||
|
||||
# Placeholder for adding sets of priority locations at generation, possibly as an option in the future
|
||||
def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}:
|
||||
priority_locations = {}
|
||||
return priority_locations
|
||||
321
worlds/adventure/Rom.py
Normal file
321
worlds/adventure/Rom.py
Normal file
@@ -0,0 +1,321 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from typing import Optional, Any
|
||||
|
||||
import Utils
|
||||
from .Locations import AdventureLocation, LocationData
|
||||
from Utils import OptionsType
|
||||
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
|
||||
from itertools import chain
|
||||
|
||||
import bsdiff4
|
||||
|
||||
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"
|
||||
|
||||
|
||||
class AdventureAutoCollectLocation:
|
||||
short_location_id: int = 0
|
||||
room_id: int = 0
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
}
|
||||
|
||||
|
||||
class AdventureForeignItemInfo:
|
||||
short_location_id: int = 0
|
||||
room_id: int = 0
|
||||
room_x: int = 0
|
||||
room_y: int = 0
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
"room_x": self.room_x,
|
||||
"room_y": self.room_y,
|
||||
}
|
||||
|
||||
|
||||
class BatNoTouchLocation:
|
||||
short_location_id: int
|
||||
room_id: int
|
||||
room_x: int
|
||||
room_y: int
|
||||
local_item: int
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int, local_item: int = None):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
self.local_item = local_item
|
||||
|
||||
def get_dict(self):
|
||||
ret_dict = {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
"room_x": self.room_x,
|
||||
"room_y": self.room_y,
|
||||
}
|
||||
if self.local_item is not None:
|
||||
ret_dict["local_item"] = self.local_item
|
||||
else:
|
||||
ret_dict["local_item"] = 255
|
||||
return ret_dict
|
||||
|
||||
|
||||
class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
hash = ADVENTUREHASH
|
||||
game = "Adventure"
|
||||
patch_file_ending = ".apadvn"
|
||||
zip_version: int = 2
|
||||
|
||||
# locations: [], autocollect: [], seed_name: bytes,
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
patch_only = True
|
||||
if "autocollect" in kwargs:
|
||||
patch_only = False
|
||||
self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y)
|
||||
for loc in kwargs["locations"]]
|
||||
|
||||
self.autocollect_items: [AdventureAutoCollectLocation] = kwargs["autocollect"]
|
||||
self.seedName: bytes = kwargs["seed_name"]
|
||||
self.local_item_locations: {} = kwargs["local_item_locations"]
|
||||
self.dragon_speed_reducer_info: {} = kwargs["dragon_speed_reducer_info"]
|
||||
self.diff_a_mode: int = kwargs["diff_a_mode"]
|
||||
self.diff_b_mode: int = kwargs["diff_b_mode"]
|
||||
self.bat_logic: int = kwargs["bat_logic"]
|
||||
self.bat_no_touch_locations: [LocationData] = kwargs["bat_no_touch_locations"]
|
||||
self.rom_deltas: {int, int} = kwargs["rom_deltas"]
|
||||
del kwargs["locations"]
|
||||
del kwargs["autocollect"]
|
||||
del kwargs["seed_name"]
|
||||
del kwargs["local_item_locations"]
|
||||
del kwargs["dragon_speed_reducer_info"]
|
||||
del kwargs["diff_a_mode"]
|
||||
del kwargs["diff_b_mode"]
|
||||
del kwargs["bat_logic"]
|
||||
del kwargs["bat_no_touch_locations"]
|
||||
del kwargs["rom_deltas"]
|
||||
super(AdventureDeltaPatch, self).__init__(*args, **kwargs)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(AdventureDeltaPatch, self).write_contents(opened_zipfile)
|
||||
# write Delta
|
||||
opened_zipfile.writestr("zip_version",
|
||||
self.zip_version.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.foreign_items is not None:
|
||||
loc_bytes = []
|
||||
for foreign_item in self.foreign_items:
|
||||
loc_bytes.append(foreign_item.short_location_id)
|
||||
loc_bytes.append(foreign_item.room_id)
|
||||
loc_bytes.append(foreign_item.room_x)
|
||||
loc_bytes.append(foreign_item.room_y)
|
||||
opened_zipfile.writestr("adventure_locations",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.autocollect_items is not None:
|
||||
loc_bytes = []
|
||||
for item in self.autocollect_items:
|
||||
loc_bytes.append(item.short_location_id)
|
||||
loc_bytes.append(item.room_id)
|
||||
opened_zipfile.writestr("adventure_autocollect",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.player_name is not None:
|
||||
opened_zipfile.writestr("player",
|
||||
self.player_name, # UTF-8
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.seedName is not None:
|
||||
opened_zipfile.writestr("seedName",
|
||||
self.seedName,
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.local_item_locations is not None:
|
||||
opened_zipfile.writestr("local_item_locations",
|
||||
json.dumps(self.local_item_locations),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.dragon_speed_reducer_info is not None:
|
||||
opened_zipfile.writestr("dragon_speed_reducer_info",
|
||||
json.dumps(self.dragon_speed_reducer_info),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.diff_a_mode is not None:
|
||||
opened_zipfile.writestr("diff_a_mode",
|
||||
self.diff_a_mode.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.diff_b_mode is not None:
|
||||
opened_zipfile.writestr("diff_b_mode",
|
||||
self.diff_b_mode.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.bat_logic is not None:
|
||||
opened_zipfile.writestr("bat_logic",
|
||||
self.bat_logic.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.bat_no_touch_locations is not None:
|
||||
loc_bytes = []
|
||||
for loc in self.bat_no_touch_locations:
|
||||
loc_bytes.append(loc.short_location_id) # used for AP items managed by script
|
||||
loc_bytes.append(loc.room_id) # used for local items placed in rom
|
||||
loc_bytes.append(loc.room_x)
|
||||
loc_bytes.append(loc.room_y)
|
||||
loc_bytes.append(0xff if loc.local_item is None else loc.local_item)
|
||||
opened_zipfile.writestr("bat_no_touch_locations",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.rom_deltas is not None:
|
||||
# this is not an efficient way to do this AT ALL, but Adventure's data is so tiny it shouldn't matter
|
||||
# if you're looking at doing something like this for another game, consider encoding your rom changes
|
||||
# in a more efficient way
|
||||
opened_zipfile.writestr("rom_deltas",
|
||||
json.dumps(self.rom_deltas),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(AdventureDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile)
|
||||
self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile)
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
@classmethod
|
||||
def check_version(cls, opened_zipfile: zipfile.ZipFile) -> bool:
|
||||
version_bytes = opened_zipfile.read("zip_version")
|
||||
version = 0
|
||||
if version_bytes is not None:
|
||||
version = int.from_bytes(version_bytes, "little")
|
||||
if version != cls.zip_version:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def read_rom_info(cls, opened_zipfile: zipfile.ZipFile) -> (bytes, bytes, str):
|
||||
seedbytes: bytes = opened_zipfile.read("seedName")
|
||||
namebytes: bytes = opened_zipfile.read("player")
|
||||
namestr: str = namebytes.decode("utf-8")
|
||||
return seedbytes, namestr
|
||||
|
||||
@classmethod
|
||||
def read_difficulty_switch_info(cls, opened_zipfile: zipfile.ZipFile) -> (int, int):
|
||||
diff_a_bytes = opened_zipfile.read("diff_a_mode")
|
||||
diff_b_bytes = opened_zipfile.read("diff_b_mode")
|
||||
diff_a = 0
|
||||
diff_b = 0
|
||||
if diff_a_bytes is not None:
|
||||
diff_a = int.from_bytes(diff_a_bytes, "little")
|
||||
if diff_b_bytes is not None:
|
||||
diff_b = int.from_bytes(diff_b_bytes, "little")
|
||||
return diff_a, diff_b
|
||||
|
||||
@classmethod
|
||||
def read_bat_logic(cls, opened_zipfile: zipfile.ZipFile) -> int:
|
||||
bat_logic = opened_zipfile.read("bat_logic")
|
||||
if bat_logic is None:
|
||||
return 0
|
||||
return int.from_bytes(bat_logic, "little")
|
||||
|
||||
@classmethod
|
||||
def read_foreign_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
foreign_items = []
|
||||
readbytes: bytes = opened_zipfile.read("adventure_locations")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 4)):
|
||||
offset = i * 4
|
||||
foreign_items.append(AdventureForeignItemInfo(bytelist[offset],
|
||||
bytelist[offset + 1],
|
||||
bytelist[offset + 2],
|
||||
bytelist[offset + 3]))
|
||||
return foreign_items
|
||||
|
||||
@classmethod
|
||||
def read_bat_no_touch(cls, opened_zipfile: zipfile.ZipFile) -> [BatNoTouchLocation]:
|
||||
locations = []
|
||||
readbytes: bytes = opened_zipfile.read("bat_no_touch_locations")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 5)):
|
||||
offset = i * 5
|
||||
locations.append(BatNoTouchLocation(bytelist[offset],
|
||||
bytelist[offset + 1],
|
||||
bytelist[offset + 2],
|
||||
bytelist[offset + 3],
|
||||
bytelist[offset + 4]))
|
||||
return locations
|
||||
|
||||
@classmethod
|
||||
def read_autocollect_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
autocollect_items = []
|
||||
readbytes: bytes = opened_zipfile.read("adventure_autocollect")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 2)):
|
||||
offset = i * 2
|
||||
autocollect_items.append(AdventureAutoCollectLocation(bytelist[offset], bytelist[offset + 1]))
|
||||
return autocollect_items
|
||||
|
||||
@classmethod
|
||||
def read_local_item_locations(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
readbytes: bytes = opened_zipfile.read("local_item_locations")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def read_dragon_speed_info(cls, opened_zipfile: zipfile.ZipFile) -> {}:
|
||||
readbytes: bytes = opened_zipfile.read("dragon_speed_reducer_info")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def read_rom_deltas(cls, opened_zipfile: zipfile.ZipFile) -> {int, int}:
|
||||
readbytes: bytes = opened_zipfile.read("rom_deltas")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def apply_rom_deltas(cls, base_bytes: bytes, rom_deltas: {int, int}) -> bytearray:
|
||||
rom_bytes = bytearray(base_bytes)
|
||||
for offset, value in rom_deltas.items():
|
||||
int_offset = int(offset)
|
||||
rom_bytes[int_offset:int_offset+1] = int.to_bytes(value, 1, "little")
|
||||
return rom_bytes
|
||||
|
||||
|
||||
def apply_basepatch(base_rom_bytes: bytes) -> bytes:
|
||||
with open(os.path.join(os.path.dirname(__file__), "../../data/adventure_basepatch.bsdiff4"), "rb") as basepatch:
|
||||
delta: bytes = basepatch.read()
|
||||
return bsdiff4.patch(base_rom_bytes, delta)
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
file_name = get_base_rom_path(file_name)
|
||||
with open(file_name, "rb") as file:
|
||||
base_rom_bytes = bytes(file.read())
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if ADVENTUREHASH != basemd5.hexdigest():
|
||||
raise Exception(f"Supplied Base Rom does not match known MD5 for Adventure. "
|
||||
"Get the correct game and version, then dump it")
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: OptionsType = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["adventure_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
98
worlds/adventure/Rules.py
Normal file
98
worlds/adventure/Rules.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from worlds.adventure import location_table
|
||||
from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA
|
||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||
from BaseClasses import LocationProgressType
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
world = self.multiworld
|
||||
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
|
||||
|
||||
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
||||
lambda state: state.has("Yellow Key", self.player))
|
||||
set_rule(world.get_entrance("BlackCastlePort", self.player),
|
||||
lambda state: state.has("Black Key", self.player))
|
||||
set_rule(world.get_entrance("WhiteCastlePort", self.player),
|
||||
lambda state: state.has("White Key", self.player))
|
||||
|
||||
# a future thing would be to make the bat an actual item, or at least allow it to
|
||||
# be placed in a castle, which would require some additions to the rules when
|
||||
# use_bat_logic is true
|
||||
if not use_bat_logic:
|
||||
set_rule(world.get_entrance("WhiteCastleSecretPassage", self.player),
|
||||
lambda state: state.has("Bridge", self.player))
|
||||
set_rule(world.get_entrance("WhiteCastlePeekPassage", self.player),
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
set_rule(world.get_entrance("BlackCastleVaultEntrance", self.player),
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
|
||||
dragon_slay_check = world.dragon_slay_check[self.player].value
|
||||
if dragon_slay_check:
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
set_rule(world.get_location("Slay Grundle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
else:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
set_rule(world.get_location("Slay Grundle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
|
||||
# really this requires getting the dot item, and having another item or enemy
|
||||
# in the room, but the dot would be *super evil*
|
||||
# to actually make randomized, since it is invisible. May add some options
|
||||
# for how that works in the distant future, but for now, just say you need
|
||||
# the bridge and black key to get to it, as that simplifies things a lot
|
||||
set_rule(world.get_entrance("CreditsWall", self.player),
|
||||
lambda state: state.has("Bridge", self.player) and
|
||||
state.has("Black Key", self.player))
|
||||
|
||||
if not use_bat_logic:
|
||||
set_rule(world.get_entrance("CreditsToFarSide", self.player),
|
||||
lambda state: state.has("Magnet", self.player))
|
||||
|
||||
# bridge literally does not fit in this space, I think. I'll just exclude it
|
||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Bridge", self.player)
|
||||
# don't put magnet in locations that can pull in-logic items out of reach unless the bat is in play
|
||||
if not use_bat_logic:
|
||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Magnet", self.player)
|
||||
forbid_item(world.get_location("Red Maze Vault Entrance", self.player), "Magnet", self.player)
|
||||
forbid_item(world.get_location("Credits Right Side", self.player), "Magnet", self.player)
|
||||
|
||||
# and obviously we don't want to start with the game already won
|
||||
forbid_item(world.get_location("Inside Yellow Castle", self.player), "Chalice", self.player)
|
||||
overworld = world.get_region("Overworld", self.player)
|
||||
|
||||
for loc in overworld.locations:
|
||||
forbid_item(loc, "Chalice", self.player)
|
||||
|
||||
add_rule(world.get_location("Chalice Home", self.player),
|
||||
lambda state: state.has("Chalice", self.player) and state.has("Yellow Key", self.player))
|
||||
|
||||
# world.random.choice(overworld.locations).progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
# all_locations = world.get_locations(self.player).copy()
|
||||
# while priority_count < get_num_items():
|
||||
# loc = world.random.choice(all_locations)
|
||||
# if loc.progress_type == LocationProgressType.DEFAULT:
|
||||
# loc.progress_type = LocationProgressType.PRIORITY
|
||||
# priority_count += 1
|
||||
# all_locations.remove(loc)
|
||||
|
||||
# TODO: Add events for dragon_slay_check and trap_bat_check. Here? Elsewhere?
|
||||
# if self.dragon_slay_check == 1:
|
||||
# TODO - Randomize bat and dragon start rooms and use those to determine rules
|
||||
# TODO - for the requirements for the slay event (since we have to get to the
|
||||
# TODO - dragons and sword to kill them). Unless the dragons are set to be items,
|
||||
# TODO - which might be a funny option, then they can just be randoed like normal
|
||||
# TODO - just forbidden from the vaults and all credits room locations
|
||||
391
worlds/adventure/__init__.py
Normal file
391
worlds/adventure/__init__.py
Normal file
@@ -0,0 +1,391 @@
|
||||
import base64
|
||||
import copy
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \
|
||||
LocationProgressType
|
||||
from Main import __version__
|
||||
from Options import AssembleOptions
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Fill import fill_restrictive
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
|
||||
AdventureAutoCollectLocation
|
||||
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
|
||||
from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions
|
||||
from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \
|
||||
static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \
|
||||
rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
|
||||
|
||||
from worlds.LauncherComponents import Component, components, SuffixIdentifier
|
||||
|
||||
# Adventure
|
||||
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))
|
||||
|
||||
|
||||
class AdventureWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up Adventure for MultiWorld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["JusticePS"]
|
||||
)]
|
||||
theme = "dirt"
|
||||
|
||||
|
||||
def get_item_position_data_start(table_index: int):
|
||||
item_ram_address = item_ram_addresses[table_index];
|
||||
return item_position_table + item_ram_address - items_ram_start
|
||||
|
||||
|
||||
class AdventureWorld(World):
|
||||
"""
|
||||
Adventure for the Atari 2600 is an early graphical adventure game.
|
||||
Find the enchanted chalice and return it to the yellow castle,
|
||||
using magic items to enter hidden rooms, retrieve out of
|
||||
reach items, or defeat the three dragons. Beware the bat
|
||||
who likes to steal your equipment!
|
||||
"""
|
||||
game: ClassVar[str] = "Adventure"
|
||||
web: ClassVar[WebWorld] = AdventureWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||
data_version: ClassVar[int] = 1
|
||||
required_client_version: Tuple[int, int, int] = (0, 3, 9)
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.rom_name: Optional[bytearray] = bytearray("", "utf8" )
|
||||
self.dragon_rooms: [int] = [0x14, 0x19, 0x4]
|
||||
self.dragon_slay_check: Optional[int] = 0
|
||||
self.connector_multi_slot: Optional[int] = 0
|
||||
self.dragon_rando_type: Optional[int] = 0
|
||||
self.yorgle_speed: Optional[int] = 2
|
||||
self.yorgle_min_speed: Optional[int] = 2
|
||||
self.grundle_speed: Optional[int] = 2
|
||||
self.grundle_min_speed: Optional[int] = 2
|
||||
self.rhindle_speed: Optional[int] = 3
|
||||
self.rhindle_min_speed: Optional[int] = 3
|
||||
self.difficulty_switch_a: Optional[int] = 0
|
||||
self.difficulty_switch_b: Optional[int] = 0
|
||||
self.start_castle: Optional[int] = 0
|
||||
# dict of item names -> list of speed deltas
|
||||
self.dragon_speed_reducer_info: {} = {}
|
||||
self.created_items: int = 0
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, _multiworld: MultiWorld) -> None:
|
||||
# don't need rom anymore
|
||||
pass
|
||||
|
||||
def place_random_dragon(self, dragon_index: int):
|
||||
region_list = ["Overworld", "YellowCastle", "BlackCastle", "WhiteCastle"]
|
||||
self.dragon_rooms[dragon_index] = get_random_room_in_regions(region_list, self.multiworld.random)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.rom_name = \
|
||||
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
|
||||
self.rom_name.extend([0] * (21 - len(self.rom_name)))
|
||||
|
||||
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
|
||||
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
|
||||
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
|
||||
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
|
||||
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
|
||||
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
|
||||
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
|
||||
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
|
||||
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
|
||||
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
|
||||
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
|
||||
self.start_castle = self.multiworld.start_castle[self.player].value
|
||||
self.created_items = 0
|
||||
|
||||
if self.dragon_slay_check == 0:
|
||||
item_table["Sword"].classification = ItemClassification.useful
|
||||
else:
|
||||
item_table["Sword"].classification = ItemClassification.progression
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
item_table["Right Difficulty Switch"].classification = ItemClassification.progression
|
||||
|
||||
if self.dragon_rando_type == DragonRandoType.option_shuffle:
|
||||
self.multiworld.random.shuffle(self.dragon_rooms)
|
||||
elif self.dragon_rando_type == DragonRandoType.option_overworldplus:
|
||||
dragon_indices = [0, 1, 2]
|
||||
overworld_forced_index = self.multiworld.random.choice(dragon_indices)
|
||||
dragon_indices.remove(overworld_forced_index)
|
||||
region_list = ["Overworld"]
|
||||
self.dragon_rooms[overworld_forced_index] = get_random_room_in_regions(region_list, self.multiworld.random)
|
||||
self.place_random_dragon(dragon_indices[0])
|
||||
self.place_random_dragon(dragon_indices[1])
|
||||
elif self.dragon_rando_type == DragonRandoType.option_randomized:
|
||||
self.place_random_dragon(0)
|
||||
self.place_random_dragon(1)
|
||||
self.place_random_dragon(2)
|
||||
|
||||
def create_items(self) -> None:
|
||||
for event in map(self.create_item, event_table):
|
||||
self.multiworld.itempool.append(event)
|
||||
exclude = [item for item in self.multiworld.precollected_items[self.player]]
|
||||
self.created_items = 0
|
||||
for item in map(self.create_item, item_table):
|
||||
if item.code == nothing_item_id:
|
||||
continue
|
||||
if item in exclude and item.code <= standard_item_max:
|
||||
exclude.remove(item) # this is destructive. create unique list above
|
||||
else:
|
||||
if item.code <= standard_item_max:
|
||||
self.multiworld.itempool.append(item)
|
||||
self.created_items += 1
|
||||
num_locations = len(location_table) - 1 # subtract out the chalice location
|
||||
if self.dragon_slay_check == 0:
|
||||
num_locations -= 3
|
||||
|
||||
if self.difficulty_switch_a == DifficultySwitchA.option_hard_with_unlock_item:
|
||||
self.multiworld.itempool.append(self.create_item("Left Difficulty Switch"))
|
||||
self.created_items += 1
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
self.multiworld.itempool.append(self.create_item("Right Difficulty Switch"))
|
||||
self.created_items += 1
|
||||
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
self.dragon_speed_reducer_info = {}
|
||||
# make sure yorgle doesn't take 2 if there's not enough for the others to get at least one
|
||||
if extra_filler_count <= 4:
|
||||
extra_filler_count = 1
|
||||
self.create_dragon_slow_items(self.yorgle_min_speed, self.yorgle_speed, "Slow Yorgle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
if extra_filler_count <= 3:
|
||||
extra_filler_count = 1
|
||||
self.create_dragon_slow_items(self.grundle_min_speed, self.grundle_speed, "Slow Grundle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
self.create_dragon_slow_items(self.rhindle_min_speed, self.rhindle_speed, "Slow Rhindle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
# traps would probably go here, if enabled
|
||||
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
|
||||
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
|
||||
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
|
||||
self.created_items += actual_freeincarnates
|
||||
|
||||
def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, maximum_items: int):
|
||||
if min_speed < speed:
|
||||
delta = speed - min_speed
|
||||
if delta > 2 and maximum_items >= 2:
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
speed_with_one = speed - math.floor(delta / 2)
|
||||
self.dragon_speed_reducer_info[item_table[item_name].id] = [speed_with_one, min_speed]
|
||||
self.created_items += 2
|
||||
elif maximum_items >= 1:
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
self.dragon_speed_reducer_info[item_table[item_name].id] = [min_speed]
|
||||
self.created_items += 1
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self.multiworld, self.player, self.dragon_rooms)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||
self.create_event("Victory", ItemClassification.progression))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def pre_fill(self):
|
||||
# Place empty items in filler locations here, to limit
|
||||
# the number of exported empty items and the density of stuff in overworld.
|
||||
max_location_count = len(location_table) - 1
|
||||
if self.dragon_slay_check == 0:
|
||||
max_location_count -= 3
|
||||
|
||||
force_empty_item_count = (max_location_count - self.created_items)
|
||||
if force_empty_item_count <= 0:
|
||||
return
|
||||
overworld = self.multiworld.get_region("Overworld", self.player)
|
||||
overworld_locations_copy = overworld.locations.copy()
|
||||
all_locations = self.multiworld.get_locations(self.player)
|
||||
|
||||
locations_copy = all_locations.copy()
|
||||
for loc in all_locations:
|
||||
if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT:
|
||||
locations_copy.remove(loc)
|
||||
if loc in overworld_locations_copy:
|
||||
overworld_locations_copy.remove(loc)
|
||||
|
||||
# guarantee at least one overworld location, so we can for sure get a key somewhere
|
||||
# if too much stuff is plando'd though, just let it go
|
||||
if len(overworld_locations_copy) >= 3:
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# if we have few items, enforce another overworld slot, fill a hard slot, and ensure we have
|
||||
# at least one hard slot available
|
||||
if self.created_items < 15:
|
||||
hard_locations = []
|
||||
for loc in locations_copy:
|
||||
if "Vault" in loc.name or "Credits" in loc.name:
|
||||
hard_locations.append(loc)
|
||||
force_empty_item_count -= 1
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
hard_locations.remove(loc)
|
||||
locations_copy.remove(loc)
|
||||
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
locations_copy.remove(loc)
|
||||
hard_locations.remove(loc)
|
||||
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# if we have very few items, fill another two difficult slots
|
||||
if self.created_items < 10:
|
||||
for i in range(2):
|
||||
force_empty_item_count -= 1
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
hard_locations.remove(loc)
|
||||
locations_copy.remove(loc)
|
||||
|
||||
# for the absolute minimum number of items, enforce a third overworld slot
|
||||
if self.created_items <= 7:
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# finally, place nothing items
|
||||
while force_empty_item_count > 0 and locations_copy:
|
||||
force_empty_item_count -= 1
|
||||
# prefer somewhat to thin out the overworld.
|
||||
if len(overworld_locations_copy) > 0 and self.multiworld.random.randint(0, 10) < 4:
|
||||
loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
else:
|
||||
loc = self.multiworld.random.choice(locations_copy)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
locations_copy.remove(loc)
|
||||
if loc in overworld_locations_copy:
|
||||
overworld_locations_copy.remove(loc)
|
||||
|
||||
def place_dragons(self, rom_deltas: {int, int}):
|
||||
for i in range(3):
|
||||
table_index = static_first_dragon_index + i
|
||||
item_position_data_start = get_item_position_data_start(table_index)
|
||||
rom_deltas[item_position_data_start] = self.dragon_rooms[i]
|
||||
|
||||
def set_dragon_speeds(self, rom_deltas: {int, int}):
|
||||
rom_deltas[yorgle_speed_data_location] = self.yorgle_speed
|
||||
rom_deltas[grundle_speed_data_location] = self.grundle_speed
|
||||
rom_deltas[rhindle_speed_data_location] = self.rhindle_speed
|
||||
|
||||
def set_start_castle(self, rom_deltas):
|
||||
start_castle_value = start_castle_values[self.start_castle]
|
||||
rom_deltas[start_castle_offset] = start_castle_value
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.bin")
|
||||
foreign_item_locations: [LocationData] = []
|
||||
auto_collect_locations: [AdventureAutoCollectLocation] = []
|
||||
local_item_to_location: {int, int} = {}
|
||||
bat_no_touch_locs: [LocationData] = []
|
||||
bat_logic: int = self.multiworld.bat_logic[self.player].value
|
||||
try:
|
||||
rom_deltas: { int, int } = {}
|
||||
self.place_dragons(rom_deltas)
|
||||
self.set_dragon_speeds(rom_deltas)
|
||||
self.set_start_castle(rom_deltas)
|
||||
# start and stop indices are offsets in the ROM file, not Adventure ROM addresses (which start at f000)
|
||||
|
||||
# This places the local items (I still need to make it easy to inject the offset data)
|
||||
unplaced_local_items = dict(filter(lambda x: nothing_item_id < x[1].id <= standard_item_max,
|
||||
item_table.items()))
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
# 'nothing' items, which are autocollected when the room is entered
|
||||
if location.item.player == self.player and \
|
||||
location.item.name == "nothing":
|
||||
location_data = location_table[location.name]
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
# standard Adventure items, which are placed in the rom
|
||||
elif location.item.player == self.player and \
|
||||
location.item.name != "nothing" and \
|
||||
location.item.code is not None and \
|
||||
location.item.code <= standard_item_max:
|
||||
# I need many of the intermediate values here.
|
||||
item_table_offset = item_table[location.item.name].table_index * static_item_element_size
|
||||
item_ram_address = item_ram_addresses[item_table[location.item.name].table_index]
|
||||
item_position_data_start = item_position_table + item_ram_address - items_ram_start
|
||||
location_data = location_table[location.name]
|
||||
room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player])
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
copied_location = copy.copy(location_data)
|
||||
copied_location.local_item = item_ram_address
|
||||
bat_no_touch_locs.append(copied_location)
|
||||
del unplaced_local_items[location.item.name]
|
||||
|
||||
rom_deltas[item_position_data_start] = location_data.room_id
|
||||
rom_deltas[item_position_data_start + 1] = room_x
|
||||
rom_deltas[item_position_data_start + 2] = room_y
|
||||
local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \
|
||||
- base_location_id
|
||||
# items from other worlds, and non-standard Adventure items handled by script, like difficulty switches
|
||||
elif location.item.code is not None:
|
||||
if location.item.code != nothing_item_id:
|
||||
location_data = location_table[location.name]
|
||||
foreign_item_locations.append(location_data)
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
bat_no_touch_locs.append(location_data)
|
||||
else:
|
||||
location_data = location_table[location.name]
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
# Adventure items that are in another world get put in an invalid room until needed
|
||||
for unplaced_item_name, unplaced_item in unplaced_local_items.items():
|
||||
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
|
||||
rom_deltas[item_position_data_start] = 0xff
|
||||
|
||||
if self.multiworld.connector_multi_slot[self.player].value:
|
||||
rom_deltas[connector_port_offset] = (self.player & 0xff)
|
||||
else:
|
||||
rom_deltas[connector_port_offset] = 0
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
patch = AdventureDeltaPatch(os.path.splitext(rom_path)[0] + AdventureDeltaPatch.patch_file_ending,
|
||||
player=self.player, player_name=self.multiworld.player_name[self.player],
|
||||
locations=foreign_item_locations,
|
||||
autocollect=auto_collect_locations, local_item_locations=local_item_to_location,
|
||||
dragon_speed_reducer_info=self.dragon_speed_reducer_info,
|
||||
diff_a_mode=self.difficulty_switch_a, diff_b_mode=self.difficulty_switch_b,
|
||||
bat_logic=bat_logic, bat_no_touch_locations=bat_no_touch_locs,
|
||||
rom_deltas=rom_deltas,
|
||||
seed_name=bytes(self.multiworld.seed_name, encoding="ascii"))
|
||||
patch.write()
|
||||
finally:
|
||||
if os.path.exists(rom_path):
|
||||
os.unlink(rom_path)
|
||||
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_data: ItemData = item_table.get(name)
|
||||
return AdventureItem(name, item_data.classification, item_data.id, self.player)
|
||||
|
||||
def create_event(self, name: str, classification: ItemClassification) -> Item:
|
||||
return AdventureItem(name, classification, None, self.player)
|
||||
62
worlds/adventure/docs/en_Adventure.md
Normal file
62
worlds/adventure/docs/en_Adventure.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Adventure
|
||||
|
||||
## Where is the settings page?
|
||||
The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All
|
||||
Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized,
|
||||
slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates'
|
||||
can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist
|
||||
to reduce their speeds.
|
||||
|
||||
## What is the goal of Adventure when randomized?
|
||||
Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle
|
||||
|
||||
## Which items can be in another player's world?
|
||||
All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on
|
||||
settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
|
||||
|
||||
## What is considered a location check in Adventure?
|
||||
Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item.
|
||||
A few rooms have two potential locaions. If the location contains a 'nothing' Adventure item, it will send a check when
|
||||
that is seen. If it contains an item from another Adventure or other game, it will show a rough approximation of the
|
||||
Archipelago logo that can be touched for a check. Touching a local Adventure item also 'checks' it, allowing it to be
|
||||
retrieved after a select-reset or hard reset.
|
||||
|
||||
## Why isn't my item where the spoiler says it should be?
|
||||
If something isn't where the spoiler says, most likely the bat carried it somewhere else. The bat's ability to shuffle
|
||||
items around makes it somewhat unique in Archipelago. Touching the item, wherever it is, will award the location check
|
||||
for wherever the item was originally placed.
|
||||
|
||||
## Which notable items are not randomized?
|
||||
The bat, dot, and map are not yet randomized. If the chalice is local, it is randomized, but is always in either a
|
||||
castle or the credits screen. Forcing the chalice local in the yaml is recommended.
|
||||
|
||||
## What does another world's item look like in Adventure?
|
||||
It looks vaguely like a flashing Archipelago logo.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
A message is shown in the client log. While empty handed, the player can press the fire button to retrieve items in the
|
||||
order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to
|
||||
return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions.
|
||||
|
||||
## What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
## My yellow key is stuck in a wall! Am I softlocked?
|
||||
Maybe! That's all part of Adventure. If you have access to the magnet, bridge, or bat, you might be able to retrieve
|
||||
it. In general, since the bat always starts outside of castles, you should always be able to find it unless you lock
|
||||
it in a castle yourself. This mod's inventory system allows you to quickly recover all the items
|
||||
you've collected after a hard reset or select-reset (except for the dot), so usually it's not as bad as in vanilla.
|
||||
|
||||
## How do I get into the credits room? There's a item I need in there.
|
||||
Searching for 'Adventure dot map' should bring up an AtariAge map with a good walkthrough, but here's the basics.
|
||||
Bring the bridge into the black castle. Find the small room in the dungeon that cannot be reached without the bridge,
|
||||
enter it, and push yourself into the bottom right corner to pick up the dot. The dot color matches the background,
|
||||
so you won't be able to see it if it isn't in a wall, so be careful not to drop it. Bring it to the room one south and
|
||||
one east of the yellow castle and drop it there. Bring 2-3 more objects (the bat and dragons also count for this) until
|
||||
it lets you walk through the right wall.
|
||||
If the item is on the right side, you'll need the magnet to get it.
|
||||
70
worlds/adventure/docs/setup_en.md
Normal file
70
worlds/adventure/docs/setup_en.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Setup Guide for Adventure: Archipelago
|
||||
|
||||
## Important
|
||||
|
||||
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
|
||||
|
||||
## Required Software
|
||||
|
||||
- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
|
||||
- Detailed installation instructions for Bizhawk can be found at the above link.
|
||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
(select `Adventure Client` during installation).
|
||||
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
|
||||
|
||||
## Configuring Bizhawk
|
||||
|
||||
Once Bizhawk has been installed, open Bizhawk and change the following settings:
|
||||
|
||||
- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
|
||||
"Lua+LuaInterface". Then restart Bizhawk. This is required for the Lua script to function correctly.
|
||||
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
|
||||
**of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
|
||||
**"NLua+KopiLua" until this step is done.**
|
||||
- Under Config > Customize, check the "Run in background" box. This will prevent disconnecting from the client while
|
||||
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
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings)
|
||||
|
||||
### What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your Adventure patch file
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
|
||||
files. Your data file should have a `.apadvn` extension.
|
||||
|
||||
Drag your patch file to the AdventureClient.exe to start your client and start the ROM patch process. Once the process
|
||||
is finished (this can take a while), the client and the emulator will be started automatically (if you set the emulator
|
||||
path as recommended).
|
||||
|
||||
### Connect to the Multiserver
|
||||
|
||||
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.
|
||||
|
||||
Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`.
|
||||
|
||||
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]`)
|
||||
|
||||
Press Reset and begin playing
|
||||
@@ -7,6 +7,8 @@ from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import lookup_boss_drops
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .SubClasses import ALttPLocation
|
||||
|
||||
def create_dungeons(world, player):
|
||||
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
||||
@@ -138,9 +140,10 @@ def fill_dungeons_restrictive(world):
|
||||
if in_dungeon_items:
|
||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if
|
||||
restricted}
|
||||
locations = [location for location in get_unfilled_dungeon_locations(world)
|
||||
# filter boss
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
|
||||
locations: typing.List["ALttPLocation"] = [
|
||||
location for location in get_unfilled_dungeon_locations(world)
|
||||
# filter boss
|
||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
|
||||
if dungeon_specific:
|
||||
for location in locations:
|
||||
dungeon = location.parent_region.dungeon
|
||||
@@ -159,7 +162,7 @@ def fill_dungeons_restrictive(world):
|
||||
(5 if (item.player, item.name) in dungeon_specific else 0))
|
||||
for item in in_dungeon_items:
|
||||
all_state_base.remove(item)
|
||||
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True)
|
||||
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True)
|
||||
|
||||
|
||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
|
||||
@@ -300,4 +300,13 @@ item_table = (
|
||||
'Roomba with a Knife',
|
||||
'Wet Cat',
|
||||
'The missing moderator, Frostwares',
|
||||
'1,793 Crossbows',
|
||||
'Holographic First Edition Charizard (Gen 1)',
|
||||
'VR Headset',
|
||||
'Archipelago 1.0 Release Date',
|
||||
'Strand of Galadriel\'s Hair',
|
||||
'Can of Meow-Mix',
|
||||
'Shake-Weight',
|
||||
'DVD Collection of Billy Mays Infomercials',
|
||||
'Old CD Key',
|
||||
)
|
||||
|
||||
@@ -16,22 +16,28 @@ class ArchipIDLELogic(LogicMixin):
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
for i in range(16, 31):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 4)
|
||||
)
|
||||
|
||||
for i in range(31, 51):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 10)
|
||||
)
|
||||
|
||||
for i in range(51, 101):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 20)
|
||||
)
|
||||
|
||||
for i in range(101, 201):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 40)
|
||||
)
|
||||
|
||||
world.completion_condition[player] =\
|
||||
lambda state:\
|
||||
state.can_reach(world.get_location("IDLE for at least 50 minutes 0 seconds", player), "Location", player)
|
||||
state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
|
||||
|
||||
@@ -25,7 +25,7 @@ class ArchipIDLEWorld(World):
|
||||
"""
|
||||
game = "ArchipIDLE"
|
||||
topology_present = False
|
||||
data_version = 4
|
||||
data_version = 5
|
||||
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
|
||||
web = ArchipIDLEWebWorld()
|
||||
|
||||
@@ -37,8 +37,8 @@ class ArchipIDLEWorld(World):
|
||||
|
||||
location_name_to_id = {}
|
||||
start_id = 9000
|
||||
for i in range(1, 101):
|
||||
location_name_to_id[f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds"] = start_id
|
||||
for i in range(1, 201):
|
||||
location_name_to_id[f"IDLE item number {i}"] = start_id
|
||||
start_id += 1
|
||||
|
||||
def generate_basic(self):
|
||||
@@ -46,10 +46,10 @@ class ArchipIDLEWorld(World):
|
||||
self.multiworld.random.shuffle(item_table_copy)
|
||||
|
||||
item_pool = []
|
||||
for i in range(100):
|
||||
for i in range(200):
|
||||
item = ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 20 else ItemClassification.filler,
|
||||
ItemClassification.progression if i < 40 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
self.player
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from BaseClasses import Tutorial
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld, World
|
||||
|
||||
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
settings_page = "games/Sudoku/info/en"
|
||||
@@ -24,6 +25,7 @@ class Bk_SudokuWorld(World):
|
||||
"""
|
||||
game = "Sudoku"
|
||||
web = Bk_SudokuWebWorld()
|
||||
data_version = 1
|
||||
|
||||
item_name_to_id: Dict[str, int] = {}
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
@@ -38,10 +38,12 @@ class ExpertLogic(Toggle):
|
||||
|
||||
|
||||
class Ending(Choice):
|
||||
"""Choose which ending is required to complete the game."""
|
||||
"""Choose which ending is required to complete the game.
|
||||
Ending A: Collect all thorn upgrades.
|
||||
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
|
||||
display_name = "Ending"
|
||||
option_any_ending = 0
|
||||
option_ending_b = 1
|
||||
option_ending_a = 1
|
||||
option_ending_c = 2
|
||||
default = 0
|
||||
|
||||
|
||||
13
worlds/clique/Options.py
Normal file
13
worlds/clique/Options.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import Dict
|
||||
|
||||
from Options import Option, Toggle
|
||||
|
||||
|
||||
class HardMode(Toggle):
|
||||
"""Only for masochists: requires 2 presses!"""
|
||||
display_name = "Hard Mode"
|
||||
|
||||
|
||||
clique_options: Dict[str, type(Option)] = {
|
||||
"hard_mode": HardMode
|
||||
}
|
||||
109
worlds/clique/__init__.py
Normal file
109
worlds/clique/__init__.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.generic.Rules import set_rule
|
||||
from .Options import clique_options
|
||||
|
||||
item_table = {
|
||||
"The feeling of satisfaction.": 69696969,
|
||||
"Button Key": 69696968,
|
||||
}
|
||||
|
||||
location_table = {
|
||||
"The Button": 69696969,
|
||||
"The Desk": 69696968,
|
||||
}
|
||||
|
||||
|
||||
class CliqueWebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Clique.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["Phar"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
"""The greatest game ever designed. Full of exciting gameplay!"""
|
||||
|
||||
game = "Clique"
|
||||
topology_present = False
|
||||
data_version = 1
|
||||
web = CliqueWebWorld()
|
||||
option_definitions = clique_options
|
||||
|
||||
location_name_to_id = location_table
|
||||
item_name_to_id = item_table
|
||||
|
||||
def create_item(self, name: str) -> "Item":
|
||||
return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
|
||||
|
||||
def get_setting(self, name: str):
|
||||
return getattr(self.multiworld, name)[self.player]
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return {option_name: self.get_setting(option_name).value for option_name in self.option_definitions}
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.multiworld.itempool.append(self.create_item("The feeling of satisfaction."))
|
||||
|
||||
if self.multiworld.hard_mode[self.player]:
|
||||
self.multiworld.itempool.append(self.create_item("Button Key"))
|
||||
|
||||
def create_regions(self) -> None:
|
||||
if self.multiworld.hard_mode[self.player]:
|
||||
self.multiworld.regions += [
|
||||
create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]),
|
||||
create_region(self.multiworld, self.player, "THE BUTTON", self.location_name_to_id)
|
||||
]
|
||||
else:
|
||||
self.multiworld.regions += [
|
||||
create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]),
|
||||
create_region(self.multiworld, self.player, "THE BUTTON", {"The Button": 69696969})
|
||||
]
|
||||
|
||||
self.multiworld.get_entrance("Entrance to THE BUTTON", self.player)\
|
||||
.connect(self.multiworld.get_region("THE BUTTON", self.player))
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(item_table)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
if self.multiworld.hard_mode[self.player]:
|
||||
set_rule(
|
||||
self.multiworld.get_location("The Button", self.player),
|
||||
lambda state: state.has("Button Key", self.player)
|
||||
)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
state.has("Button Key", self.player)
|
||||
else:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
state.has("The feeling of satisfaction.", self.player)
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
region = Region(name, player, world)
|
||||
if locations:
|
||||
for location_name in locations.keys():
|
||||
location = CliqueLocation(player, location_name, locations[location_name], region)
|
||||
region.locations.append(location)
|
||||
|
||||
if exits:
|
||||
for _exit in exits:
|
||||
region.exits.append(Entrance(player, _exit, region))
|
||||
|
||||
return region
|
||||
|
||||
|
||||
class CliqueItem(Item):
|
||||
game = "Clique"
|
||||
|
||||
|
||||
class CliqueLocation(Location):
|
||||
game: str = "Clique"
|
||||
11
worlds/clique/docs/en_Clique.md
Normal file
11
worlds/clique/docs/en_Clique.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Clique
|
||||
|
||||
## What is this game?
|
||||
|
||||
Even I don't know.
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure
|
||||
and export a config file.
|
||||
|
||||
6
worlds/clique/docs/guide_en.md
Normal file
6
worlds/clique/docs/guide_en.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Clique Start Guide
|
||||
|
||||
Go to the [Clique Game](http://clique.darkshare.site.nfoservers.com/) and enter the hostname:ip address,
|
||||
then your slot name.
|
||||
|
||||
Enjoy.
|
||||
@@ -468,7 +468,6 @@ painted_world_table = { # DLC
|
||||
"PW: Vilhelm's Armor": 0x113130E8,
|
||||
"PW: Vilhelm's Gauntlets": 0x113134D0,
|
||||
"PW: Vilhelm's Leggings": 0x113138B8,
|
||||
"PW: Vilhelm's Leggings": 0x113138B8,
|
||||
"PW: Valorheart": 0x00F646E0, # GRAVETENDER FIGHT
|
||||
"PW: Champions Bones": 0x40000869, # GRAVETENDER FIGHT
|
||||
"PW: Onyx Blade": 0x00222E00, # VILHELM FIGHT
|
||||
|
||||
@@ -100,7 +100,7 @@ the lua you are using in your file explorer and copy the `socket.dll` to the bas
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the button to open a new Lua script.
|
||||
4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`.
|
||||
5. Select the `Connector.lua` file included with your client
|
||||
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
|
||||
emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
|
||||
|
||||
@@ -136,6 +136,7 @@ def generate_mod(world: "Factorio", output_directory: str):
|
||||
"goal": multiworld.goal[player].value,
|
||||
"energy_link": multiworld.energy_link[player].value,
|
||||
"useless_technologies": useless_technologies,
|
||||
"chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0,
|
||||
}
|
||||
|
||||
for factorio_option in Options.factorio_options:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle
|
||||
from schema import Schema, Optional, And, Or
|
||||
@@ -197,6 +198,14 @@ class RecipeIngredients(Choice):
|
||||
option_science_pack = 1
|
||||
|
||||
|
||||
class RecipeIngredientsOffset(Range):
|
||||
"""When randomizing ingredients, remove or add this many "slots" of items.
|
||||
For example, at -1 a randomized Automation Science Pack will only require 1 ingredient, instead of 2."""
|
||||
display_name = "Randomized Recipe Ingredients Offset"
|
||||
range_start = -1
|
||||
range_end = 5
|
||||
|
||||
|
||||
class FactorioStartItems(ItemDict):
|
||||
"""Mapping of Factorio internal item-name to amount granted on start."""
|
||||
display_name = "Starting Items"
|
||||
@@ -223,9 +232,36 @@ class AttackTrapCount(TrapCount):
|
||||
display_name = "Attack Traps"
|
||||
|
||||
|
||||
class TeleportTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger a random teleport."""
|
||||
display_name = "Teleport Traps"
|
||||
|
||||
|
||||
class GrenadeTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger a grenade explosion on each player."""
|
||||
display_name = "Grenade Traps"
|
||||
|
||||
|
||||
class ClusterGrenadeTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger a cluster grenade explosion on each player."""
|
||||
display_name = "Cluster Grenade Traps"
|
||||
|
||||
|
||||
class ArtilleryTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger an artillery shell on each player."""
|
||||
display_name = "Artillery Traps"
|
||||
|
||||
|
||||
class AtomicRocketTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger an atomic rocket explosion on each player.
|
||||
Warning: there is no warning. The launch is instantaneous."""
|
||||
display_name = "Atomic Rocket Traps"
|
||||
|
||||
|
||||
class EvolutionTrapCount(TrapCount):
|
||||
"""Trap items that when received increase the enemy evolution."""
|
||||
display_name = "Evolution Traps"
|
||||
range_end = 10
|
||||
|
||||
|
||||
class EvolutionTrapIncrease(Range):
|
||||
@@ -404,12 +440,31 @@ factorio_options: typing.Dict[str, type(Option)] = {
|
||||
"free_sample_whitelist": FactorioFreeSampleWhitelist,
|
||||
"recipe_time": RecipeTime,
|
||||
"recipe_ingredients": RecipeIngredients,
|
||||
"recipe_ingredients_offset": RecipeIngredientsOffset,
|
||||
"imported_blueprints": ImportedBlueprint,
|
||||
"world_gen": FactorioWorldGen,
|
||||
"progressive": Progressive,
|
||||
"evolution_traps": EvolutionTrapCount,
|
||||
"teleport_traps": TeleportTrapCount,
|
||||
"grenade_traps": GrenadeTrapCount,
|
||||
"cluster_grenade_traps": ClusterGrenadeTrapCount,
|
||||
"artillery_traps": ArtilleryTrapCount,
|
||||
"atomic_rocket_traps": AtomicRocketTrapCount,
|
||||
"attack_traps": AttackTrapCount,
|
||||
"evolution_traps": EvolutionTrapCount,
|
||||
"evolution_trap_increase": EvolutionTrapIncrease,
|
||||
"death_link": DeathLink,
|
||||
"energy_link": EnergyLink
|
||||
"energy_link": EnergyLink,
|
||||
}
|
||||
|
||||
# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else.
|
||||
if datetime.datetime.today().month == 4:
|
||||
|
||||
class ChunkShuffle(Toggle):
|
||||
"""Entrance Randomizer."""
|
||||
display_name = "Chunk Shuffle"
|
||||
|
||||
|
||||
if datetime.datetime.today().day > 1:
|
||||
ChunkShuffle.__doc__ += """
|
||||
2023 April Fool's option. Shuffles chunk border transitions."""
|
||||
factorio_options["chunk_shuffle"] = ChunkShuffle
|
||||
|
||||
@@ -15,6 +15,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
||||
fluids, stacking_items, valid_ingredients, progressive_rows
|
||||
from .Locations import location_pools, location_table
|
||||
from worlds.LauncherComponents import Component, components
|
||||
|
||||
components.append(Component("Factorio Client", "FactorioClient"))
|
||||
|
||||
|
||||
class FactorioWeb(WebWorld):
|
||||
@@ -35,6 +38,11 @@ class FactorioItem(Item):
|
||||
all_items = tech_table.copy()
|
||||
all_items["Attack Trap"] = factorio_base_id - 1
|
||||
all_items["Evolution Trap"] = factorio_base_id - 2
|
||||
all_items["Teleport Trap"] = factorio_base_id - 3
|
||||
all_items["Grenade Trap"] = factorio_base_id - 4
|
||||
all_items["Cluster Grenade Trap"] = factorio_base_id - 5
|
||||
all_items["Artillery Trap"] = factorio_base_id - 6
|
||||
all_items["Atomic Rocket Trap"] = factorio_base_id - 7
|
||||
|
||||
|
||||
class Factorio(World):
|
||||
@@ -43,7 +51,7 @@ class Factorio(World):
|
||||
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
|
||||
research new technologies, and become more efficient in your quest to build a rocket and return home.
|
||||
"""
|
||||
game: str = "Factorio"
|
||||
game = "Factorio"
|
||||
special_nodes = {"automation", "logistics", "rocket-silo"}
|
||||
custom_recipes: typing.Dict[str, Recipe]
|
||||
location_pool: typing.List[FactorioScienceLocation]
|
||||
@@ -52,12 +60,11 @@ class Factorio(World):
|
||||
web = FactorioWeb()
|
||||
|
||||
item_name_to_id = all_items
|
||||
# TODO: remove base_tech_table ~ 0.3.7
|
||||
location_name_to_id = {**base_tech_table, **location_table}
|
||||
location_name_to_id = location_table
|
||||
item_name_groups = {
|
||||
"Progressive": set(progressive_tech_table.keys()),
|
||||
}
|
||||
data_version = 6
|
||||
data_version = 7
|
||||
required_client_version = (0, 3, 6)
|
||||
|
||||
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
|
||||
@@ -73,8 +80,10 @@ class Factorio(World):
|
||||
generate_output = generate_mod
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player],
|
||||
self.multiworld.min_tech_cost[self.player])
|
||||
# if max < min, then swap max and min
|
||||
if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]:
|
||||
self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \
|
||||
self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value
|
||||
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
|
||||
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn
|
||||
|
||||
@@ -87,14 +96,25 @@ class Factorio(World):
|
||||
nauvis = Region("Nauvis", player, self.multiworld)
|
||||
|
||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||
self.multiworld.evolution_traps[player].value + self.multiworld.attack_traps[player].value
|
||||
self.multiworld.evolution_traps[player] + \
|
||||
self.multiworld.attack_traps[player] + \
|
||||
self.multiworld.teleport_traps[player] + \
|
||||
self.multiworld.grenade_traps[player] + \
|
||||
self.multiworld.cluster_grenade_traps[player] + \
|
||||
self.multiworld.atomic_rocket_traps[player] + \
|
||||
self.multiworld.artillery_traps[player]
|
||||
|
||||
location_pool = []
|
||||
|
||||
for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()):
|
||||
location_pool.extend(location_pools[pack])
|
||||
try:
|
||||
location_names = self.multiworld.random.sample(location_pool, location_count)
|
||||
except ValueError as e:
|
||||
# should be "ValueError: Sample larger than population or is negative"
|
||||
raise Exception("Too many traps for too few locations. Either decrease the trap count, "
|
||||
f"or increase the location count (higher max science pack). (Player {self.player})") from e
|
||||
|
||||
location_names = self.multiworld.random.sample(location_pool, location_count)
|
||||
self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
|
||||
for loc_name in location_names]
|
||||
distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player]
|
||||
@@ -132,6 +152,14 @@ class Factorio(World):
|
||||
crash.connect(nauvis)
|
||||
self.multiworld.regions += [menu, nauvis]
|
||||
|
||||
def create_items(self) -> None:
|
||||
player = self.player
|
||||
traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket")
|
||||
for trap_name in traps:
|
||||
self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in
|
||||
range(getattr(self.multiworld,
|
||||
f"{trap_name.lower().replace(' ', '_')}_traps")[player]))
|
||||
|
||||
def set_rules(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
@@ -184,10 +212,6 @@ class Factorio(World):
|
||||
player = self.player
|
||||
want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player].
|
||||
want_progressives(self.multiworld.random))
|
||||
self.multiworld.itempool.extend(self.create_item("Evolution Trap") for _ in
|
||||
range(self.multiworld.evolution_traps[player].value))
|
||||
self.multiworld.itempool.extend(self.create_item("Attack Trap") for _ in
|
||||
range(self.multiworld.attack_traps[player].value))
|
||||
|
||||
cost_sorted_locations = sorted(self.locations, key=lambda location: location.name)
|
||||
special_index = {"automation": 0,
|
||||
@@ -265,10 +289,11 @@ class Factorio(World):
|
||||
2: "chemistry"}
|
||||
return categories.get(liquids, category)
|
||||
|
||||
def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2) -> Recipe:
|
||||
def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2,
|
||||
ingredients_offset: int = 0) -> Recipe:
|
||||
new_ingredients = {}
|
||||
liquids_used = 0
|
||||
for _ in original.ingredients:
|
||||
for _ in range(len(original.ingredients) + ingredients_offset):
|
||||
new_ingredient = pool.pop()
|
||||
if new_ingredient in fluids:
|
||||
while liquids_used == allow_liquids and new_ingredient in fluids:
|
||||
@@ -282,7 +307,7 @@ class Factorio(World):
|
||||
original.products, original.energy)
|
||||
|
||||
def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: float = 1,
|
||||
allow_liquids: int = 2) -> Recipe:
|
||||
allow_liquids: int = 2, ingredients_offset: int = 0) -> Recipe:
|
||||
"""Generate a recipe from pool with time and cost similar to original * factor"""
|
||||
new_ingredients = {}
|
||||
# have to first sort for determinism, while filtering out non-stacking items
|
||||
@@ -291,7 +316,7 @@ class Factorio(World):
|
||||
self.multiworld.random.shuffle(pool)
|
||||
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
|
||||
target_energy = original.total_energy * factor
|
||||
target_num_ingredients = len(original.ingredients)
|
||||
target_num_ingredients = len(original.ingredients) + ingredients_offset
|
||||
remaining_raw = target_raw
|
||||
remaining_energy = target_energy
|
||||
remaining_num_ingredients = target_num_ingredients
|
||||
@@ -382,12 +407,13 @@ class Factorio(World):
|
||||
return custom_technologies
|
||||
|
||||
def set_custom_recipes(self):
|
||||
ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player]
|
||||
original_rocket_part = recipes["rocket-part"]
|
||||
science_pack_pools = get_science_pack_pools()
|
||||
valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
|
||||
self.multiworld.random.shuffle(valid_pool)
|
||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||
{valid_pool[x]: 10 for x in range(3)},
|
||||
{valid_pool[x]: 10 for x in range(3 + ingredients_offset)},
|
||||
original_rocket_part.products,
|
||||
original_rocket_part.energy)}
|
||||
|
||||
@@ -397,7 +423,8 @@ class Factorio(World):
|
||||
valid_pool += sorted(science_pack_pools[pack])
|
||||
self.multiworld.random.shuffle(valid_pool)
|
||||
if pack in recipes: # skips over space science pack
|
||||
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool)
|
||||
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset=
|
||||
ingredients_offset)
|
||||
self.custom_recipes[pack] = new_recipe
|
||||
|
||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
|
||||
@@ -407,21 +434,27 @@ class Factorio(World):
|
||||
valid_pool |= science_pack_pools[pack]
|
||||
|
||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe:
|
||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7)
|
||||
new_recipe = self.make_balanced_recipe(
|
||||
recipes["rocket-silo"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
||||
ingredients_offset=ingredients_offset)
|
||||
self.custom_recipes["rocket-silo"] = new_recipe
|
||||
|
||||
if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe:
|
||||
new_recipe = self.make_balanced_recipe(recipes["satellite"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7)
|
||||
new_recipe = self.make_balanced_recipe(
|
||||
recipes["satellite"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7,
|
||||
ingredients_offset=ingredients_offset)
|
||||
self.custom_recipes["satellite"] = new_recipe
|
||||
bridge = "ap-energy-bridge"
|
||||
new_recipe = self.make_quick_recipe(
|
||||
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1},
|
||||
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1,
|
||||
"replace_4": 1, "replace_5": 1, "replace_6": 1},
|
||||
{bridge: 1}, 10),
|
||||
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]))
|
||||
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
|
||||
ingredients_offset=ingredients_offset)
|
||||
for ingredient_name in new_recipe.ingredients:
|
||||
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(10, 100)
|
||||
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
|
||||
self.custom_recipes[bridge] = new_recipe
|
||||
|
||||
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
|
||||
@@ -452,7 +485,7 @@ class Factorio(World):
|
||||
tech_table[name], self.player)
|
||||
|
||||
item = FactorioItem(name,
|
||||
ItemClassification.trap if "Trap" in name else ItemClassification.filler,
|
||||
ItemClassification.trap if name.endswith("Trap") else ItemClassification.filler,
|
||||
all_items[name], self.player)
|
||||
return item
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Berserker55 and Dewiniaid
|
||||
Copyright (c) 2023 Berserker55
|
||||
Copyright (c) 2021 Dewiniaid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,32 +1,3 @@
|
||||
function filter_ingredients(ingredients, ingredient_filter)
|
||||
local new_ingredient_list = {}
|
||||
for _, ingredient_table in pairs(ingredients) do
|
||||
if ingredient_filter[ingredient_table[1]] then -- name of ingredient_table
|
||||
table.insert(new_ingredient_list, ingredient_table)
|
||||
end
|
||||
end
|
||||
|
||||
return new_ingredient_list
|
||||
end
|
||||
|
||||
function add_ingredients(ingredients, added_ingredients)
|
||||
local new_ingredient_list = table.deepcopy(ingredients)
|
||||
for new_ingredient, count in pairs(added_ingredients) do
|
||||
local found = false
|
||||
for _, old_ingredient in pairs(ingredients) do
|
||||
if old_ingredient[1] == new_ingredient then
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
table.insert(new_ingredient_list, {new_ingredient, count})
|
||||
end
|
||||
end
|
||||
|
||||
return new_ingredient_list
|
||||
end
|
||||
|
||||
function get_any_stack_size(name)
|
||||
local item = game.item_prototypes[name]
|
||||
if item ~= nil then
|
||||
@@ -50,4 +21,19 @@ function split(s, sep)
|
||||
string.gsub(s, pattern, function(c) fields[#fields + 1] = c end)
|
||||
|
||||
return fields
|
||||
end
|
||||
|
||||
function random_offset_position(position, offset)
|
||||
return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-1024, 1024)}
|
||||
end
|
||||
|
||||
function fire_entity_at_players(entity_name, speed)
|
||||
for _, player in ipairs(game.forces["player"].players) do
|
||||
current_character = player.character
|
||||
if current_character ~= nil then
|
||||
current_character.surface.create_entity{name=entity_name,
|
||||
position=random_offset_position(current_character.position, 128),
|
||||
target=current_character, speed=speed}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -11,7 +11,7 @@ TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
|
||||
MAX_SCIENCE_PACK = {{ max_science_pack }}
|
||||
GOAL = {{ goal }}
|
||||
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
|
||||
ENERGY_INCREMENT = {{ energy_link * 1000000 }}
|
||||
ENERGY_INCREMENT = {{ energy_link * 10000000 }}
|
||||
ENERGY_LINK_EFFICIENCY = 0.75
|
||||
|
||||
if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
|
||||
@@ -22,6 +22,119 @@ end
|
||||
|
||||
CURRENTLY_DEATH_LOCK = 0
|
||||
|
||||
{% if chunk_shuffle %}
|
||||
LAST_POSITIONS = {}
|
||||
GENERATOR = nil
|
||||
NORTH = 1
|
||||
EAST = 2
|
||||
SOUTH = 3
|
||||
WEST = 4
|
||||
ER_COLOR = {1, 1, 1, 0.2}
|
||||
ER_SEED = {{ random.randint(4294967295, 2*4294967295)}}
|
||||
CURRENTLY_MOVING = false
|
||||
ER_FRAMES = {}
|
||||
CHUNK_OFFSET = {
|
||||
[NORTH] = {0, 1},
|
||||
[EAST] = {1, 0},
|
||||
[SOUTH] = {0, -1},
|
||||
[WEST] = {-1, 0}
|
||||
}
|
||||
|
||||
|
||||
function on_player_changed_position(event)
|
||||
if CURRENTLY_MOVING == true then
|
||||
return
|
||||
end
|
||||
local player_id = event.player_index
|
||||
local player = game.get_player(player_id)
|
||||
local character = player.character -- can be nil, such as spectators
|
||||
|
||||
if character == nil then
|
||||
return
|
||||
end
|
||||
local last_position = LAST_POSITIONS[player_id]
|
||||
if last_position == nil then
|
||||
LAST_POSITIONS[player_id] = character.position
|
||||
return
|
||||
end
|
||||
|
||||
last_x_chunk = math.floor(last_position.x / 32)
|
||||
current_x_chunk = math.floor(character.position.x / 32)
|
||||
last_y_chunk = math.floor(last_position.y / 32)
|
||||
current_y_chunk = math.floor(character.position.y / 32)
|
||||
if (ER_FRAMES[player_id] ~= nil and rendering.is_valid(ER_FRAMES[player_id])) then
|
||||
rendering.destroy(ER_FRAMES[player_id])
|
||||
end
|
||||
ER_FRAMES[player_id] = rendering.draw_rectangle{
|
||||
color=ER_COLOR, width=1, filled=false, left_top = {current_x_chunk*32, current_y_chunk*32},
|
||||
right_bottom={current_x_chunk*32+32, current_y_chunk*32+32}, players={player}, time_to_live=60,
|
||||
draw_on_ground= true, only_in_alt_mode = true, surface=character.surface}
|
||||
if current_x_chunk == last_x_chunk and current_y_chunk == last_y_chunk then -- nothing needs doing
|
||||
return
|
||||
end
|
||||
if ((last_position.x - character.position.x) ^ 2 + (last_position.y - character.position.y) ^ 2) > 4000 then
|
||||
-- distance too high, death or other teleport took place
|
||||
LAST_POSITIONS[player_id] = character.position
|
||||
return
|
||||
end
|
||||
-- we'll need a deterministic random state
|
||||
if GENERATOR == nil or not GENERATOR.valid then
|
||||
GENERATOR = game.create_random_generator()
|
||||
end
|
||||
|
||||
-- sufficiently random pattern
|
||||
GENERATOR.re_seed((ER_SEED + (last_x_chunk * 1730000000) + (last_y_chunk * 97000)) % 4294967295)
|
||||
-- we now need all 4 exit directions deterministically shuffled to the 4 outgoing directions.
|
||||
local exit_table = {
|
||||
[1] = 1,
|
||||
[2] = 2,
|
||||
[3] = 3,
|
||||
[4] = 4
|
||||
}
|
||||
exit_table = fisher_yates_shuffle(exit_table)
|
||||
if current_x_chunk > last_x_chunk then -- going right/east
|
||||
outbound_direction = EAST
|
||||
elseif current_x_chunk < last_x_chunk then -- going left/west
|
||||
outbound_direction = WEST
|
||||
end
|
||||
|
||||
if current_y_chunk > last_y_chunk then -- going down/south
|
||||
outbound_direction = SOUTH
|
||||
elseif current_y_chunk < last_y_chunk then -- going up/north
|
||||
outbound_direction = NORTH
|
||||
end
|
||||
local target_direction = exit_table[outbound_direction]
|
||||
|
||||
local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16,
|
||||
(CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16}
|
||||
target_position = character.surface.find_non_colliding_position(character.prototype.name,
|
||||
target_position, 32, 0.5)
|
||||
if target_position ~= nil then
|
||||
rendering.draw_circle{color = ER_COLOR, radius = 1, filled = true,
|
||||
target = {character.position.x, character.position.y}, surface = character.surface,
|
||||
time_to_live = 300, draw_on_ground = true}
|
||||
rendering.draw_line{color = ER_COLOR, width = 3, gap_length = 0.5, dash_length = 0.5,
|
||||
from = {character.position.x, character.position.y}, to = target_position,
|
||||
surface = character.surface,
|
||||
time_to_live = 300, draw_on_ground = true}
|
||||
CURRENTLY_MOVING = true -- prevent recursive event
|
||||
character.teleport(target_position)
|
||||
CURRENTLY_MOVING = false
|
||||
end
|
||||
LAST_POSITIONS[player_id] = character.position
|
||||
end
|
||||
|
||||
function fisher_yates_shuffle(tbl)
|
||||
for i = #tbl, 2, -1 do
|
||||
local j = GENERATOR(i)
|
||||
tbl[i], tbl[j] = tbl[j], tbl[i]
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
|
||||
{% endif %}
|
||||
|
||||
function on_check_energy_link(event)
|
||||
--- assuming 1 MJ increment and 5MJ battery:
|
||||
--- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
|
||||
@@ -180,8 +293,8 @@ script.on_event(defines.events.on_player_removed, on_player_removed)
|
||||
|
||||
function on_rocket_launched(event)
|
||||
if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then
|
||||
if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then
|
||||
global.forcedata[event.rocket.force.name]['victory'] = 1
|
||||
if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then
|
||||
global.forcedata[event.rocket.force.name]['victory'] = 1
|
||||
dumpInfo(event.rocket.force)
|
||||
game.set_game_state
|
||||
{
|
||||
@@ -190,8 +303,8 @@ function on_rocket_launched(event)
|
||||
can_continue = true,
|
||||
victorious_force = event.rocket.force
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)
|
||||
|
||||
@@ -236,7 +349,7 @@ function update_player(index)
|
||||
end
|
||||
else
|
||||
player.print("Unable to receive " .. count .. "x [item=" .. name .. "] as this item does not exist.")
|
||||
samples[name] = nil
|
||||
samples[name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -254,9 +367,9 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e
|
||||
function add_samples(force, name, count)
|
||||
local function add_to_table(t)
|
||||
if count <= 0 then
|
||||
-- Fixes a bug with single craft, if a recipe gives 0 of a given item.
|
||||
return
|
||||
end
|
||||
-- Fixes a bug with single craft, if a recipe gives 0 of a given item.
|
||||
return
|
||||
end
|
||||
t[name] = (t[name] or 0) + count
|
||||
end
|
||||
-- Add to global table of earned samples for future new players
|
||||
@@ -298,8 +411,8 @@ script.on_event(defines.events.on_research_finished, function(event)
|
||||
--Don't acknowledge AP research as an Editor Extensions test force
|
||||
--Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces
|
||||
--are worked on exclusively in editor mode.
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
||||
-- check if it came from the server anyway, then we don't need to double send.
|
||||
dumpInfo(technology.force) --is sendable
|
||||
@@ -510,6 +623,37 @@ commands.add_command("ap-print", "Used by the Archipelago client to print messag
|
||||
game.print(call.parameter)
|
||||
end)
|
||||
|
||||
TRAP_TABLE = {
|
||||
["Attack Trap"] = function ()
|
||||
game.surfaces["nauvis"].build_enemy_base(game.forces["player"].get_spawn_position(game.get_surface(1)), 25)
|
||||
end,
|
||||
["Evolution Trap"] = function ()
|
||||
game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor))
|
||||
game.print({"", "New evolution factor:", game.forces["enemy"].evolution_factor})
|
||||
end,
|
||||
["Teleport Trap"] = function ()
|
||||
for _, player in ipairs(game.forces["player"].players) do
|
||||
current_character = player.character
|
||||
if current_character ~= nil then
|
||||
current_character.teleport(current_character.surface.find_non_colliding_position(
|
||||
current_character.prototype.name, random_offset_position(current_character.position, 1024), 0, 1))
|
||||
end
|
||||
end
|
||||
end,
|
||||
["Grenade Trap"] = function ()
|
||||
fire_entity_at_players("grenade", 0.1)
|
||||
end,
|
||||
["Cluster Grenade Trap"] = function ()
|
||||
fire_entity_at_players("cluster-grenade", 0.1)
|
||||
end,
|
||||
["Artillery Trap"] = function ()
|
||||
fire_entity_at_players("artillery-projectile", 1)
|
||||
end,
|
||||
["Atomic Rocket Trap"] = function ()
|
||||
fire_entity_at_players("atomic-rocket", 0.1)
|
||||
end,
|
||||
}
|
||||
|
||||
commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call)
|
||||
if global.index_sync == nil then
|
||||
global.index_sync = {}
|
||||
@@ -552,18 +696,11 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
tech.researched = true
|
||||
end
|
||||
end
|
||||
elseif item_name == "Attack Trap" then
|
||||
if global.index_sync[index] == nil then -- not yet received trap
|
||||
game.print({"", "Received Attack Trap from ", source})
|
||||
global.index_sync[index] = item_name
|
||||
local spawn_position = force.get_spawn_position(game.get_surface(1))
|
||||
game.surfaces["nauvis"].build_enemy_base(spawn_position, 25)
|
||||
end
|
||||
elseif item_name == "Evolution Trap" then
|
||||
elseif TRAP_TABLE[item_name] ~= nil then
|
||||
if global.index_sync[index] == nil then -- not yet received trap
|
||||
global.index_sync[index] = item_name
|
||||
game.forces["enemy"].evolution_factor = game.forces["enemy"].evolution_factor + (TRAP_EVO_FACTOR * (1 - game.forces["enemy"].evolution_factor))
|
||||
game.print({"", "Received Evolution Trap from ", source, ". New factor:", game.forces["enemy"].evolution_factor})
|
||||
game.print({"", "Received ", item_name, " from ", source})
|
||||
TRAP_TABLE[item_name]()
|
||||
end
|
||||
else
|
||||
game.print("Unknown Item " .. item_name)
|
||||
|
||||
@@ -14,9 +14,9 @@ local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"])
|
||||
energy_bridge.name = "ap-energy-bridge"
|
||||
energy_bridge.minable.result = "ap-energy-bridge"
|
||||
energy_bridge.localised_name = "Archipelago EnergyLink Bridge"
|
||||
energy_bridge.energy_source.buffer_capacity = "5MJ"
|
||||
energy_bridge.energy_source.input_flow_limit = "1MW"
|
||||
energy_bridge.energy_source.output_flow_limit = "1MW"
|
||||
energy_bridge.energy_source.buffer_capacity = "50MJ"
|
||||
energy_bridge.energy_source.input_flow_limit = "10MW"
|
||||
energy_bridge.energy_source.output_flow_limit = "10MW"
|
||||
tint_icon(energy_bridge, energy_bridge_tint())
|
||||
energy_bridge.picture.layers[1].tint = energy_bridge_tint()
|
||||
energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint()
|
||||
|
||||
@@ -39,6 +39,6 @@ EnergyLink is an energy storage supported by certain games that is shared across
|
||||
In Factorio, if enabled in the player settings, EnergyLink Bridge buildings can be crafted and placed, which allow
|
||||
depositing excess energy and supplementing energy deficits, much like Accumulators.
|
||||
|
||||
Each placed EnergyLink Bridge provides 1 MW of throughput. The shared storage has unlimited capacity, but 25% of energy
|
||||
Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy
|
||||
is lost during depositing. The amount of energy currently in the shared storage is displayed in the Archipelago client.
|
||||
It can also be queried by typing `/energy-link` in-game.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user