Skip to content

Commit

Permalink
AP Ocarina of Time Client (ArchipelagoMW#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
espeon65536 authored Mar 27, 2022
1 parent 3c2933d commit 469dda7
Show file tree
Hide file tree
Showing 10 changed files with 2,700 additions and 27 deletions.
287 changes: 287 additions & 0 deletions OoTClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
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"]

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}")


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

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 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
if payload['deathlinkActive'] and not ctx.deathlink_enabled:
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 five fields:
# 1. str: player name (always)
# 2. bool: deathlink active (always)
# 3. dict[str, bool]: checked locations
# 4. bool: whether Link is currently at 0 HP
# 5. bool: whether the game currently registers as complete
data = await asyncio.wait_for(reader.readline(), timeout=10)
data_decoded = json.loads(data.decode())
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)
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.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:
input_task = None
from kvui import OoTManager
ctx.ui = OoTManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
ui_task = None

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

if ui_task:
await ui_task

if input_task:
input_task.cancel()

import colorama

colorama.init()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
colorama.deinit()
64 changes: 40 additions & 24 deletions WebHostLib/static/assets/tutorial/zelda5/setup_en.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
# Setup Guide for Ocarina of time Archipelago
# Setup Guide for Ocarina of Time Archipelago

## Important

As we are using Z5Client and BizHawk, this guide is only applicable to Windows.
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.

## Required Software

- BizHawk and Z5Client from: [Z5Client Releases Page](https://github.com/ArchipelagoMW/Z5Client/releases)
- We recommend download Z5Client-setup as it makes some steps automatic.
- 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.
- An Archipelago client for Ocarina of Time. There are two options available:
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Ocarina of Time Client` during installation). This client is kept up-to-date with the latest Archipelago version
and will always be supported.
- Z5Client, which can be installed [here](https://github.com/ArchipelagoMW/Z5Client/releases), and its associated Lua script `ootMulti.lua`.
- An Ocarina of Time v1.0 ROM.

## Install Emulator and client
## Configuring Bizhawk

Download getBizhawk.ps1 from previous link. Place it on the folder where you want your emulator to be installed, right
click on it and select "Run with PowerShell". This will download all the needed dependencies used by the emulator. This
can take a while.
Once Bizhawk has been installed, open Bizhawk and change the following settings:

It is strongly recommended to associate N64 rom extension (\*.n64) to the BizHawk we've just installed. To do so, we
simply have to search any N64 rom we happened to own, right click and select "Open with...", we unfold the list that
appears and select the bottom option "Look for another application", we browse to BizHawk folder and select EmuHawk.exe
- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to "Lua+LuaInterface".
This is required for the Lua script to function correctly.
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button. This reduces
the possibility of losing save data in emulator crashes.
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to continue
playing in the background, even if another window is selected.
- Under Config > Hotkeys, many hotkeys are listed, with many bound to common keys on the keyboard. You will likely want
to disable most of these, which you can do quickly using `Esc`.
- If playing with a controller, when you bind controls, disable "P1 A Up", "P1 A Down", "P1 A Left", and "P1 A Right" as these interfere
with aiming if bound. Set directional input using the Analog tab instead.

Place the ootMulti.lua file from the previous link inside the "lua" folder from the just installed emulator.

Install the Z5Client using its setup.
It is strongly recommended to associate N64 rom extensions (\*.n64, \*.z64) to the Bizhawk we've just installed. To do so, we
simply have to search any N64 rom we happened to own, right click and select "Open with...", unfold the list that
appears and select the bottom option "Look for another application", then browse to the Bizhawk folder and select EmuHawk.exe.

## Configuring your YAML file

Expand All @@ -33,7 +46,7 @@ an experience customized for their taste, and different players in the same mult

### Where do I get a YAML file?

A basic OOT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this
A basic OoT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this
tutorial, if you want to see a complete list, download Archipelago from
the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in
the "Players" folder.
Expand Down Expand Up @@ -382,20 +395,23 @@ Ocarina of Time:

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 `.z5ap` extension.
files. Your data file should have a `.apz5` extension.

Double-click on your `.z5ap` file to start Z5Client and start the ROM patch process. Once the process is finished (this
can take a while), the emulator will be started automatically (If we associated the extension to the emulator as
recommended)
Double-click on your `.apz5` file 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 associated the extension to the emulator as
recommended).

### Connect to the Multiserver

Once both the Z5Client and the emulator are started we must connect them. Within the emulator we click on the "Tools"
menu and select "Lua console". In the new window click on the folder icon and look for the ootMulti.lua file. Once the
file is loaded it will connect automatically to Z5Client.
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.

If you are using the Archipelago OoTClient, navigate to your Archipelago install folder and open `data/lua/OOT/oot_connector.lua`.

If you are using Z5Client, find the `ootMulti.lua` file and open it.

Note: We strongly advise you don't open any emulator menu while it and Z5client are connected, as the script will halt
and disconnects can happen. If you get disconnected just double-click on the script again.
Note: If using Z5Client, we strongly advise you don't open any menus in Bizhawk while the emulator and Z5Client are connected, as the script will halt
and force-disconnect from the server. If you get disconnected just double-click on the script again.

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]`)
Expand Down
Binary file added data/lua/OOT/core.dll
Binary file not shown.
Loading

0 comments on commit 469dda7

Please sign in to comment.