mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 04:01:32 -06:00

* oot: remove all escape characters in LogicTricks.py * only attempt to connect to client once * oot: don't kill player outside ToT or in market entrance fixed camera makes the game crash outside ToT. added market entrance to be safe, it doesn't matter if you don't die there
308 lines
11 KiB
Python
308 lines
11 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import multiprocessing
|
|
import subprocess
|
|
from asyncio import StreamReader, StreamWriter
|
|
|
|
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, \
|
|
ClientCommandProcessor, logger, get_base_parser
|
|
import Utils
|
|
from worlds import network_data_package
|
|
from worlds.oot.Rom import Rom, compress_rom_file
|
|
from worlds.oot.N64Patch import apply_patch_file
|
|
from worlds.oot.Utils import data_path
|
|
|
|
|
|
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart oot_connector.lua"
|
|
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure oot_connector.lua is running"
|
|
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart oot_connector.lua"
|
|
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
|
CONNECTION_CONNECTED_STATUS = "Connected"
|
|
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
|
|
|
"""
|
|
Payload: lua -> client
|
|
{
|
|
playerName: string,
|
|
locations: dict,
|
|
deathlinkActive: bool,
|
|
isDead: bool,
|
|
gameComplete: bool
|
|
}
|
|
|
|
Payload: client -> lua
|
|
{
|
|
items: list,
|
|
playerNames: list,
|
|
triggerDeath: bool
|
|
}
|
|
|
|
Deathlink logic:
|
|
"Dead" is true <-> Link is at 0 hp.
|
|
|
|
deathlink_pending: we need to kill the player
|
|
deathlink_sent_this_death: we interacted with the multiworld on this death, waiting to reset with living link
|
|
|
|
"""
|
|
|
|
oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"]
|
|
|
|
script_version: int = 2
|
|
|
|
def get_item_value(ap_id):
|
|
return ap_id - 66000
|
|
|
|
|
|
class OoTCommandProcessor(ClientCommandProcessor):
|
|
def __init__(self, ctx):
|
|
super().__init__(ctx)
|
|
|
|
def _cmd_n64(self):
|
|
"""Check N64 Connection State"""
|
|
if isinstance(self.ctx, OoTContext):
|
|
logger.info(f"N64 Status: {self.ctx.n64_status}")
|
|
|
|
def _cmd_deathlink(self):
|
|
"""Toggle deathlink from client. Overrides default setting."""
|
|
if isinstance(self.ctx, OoTContext):
|
|
self.ctx.deathlink_client_override = True
|
|
self.ctx.deathlink_enabled = not self.ctx.deathlink_enabled
|
|
asyncio.create_task(self.ctx.update_death_link(self.ctx.deathlink_enabled), name="Update Deathlink")
|
|
|
|
|
|
class OoTContext(CommonContext):
|
|
command_processor = OoTCommandProcessor
|
|
items_handling = 0b001 # full local
|
|
|
|
def __init__(self, server_address, password):
|
|
super().__init__(server_address, password)
|
|
self.game = 'Ocarina of Time'
|
|
self.n64_streams: (StreamReader, StreamWriter) = None
|
|
self.n64_sync_task = None
|
|
self.n64_status = CONNECTION_INITIAL_STATUS
|
|
self.awaiting_rom = False
|
|
self.location_table = {}
|
|
self.deathlink_enabled = False
|
|
self.deathlink_pending = False
|
|
self.deathlink_sent_this_death = False
|
|
self.deathlink_client_override = False
|
|
self.version_warning = False
|
|
|
|
async def server_auth(self, password_requested: bool = False):
|
|
if password_requested and not self.password:
|
|
await super(OoTContext, self).server_auth(password_requested)
|
|
if not self.auth:
|
|
self.awaiting_rom = True
|
|
logger.info('Awaiting connection to Bizhawk to get player information')
|
|
return
|
|
|
|
await self.send_connect()
|
|
|
|
def on_deathlink(self, data: dict):
|
|
self.deathlink_pending = True
|
|
super().on_deathlink(data)
|
|
|
|
def run_gui(self):
|
|
from kvui import GameManager
|
|
|
|
class OoTManager(GameManager):
|
|
logging_pairs = [
|
|
("Client", "Archipelago")
|
|
]
|
|
base_title = "Archipelago Ocarina of Time Client"
|
|
|
|
self.ui = OoTManager(self)
|
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
|
|
|
|
|
def get_payload(ctx: OoTContext):
|
|
if ctx.deathlink_enabled and ctx.deathlink_pending:
|
|
trigger_death = True
|
|
ctx.deathlink_sent_this_death = True
|
|
else:
|
|
trigger_death = False
|
|
|
|
return json.dumps({
|
|
"items": [get_item_value(item.item) for item in ctx.items_received],
|
|
"playerNames": [name for (i, name) in ctx.player_names.items() if i != 0],
|
|
"triggerDeath": trigger_death
|
|
})
|
|
|
|
|
|
async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
|
|
|
|
# Turn on deathlink if it is on, and if the client hasn't overriden it
|
|
if payload['deathlinkActive'] and not ctx.deathlink_enabled and not ctx.deathlink_client_override:
|
|
await ctx.update_death_link(True)
|
|
ctx.deathlink_enabled = True
|
|
|
|
# Game completion handling
|
|
if payload['gameComplete'] and not ctx.finished_game:
|
|
await ctx.send_msgs([{
|
|
"cmd": "StatusUpdate",
|
|
"status": 30
|
|
}])
|
|
ctx.finished_game = True
|
|
|
|
# Locations handling
|
|
if ctx.location_table != payload['locations']:
|
|
ctx.location_table = payload['locations']
|
|
await ctx.send_msgs([{
|
|
"cmd": "LocationChecks",
|
|
"locations": [oot_loc_name_to_id[loc] for loc in ctx.location_table if ctx.location_table[loc]]
|
|
}])
|
|
|
|
# Deathlink handling
|
|
if ctx.deathlink_enabled:
|
|
if payload['isDead']: # link is dead
|
|
ctx.deathlink_pending = False
|
|
if not ctx.deathlink_sent_this_death:
|
|
ctx.deathlink_sent_this_death = True
|
|
await ctx.send_death()
|
|
else: # link is alive
|
|
ctx.deathlink_sent_this_death = False
|
|
|
|
|
|
async def n64_sync_task(ctx: OoTContext):
|
|
logger.info("Starting n64 connector. Use /n64 for status information.")
|
|
while not ctx.exit_event.is_set():
|
|
error_status = None
|
|
if ctx.n64_streams:
|
|
(reader, writer) = ctx.n64_streams
|
|
msg = get_payload(ctx).encode()
|
|
writer.write(msg)
|
|
writer.write(b'\n')
|
|
try:
|
|
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
|
try:
|
|
# Data will return a dict with up to six fields:
|
|
# 1. str: player name (always)
|
|
# 2. int: script version (always)
|
|
# 3. bool: deathlink active (always)
|
|
# 4. dict[str, bool]: checked locations
|
|
# 5. bool: whether Link is currently at 0 HP
|
|
# 6. bool: whether the game currently registers as complete
|
|
data = await asyncio.wait_for(reader.readline(), timeout=10)
|
|
data_decoded = json.loads(data.decode())
|
|
reported_version = data_decoded.get('scriptVersion', 0)
|
|
if reported_version >= script_version:
|
|
if ctx.game is not None and 'locations' in data_decoded:
|
|
# Not just a keep alive ping, parse
|
|
asyncio.create_task(parse_payload(data_decoded, ctx, False))
|
|
if not ctx.auth:
|
|
ctx.auth = data_decoded['playerName']
|
|
if ctx.awaiting_rom:
|
|
await ctx.server_auth(False)
|
|
else:
|
|
if not ctx.version_warning:
|
|
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. "
|
|
"Please update to the latest version. "
|
|
"Your connection to the Archipelago server will not be accepted.")
|
|
ctx.version_warning = True
|
|
except asyncio.TimeoutError:
|
|
logger.debug("Read Timed Out, Reconnecting")
|
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
|
writer.close()
|
|
ctx.n64_streams = None
|
|
except ConnectionResetError as e:
|
|
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
|
error_status = CONNECTION_RESET_STATUS
|
|
writer.close()
|
|
ctx.n64_streams = None
|
|
except TimeoutError:
|
|
logger.debug("Connection Timed Out, Reconnecting")
|
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
|
writer.close()
|
|
ctx.n64_streams = None
|
|
except ConnectionResetError:
|
|
logger.debug("Connection Lost, Reconnecting")
|
|
error_status = CONNECTION_RESET_STATUS
|
|
writer.close()
|
|
ctx.n64_streams = None
|
|
if ctx.n64_status == CONNECTION_TENTATIVE_STATUS:
|
|
if not error_status:
|
|
logger.info("Successfully Connected to N64")
|
|
ctx.n64_status = CONNECTION_CONNECTED_STATUS
|
|
else:
|
|
ctx.n64_status = f"Was tentatively connected but error occured: {error_status}"
|
|
elif error_status:
|
|
ctx.n64_status = error_status
|
|
logger.info("Lost connection to N64 and attempting to reconnect. Use /n64 for status updates")
|
|
else:
|
|
try:
|
|
logger.debug("Attempting to connect to N64")
|
|
ctx.n64_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 28921), timeout=10)
|
|
ctx.n64_status = CONNECTION_TENTATIVE_STATUS
|
|
except TimeoutError:
|
|
logger.debug("Connection Timed Out, Trying Again")
|
|
ctx.n64_status = CONNECTION_TIMING_OUT_STATUS
|
|
continue
|
|
except ConnectionRefusedError:
|
|
logger.debug("Connection Refused, Trying Again")
|
|
ctx.n64_status = CONNECTION_REFUSED_STATUS
|
|
continue
|
|
|
|
|
|
async def run_game(romfile):
|
|
auto_start = Utils.get_options()["oot_options"].get("rom_start", True)
|
|
if auto_start is True:
|
|
import webbrowser
|
|
webbrowser.open(romfile)
|
|
elif os.path.isfile(auto_start):
|
|
subprocess.Popen([auto_start, romfile],
|
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
async def patch_and_run_game(apz5_file):
|
|
base_name = os.path.splitext(apz5_file)[0]
|
|
decomp_path = base_name + '-decomp.z64'
|
|
comp_path = base_name + '.z64'
|
|
# Load vanilla ROM, patch file, compress ROM
|
|
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
|
|
apply_patch_file(rom, apz5_file)
|
|
rom.write_to_file(decomp_path)
|
|
os.chdir(data_path("Compress"))
|
|
compress_rom_file(decomp_path, comp_path)
|
|
os.remove(decomp_path)
|
|
asyncio.create_task(run_game(comp_path))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
Utils.init_logging("OoTClient")
|
|
|
|
async def main():
|
|
multiprocessing.freeze_support()
|
|
parser = get_base_parser()
|
|
parser.add_argument('apz5_file', default="", type=str, nargs="?",
|
|
help='Path to an APZ5 file')
|
|
args = parser.parse_args()
|
|
|
|
if args.apz5_file:
|
|
logger.info("APZ5 file supplied, beginning patching process...")
|
|
asyncio.create_task(patch_and_run_game(args.apz5_file))
|
|
|
|
ctx = OoTContext(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()
|
|
|
|
ctx.n64_sync_task = asyncio.create_task(n64_sync_task(ctx), name="N64 Sync")
|
|
|
|
await ctx.exit_event.wait()
|
|
ctx.server_address = None
|
|
|
|
await ctx.shutdown()
|
|
|
|
if ctx.n64_sync_task:
|
|
await ctx.n64_sync_task
|
|
|
|
import colorama
|
|
|
|
colorama.init()
|
|
|
|
asyncio.run(main())
|
|
colorama.deinit()
|