mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-16 04:23:45 -07:00
Compare commits
5 Commits
0.4.0
...
allow_coll
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce892cbb8 | ||
|
|
bc7389fbaa | ||
|
|
9cb5a7fc3a | ||
|
|
4311e8dbe2 | ||
|
|
d961022bff |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -36,7 +36,8 @@ jobs:
|
||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||
- name: Build
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade pip setuptools
|
||||
pip install -r requirements.txt
|
||||
python setup.py build_exe --yes
|
||||
$NAME="$(ls build)".Split('.',2)[1]
|
||||
$ZIP_NAME="Archipelago_$NAME.7z"
|
||||
@@ -84,7 +85,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 charset-normalizer
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
|
||||
pip install -r requirements.txt
|
||||
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`"
|
||||
@@ -94,10 +96,6 @@ 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:
|
||||
flake8:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8
|
||||
pip install flake8 pytest pytest-subtests
|
||||
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,8 +63,9 @@ 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 charset-normalizer
|
||||
python setup.py build_exe --yes bdist_appimage --yes
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject setuptools charset-normalizer
|
||||
pip install -r requirements.txt
|
||||
python setup.py build --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
|
||||
pip install pytest pytest-subtests
|
||||
python -m pip install --upgrade pip wheel
|
||||
pip install flake8 pytest pytest-subtests
|
||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||
- name: Unittests
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,7 +26,6 @@
|
||||
*multisave
|
||||
*.archipelago
|
||||
*.apsave
|
||||
*.BIN
|
||||
|
||||
build
|
||||
bundle/components.wxs
|
||||
@@ -53,7 +52,6 @@ Output Logs/
|
||||
/setup.ini
|
||||
/installdelete.iss
|
||||
/data/user.kv
|
||||
/datapackage
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -1,516 +0,0 @@
|
||||
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,6 +67,7 @@ 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]]
|
||||
@@ -336,7 +337,7 @@ class MultiWorld():
|
||||
return self.player_name[player]
|
||||
|
||||
def get_file_safe_player_name(self, player: int) -> str:
|
||||
return Utils.get_file_safe_name(self.get_player_name(player))
|
||||
return ''.join(c for c in self.get_player_name(player) if c not in '<>:"/\\|?*')
|
||||
|
||||
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 """
|
||||
@@ -1208,7 +1209,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([self.multiworld.get_name_string_for_object(item) for item in
|
||||
self.playthrough = {"0": sorted([str(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
|
||||
|
||||
# data package
|
||||
# datapackage
|
||||
# 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_data_package(network_data_package)
|
||||
self.update_datapackage(network_data_package)
|
||||
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||
@@ -399,40 +399,32 @@ class CommonContext:
|
||||
self.input_task.cancel()
|
||||
|
||||
# DataPackage
|
||||
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]):
|
||||
async def prepare_datapackage(self, relevant_games: typing.Set[str],
|
||||
remote_datepackage_versions: typing.Dict[str, int]):
|
||||
"""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_date_package_versions and game not in remote_data_package_checksums:
|
||||
if game not in remote_datepackage_versions:
|
||||
continue
|
||||
remote_version: int = remote_datepackage_versions[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
|
||||
if remote_version == 0: # custom datapackage 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 (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")
|
||||
if remote_version > local_version:
|
||||
cache_version: int = cache_package.get(game, {}).get("version", 0)
|
||||
# download remote version if cache is not new enough
|
||||
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
|
||||
or remote_checksum != cache_checksum:
|
||||
if remote_version > cache_version:
|
||||
needed_updates.add(game)
|
||||
else:
|
||||
self.update_game(cached_game)
|
||||
self.update_game(cache_package[game])
|
||||
if needed_updates:
|
||||
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
|
||||
|
||||
@@ -442,17 +434,15 @@ class CommonContext:
|
||||
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||
self.location_names[location_id] = location_name
|
||||
|
||||
def update_data_package(self, data_package: dict):
|
||||
for game, game_data in data_package["games"].items():
|
||||
self.update_game(game_data)
|
||||
def update_datapackage(self, data_package: dict):
|
||||
for game, gamedata in data_package["games"].items():
|
||||
self.update_game(gamedata)
|
||||
|
||||
def consume_network_data_package(self, data_package: dict):
|
||||
self.update_data_package(data_package)
|
||||
def consume_network_datapackage(self, data_package: dict):
|
||||
self.update_datapackage(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
|
||||
|
||||
@@ -671,16 +661,14 @@ 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 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)
|
||||
# update datapackage
|
||||
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
|
||||
|
||||
await ctx.server_auth(args['password'])
|
||||
|
||||
elif cmd == 'DataPackage':
|
||||
logger.info("Got new ID/Name DataPackage")
|
||||
ctx.consume_network_data_package(args['data'])
|
||||
ctx.consume_network_datapackage(args['data'])
|
||||
|
||||
elif cmd == 'ConnectionRefused':
|
||||
errors = args["errors"]
|
||||
|
||||
41
Fill.py
41
Fill.py
@@ -23,27 +23,15 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
|
||||
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
itempool: 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, 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
|
||||
"""
|
||||
allow_partial: bool = False) -> None:
|
||||
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 item_pool:
|
||||
for item in itempool:
|
||||
reachable_items.setdefault(item.player, deque()).append(item)
|
||||
|
||||
while any(reachable_items.values()) and locations:
|
||||
@@ -51,9 +39,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:
|
||||
item_pool.remove(item)
|
||||
itempool.remove(item)
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, item_pool + unplaced_items)
|
||||
base_state, itempool + unplaced_items)
|
||||
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
@@ -123,7 +111,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
reachable_items[placed_item.player].appendleft(
|
||||
placed_item)
|
||||
item_pool.append(placed_item)
|
||||
itempool.append(placed_item)
|
||||
|
||||
break
|
||||
|
||||
@@ -145,21 +133,6 @@ 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():
|
||||
@@ -169,7 +142,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)}')
|
||||
|
||||
item_pool.extend(unplaced_items)
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def remaining_fill(world: MultiWorld,
|
||||
|
||||
906
KH2Client.py
906
KH2Client.py
@@ -1,906 +0,0 @@
|
||||
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,11 +14,10 @@ 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 Sequence, Union, Optional
|
||||
|
||||
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier
|
||||
from typing import Iterable, Sequence, Callable, Union, Optional
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
@@ -71,12 +70,104 @@ def browse_files():
|
||||
webbrowser.open(file)
|
||||
|
||||
|
||||
components.extend([
|
||||
# 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')),
|
||||
# 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]):
|
||||
|
||||
@@ -1,609 +0,0 @@
|
||||
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])
|
||||
world.player_types[slot], bool(world.allow_collect[slot].value))
|
||||
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,11 +355,13 @@ 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)
|
||||
|
||||
# embedded data package
|
||||
data_package = {
|
||||
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||
for game_world in world.worlds.values()
|
||||
}
|
||||
# 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
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
@@ -376,7 +378,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": data_package,
|
||||
"datapackage": datapackage,
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
|
||||
|
||||
@@ -77,34 +77,49 @@ def read_apmc_file(apmc_file):
|
||||
return json.loads(b64decode(f.read()))
|
||||
|
||||
|
||||
def update_mod(forge_dir, url: str):
|
||||
def update_mod(forge_dir, minecraft_version: str, get_prereleases=False):
|
||||
"""Check mod version, download new mod from GitHub releases page if needed. """
|
||||
ap_randomizer = find_ap_randomizer_jar(forge_dir)
|
||||
os.path.basename(url)
|
||||
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 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)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
def check_eula(forge_dir):
|
||||
@@ -249,13 +264,8 @@ 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, 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)
|
||||
except StopIteration:
|
||||
logging.error(f"No compatible mod version found for client version {version}.")
|
||||
|
||||
|
||||
def is_correct_forge(forge_dir) -> bool:
|
||||
@@ -276,8 +286,6 @@ 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
|
||||
@@ -288,12 +296,12 @@ if __name__ == '__main__':
|
||||
options = Utils.get_options()
|
||||
channel = args.channel or options["minecraft_options"]["release_channel"]
|
||||
apmc_data = None
|
||||
data_version = args.data_version or None
|
||||
data_version = 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 and data_version is None:
|
||||
if apmc_file is not None:
|
||||
apmc_data = read_apmc_file(apmc_file)
|
||||
data_version = apmc_data.get('client_version', '')
|
||||
|
||||
@@ -303,7 +311,6 @@ 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:
|
||||
@@ -337,7 +344,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, mod_url)
|
||||
update_mod(forge_dir, f"MC{forge_version.split('-')[0]}", channel != "release")
|
||||
replace_apmc_files(forge_dir, apmc_file)
|
||||
check_eula(forge_dir)
|
||||
server_process = run_forge_server(forge_dir, java_version, max_heap)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import pkg_resources
|
||||
import warnings
|
||||
|
||||
local_dir = os.path.dirname(__file__)
|
||||
@@ -21,50 +22,18 @@ 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"])
|
||||
|
||||
|
||||
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"])
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
|
||||
|
||||
|
||||
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):
|
||||
@@ -83,7 +52,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 = ""
|
||||
@@ -110,7 +79,11 @@ def update(yes=False, force=False):
|
||||
if not yes:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
confirm(f"Requirement {requirement} is not satisfied, press enter to install it")
|
||||
try:
|
||||
input(f"\nRequirement {requirement} is not satisfied, press enter to install it")
|
||||
except KeyboardInterrupt:
|
||||
print("\nAborting")
|
||||
sys.exit(1)
|
||||
update_command()
|
||||
return
|
||||
|
||||
|
||||
@@ -7,20 +7,17 @@ import functools
|
||||
import logging
|
||||
import zlib
|
||||
import collections
|
||||
import datetime
|
||||
import functools
|
||||
import hashlib
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import operator
|
||||
import pickle
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
import inspect
|
||||
import weakref
|
||||
import zlib
|
||||
import datetime
|
||||
import threading
|
||||
import random
|
||||
import pickle
|
||||
import itertools
|
||||
import time
|
||||
import operator
|
||||
import hashlib
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
@@ -163,7 +160,6 @@ 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})')
|
||||
@@ -226,6 +222,7 @@ 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 = {}
|
||||
@@ -237,7 +234,6 @@ 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 = {}
|
||||
@@ -246,7 +242,7 @@ class Context:
|
||||
|
||||
self._load_game_data()
|
||||
|
||||
# Data package retrieval
|
||||
# Datapackage retrieval
|
||||
def _load_game_data(self):
|
||||
import worlds
|
||||
self.gamespackage = worlds.network_data_package["games"]
|
||||
@@ -260,7 +256,6 @@ 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():
|
||||
@@ -356,7 +351,6 @@ 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):
|
||||
@@ -373,7 +367,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
|
||||
@@ -383,8 +377,7 @@ class Context:
|
||||
raise Utils.VersionException("Incompatible multidata.")
|
||||
return restricted_loads(zlib.decompress(data[1:]))
|
||||
|
||||
def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any],
|
||||
use_embedded_server_options: bool):
|
||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||
self.read_data = {}
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > Utils.version_tuple:
|
||||
@@ -399,6 +392,9 @@ 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
|
||||
@@ -439,15 +435,13 @@ class Context:
|
||||
server_options = decoded_obj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
# embedded data package
|
||||
# custom datapackage
|
||||
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
||||
if game_name in game_data_packages:
|
||||
data = game_data_packages[game_name]
|
||||
logging.info(f"Loading embedded data package for game {game_name}")
|
||||
logging.info(f"Loading custom datapackage 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 data package, but keep in self.item_name_groups
|
||||
del data["item_name_groups"] # remove from datapackage, 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():
|
||||
@@ -597,7 +591,7 @@ class Context:
|
||||
|
||||
def get_hint_cost(self, slot):
|
||||
if self.hint_cost:
|
||||
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return max(0, 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):
|
||||
@@ -745,12 +739,10 @@ 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': games,
|
||||
'games': {ctx.games[x] for x in range(1, len(ctx.games) + 1)},
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
'tags': ctx.tags,
|
||||
@@ -759,9 +751,7 @@ 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() if game in games},
|
||||
'datapackage_checksums': {game: game_data["checksum"] for game, game_data
|
||||
in ctx.gamespackage.items() if game in games},
|
||||
in ctx.gamespackage.items()},
|
||||
'seed_name': ctx.seed_name,
|
||||
'time': time.time(),
|
||||
}])
|
||||
@@ -782,8 +772,7 @@ async def on_client_disconnected(ctx: Context, client: Client):
|
||||
|
||||
|
||||
async def on_client_joined(ctx: Context, client: Client):
|
||||
if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN:
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
|
||||
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(
|
||||
@@ -800,12 +789,11 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||
|
||||
|
||||
async def on_client_left(ctx: Context, client: Client):
|
||||
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)
|
||||
update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN)
|
||||
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):
|
||||
@@ -901,6 +889,8 @@ 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 as having reached its goal instantly."""
|
||||
"""Mark this slot has having reached its goal instantly."""
|
||||
return self.value != 0b01
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ 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,10 +289,7 @@ 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_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)
|
||||
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
|
||||
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,9 +875,17 @@ 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
|
||||
"accessibility": Accessibility,
|
||||
"allow_collect": AllowCollect
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,10 +39,6 @@ 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: typing.Optional[str] = None # set in validate_rom
|
||||
items_handling: typing.Optional[int] = None # set in game_watcher
|
||||
game = None # set in validate_rom
|
||||
items_handling = 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,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import typing
|
||||
import builtins
|
||||
import os
|
||||
@@ -39,7 +38,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.4.0"
|
||||
__version__ = "0.3.9"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -88,10 +87,7 @@ def is_frozen() -> bool:
|
||||
|
||||
|
||||
def local_path(*path: str) -> str:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
"""Returns path to a file in the local Archipelago installation or source."""
|
||||
if hasattr(local_path, 'cached_path'):
|
||||
pass
|
||||
elif is_frozen():
|
||||
@@ -146,17 +142,6 @@ 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)
|
||||
@@ -263,9 +248,6 @@ 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,
|
||||
@@ -335,13 +317,7 @@ 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
|
||||
|
||||
@@ -409,45 +385,6 @@ 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,21 +39,12 @@ def get_datapackage():
|
||||
|
||||
@api_endpoints.route('/datapackage_version')
|
||||
@cache.cached()
|
||||
|
||||
def get_datapackage_versions():
|
||||
from worlds import AutoWorldRegister
|
||||
from worlds import network_data_package, 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 Command, GameDataPackage, Room, db
|
||||
from .models import Room, Command, db
|
||||
|
||||
|
||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
@@ -92,20 +92,7 @@ class WebHostContext(Context):
|
||||
else:
|
||||
self.port = get_random_port()
|
||||
|
||||
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)
|
||||
return self._load(self.decompress(room.seed.multidata), True)
|
||||
|
||||
@db_session
|
||||
def init_save(self, enabled: bool = True):
|
||||
@@ -203,11 +190,6 @@ 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,8 +88,6 @@ 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,8 +56,3 @@ 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", "priority_locations"}
|
||||
"exclude_locations"}
|
||||
|
||||
|
||||
def create():
|
||||
@@ -88,7 +88,7 @@ def create():
|
||||
if option_name in handled_in_js:
|
||||
pass
|
||||
|
||||
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
|
||||
elif option.options:
|
||||
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():
|
||||
if sub_option_name != "random":
|
||||
this_option["options"].append({
|
||||
"name": option.get_option_name(sub_option_id),
|
||||
"value": sub_option_name,
|
||||
})
|
||||
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 not this_option["defaultValue"]:
|
||||
if option.default == "random":
|
||||
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 issubclass(option, Options.ItemSet):
|
||||
elif getattr(option, "verify_item_name", False):
|
||||
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 issubclass(option, Options.LocationSet):
|
||||
elif getattr(option, "verify_location_name", False):
|
||||
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.VerifyKeys):
|
||||
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
|
||||
if option.valid_keys:
|
||||
game_options[option_name] = {
|
||||
"type": "custom-list",
|
||||
@@ -160,14 +160,6 @@ 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,11 +1,9 @@
|
||||
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';
|
||||
@@ -17,24 +15,4 @@ 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,6 +78,8 @@ 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;
|
||||
@@ -101,7 +103,6 @@ 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 = [];
|
||||
@@ -137,28 +138,21 @@ const buildUI = (settingData) => {
|
||||
expandButton.classList.add('invisible');
|
||||
gameDiv.appendChild(expandButton);
|
||||
|
||||
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);
|
||||
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings);
|
||||
gameDiv.appendChild(weightedSettingsDiv);
|
||||
|
||||
const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
||||
gameDiv.appendChild(itemPoolDiv);
|
||||
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
||||
gameDiv.appendChild(itemsDiv);
|
||||
|
||||
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');
|
||||
itemPoolDiv.classList.add('invisible');
|
||||
itemsDiv.classList.add('invisible');
|
||||
hintsDiv.classList.add('invisible');
|
||||
expandButton.classList.remove('invisible');
|
||||
});
|
||||
@@ -166,7 +160,7 @@ const buildUI = (settingData) => {
|
||||
expandButton.addEventListener('click', () => {
|
||||
collapseButton.classList.remove('invisible');
|
||||
weightedSettingsDiv.classList.remove('invisible');
|
||||
itemPoolDiv.classList.remove('invisible');
|
||||
itemsDiv.classList.remove('invisible');
|
||||
hintsDiv.classList.remove('invisible');
|
||||
expandButton.classList.add('invisible');
|
||||
});
|
||||
@@ -234,7 +228,7 @@ const buildGameChoice = (games) => {
|
||||
gameChoiceDiv.appendChild(table);
|
||||
};
|
||||
|
||||
const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
const buildWeightedSettingsDiv = (game, settings) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const settingsWrapper = document.createElement('div');
|
||||
settingsWrapper.classList.add('settings-wrapper');
|
||||
@@ -276,7 +270,7 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
range.setAttribute('data-type', setting.type);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][option.value];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -302,33 +296,33 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
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', updateRangeSetting);
|
||||
range.value = currentSettings[game][settingName][i] || 0;
|
||||
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', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][i];
|
||||
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');
|
||||
@@ -385,7 +379,7 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -436,7 +430,7 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -470,17 +464,7 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
switch(option){
|
||||
case 'random':
|
||||
tdLeft.innerText = 'Random';
|
||||
break;
|
||||
case 'random-low':
|
||||
tdLeft.innerText = "Random (Low)";
|
||||
break;
|
||||
case 'random-high':
|
||||
tdLeft.innerText = "Random (High)";
|
||||
break;
|
||||
}
|
||||
tdLeft.innerText = option;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
@@ -493,7 +477,7 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateRangeSetting);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][option];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
@@ -511,108 +495,15 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
|
||||
break;
|
||||
|
||||
case 'items-list':
|
||||
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);
|
||||
// TODO
|
||||
break;
|
||||
|
||||
case 'locations-list':
|
||||
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);
|
||||
// TODO
|
||||
break;
|
||||
|
||||
case 'custom-list':
|
||||
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);
|
||||
// TODO
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -838,22 +729,21 @@ 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.';
|
||||
' items are, or what those locations contain. Excluded locations will not contain progression items.';
|
||||
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('simple-list');
|
||||
itemHintsDiv.classList.add('item-container');
|
||||
items.forEach((item) => {
|
||||
const itemRow = document.createElement('div');
|
||||
itemRow.classList.add('list-row');
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.classList.add('hint-div');
|
||||
|
||||
const itemLabel = document.createElement('label');
|
||||
itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
|
||||
@@ -867,30 +757,29 @@ const buildHintsDiv = (game, items, locations) => {
|
||||
if (currentSettings[game].start_hints.includes(item)) {
|
||||
itemCheckbox.setAttribute('checked', 'true');
|
||||
}
|
||||
itemCheckbox.addEventListener('change', updateListSetting);
|
||||
itemCheckbox.addEventListener('change', hintChangeHandler);
|
||||
itemLabel.appendChild(itemCheckbox);
|
||||
|
||||
const itemName = document.createElement('span');
|
||||
itemName.innerText = item;
|
||||
itemLabel.appendChild(itemName);
|
||||
|
||||
itemRow.appendChild(itemLabel);
|
||||
itemHintsDiv.appendChild(itemRow);
|
||||
itemDiv.appendChild(itemLabel);
|
||||
itemHintsDiv.appendChild(itemDiv);
|
||||
});
|
||||
|
||||
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('simple-list');
|
||||
locationHintsDiv.classList.add('item-container');
|
||||
locations.forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
const locationDiv = document.createElement('div');
|
||||
locationDiv.classList.add('hint-div');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
|
||||
@@ -904,89 +793,29 @@ const buildHintsDiv = (game, items, locations) => {
|
||||
if (currentSettings[game].start_location_hints.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', updateListSetting);
|
||||
locationCheckbox.addEventListener('change', hintChangeHandler);
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
locationHintsDiv.appendChild(locationRow);
|
||||
locationDiv.appendChild(locationLabel);
|
||||
locationHintsDiv.appendChild(locationDiv);
|
||||
});
|
||||
|
||||
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('locations-wrapper');
|
||||
excludeLocationsWrapper.classList.add('hints-wrapper');
|
||||
excludeLocationsWrapper.innerText = 'Exclude Locations';
|
||||
|
||||
const excludeLocationsDiv = document.createElement('div');
|
||||
excludeLocationsDiv.classList.add('simple-list');
|
||||
excludeLocationsDiv.classList.add('item-container');
|
||||
locations.forEach((location) => {
|
||||
const locationRow = document.createElement('div');
|
||||
locationRow.classList.add('list-row');
|
||||
const locationDiv = document.createElement('div');
|
||||
locationDiv.classList.add('hint-div');
|
||||
|
||||
const locationLabel = document.createElement('label');
|
||||
locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
|
||||
@@ -1000,22 +829,40 @@ const buildLocationsDiv = (game, locations) => {
|
||||
if (currentSettings[game].exclude_locations.includes(location)) {
|
||||
locationCheckbox.setAttribute('checked', '1');
|
||||
}
|
||||
locationCheckbox.addEventListener('change', updateListSetting);
|
||||
locationCheckbox.addEventListener('change', hintChangeHandler);
|
||||
locationLabel.appendChild(locationCheckbox);
|
||||
|
||||
const locationName = document.createElement('span');
|
||||
locationName.innerText = location;
|
||||
locationLabel.appendChild(locationName);
|
||||
|
||||
locationRow.appendChild(locationLabel);
|
||||
excludeLocationsDiv.appendChild(locationRow);
|
||||
locationDiv.appendChild(locationLabel);
|
||||
excludeLocationsDiv.appendChild(locationDiv);
|
||||
});
|
||||
|
||||
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
|
||||
locationsContainer.appendChild(excludeLocationsWrapper);
|
||||
itemHintsContainer.appendChild(excludeLocationsWrapper);
|
||||
|
||||
locationsDiv.appendChild(locationsContainer);
|
||||
return locationsDiv;
|
||||
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));
|
||||
};
|
||||
|
||||
const updateVisibleGames = () => {
|
||||
@@ -1061,12 +908,13 @@ const updateBaseSetting = (event) => {
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(settings));
|
||||
};
|
||||
|
||||
const updateRangeSetting = (evt) => {
|
||||
const updateGameSetting = (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 {
|
||||
@@ -1075,26 +923,6 @@ const updateRangeSetting = (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');
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.6 KiB |
@@ -15,33 +15,3 @@
|
||||
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,6 +21,7 @@ html{
|
||||
margin-right: auto;
|
||||
margin-top: 10px;
|
||||
height: 140px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#landing-header h4{
|
||||
@@ -222,7 +223,7 @@ html{
|
||||
}
|
||||
|
||||
#landing{
|
||||
max-width: 700px;
|
||||
width: 700px;
|
||||
min-height: 280px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@@ -30,8 +30,6 @@ html{
|
||||
}
|
||||
|
||||
#base-header-right{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -44,7 +42,7 @@ html{
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#base-header a, #base-header-mobile-menu a, #base-header-popover-text{
|
||||
#base-header a, #base-header-mobile-menu a{
|
||||
color: #2f6b83;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
@@ -74,92 +72,22 @@ html{
|
||||
position: absolute;
|
||||
top: 7rem;
|
||||
right: 0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu a{
|
||||
padding: 3rem 1.5rem;
|
||||
font-size: 4rem;
|
||||
padding: 4rem 2rem;
|
||||
font-size: 5rem;
|
||||
line-height: 5rem;
|
||||
color: #699ca8;
|
||||
border-top: 1px solid #d3d3d3;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu :first-child, #base-header-popover-menu :first-child{
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
#base-header-right-mobile img{
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
#base-header-popover-menu{
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
margin-left: -108px;
|
||||
margin-top: 2.25rem;
|
||||
border-radius: 10px;
|
||||
border-left: 2px solid #d0ebe6;
|
||||
border-bottom: 2px solid #d0ebe6;
|
||||
border-right: 1px solid #d0ebe6;
|
||||
filter: drop-shadow(-6px 6px 2px #2e3e83);
|
||||
}
|
||||
|
||||
#base-header-popover-menu a{
|
||||
color: #699ca8;
|
||||
border-top: 1px solid #d3d3d3;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
line-height: 3rem;
|
||||
margin-right: 2px;
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
|
||||
#base-header-popover-icon {
|
||||
width: 14px;
|
||||
margin-bottom: 3px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px), only screen and (max-device-width: 768px) {
|
||||
#base-header-right{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#base-header-right-mobile{
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px){
|
||||
#base-header-right-mobile{
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#base-header-right-mobile img{
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu{
|
||||
top: 3.3rem;
|
||||
width: unset;
|
||||
border-left: 2px solid #d0ebe6;
|
||||
border-bottom: 2px solid #d0ebe6;
|
||||
filter: drop-shadow(-6px 6px 2px #2e3e83);
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu a{
|
||||
font-size: 1.5rem;
|
||||
line-height: 3rem;
|
||||
margin: 0;
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 768px){
|
||||
@media all and (max-width: 1580px){
|
||||
html{
|
||||
padding-top: 260px;
|
||||
scroll-padding-top: 230px;
|
||||
@@ -175,4 +103,12 @@ html{
|
||||
margin-top: 30px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#base-header-right{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#base-header-right-mobile{
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,29 +157,41 @@ html{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div, #weighted-settings .locations-div{
|
||||
#weighted-settings .hints-div{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
|
||||
#weighted-settings .hints-div h3{
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .hints-container, #weighted-settings .locations-container{
|
||||
#weighted-settings .hints-div .hints-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-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
|
||||
margin-top: 0.25rem;
|
||||
height: 300px;
|
||||
font-weight: normal;
|
||||
#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 #weighted-settings-button-row{
|
||||
@@ -268,30 +280,6 @@ 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,18 +11,10 @@
|
||||
</a>
|
||||
</div>
|
||||
<div id="base-header-right">
|
||||
<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="/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="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
<div id="base-header-right-mobile">
|
||||
@@ -34,10 +26,8 @@
|
||||
<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,9 +31,6 @@
|
||||
{% 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,7 +36,6 @@
|
||||
{% 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>
|
||||
@@ -52,10 +51,8 @@
|
||||
{% endblock %}
|
||||
<td class="center-column">{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}</td>
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
<td>{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing",
|
||||
30: "Goal Completed"}.get(states[team, player], "Unknown State") }}</td>
|
||||
{%- if activity_timers[team, player] -%}
|
||||
<td class="center-column">{{ activity_timers[team, player].total_seconds() }}</td>
|
||||
{%- 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, network_data_package
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
from worlds.alttp import Items
|
||||
from . import app, cache
|
||||
from .models import GameDataPackage, Room
|
||||
from .models import Room
|
||||
|
||||
alttp_icons = {
|
||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
@@ -229,15 +229,14 @@ 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(context_locations, lookup_any_location_id_to_name).get(loc, loc)
|
||||
return collections.ChainMap(lookup_any_location_id_to_name, context_locations).get(loc, loc)
|
||||
|
||||
|
||||
@pass_context
|
||||
def get_item_name(context: runtime.Context, item: int) -> str:
|
||||
context_items = context.get("custom_items", {})
|
||||
return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item)
|
||||
return collections.ChainMap(lookup_any_item_id_to_name, context_items).get(item, item)
|
||||
|
||||
|
||||
app.jinja_env.filters["location_name"] = get_location_name
|
||||
@@ -275,21 +274,11 @@ def get_static_room_data(room: Room):
|
||||
if slot_info.type == SlotType.group}
|
||||
|
||||
for game in games.values():
|
||||
if game not in multidata["datapackage"]:
|
||||
continue
|
||||
game_data = multidata["datapackage"][game]
|
||||
if "checksum" in game_data:
|
||||
if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]:
|
||||
# non-custom. remove from multidata
|
||||
# network_data_package import could be skipped once all rooms embed data package
|
||||
del multidata["datapackage"][game]
|
||||
continue
|
||||
else:
|
||||
game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
|
||||
custom_locations.update(
|
||||
{id_: name for name, id_ in game_data["location_name_to_id"].items()})
|
||||
custom_items.update(
|
||||
{id_: name for name, id_ in game_data["item_name_to_id"].items()})
|
||||
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()})
|
||||
elif "games" in multidata:
|
||||
games = multidata["games"]
|
||||
seed_checks_in_area = checks_in_area.copy()
|
||||
@@ -1384,26 +1373,24 @@ 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
|
||||
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
player_names[(team, player)] = name
|
||||
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, states=states)
|
||||
locations=locations, games=games)
|
||||
|
||||
|
||||
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import base64
|
||||
import json
|
||||
import pickle
|
||||
import typing
|
||||
import uuid
|
||||
import zipfile
|
||||
import zlib
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template, Markup
|
||||
from pony.orm import commit, flush, select, rollback
|
||||
from pony.orm.core import TransactionIntegrityError
|
||||
from pony.orm import flush, select
|
||||
|
||||
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, GameDataPackage
|
||||
from .models import Seed, Room, Slot
|
||||
|
||||
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||
|
||||
@@ -81,27 +78,6 @@ 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)
|
||||
@@ -114,9 +90,6 @@ 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
|
||||
|
||||
Binary file not shown.
@@ -1,851 +0,0 @@
|
||||
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()
|
||||
@@ -1,380 +0,0 @@
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2015 rxi
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
local json = { _version = "0.1.0" }
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\\\",
|
||||
[ "\"" ] = "\\\"",
|
||||
[ "\b" ] = "\\b",
|
||||
[ "\f" ] = "\\f",
|
||||
[ "\n" ] = "\\n",
|
||||
[ "\r" ] = "\\r",
|
||||
[ "\t" ] = "\\t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if val[1] ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
--local line_count = 1
|
||||
--local col_count = 1
|
||||
--for i = 1, idx - 1 do
|
||||
-- col_count = col_count + 1
|
||||
-- if str:sub(i, i) == "\n" then
|
||||
-- line_count = line_count + 1
|
||||
-- col_count = 1
|
||||
-- end
|
||||
-- end
|
||||
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local has_unicode_escape = false
|
||||
local has_surrogate_escape = false
|
||||
local has_escape = false
|
||||
local last
|
||||
for j = i + 1, #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
end
|
||||
|
||||
if last == 92 then -- "\\" (escape char)
|
||||
if x == 117 then -- "u" (unicode escape sequence)
|
||||
local hex = str:sub(j + 1, j + 5)
|
||||
if not hex:find("%x%x%x%x") then
|
||||
decode_error(str, j, "invalid unicode escape in string")
|
||||
end
|
||||
if hex:find("^[dD][89aAbB]") then
|
||||
has_surrogate_escape = true
|
||||
else
|
||||
has_unicode_escape = true
|
||||
end
|
||||
else
|
||||
local c = string.char(x)
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
has_escape = true
|
||||
end
|
||||
last = nil
|
||||
|
||||
elseif x == 34 then -- '"' (end of string)
|
||||
local s = str:sub(i + 1, j - 1)
|
||||
if has_surrogate_escape then
|
||||
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_unicode_escape then
|
||||
s = s:gsub("\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_escape then
|
||||
s = s:gsub("\\.", escape_char_map_inv)
|
||||
end
|
||||
return s, j + 1
|
||||
|
||||
else
|
||||
last = x
|
||||
end
|
||||
end
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
return ( parse(str, next_char(str, 1, space_chars, true)) )
|
||||
end
|
||||
|
||||
|
||||
return json
|
||||
@@ -1,132 +0,0 @@
|
||||
-----------------------------------------------------------------------------
|
||||
-- LuaSocket helper module
|
||||
-- Author: Diego Nehab
|
||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Declare module and import dependencies
|
||||
-----------------------------------------------------------------------------
|
||||
local base = _G
|
||||
local string = require("string")
|
||||
local math = require("math")
|
||||
local socket = require("socket.core")
|
||||
module("socket")
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
-----------------------------------------------------------------------------
|
||||
function connect(address, port, laddress, lport)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
if laddress then
|
||||
local res, err = sock:bind(laddress, lport, -1)
|
||||
if not res then return nil, err end
|
||||
end
|
||||
local res, err = sock:connect(address, port)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
function bind(host, port, backlog)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
sock:setoption("reuseaddr", true)
|
||||
local res, err = sock:bind(host, port)
|
||||
if not res then return nil, err end
|
||||
res, err = sock:listen(backlog)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
try = newtry()
|
||||
|
||||
function choose(table)
|
||||
return function(name, opt1, opt2)
|
||||
if base.type(name) ~= "string" then
|
||||
name, opt1, opt2 = "default", name, opt1
|
||||
end
|
||||
local f = table[name or "nil"]
|
||||
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||
else return f(opt1, opt2) end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Socket sources and sinks, conforming to LTN12
|
||||
-----------------------------------------------------------------------------
|
||||
-- create namespaces inside LuaSocket namespace
|
||||
sourcet = {}
|
||||
sinkt = {}
|
||||
|
||||
BLOCKSIZE = 2048
|
||||
|
||||
sinkt["close-when-done"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if not chunk then
|
||||
sock:close()
|
||||
return 1
|
||||
else return sock:send(chunk) end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["keep-open"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if chunk then return sock:send(chunk)
|
||||
else return 1 end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["default"] = sinkt["keep-open"]
|
||||
|
||||
sink = choose(sinkt)
|
||||
|
||||
sourcet["by-length"] = function(sock, length)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if length <= 0 then return nil end
|
||||
local size = math.min(socket.BLOCKSIZE, length)
|
||||
local chunk, err = sock:receive(size)
|
||||
if err then return nil, err end
|
||||
length = length - string.len(chunk)
|
||||
return chunk
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sourcet["until-closed"] = function(sock)
|
||||
local done
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if done then return nil end
|
||||
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||
if not err then return chunk
|
||||
elseif err == "closed" then
|
||||
sock:close()
|
||||
done = 1
|
||||
return partial
|
||||
else return nil, err end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
sourcet["default"] = sourcet["until-closed"]
|
||||
|
||||
source = choose(sourcet)
|
||||
@@ -621,7 +621,7 @@ function receive()
|
||||
|
||||
-- Determine Message to send back
|
||||
memDomain.rom()
|
||||
local playerName = uRange(0x1F, 0x11)
|
||||
local playerName = uRange(0x1F, 0x10)
|
||||
playerName[0] = nil
|
||||
local retTable = {}
|
||||
retTable["playerName"] = playerName
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
-- 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
|
||||
Binary file not shown.
@@ -1,132 +0,0 @@
|
||||
-----------------------------------------------------------------------------
|
||||
-- LuaSocket helper module
|
||||
-- Author: Diego Nehab
|
||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Declare module and import dependencies
|
||||
-----------------------------------------------------------------------------
|
||||
local base = _G
|
||||
local string = require("string")
|
||||
local math = require("math")
|
||||
local socket = require("socket.core")
|
||||
module("socket")
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
-----------------------------------------------------------------------------
|
||||
function connect(address, port, laddress, lport)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
if laddress then
|
||||
local res, err = sock:bind(laddress, lport, -1)
|
||||
if not res then return nil, err end
|
||||
end
|
||||
local res, err = sock:connect(address, port)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
function bind(host, port, backlog)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
sock:setoption("reuseaddr", true)
|
||||
local res, err = sock:bind(host, port)
|
||||
if not res then return nil, err end
|
||||
res, err = sock:listen(backlog)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
try = newtry()
|
||||
|
||||
function choose(table)
|
||||
return function(name, opt1, opt2)
|
||||
if base.type(name) ~= "string" then
|
||||
name, opt1, opt2 = "default", name, opt1
|
||||
end
|
||||
local f = table[name or "nil"]
|
||||
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||
else return f(opt1, opt2) end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Socket sources and sinks, conforming to LTN12
|
||||
-----------------------------------------------------------------------------
|
||||
-- create namespaces inside LuaSocket namespace
|
||||
sourcet = {}
|
||||
sinkt = {}
|
||||
|
||||
BLOCKSIZE = 2048
|
||||
|
||||
sinkt["close-when-done"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if not chunk then
|
||||
sock:close()
|
||||
return 1
|
||||
else return sock:send(chunk) end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["keep-open"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if chunk then return sock:send(chunk)
|
||||
else return 1 end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["default"] = sinkt["keep-open"]
|
||||
|
||||
sink = choose(sinkt)
|
||||
|
||||
sourcet["by-length"] = function(sock, length)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if length <= 0 then return nil end
|
||||
local size = math.min(socket.BLOCKSIZE, length)
|
||||
local chunk, err = sock:receive(size)
|
||||
if err then return nil, err end
|
||||
length = length - string.len(chunk)
|
||||
return chunk
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sourcet["until-closed"] = function(sock)
|
||||
local done
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if done then return nil end
|
||||
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||
if not err then return chunk
|
||||
elseif err == "closed" then
|
||||
sock:close()
|
||||
done = 1
|
||||
return partial
|
||||
else return nil, err end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
sourcet["default"] = sourcet["until-closed"]
|
||||
|
||||
source = choose(sourcet)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 535 KiB After Width: | Height: | Size: 526 KiB |
@@ -75,18 +75,6 @@ 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: 91 KiB After Width: | Height: | Size: 92 KiB |
@@ -64,19 +64,18 @@ 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 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. |
|
||||
| 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. |
|
||||
|
||||
#### 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.
|
||||
@@ -107,8 +106,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.
|
||||
@@ -555,16 +554,12 @@ 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). The MultiServer automatically sets
|
||||
the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection
|
||||
to a slot.
|
||||
An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate).
|
||||
|
||||
```python
|
||||
import enum
|
||||
class ClientStatus(enum.IntEnum):
|
||||
CLIENT_UNKNOWN = 0
|
||||
CLIENT_CONNECTED = 5
|
||||
CLIENT_READY = 10
|
||||
CLIENT_PLAYING = 20
|
||||
CLIENT_GOAL = 30
|
||||
@@ -649,12 +644,11 @@ 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. Deprecated. Used by older clients to request an updated datapackage if cache is outdated. |
|
||||
| checksum | str | A checksum hash 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 |
|
||||
|
||||
### Tags
|
||||
Tags are represented as a list of strings, the common Client tags follow:
|
||||
|
||||
@@ -7,11 +7,10 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* [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
|
||||
* Python 3.8.7 or newer
|
||||
* pip (Depending on platform may come included)
|
||||
* A C compiler
|
||||
* possibly optional, read OS-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.
|
||||
@@ -30,8 +29,6 @@ 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
|
||||
@@ -43,8 +40,6 @@ 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
|
||||
@@ -64,7 +59,7 @@ setting in host.yaml at your Enemizer executable.
|
||||
|
||||
## Optional: SNI
|
||||
|
||||
[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.
|
||||
SNI 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,9 +364,14 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
|
||||
|
||||
class MyGameWorld(World):
|
||||
"""Insert description of the world/game here."""
|
||||
game = "My Game" # name of the game/world
|
||||
game: str = "My Game" # name of the game/world
|
||||
option_definitions = mygame_options # options the player can set
|
||||
topology_present = True # show path to required location checks in spoiler
|
||||
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
|
||||
|
||||
# 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,10 +93,6 @@ 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"
|
||||
@@ -167,23 +163,3 @@ 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,8 +63,6 @@ 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
|
||||
@@ -74,20 +72,15 @@ 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]
|
||||
@@ -104,20 +97,15 @@ 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
|
||||
@@ -127,10 +115,6 @@ 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]
|
||||
@@ -146,9 +130,6 @@ 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
|
||||
@@ -161,10 +142,6 @@ 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]
|
||||
|
||||
@@ -242,21 +219,6 @@ 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
|
||||
@@ -324,15 +286,6 @@ 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
|
||||
@@ -393,25 +346,6 @@ 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 :=
|
||||
@@ -458,21 +392,6 @@ 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());
|
||||
@@ -503,21 +422,6 @@ 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
|
||||
@@ -536,16 +440,6 @@ 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;
|
||||
@@ -682,7 +576,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
|
||||
@@ -698,7 +592,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
|
||||
@@ -709,54 +603,6 @@ 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
|
||||
@@ -794,21 +640,9 @@ 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;
|
||||
|
||||
|
||||
@@ -835,10 +669,4 @@ 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,11 +148,9 @@ 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 " \
|
||||
f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))}" \
|
||||
f" location checks."
|
||||
f"For you this means every {max(0, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
|
||||
"location checks."
|
||||
elif ctx.hint_cost == 0:
|
||||
text += "\n!hint is free to use."
|
||||
|
||||
@@ -488,10 +486,6 @@ 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):
|
||||
@@ -612,7 +606,7 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
ExceptionManager.add_handler(E())
|
||||
|
||||
Builder.load_file(Utils.local_path("data", "client.kv"))
|
||||
user_file = Utils.user_path("data", "user.kv")
|
||||
user_file = Utils.local_path("data", "user.kv")
|
||||
if os.path.exists(user_file):
|
||||
logging.info("Loading user.kv into builder.")
|
||||
Builder.load_file(user_file)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
colorama>=0.4.5
|
||||
websockets>=10.3
|
||||
PyYAML>=6.0
|
||||
jellyfish>=0.11.0
|
||||
jellyfish>=0.9.0
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.1.0
|
||||
bsdiff4>=1.2.3
|
||||
platformdirs>=3.2.0
|
||||
bsdiff4>=1.2.2
|
||||
53
setup.py
53
setup.py
@@ -12,6 +12,7 @@ import io
|
||||
import json
|
||||
import threading
|
||||
import subprocess
|
||||
import pkg_resources
|
||||
|
||||
from collections.abc import Iterable
|
||||
from hashlib import sha3_512
|
||||
@@ -21,29 +22,13 @@ 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'
|
||||
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
|
||||
pkg_resources.require(requirement)
|
||||
import cx_Freeze
|
||||
except pkg_resources.ResolutionError:
|
||||
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 pkg_resources
|
||||
|
||||
import cx_Freeze
|
||||
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
|
||||
@@ -55,7 +40,7 @@ if __name__ == "__main__":
|
||||
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
|
||||
ModuleUpdate.update_ran = False # restore for later
|
||||
|
||||
from worlds.LauncherComponents import components, icon_paths
|
||||
from Launcher import components, icon_paths
|
||||
from Utils import version_tuple, is_windows, is_linux
|
||||
|
||||
|
||||
@@ -64,7 +49,6 @@ apworlds: set = {
|
||||
"Subnautica",
|
||||
"Factorio",
|
||||
"Rogue Legacy",
|
||||
"Sonic Adventure 2 Battle",
|
||||
"Donkey Kong Country 3",
|
||||
"Super Mario World",
|
||||
"Stardew Valley",
|
||||
@@ -135,7 +119,6 @@ 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:
|
||||
@@ -160,7 +143,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 and c.frozen_name
|
||||
) for c in components if c.script_name
|
||||
]
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
|
||||
@@ -323,6 +306,7 @@ 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)
|
||||
@@ -345,9 +329,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("Signing SNI")
|
||||
print(f"Signing SNI")
|
||||
os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe"))
|
||||
print("Signing OoT Utils")
|
||||
print(f"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))
|
||||
|
||||
@@ -401,8 +385,7 @@ class AppImageCommand(setuptools.Command):
|
||||
yes: bool
|
||||
|
||||
def write_desktop(self):
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
desktop_filename = self.app_dir / f"{self.app_id}.desktop"
|
||||
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]",
|
||||
@@ -416,8 +399,7 @@ class AppImageCommand(setuptools.Command):
|
||||
desktop_filename.chmod(0o755)
|
||||
|
||||
def write_launcher(self, default_exe: Path):
|
||||
assert self.app_dir, "Invalid app_dir"
|
||||
launcher_filename = self.app_dir / "AppRun"
|
||||
launcher_filename = self.app_dir / f'AppRun'
|
||||
with open(launcher_filename, 'w', encoding="utf-8") as f:
|
||||
f.write(f"""#!/bin/sh
|
||||
exe="{default_exe}"
|
||||
@@ -439,12 +421,11 @@ $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("Requirement PIL is not satisfied, press enter to install it")
|
||||
input(f'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)
|
||||
@@ -521,12 +502,8 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
|
||||
return (lib, lib_arch, lib_libc), path
|
||||
|
||||
if not hasattr(find_libs, "cache"):
|
||||
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)
|
||||
}
|
||||
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)}
|
||||
|
||||
def find_lib(lib, arch, libc):
|
||||
for k, v in find_libs.cache.items():
|
||||
|
||||
@@ -199,15 +199,11 @@ class WorldTestBase(unittest.TestCase):
|
||||
|
||||
self.collect_all_but(all_items)
|
||||
for location in self.multiworld.get_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}")
|
||||
self.assertEqual(self.multiworld.state.can_reach(location), location.name not in locations)
|
||||
for item_names in possible_items:
|
||||
items = self.collect_by_name(item_names)
|
||||
for location in locations:
|
||||
self.assertTrue(self.can_reach_location(location),
|
||||
f"{location} not reachable with {item_names}")
|
||||
self.assertTrue(self.can_reach_location(location))
|
||||
self.remove(items)
|
||||
|
||||
def assertBeatable(self, beatable: bool):
|
||||
|
||||
@@ -6,19 +6,13 @@ from . import setup_solo_multiworld
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def testCreateDuplicateLocations(self):
|
||||
"""Tests that no two Locations share a name or ID."""
|
||||
"""Tests that no two Locations share a name."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
locations = Counter(location.name for location in multiworld.get_locations())
|
||||
locations = Counter(multiworld.get_locations())
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 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)}")
|
||||
f"{world_type.game} has duplicate of location {locations.most_common(1)}")
|
||||
|
||||
def testLocationsInDatapackage(self):
|
||||
"""Tests that created locations not filled before fill starts exist in the datapackage."""
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Any, Callable, ClassVar, Dict, FrozenSet, List, Optional, Set, TYPE_CHECKING, TextIO, Tuple, Type, \
|
||||
Union
|
||||
import pathlib
|
||||
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \
|
||||
ClassVar
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from Options import AssembleOptions
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
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:
|
||||
@@ -36,9 +32,6 @@ 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
|
||||
@@ -161,14 +154,9 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
data_version: ClassVar[int] = 1
|
||||
"""
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
|
||||
required_client_version: Tuple[int, int, int] = (0, 1, 6)
|
||||
@@ -355,35 +343,8 @@ 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()
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
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,19 +20,14 @@ if typing.TYPE_CHECKING:
|
||||
from .AutoWorld import World
|
||||
|
||||
|
||||
class GamesData(typing.TypedDict):
|
||||
item_name_groups: typing.Dict[str, typing.List[str]]
|
||||
class GamesPackage(typing.TypedDict):
|
||||
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]
|
||||
|
||||
|
||||
@@ -40,9 +35,6 @@ 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] = []
|
||||
@@ -58,35 +50,24 @@ for file in os.scandir(folder):
|
||||
# import all submodules to trigger AutoWorldRegister
|
||||
world_sources.sort()
|
||||
for world_source in world_sources:
|
||||
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])
|
||||
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")
|
||||
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())
|
||||
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")
|
||||
|
||||
lookup_any_item_id_to_name = {}
|
||||
lookup_any_location_id_to_name = {}
|
||||
@@ -94,9 +75,14 @@ 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] = world.get_data_package_data()
|
||||
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()}
|
||||
}
|
||||
lookup_any_item_id_to_name.update(world.item_id_to_name)
|
||||
lookup_any_location_id_to_name.update(world.location_id_to_name)
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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 = {
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
# 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)
|
||||
]
|
||||
@@ -1,244 +0,0 @@
|
||||
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,
|
||||
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
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
|
||||
@@ -1,321 +0,0 @@
|
||||
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
|
||||
@@ -1,98 +0,0 @@
|
||||
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
|
||||
@@ -1,391 +0,0 @@
|
||||
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)
|
||||
@@ -1,62 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,70 +0,0 @@
|
||||
# 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,8 +7,6 @@ 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):
|
||||
@@ -140,10 +138,9 @@ 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: 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)]
|
||||
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)]
|
||||
if dungeon_specific:
|
||||
for location in locations:
|
||||
dungeon = location.parent_region.dungeon
|
||||
@@ -162,7 +159,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, allow_excluded=True)
|
||||
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True)
|
||||
|
||||
|
||||
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
|
||||
|
||||
@@ -300,13 +300,4 @@ 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,28 +16,22 @@ class ArchipIDLELogic(LogicMixin):
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
for i in range(16, 31):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 4)
|
||||
)
|
||||
|
||||
for i in range(31, 51):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 10)
|
||||
)
|
||||
|
||||
for i in range(51, 101):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", 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 item number 200", player), "Location", player)
|
||||
state.can_reach(world.get_location("IDLE for at least 50 minutes 0 seconds", player), "Location", player)
|
||||
|
||||
@@ -25,7 +25,7 @@ class ArchipIDLEWorld(World):
|
||||
"""
|
||||
game = "ArchipIDLE"
|
||||
topology_present = False
|
||||
data_version = 5
|
||||
data_version = 4
|
||||
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, 201):
|
||||
location_name_to_id[f"IDLE item number {i}"] = start_id
|
||||
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
|
||||
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(200):
|
||||
for i in range(100):
|
||||
item = ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 40 else ItemClassification.filler,
|
||||
ItemClassification.progression if i < 20 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
self.player
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld, World
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
@@ -25,7 +24,6 @@ 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,12 +38,10 @@ class ExpertLogic(Toggle):
|
||||
|
||||
|
||||
class Ending(Choice):
|
||||
"""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."""
|
||||
"""Choose which ending is required to complete the game."""
|
||||
display_name = "Ending"
|
||||
option_any_ending = 0
|
||||
option_ending_a = 1
|
||||
option_ending_b = 1
|
||||
option_ending_c = 2
|
||||
default = 0
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
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"
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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,6 +468,7 @@ 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 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`.
|
||||
4. Click the button to open a new Lua script.
|
||||
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,7 +136,6 @@ 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,6 +1,5 @@
|
||||
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
|
||||
@@ -198,14 +197,6 @@ 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"
|
||||
@@ -232,36 +223,9 @@ 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):
|
||||
@@ -440,31 +404,12 @@ 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,
|
||||
"teleport_traps": TeleportTrapCount,
|
||||
"grenade_traps": GrenadeTrapCount,
|
||||
"cluster_grenade_traps": ClusterGrenadeTrapCount,
|
||||
"artillery_traps": ArtilleryTrapCount,
|
||||
"atomic_rocket_traps": AtomicRocketTrapCount,
|
||||
"attack_traps": AttackTrapCount,
|
||||
"evolution_traps": EvolutionTrapCount,
|
||||
"attack_traps": AttackTrapCount,
|
||||
"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,9 +15,6 @@ 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):
|
||||
@@ -38,11 +35,6 @@ 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):
|
||||
@@ -51,7 +43,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 = "Factorio"
|
||||
game: str = "Factorio"
|
||||
special_nodes = {"automation", "logistics", "rocket-silo"}
|
||||
custom_recipes: typing.Dict[str, Recipe]
|
||||
location_pool: typing.List[FactorioScienceLocation]
|
||||
@@ -60,11 +52,12 @@ class Factorio(World):
|
||||
web = FactorioWeb()
|
||||
|
||||
item_name_to_id = all_items
|
||||
location_name_to_id = location_table
|
||||
# TODO: remove base_tech_table ~ 0.3.7
|
||||
location_name_to_id = {**base_tech_table, **location_table}
|
||||
item_name_groups = {
|
||||
"Progressive": set(progressive_tech_table.keys()),
|
||||
}
|
||||
data_version = 7
|
||||
data_version = 6
|
||||
required_client_version = (0, 3, 6)
|
||||
|
||||
ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs()
|
||||
@@ -80,10 +73,8 @@ class Factorio(World):
|
||||
generate_output = generate_mod
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# 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.multiworld.max_tech_cost[self.player] = max(self.multiworld.max_tech_cost[self.player],
|
||||
self.multiworld.min_tech_cost[self.player])
|
||||
self.tech_mix = self.multiworld.tech_cost_mix[self.player]
|
||||
self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn
|
||||
|
||||
@@ -96,25 +87,14 @@ 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] + \
|
||||
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]
|
||||
self.multiworld.evolution_traps[player].value + self.multiworld.attack_traps[player].value
|
||||
|
||||
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]
|
||||
@@ -152,14 +132,6 @@ 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
|
||||
@@ -212,6 +184,10 @@ 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,
|
||||
@@ -289,11 +265,10 @@ class Factorio(World):
|
||||
2: "chemistry"}
|
||||
return categories.get(liquids, category)
|
||||
|
||||
def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2,
|
||||
ingredients_offset: int = 0) -> Recipe:
|
||||
def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2) -> Recipe:
|
||||
new_ingredients = {}
|
||||
liquids_used = 0
|
||||
for _ in range(len(original.ingredients) + ingredients_offset):
|
||||
for _ in original.ingredients:
|
||||
new_ingredient = pool.pop()
|
||||
if new_ingredient in fluids:
|
||||
while liquids_used == allow_liquids and new_ingredient in fluids:
|
||||
@@ -307,7 +282,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, ingredients_offset: int = 0) -> Recipe:
|
||||
allow_liquids: int = 2) -> 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
|
||||
@@ -316,7 +291,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) + ingredients_offset
|
||||
target_num_ingredients = len(original.ingredients)
|
||||
remaining_raw = target_raw
|
||||
remaining_energy = target_energy
|
||||
remaining_num_ingredients = target_num_ingredients
|
||||
@@ -407,13 +382,12 @@ 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 + ingredients_offset)},
|
||||
{valid_pool[x]: 10 for x in range(3)},
|
||||
original_rocket_part.products,
|
||||
original_rocket_part.energy)}
|
||||
|
||||
@@ -423,8 +397,7 @@ 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, ingredients_offset=
|
||||
ingredients_offset)
|
||||
new_recipe = self.make_quick_recipe(recipes[pack], valid_pool)
|
||||
self.custom_recipes[pack] = new_recipe
|
||||
|
||||
if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \
|
||||
@@ -434,27 +407,21 @@ 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,
|
||||
ingredients_offset=ingredients_offset)
|
||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7)
|
||||
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,
|
||||
ingredients_offset=ingredients_offset)
|
||||
new_recipe = self.make_balanced_recipe(recipes["satellite"], valid_pool,
|
||||
factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7)
|
||||
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,
|
||||
"replace_4": 1, "replace_5": 1, "replace_6": 1},
|
||||
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1},
|
||||
{bridge: 1}, 10),
|
||||
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]),
|
||||
ingredients_offset=ingredients_offset)
|
||||
sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]))
|
||||
for ingredient_name in new_recipe.ingredients:
|
||||
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500)
|
||||
new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(10, 100)
|
||||
self.custom_recipes[bridge] = new_recipe
|
||||
|
||||
needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
|
||||
@@ -485,7 +452,7 @@ class Factorio(World):
|
||||
tech_table[name], self.player)
|
||||
|
||||
item = FactorioItem(name,
|
||||
ItemClassification.trap if name.endswith("Trap") else ItemClassification.filler,
|
||||
ItemClassification.trap if "Trap" in name else ItemClassification.filler,
|
||||
all_items[name], self.player)
|
||||
return item
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Berserker55
|
||||
Copyright (c) 2021 Dewiniaid
|
||||
Copyright (c) 2021 Berserker55 and 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,3 +1,32 @@
|
||||
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
|
||||
@@ -21,19 +50,4 @@ 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 * 10000000 }}
|
||||
ENERGY_INCREMENT = {{ energy_link * 1000000 }}
|
||||
ENERGY_LINK_EFFICIENCY = 0.75
|
||||
|
||||
if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
|
||||
@@ -22,119 +22,6 @@ 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
|
||||
@@ -293,8 +180,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
|
||||
{
|
||||
@@ -303,8 +190,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)
|
||||
|
||||
@@ -349,7 +236,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
|
||||
|
||||
@@ -367,9 +254,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
|
||||
@@ -411,8 +298,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
|
||||
@@ -623,37 +510,6 @@ 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 = {}
|
||||
@@ -696,11 +552,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
tech.researched = true
|
||||
end
|
||||
end
|
||||
elseif TRAP_TABLE[item_name] ~= nil then
|
||||
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
|
||||
if global.index_sync[index] == nil then -- not yet received trap
|
||||
global.index_sync[index] = item_name
|
||||
game.print({"", "Received ", item_name, " from ", source})
|
||||
TRAP_TABLE[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})
|
||||
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 = "50MJ"
|
||||
energy_bridge.energy_source.input_flow_limit = "10MW"
|
||||
energy_bridge.energy_source.output_flow_limit = "10MW"
|
||||
energy_bridge.energy_source.buffer_capacity = "5MJ"
|
||||
energy_bridge.energy_source.input_flow_limit = "1MW"
|
||||
energy_bridge.energy_source.output_flow_limit = "1MW"
|
||||
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 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy
|
||||
Each placed EnergyLink Bridge provides 1 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