diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 8c5816b..09aae71 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -12,10 +12,11 @@ jobs: - uses: actions/checkout@v3 - name: Compile run: | - deno compile --target aarch64-apple-darwin --allow-net --allow-read -o build/sail-mac-aarch64 twitch_json_sail.ts - deno compile --target x86_64-apple-darwin --allow-net --allow-read -o build/sail-mac-x86_64 twitch_json_sail.ts - deno compile --target x86_64-pc-windows-msvc --allow-net --allow-read -o build/sail-windows-x86_64.exe twitch_json_sail.ts - deno compile --target x86_64-unknown-linux-gnu --allow-net --allow-read -o build/sail-linux-x86_64 twitch_json_sail.ts + deno compile --target aarch64-apple-darwin --allow-net --allow-read -o build/twitch-json-sail-mac-aarch64 examples/twitch_json_sail.ts + deno compile --target x86_64-apple-darwin --allow-net --allow-read -o build/twitch-json-sail-mac-x86_64 examples/twitch_json_sail.ts + deno compile --target x86_64-pc-windows-msvc --allow-net --allow-read -o build/twitch-json-sail-windows-x86_64.exe examples/twitch_json_sail.ts + deno compile --target x86_64-unknown-linux-gnu --allow-net --allow-read -o build/twitch-json-sail-linux-x86_64 examples/twitch_json_sail.ts + deno compile --target x86_64-pc-windows-msvc --allow-net --allow-read -o build/sammi-sail-windows-x86_64.exe examples/sammi_sail.ts - name: Upload macOS aarch64 uses: actions/upload-artifact@v3 with: diff --git a/.gitignore b/.gitignore index 2eea525..a72f057 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +config.json \ No newline at end of file diff --git a/Sail.ts b/Sail.ts index 242b910..7df1035 100644 --- a/Sail.ts +++ b/Sail.ts @@ -1,67 +1,51 @@ import EventEmitter from "https://deno.land/x/eventemitter@1.2.4/mod.ts"; -import { TcpServer } from "./TcpServer.ts"; -import { OutgoingPacket } from "./types.ts"; -import { nanoid } from "https://deno.land/x/nanoid@v3.0.0/nanoid.ts"; - -export class Sail extends EventEmitter { - public server = new TcpServer(); +import { SohClient } from "./SohClient.ts"; + +export class Sail extends EventEmitter<{ + clientConnected: (client: SohClient) => void; +}> { + private listener?: Deno.Listener; + public clients: SohClient[] = []; + public port = 43384; + public debug = false; + + constructor({ port, debug }: { port?: number; debug?: boolean } = {}) { + super(); + + if (port) { + this.port = port; + } + if (debug) { + this.debug = debug; + } + } - lift() { - this.server.start(); - this.log(`Sail has been lifted!`); + async start() { + try { + this.listener = Deno.listen({ port: this.port }); + + this.log(`Server listening on port ${this.port}`); + for await (const connection of this.listener) { + try { + const client = new SohClient(connection, this, { debug: this.debug }); + this.clients.push(client); + this.emit("clientConnected", client); + } catch (error) { + this.log("Error connecting client:", error); + } + } + } catch (error) { + this.log("Error starting server:", error); + } } - queuePackets(packets: OutgoingPacket[] | OutgoingPacket) { - this.server.queuePackets(packets); + removeClient(client: SohClient) { + const index = this.clients.indexOf(client); + this.clients.splice(index, 1); } // deno-lint-ignore no-explicit-any log(...data: any[]) { console.log("[Sail]:", ...data); } - - /* Effect helpers */ - - command(command: string) { - this.queuePackets({ - id: nanoid(), - type: "command", - command: command, - }); - } - - knockbackPlayer(strength: number) { - this.queuePackets({ - id: nanoid(), - type: "effect", - effect: { - type: "apply", - name: "KnockbackPlayer", - parameters: [strength], - }, - }); - } - - modifyLinkSize(size: number, lengthSeconds: number) { - this.queuePackets({ - id: nanoid(), - type: "effect", - effect: { - type: "apply", - name: "ModifyLinkSize", - parameters: [size], - }, - }); - - setTimeout(() => { - this.queuePackets({ - id: nanoid(), - type: "effect", - effect: { - type: "remove", - name: "ModifyLinkSize", - }, - }); - }, 1000 * lengthSeconds); - } } diff --git a/SohClient.ts b/SohClient.ts new file mode 100644 index 0000000..634e4f5 --- /dev/null +++ b/SohClient.ts @@ -0,0 +1,257 @@ +import { writeAll } from "https://deno.land/std@0.192.0/streams/write_all.ts"; +import EventEmitter from "https://deno.land/x/eventemitter@1.2.4/mod.ts"; +import { nanoid } from "https://deno.land/x/nanoid@v3.0.0/nanoid.ts"; +import { Sail } from "./Sail.ts"; +import { + Hook, + IncomingPacket, + OnActorInitHook, + OnEnemyDefeatHook, + OnExitGameHook, + OnFlagSetHook, + OnFlagUnsetHook, + OnItemReceiveHook, + OnLoadGameHook, + OnSceneFlagSetHook, + OnSceneFlagUnsetHook, + OnTransitionEndHook, + OutgoingPacket, + ResultStatus, +} from "./types.ts"; + +const decoder = new TextDecoder(); +const encoder = new TextEncoder(); + +const hookToEventMap = { + OnTransitionEnd: "transitionEnd", + OnLoadGame: "loadGame", + OnExitGame: "exitGame", + OnItemReceive: "itemReceive", + OnEnemyDefeat: "enemyDefeat", + OnActorInit: "actorInit", + OnFlagSet: "flagSet", + OnFlagUnset: "flagUnset", + OnSceneFlagSet: "sceneFlagSet", + OnSceneFlagUnset: "sceneFlagUnset", +} as const; + +export class SohClient extends EventEmitter<{ + transitionEnd: (event: OnTransitionEndHook) => void; + loadGame: (event: OnLoadGameHook) => void; + exitGame: (event: OnExitGameHook) => void; + itemReceive: (event: OnItemReceiveHook) => void; + enemyDefeat: (event: OnEnemyDefeatHook) => void; + actorInit: (event: OnActorInitHook) => void; + flagSet: (event: OnFlagSetHook) => void; + flagUnset: (event: OnFlagUnsetHook) => void; + sceneFlagSet: (event: OnSceneFlagSetHook) => void; + sceneFlagUnset: (event: OnSceneFlagUnsetHook) => void; + anyHook: (event: Hook) => void; + disconnected: () => void; +}> { + public id: number; + private connection: Deno.Conn; + public sail: Sail; + private packetResolvers: { + [id: string]: (value: ResultStatus | PromiseLike) => void; + } = {}; + public debug = false; + + constructor( + connection: Deno.Conn, + sail: Sail, + { debug }: { debug?: boolean } = {}, + ) { + super(); + this.connection = connection; + this.sail = sail; + this.id = connection.rid; + + if (debug) { + this.debug = debug; + } + + this.log("Connected"); + this.waitForData(); + } + + async waitForData() { + const buffer = new Uint8Array(1024); + let data = new Uint8Array(0); + + while (true) { + let count: null | number = 0; + + try { + count = await this.connection.read(buffer); + } catch (error) { + this.log(`Error reading from connection: ${error.message}`); + this.disconnect(); + break; + } + + if (!count) { + this.disconnect(); + break; + } + + // Concatenate received data with the existing data + const receivedData = buffer.subarray(0, count); + data = concatUint8Arrays(data, receivedData); + + // Handle all complete packets (while loop in case multiple packets were received at once) + while (true) { + const delimiterIndex = findDelimiterIndex(data); + if (delimiterIndex === -1) { + break; // Incomplete packet, wait for more data + } + + // Extract the packet + const packet = data.subarray(0, delimiterIndex); + data = data.subarray(delimiterIndex + 1); + + this.handlePacket(packet); + } + } + } + + handlePacket(packet: Uint8Array) { + try { + const packetString = decoder.decode(packet); + const packetObject: IncomingPacket = JSON.parse(packetString); + + if (this.debug) this.log("->", packetObject); + + if (packetObject.type === "result") { + const resolver = this.packetResolvers[packetObject.id]; + if (resolver) { + resolver(packetObject.status); + delete this.packetResolvers[packetObject.id]; + } + } else if (packetObject.type == "hook") { + this.emit("anyHook", packetObject.hook); + if (packetObject.hook.type in hookToEventMap) { + this.emit( + hookToEventMap[packetObject.hook.type], + packetObject.hook as any, + ); + } + } + } catch (error) { + this.log(`Error handling packet: ${error.message}`); + } + } + + async sendPacket(packetObject: OutgoingPacket): Promise { + try { + if (this.debug) this.log("<-", packetObject); + const packetString = JSON.stringify(packetObject); + const packet = encoder.encode(packetString + "\0"); + + await writeAll(this.connection, packet); + + const result: ResultStatus = await new Promise((resolve) => { + this.packetResolvers[packetObject.id] = resolve; + + setTimeout(() => { + if (this.packetResolvers[packetObject.id]) { + resolve("timeout"); + delete this.packetResolvers[packetObject.id]; + } + }, 5000); + }); + + if (result === "try_again") { + await new Promise((resolve) => setTimeout(resolve, 500)); + + return this.sendPacket(packetObject); + } + + return result; + } catch (error) { + this.log(`Error sending packet: ${error.message}`); + this.disconnect(); + return "failure"; + } + } + + disconnect() { + try { + this.sail.removeClient(this); + this.connection.close(); + } catch (error) { + this.log(`Error disconnecting: ${error.message}`); + } finally { + this.emit("disconnected"); + this.log("Disconnected"); + } + } + + // deno-lint-ignore no-explicit-any + log(...data: any[]) { + console.log(`[SohClient ${this.id}]:`, ...data); + } + + /* Effect helpers */ + + command(command: string) { + return this.sendPacket({ + id: nanoid(), + type: "command", + command: command, + }); + } + + knockbackPlayer(strength: number) { + return this.sendPacket({ + id: nanoid(), + type: "effect", + effect: { + type: "apply", + name: "KnockbackPlayer", + parameters: [strength], + }, + }); + } + + async modifyLinkSize(size: number, lengthSeconds: number) { + await this.sendPacket({ + id: nanoid(), + type: "effect", + effect: { + type: "apply", + name: "ModifyLinkSize", + parameters: [size], + }, + }); + + return new Promise((resolve) => { + setTimeout(() => { + this.sendPacket({ + id: nanoid(), + type: "effect", + effect: { + type: "remove", + name: "ModifyLinkSize", + }, + }).then(resolve); + }, 1000 * lengthSeconds); + }); + } +} + +function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { + const result = new Uint8Array(a.length + b.length); + result.set(a, 0); + result.set(b, a.length); + return result; +} + +function findDelimiterIndex(data: Uint8Array): number { + for (let i = 0; i < data.length; i++) { + if (data[i] === 0 /* null terminator */) { + return i; + } + } + return -1; +} diff --git a/TcpClient.ts b/TcpClient.ts deleted file mode 100644 index 62a8ff1..0000000 --- a/TcpClient.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { writeAll } from "https://deno.land/std@0.192.0/streams/write_all.ts"; -import { TcpServer } from "./TcpServer.ts"; -import { Packet, ResultPacket } from "./types.ts"; - -const decoder = new TextDecoder(); -const encoder = new TextEncoder(); - -export class TcpClient { - public id: number; - private connection: Deno.Conn; - public server: TcpServer; - private waitingOnResponse = false; - private packetQueue: Packet[] = []; - - constructor(connection: Deno.Conn, server: TcpServer) { - this.connection = connection; - this.server = server; - this.id = connection.rid; - - this.log("Connected"); - this.waitForData(); - this.processQueue(); - } - - async waitForData() { - const buffer = new Uint8Array(1024); - let data = new Uint8Array(0); - - while (true) { - let count: null | number = 0; - - try { - count = await this.connection.read(buffer); - } catch (error) { - this.log(`Error reading from connection: ${error.message}`); - this.disconnect(); - break; - } - - if (!count) { - this.disconnect(); - break; - } - - // Concatenate received data with the existing data - const receivedData = buffer.subarray(0, count); - data = concatUint8Arrays(data, receivedData); - - // Handle all complete packets (while loop in case multiple packets were received at once) - while (true) { - const delimiterIndex = findDelimiterIndex(data); - if (delimiterIndex === -1) { - break; // Incomplete packet, wait for more data - } - - // Extract the packet - const packet = data.subarray(0, delimiterIndex); - data = data.subarray(delimiterIndex + 1); - - this.handlePacket(packet); - } - } - } - - async processQueue() { - if (this.packetQueue.length && !this.waitingOnResponse) { - const command = this.packetQueue.shift()!; - this.packetQueue.push(command); - this.waitingOnResponse = true; - await this.sendPacket(command); - } - - setTimeout(() => { - this.processQueue(); - }, 100); - } - - handlePacket(packet: Uint8Array) { - try { - const packetString = decoder.decode(packet); - const packetObject: ResultPacket = JSON.parse(packetString); - - this.log("->", packetObject); - - if ( - packetObject.status == "success" || packetObject.status == "failure" - ) { - this.packetQueue = this.packetQueue.filter( - (payload) => payload.id !== packetObject.id, - ); - } - - this.waitingOnResponse = false; - } catch (error) { - this.log(`Error handling packet: ${error.message}`); - } - } - - async sendPacket(packetObject: Packet) { - try { - this.log("<-", packetObject); - const packetString = JSON.stringify(packetObject); - const packet = encoder.encode(packetString + "\0"); - - await writeAll(this.connection, packet); - } catch (error) { - this.log(`Error sending packet: ${error.message}`); - this.disconnect(); - } - } - - queuePackets(packets: Packet[] | Packet) { - if (Array.isArray(packets)) { - this.packetQueue.push(...packets); - } else { - this.packetQueue.push(packets); - } - } - - disconnect() { - try { - this.server.removeClient(this); - this.connection.close(); - } catch (error) { - this.log(`Error disconnecting: ${error.message}`); - } finally { - this.log("Disconnected"); - } - } - - // deno-lint-ignore no-explicit-any - log(...data: any[]) { - console.log(`[TcpClient ${this.id}]:`, ...data); - } -} - -function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { - const result = new Uint8Array(a.length + b.length); - result.set(a, 0); - result.set(b, a.length); - return result; -} - -function findDelimiterIndex(data: Uint8Array): number { - for (let i = 0; i < data.length; i++) { - if (data[i] === 0 /* null terminator */) { - return i; - } - } - return -1; -} diff --git a/TcpServer.ts b/TcpServer.ts deleted file mode 100644 index 69ce937..0000000 --- a/TcpServer.ts +++ /dev/null @@ -1,47 +0,0 @@ -import "https://deno.land/std@0.208.0/dotenv/load.ts"; -import { TcpClient } from "./TcpClient.ts"; -import { Packet } from "./types.ts"; - -export class TcpServer { - private listener?: Deno.Listener; - public clients: TcpClient[] = []; - - async start() { - try { - const port = Deno.env.has("PORT") - ? parseInt(Deno.env.get("PORT")!, 10) - : 43384; - if (isNaN(port)) { - throw new Error("Invalid PORT environment variable"); - } - - this.listener = Deno.listen({ port: port }); - - this.log(`Server listening on port ${port}`); - for await (const connection of this.listener) { - try { - const client = new TcpClient(connection, this); - this.clients.push(client); - } catch (error) { - this.log("Error connecting client:", error); - } - } - } catch (error) { - this.log("Error starting server:", error); - } - } - - removeClient(client: TcpClient) { - const index = this.clients.indexOf(client); - this.clients.splice(index, 1); - } - - queuePackets(packets: Packet[] | Packet) { - this.clients.forEach((client) => client.queuePackets(packets)); - } - - // deno-lint-ignore no-explicit-any - log(...data: any[]) { - console.log("[TcpServer]:", ...data); - } -} diff --git a/TwitchClient.ts b/TwitchClient.ts index 7f46c9c..bb93847 100644 --- a/TwitchClient.ts +++ b/TwitchClient.ts @@ -9,17 +9,25 @@ export class TwitchClient extends EventEmitter<{ raw: (raw: Privmsg) => void; }> { public ircClient = new TwitchIrc.Client(); + public channel: string; + public debug = false; - constructor() { + constructor({ channel, debug }: { channel: string; debug?: boolean }) { super(); + + if (debug) { + this.debug = debug; + } + + this.channel = channel; this.ircClient.on("privmsg", (e) => this.handleMessage(e)); } - async connect(channel: string) { + async connect() { await new Promise((resolve) => { this.ircClient.on("open", async () => { - await this.ircClient.join(`#${channel}`); - this.log(`Connected to chat for ${channel}`); + await this.ircClient.join(`#${this.channel}`); + this.log(`Connected to chat for ${this.channel}`); resolve(); }); }); @@ -27,6 +35,8 @@ export class TwitchClient extends EventEmitter<{ handleMessage(event: Privmsg) { this.emit("raw", event); + if (this.debug) this.log("Raw:", event.raw); + if (event.raw.tags?.customRewardId) { this.log("Redeem Used:", event.raw.tags?.customRewardId); this.emit( diff --git a/examples/.env.example b/examples/.env.example deleted file mode 100644 index 6402e39..0000000 --- a/examples/.env.example +++ /dev/null @@ -1 +0,0 @@ -PORT=43384 \ No newline at end of file diff --git a/examples/sammi_sail.ts b/examples/sammi_sail.ts new file mode 100644 index 0000000..d9bb889 --- /dev/null +++ b/examples/sammi_sail.ts @@ -0,0 +1,93 @@ +import { Sail } from "../Sail.ts"; +import { SohClient } from "../SohClient.ts"; + +let port = 43384; +if (Deno.env.has("PORT")) { + const parsedPort = parseInt(Deno.env.get("PORT")!, 10); + if (isNaN(parsedPort)) { + console.warn("The PORT environment variable is not a valid number"); + } else { + port = parsedPort; + } +} + +let httpPort = 43383; +if (Deno.env.has("HTTP_PORT")) { + const parsedPort = parseInt(Deno.env.get("HTTP_PORT")!, 10); + if (isNaN(parsedPort)) { + console.warn("The HTTP_PORT environment variable is not a valid number"); + } else { + httpPort = parsedPort; + } +} + +const sammiUrl = Deno.env.has("SAMMI_WEBHOOK_URL") + ? Deno.env.get("SAMMI_WEBHOOK_URL")! + : "http://localhost:9450/webhook"; + +const sail = new Sail({ port, debug: true }); +let sohClient: SohClient | undefined; + +sail.on("clientConnected", (client) => { + sohClient = client; + + client.on("disconnected", () => { + sohClient = undefined; + }); + + client.on("anyHook", async (event) => { + const { type, ...rest } = event; + + try { + await fetch(sammiUrl, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + trigger: type, + ...rest, + }), + }); + } catch (error) { + console.error("Error sending webhook to SAMMI", error); + } + }); +}); + +async function handler(request: Request): Promise { + if (!request.body) { + return new Response(JSON.stringify({ status: "BAD_REQUEST" }), { + status: 400, + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); + } + + const body = await request.json(); + const status = await sohClient?.sendPacket(body); + + return new Response( + JSON.stringify({ + type: "response", + status, + }), + { + status: 200, + headers: { + "content-type": "application/json; charset=utf-8", + }, + }, + ); +} + +(async () => { + try { + Deno.serve({ port: httpPort }, handler); + await sail.start(); + } catch (error) { + console.error("There was an error starting the Custom Sail", error); + Deno.exit(1); + } +})(); diff --git a/examples/twitch_custom_sail.ts b/examples/twitch_custom_sail.ts index 51f2d1f..4e5350b 100644 --- a/examples/twitch_custom_sail.ts +++ b/examples/twitch_custom_sail.ts @@ -1,43 +1,60 @@ import { Sail } from "../Sail.ts"; +import { SohClient } from "../SohClient.ts"; import { TwitchClient } from "../TwitchClient.ts"; -const sail = new Sail(); -const twitchClient = new TwitchClient(); +const sail = new Sail({ port: 43384, debug: true }); +const twitchClient = new TwitchClient({ channel: "proxysaw" }); +let sohClient: SohClient | undefined; twitchClient.on("chat", (message, user) => { if (message.match(/!kick (\d+)/)) { - if (onCooldown("kick", 300)) return; + if (onCooldown("kick", 1)) return; const strengthStr = message.match(/!kick (\d+)/)![1]; const strength = Math.max(1, Math.min(3, parseInt(strengthStr))); - sail.knockbackPlayer(strength); + sohClient?.knockbackPlayer(strength); } if (message.match(/!rave/)) { - if (onCooldown("rave", 300)) return; - sail.command("set gCosmetics.Link_KokiriTunic.Changed 1"); - sail.command("set gCosmetics.Link_KokiriTunic.Rainbow 1"); + if (onCooldown("rave", 1)) return; - setTimeout(() => { - sail.command("set gCosmetics.Link_KokiriTunic.Changed 0"); - sail.command("set gCosmetics.Link_KokiriTunic.Rainbow 0"); - }, 1000 * 20); + Promise.all([ + sohClient?.command("set gCosmetics.Link_KokiriTunic.Changed 1"), + sohClient?.command("set gCosmetics.Link_KokiriTunic.Rainbow 1"), + ]).then(() => { + setTimeout(() => { + sohClient?.command("set gCosmetics.Link_KokiriTunic.Changed 0"); + sohClient?.command("set gCosmetics.Link_KokiriTunic.Rainbow 0"); + }, 1000 * 20); + }); } if (message.match(/!tiny/)) { - if (onCooldown("tiny", 300)) return; - sail.modifyLinkSize(2, 10); + if (onCooldown("tiny", 1)) return; + sohClient?.modifyLinkSize(2, 10); } }); twitchClient.on("redeem", (reward, message, user) => { if (reward === "878f54ca-b3ec-4acd-acc1-c5482b5c2f8e") { - sail.command("reset"); + sohClient?.command("reset"); } }); twitchClient.on("bits", (bits, message, user) => { }); +sail.on("clientConnected", (client) => { + sohClient = client; + + client.on("transitionEnd", ({ sceneNum }) => { + console.log("OnTransitionEnd sceneNum:", sceneNum); + }); + + client.on("disconnected", () => { + sohClient = undefined; + }); +}); + const cooldownMap: Record = {}; function onCooldown(command: string, cooldownSeconds: number) { if (!cooldownMap[command]) { @@ -53,8 +70,8 @@ function onCooldown(command: string, cooldownSeconds: number) { (async () => { try { - await twitchClient.connect("proxysaw"); - await sail.lift(); + await twitchClient.connect(); + await sail.start(); } catch (error) { console.error("There was an error starting the Custom Sail", error); Deno.exit(1); diff --git a/examples/twitch_json_example.json b/examples/twitch_json_example.config.json similarity index 95% rename from examples/twitch_json_example.json rename to examples/twitch_json_example.config.json index 733b0fa..329fdde 100644 --- a/examples/twitch_json_example.json +++ b/examples/twitch_json_example.config.json @@ -1,5 +1,6 @@ { - "channel": "proxysaw", + "port": 43384, + "channel": "your_channel_name_lowercase", "commands": { "!rave": { "cooldownSeconds": 300, diff --git a/examples/twitch_json_sail.ts b/examples/twitch_json_sail.ts index ae60b2b..b8dfc49 100644 --- a/examples/twitch_json_sail.ts +++ b/examples/twitch_json_sail.ts @@ -3,8 +3,10 @@ import { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts"; import { Effect, OutgoingPacket } from "../types.ts"; import { Sail } from "../Sail.ts"; import { TwitchClient } from "../TwitchClient.ts"; +import { SohClient } from "../SohClient.ts"; let config: { + port?: number; channel: string; commands: { [index: string]: { @@ -16,10 +18,9 @@ let config: { }; }; -const sail = new Sail(); -const twitchClient = new TwitchClient(); const tpl = new Template(); const onCooldown: Record = {}; +let sohClient: SohClient | undefined; const configString = await Deno.readTextFileSync("./config.json"); if (!configString) { @@ -32,6 +33,17 @@ try { throw new Error("Failed to parse config.json"); } +const sail = new Sail({ port: config.port || 43384, debug: true }); +const twitchClient = new TwitchClient({ channel: config.channel }); + +sail.on("clientConnected", (client) => { + sohClient = client; + + client.on("disconnected", () => { + sohClient = undefined; + }); +}); + twitchClient.on("raw", (event) => { let command: string; let args: string[]; @@ -70,11 +82,13 @@ twitchClient.on("raw", (event) => { argObject, ); - sail.queuePackets(packets); + Promise.all(packets.map((packet) => sohClient?.sendPacket(packet))) + .then(() => { + setTimeout(() => { + endPackets.map((packet) => sohClient?.sendPacket(packet)); + }, (commandConfig.lengthSeconds || 0) * 1000); + }); - setTimeout(() => { - sail.queuePackets(endPackets); - }, (commandConfig.lengthSeconds || 0) * 1000); if (commandConfig.cooldownSeconds) { setTimeout(() => { onCooldown[command] = false; @@ -117,8 +131,8 @@ function preparePackets(effects: Effect[], argObject: any): OutgoingPacket[] { (async () => { try { - await twitchClient.connect(config.channel); - await sail.lift(); + await twitchClient.connect(); + await sail.start(); } catch (error) { console.error("There was an error starting the JSON Twitch Sail", error); Deno.exit(1); diff --git a/types.ts b/types.ts index d009df0..1aa8645 100644 --- a/types.ts +++ b/types.ts @@ -28,85 +28,89 @@ export interface CommandPacket { command: string; } +export type ResultStatus = "success" | "failure" | "try_again" | "timeout"; + export interface ResultPacket { id: string; type: "result"; - status: "success" | "failure" | "try_again"; + status: ResultStatus; } -interface OnTransitionEndHook { +export interface OnTransitionEndHook { type: "OnTransitionEnd"; sceneNum: number; } -interface OnLoadGameHook { +export interface OnLoadGameHook { type: "OnLoadGame"; fileNum: number; } -interface OnExitGameHook { +export interface OnExitGameHook { type: "OnExitGame"; fileNum: number; } -interface OnItemReceiveHook { +export interface OnItemReceiveHook { type: "OnItemReceive"; tableId: number; getItemId: number; } -interface OnEnemyDefeatHook { +export interface OnEnemyDefeatHook { type: "OnEnemyDefeat"; actorId: number; params: number; } -interface OnActorInitHook { +export interface OnActorInitHook { type: "OnActorInit"; actorId: number; params: number; } -interface OnFlagSetHook { +export interface OnFlagSetHook { type: "OnFlagSet"; flagType: number; flag: number; } -interface OnFlagUnsetHook { +export interface OnFlagUnsetHook { type: "OnFlagUnset"; flagType: number; flag: number; } -interface OnSceneFlagSetHook { +export interface OnSceneFlagSetHook { type: "OnSceneFlagSet"; flagType: number; flag: number; sceneNum: number; } -interface OnSceneFlagUnsetHook { +export interface OnSceneFlagUnsetHook { type: "OnSceneFlagUnset"; flagType: number; flag: number; sceneNum: number; } +export type Hook = + | OnTransitionEndHook + | OnLoadGameHook + | OnExitGameHook + | OnItemReceiveHook + | OnEnemyDefeatHook + | OnActorInitHook + | OnFlagSetHook + | OnFlagUnsetHook + | OnSceneFlagSetHook + | OnSceneFlagUnsetHook; + export interface HookPacket { id: string; type: "hook"; - hook: - | OnTransitionEndHook - | OnLoadGameHook - | OnExitGameHook - | OnItemReceiveHook - | OnEnemyDefeatHook - | OnActorInitHook - | OnFlagSetHook - | OnFlagUnsetHook - | OnSceneFlagSetHook - | OnSceneFlagUnsetHook; + hook: Hook; } export type IncomingPacket = ResultPacket | HookPacket;