Skip to content

Commit

Permalink
More refactoring and added sammi_sail example
Browse files Browse the repository at this point in the history
  • Loading branch information
garrettjoecox committed Dec 4, 2023
1 parent 31ef871 commit ac7d944
Show file tree
Hide file tree
Showing 13 changed files with 494 additions and 311 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.env
.env
config.json
96 changes: 40 additions & 56 deletions Sail.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,51 @@
import EventEmitter from "https://deno.land/x/[email protected]/mod.ts";
import { TcpServer } from "./TcpServer.ts";
import { OutgoingPacket } from "./types.ts";
import { nanoid } from "https://deno.land/x/[email protected]/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);
}
}
257 changes: 257 additions & 0 deletions SohClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { writeAll } from "https://deno.land/[email protected]/streams/write_all.ts";
import EventEmitter from "https://deno.land/x/[email protected]/mod.ts";
import { nanoid } from "https://deno.land/x/[email protected]/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<ResultStatus>) => 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<ResultStatus> {
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;
}
Loading

0 comments on commit ac7d944

Please sign in to comment.