Skip to content

Commit

Permalink
feat: Operators
Browse files Browse the repository at this point in the history
  • Loading branch information
oliversalzburg committed Sep 21, 2024
1 parent 9a8db48 commit 9ccbeb9
Show file tree
Hide file tree
Showing 51 changed files with 2,222 additions and 161 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

26 changes: 18 additions & 8 deletions packages/kitten-analysts/source/entrypoint-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isNil } from "@oliversalzburg/js-utils/data/nil.js";
import { redirectErrorsToConsole } from "@oliversalzburg/js-utils/errors/console.js";
import Koa from "koa";
import Router from "koa-router";
import { compressToUTF16, decompressFromUTF16 } from "lz-string";
import { writeFileSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
Expand All @@ -12,6 +13,7 @@ import {
KGNetSaveFromGame,
KGNetSavePersisted,
KGNetSaveUpdate,
KGSaveData,
LOCAL_STORAGE_PATH,
} from "./globals.js";
import {
Expand Down Expand Up @@ -57,6 +59,7 @@ const PORT_HTTP_METRICS = process.env.PORT_WS_BACKEND
? Number(process.env.PORT_HTTP_METRICS)
: 9091;
const PORT_WS_BACKEND = process.env.PORT_WS_BACKEND ? Number(process.env.PORT_WS_BACKEND) : 9093;
const PROTOCOL_DEBUG = Boolean(process.env.PROTOCOL_DEBUG);

const saveStore = new Map<string, KGNetSavePersisted>();
saveStore.set("ka-internal-savestate", {
Expand All @@ -76,7 +79,7 @@ saveStore.set("ka-internal-savestate", {

// Websocket stuff

const remote = new KittensGameRemote(saveStore, PORT_WS_BACKEND);
const remote = new KittensGameRemote(saveStore, PORT_WS_BACKEND, PROTOCOL_DEBUG);

// Prometheus stuff

Expand Down Expand Up @@ -199,7 +202,7 @@ routerNetwork.get("/kgnet/save", context => {

routerNetwork.post("/kgnet/save/upload", context => {
try {
console.debug(`=> Received savegame.`);
if (PROTOCOL_DEBUG) process.stderr.write(`=> Received savegame.`);

const gameSave = context.request.body as KGNetSaveFromGame;
const gameGUID = gameSave.guid;
Expand All @@ -216,12 +219,17 @@ routerNetwork.post("/kgnet/save/upload", context => {
saveStore.set(gameGUID, savegame);
writeFileSync(`${LOCAL_STORAGE_PATH}/${gameGUID}.json`, JSON.stringify(savegame));

// Rebuild payload to also contain the fixed-string telemetry GUID.
const uncompressed = JSON.parse(decompressFromUTF16(gameSave.saveData)) as KGSaveData;
uncompressed.telemetry.guid = "ka-internal-savestate";
const recompressedSaveData = compressToUTF16(JSON.stringify(uncompressed));

const savegameEphemeral: KGNetSavePersisted = {
archived: false,
guid: "ka-internal-savestate",
index: { calendar: { day: calendar.day, year: calendar.year } },
label: "Background Game",
saveData: gameSave.saveData,
saveData: recompressedSaveData,
size: context.request.length,
timestamp: Date.now(),
};
Expand All @@ -231,9 +239,9 @@ routerNetwork.post("/kgnet/save/upload", context => {
JSON.stringify(savegameEphemeral),
);

console.debug(`=> Savegame persisted to disc.`);
process.stderr.write(`=> Savegame persisted to disc.\n`);

console.warn(`=> Injecting savegame into headless session...`);
process.stderr.write(`=> Injecting savegame into headless session...\n`);
remote
.toHeadless({
type: "injectSavegame",
Expand All @@ -251,21 +259,23 @@ routerNetwork.post("/kgnet/save/upload", context => {
});
routerNetwork.post("/kgnet/save/update", context => {
try {
console.debug(`=> Received savegame update.`);
process.stderr.write(`=> Received savegame update.\n`);

const gameSave = context.request.body as KGNetSaveUpdate;
const gameGUID = gameSave.guid;
const existingSave = saveStore.get(gameGUID);
if (isNil(existingSave)) {
console.warn(`=> Couldn't find existing savegame with ID '${gameGUID}'! Update is ignored.`);
process.stderr.write(
`=> Couldn't find existing savegame with ID '${gameGUID}'! Update is ignored.\n`,
);
return;
}

existingSave.archived = gameSave.metadata?.archived === "true";
existingSave.label = gameSave.metadata?.label ?? existingSave.label;
writeFileSync(`${LOCAL_STORAGE_PATH}/${gameGUID}.json`, JSON.stringify(existingSave));
saveStore.set(gameGUID, existingSave);
console.debug(`=> Savegame persisted to disc.`);
process.stderr.write(`=> Savegame persisted to disc.\n`);

context.body = [...saveStore.values()];
context.status = 200;
Expand Down
21 changes: 21 additions & 0 deletions packages/kitten-analysts/source/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ export const LOCAL_STORAGE_PATH = "/local_storage";

// KGNet Savegame Storage

export interface KGSaveData {
saveVersion: number;
resources: unknown;
telemetry: {
guid: string;
};
game: {
forceShowLimits: unknown;
isCMBREnabled: unknown;
useWorkers: unknown;
colorScheme: unknown;
unlockedSchemes: unknown;
karmaKittens: unknown;
karmaZebras: unknown;
ironWill: unknown;
deadKittens: unknown;
cheatMode: unknown;
opts: unknown;
lastBackup: unknown;
};
}
export interface KGNetSaveFromGame {
guid: string;
metadata: {
Expand Down
44 changes: 30 additions & 14 deletions packages/kitten-analysts/source/network/KittensGameRemote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { v4 as uuid } from "uuid";
import { AddressInfo, RawData, WebSocket, WebSocketServer } from "ws";
import { KGNetSaveFromAnalysts, KGNetSavePersisted, LOCAL_STORAGE_PATH } from "../globals.js";
import { KittenAnalystsMessage, KittenAnalystsMessageId } from "../KittenAnalysts.js";
import { cwarn } from "../tools/Log.js";
import { identifyExchange } from "../tools/MessageFormat.js";

interface RemoteConnection {
Expand All @@ -20,6 +19,7 @@ export class KittensGameRemote {
location: string;
port: number;
pendingRequests = new Map<string, { resolve: AnyFunction; reject: AnyFunction }>();
printProtocolMessages: boolean;
saveStore: Map<string, KGNetSavePersisted>;
sockets = new Set<RemoteConnection>();
wss: WebSocketServer;
Expand All @@ -33,8 +33,13 @@ export class KittensGameRemote {

#lastKnownHeadlessSocket: RemoteConnection | null = null;

constructor(saveStore: Map<string, KGNetSavePersisted>, port = 9093) {
constructor(
saveStore: Map<string, KGNetSavePersisted>,
port = 9093,
printProtocolMessages = false,
) {
this.port = port;
this.printProtocolMessages = printProtocolMessages;
this.saveStore = saveStore;
this.wss = new WebSocketServer({ port });
this.location = `ws://${(this.wss.address() as AddressInfo | null)?.address ?? "localhost"}:${this.port}/`;
Expand Down Expand Up @@ -92,7 +97,8 @@ export class KittensGameRemote {
case "reportFrame": {
const payload = message.data as FrameContext;
const delta = payload.exit - payload.entry;
console.info(`=> Received frame report (${message.location}).`, delta);
if (this.printProtocolMessages)
process.stderr.write(`=> Received frame report (${message.location}).\n`);

this.ks_iterate_duration.observe(
{
Expand Down Expand Up @@ -123,7 +129,8 @@ export class KittensGameRemote {
}
case "reportSavegame": {
const payload = message.data as KGNetSaveFromAnalysts;
console.info(`=> Received savegame (${message.location}).`);
if (this.printProtocolMessages)
process.stderr.write(`=> Received savegame (${message.location}).\n`);

const isHeadlessReport = message.location.includes("headless.html");
if (isHeadlessReport) {
Expand All @@ -148,29 +155,34 @@ export class KittensGameRemote {
`${LOCAL_STORAGE_PATH}/${payload.telemetry.guid}.json`,
JSON.stringify(savegame),
);
console.debug(`=> Savegame persisted to disc.`);
process.stderr.write(`=> Savegame persisted to disc.\n`);
} catch (error) {
console.error("!> Error while persisting savegame to disc!", error);
}

return;
}
default:
console.warn(`!> Report with type '${message.type}' is unexpected! Message ignored.`);
process.stderr.write(
`!> Report with type '${message.type}' is unexpected! Message ignored.\n`,
);
return;
}
}

if (!this.pendingRequests.has(message.responseId)) {
console.warn(`!> Response ID '${message.responseId}' is unexpected! Message ignored.`);
process.stderr.write(
`!> Response ID '${message.responseId}' is unexpected! Message ignored.\n`,
);
return;
}

const pendingRequest = this.pendingRequests.get(message.responseId);
this.pendingRequests.delete(message.responseId);

pendingRequest?.resolve(message);
console.debug(`=> Request ID '${message.responseId}' was resolved.`);
if (this.printProtocolMessages)
process.stderr.write(`=> Request ID '${message.responseId}' was resolved.\n`);
}

sendMessage<TMessage extends KittenAnalystsMessageId>(
Expand Down Expand Up @@ -198,11 +210,15 @@ export class KittensGameRemote {
const requestId = uuid();
message.responseId = requestId;

console.debug(`<= ${identifyExchange(message)}...`);
if (this.printProtocolMessages) process.stderr.write(`<= ${identifyExchange(message)}...\n`);

const request = new Promise<KittenAnalystsMessage<TMessage> | null>((resolve, reject) => {
if (!socket.isAlive || socket.ws.readyState === WebSocket.CLOSED) {
console.warn("Send request can't be handled, because socket is dead!");
if (
!socket.isAlive ||
socket.ws.readyState === WebSocket.CLOSED ||
socket.ws.readyState === WebSocket.CLOSING
) {
process.stderr.write("Send request can't be handled, because socket is dead!\n");
socket.isAlive = false;
resolve(null);
return;
Expand All @@ -223,13 +239,13 @@ export class KittensGameRemote {
message: Omit<KittenAnalystsMessage<TMessage>, "client_type" | "location" | "guid">,
): Promise<KittenAnalystsMessage<TMessage> | null> {
if (isNil(this.#lastKnownHeadlessSocket)) {
cwarn("No headless connection registered. Message is dropped.");
process.stderr.write("No headless connection registered. Message is dropped!\n");
return Promise.resolve(null);
}

if (!this.#lastKnownHeadlessSocket.isAlive) {
cwarn(
"Trying to send to headless session, but last known headless socket is no longer alive. Request is dropped!",
process.stderr.write(
"Trying to send to headless session, but last known headless socket is no longer alive. Request is dropped!\n",
);
return Promise.resolve(null);
}
Expand Down
Loading

0 comments on commit 9ccbeb9

Please sign in to comment.