Skip to content

Commit 64cd0cc

Browse files
feat: Operators
1 parent 82ad544 commit 64cd0cc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2483
-210
lines changed

packages/kitten-analysts/examples/a708b0ad-5f94-4466-8a2e-1d381117d0e0.json

+1-1
Large diffs are not rendered by default.

packages/kitten-analysts/examples/ka-internal-savestate.json

+1-1
Large diffs are not rendered by default.

packages/kitten-analysts/source/entrypoint-backend.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { isNil } from "@oliversalzburg/js-utils/data/nil.js";
44
import { redirectErrorsToConsole } from "@oliversalzburg/js-utils/errors/console.js";
55
import Koa from "koa";
66
import Router from "koa-router";
7+
import { compressToUTF16, decompressFromUTF16 } from "lz-string";
78
import { writeFileSync } from "node:fs";
89
import { readdir, readFile } from "node:fs/promises";
910
import { join } from "node:path";
@@ -12,6 +13,7 @@ import {
1213
KGNetSaveFromGame,
1314
KGNetSavePersisted,
1415
KGNetSaveUpdate,
16+
KGSaveData,
1517
LOCAL_STORAGE_PATH,
1618
} from "./globals.js";
1719
import {
@@ -57,6 +59,7 @@ const PORT_HTTP_METRICS = process.env.PORT_WS_BACKEND
5759
? Number(process.env.PORT_HTTP_METRICS)
5860
: 9091;
5961
const PORT_WS_BACKEND = process.env.PORT_WS_BACKEND ? Number(process.env.PORT_WS_BACKEND) : 9093;
62+
const PROTOCOL_DEBUG = Boolean(process.env.PROTOCOL_DEBUG);
6063

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

7780
// Websocket stuff
7881

79-
const remote = new KittensGameRemote(saveStore, PORT_WS_BACKEND);
82+
const remote = new KittensGameRemote(saveStore, PORT_WS_BACKEND, PROTOCOL_DEBUG);
8083

8184
// Prometheus stuff
8285

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

200203
routerNetwork.post("/kgnet/save/upload", context => {
201204
try {
202-
console.debug(`=> Received savegame.`);
205+
if (PROTOCOL_DEBUG) process.stderr.write(`=> Received savegame.`);
203206

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

222+
// Rebuild payload to also contain the fixed-string telemetry GUID.
223+
const uncompressed = JSON.parse(decompressFromUTF16(gameSave.saveData)) as KGSaveData;
224+
uncompressed.telemetry.guid = "ka-internal-savestate";
225+
const recompressedSaveData = compressToUTF16(JSON.stringify(uncompressed));
226+
219227
const savegameEphemeral: KGNetSavePersisted = {
220228
archived: false,
221229
guid: "ka-internal-savestate",
222230
index: { calendar: { day: calendar.day, year: calendar.year } },
223231
label: "Background Game",
224-
saveData: gameSave.saveData,
232+
saveData: recompressedSaveData,
225233
size: context.request.length,
226234
timestamp: Date.now(),
227235
};
@@ -231,9 +239,9 @@ routerNetwork.post("/kgnet/save/upload", context => {
231239
JSON.stringify(savegameEphemeral),
232240
);
233241

234-
console.debug(`=> Savegame persisted to disc.`);
242+
process.stderr.write(`=> Savegame persisted to disc.\n`);
235243

236-
console.warn(`=> Injecting savegame into headless session...`);
244+
process.stderr.write(`=> Injecting savegame into headless session...\n`);
237245
remote
238246
.toHeadless({
239247
type: "injectSavegame",
@@ -251,21 +259,23 @@ routerNetwork.post("/kgnet/save/upload", context => {
251259
});
252260
routerNetwork.post("/kgnet/save/update", context => {
253261
try {
254-
console.debug(`=> Received savegame update.`);
262+
process.stderr.write(`=> Received savegame update.\n`);
255263

256264
const gameSave = context.request.body as KGNetSaveUpdate;
257265
const gameGUID = gameSave.guid;
258266
const existingSave = saveStore.get(gameGUID);
259267
if (isNil(existingSave)) {
260-
console.warn(`=> Couldn't find existing savegame with ID '${gameGUID}'! Update is ignored.`);
268+
process.stderr.write(
269+
`=> Couldn't find existing savegame with ID '${gameGUID}'! Update is ignored.\n`,
270+
);
261271
return;
262272
}
263273

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

270280
context.body = [...saveStore.values()];
271281
context.status = 200;

packages/kitten-analysts/source/globals.ts

+21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@ export const LOCAL_STORAGE_PATH = "/local_storage";
22

33
// KGNet Savegame Storage
44

5+
export interface KGSaveData {
6+
saveVersion: number;
7+
resources: unknown;
8+
telemetry: {
9+
guid: string;
10+
};
11+
game: {
12+
forceShowLimits: unknown;
13+
isCMBREnabled: unknown;
14+
useWorkers: unknown;
15+
colorScheme: unknown;
16+
unlockedSchemes: unknown;
17+
karmaKittens: unknown;
18+
karmaZebras: unknown;
19+
ironWill: unknown;
20+
deadKittens: unknown;
21+
cheatMode: unknown;
22+
opts: unknown;
23+
lastBackup: unknown;
24+
};
25+
}
526
export interface KGNetSaveFromGame {
627
guid: string;
728
metadata: {

packages/kitten-analysts/source/network/KittensGameRemote.ts

+30-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { v4 as uuid } from "uuid";
99
import { AddressInfo, RawData, WebSocket, WebSocketServer } from "ws";
1010
import { KGNetSaveFromAnalysts, KGNetSavePersisted, LOCAL_STORAGE_PATH } from "../globals.js";
1111
import { KittenAnalystsMessage, KittenAnalystsMessageId } from "../KittenAnalysts.js";
12-
import { cwarn } from "../tools/Log.js";
1312
import { identifyExchange } from "../tools/MessageFormat.js";
1413

1514
interface RemoteConnection {
@@ -20,6 +19,7 @@ export class KittensGameRemote {
2019
location: string;
2120
port: number;
2221
pendingRequests = new Map<string, { resolve: AnyFunction; reject: AnyFunction }>();
22+
printProtocolMessages: boolean;
2323
saveStore: Map<string, KGNetSavePersisted>;
2424
sockets = new Set<RemoteConnection>();
2525
wss: WebSocketServer;
@@ -33,8 +33,13 @@ export class KittensGameRemote {
3333

3434
#lastKnownHeadlessSocket: RemoteConnection | null = null;
3535

36-
constructor(saveStore: Map<string, KGNetSavePersisted>, port = 9093) {
36+
constructor(
37+
saveStore: Map<string, KGNetSavePersisted>,
38+
port = 9093,
39+
printProtocolMessages = false,
40+
) {
3741
this.port = port;
42+
this.printProtocolMessages = printProtocolMessages;
3843
this.saveStore = saveStore;
3944
this.wss = new WebSocketServer({ port });
4045
this.location = `ws://${(this.wss.address() as AddressInfo | null)?.address ?? "localhost"}:${this.port}/`;
@@ -92,7 +97,8 @@ export class KittensGameRemote {
9297
case "reportFrame": {
9398
const payload = message.data as FrameContext;
9499
const delta = payload.exit - payload.entry;
95-
console.info(`=> Received frame report (${message.location}).`, delta);
100+
if (this.printProtocolMessages)
101+
process.stderr.write(`=> Received frame report (${message.location}).\n`);
96102

97103
this.ks_iterate_duration.observe(
98104
{
@@ -123,7 +129,8 @@ export class KittensGameRemote {
123129
}
124130
case "reportSavegame": {
125131
const payload = message.data as KGNetSaveFromAnalysts;
126-
console.info(`=> Received savegame (${message.location}).`);
132+
if (this.printProtocolMessages)
133+
process.stderr.write(`=> Received savegame (${message.location}).\n`);
127134

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

156163
return;
157164
}
158165
default:
159-
console.warn(`!> Report with type '${message.type}' is unexpected! Message ignored.`);
166+
process.stderr.write(
167+
`!> Report with type '${message.type}' is unexpected! Message ignored.\n`,
168+
);
160169
return;
161170
}
162171
}
163172

164173
if (!this.pendingRequests.has(message.responseId)) {
165-
console.warn(`!> Response ID '${message.responseId}' is unexpected! Message ignored.`);
174+
process.stderr.write(
175+
`!> Response ID '${message.responseId}' is unexpected! Message ignored.\n`,
176+
);
166177
return;
167178
}
168179

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

172183
pendingRequest?.resolve(message);
173-
console.debug(`=> Request ID '${message.responseId}' was resolved.`);
184+
if (this.printProtocolMessages)
185+
process.stderr.write(`=> Request ID '${message.responseId}' was resolved.\n`);
174186
}
175187

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

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

203215
const request = new Promise<KittenAnalystsMessage<TMessage> | null>((resolve, reject) => {
204-
if (!socket.isAlive || socket.ws.readyState === WebSocket.CLOSED) {
205-
console.warn("Send request can't be handled, because socket is dead!");
216+
if (
217+
!socket.isAlive ||
218+
socket.ws.readyState === WebSocket.CLOSED ||
219+
socket.ws.readyState === WebSocket.CLOSING
220+
) {
221+
process.stderr.write("Send request can't be handled, because socket is dead!\n");
206222
socket.isAlive = false;
207223
resolve(null);
208224
return;
@@ -223,13 +239,13 @@ export class KittensGameRemote {
223239
message: Omit<KittenAnalystsMessage<TMessage>, "client_type" | "location" | "guid">,
224240
): Promise<KittenAnalystsMessage<TMessage> | null> {
225241
if (isNil(this.#lastKnownHeadlessSocket)) {
226-
cwarn("No headless connection registered. Message is dropped.");
242+
process.stderr.write("No headless connection registered. Message is dropped!\n");
227243
return Promise.resolve(null);
228244
}
229245

230246
if (!this.#lastKnownHeadlessSocket.isAlive) {
231-
cwarn(
232-
"Trying to send to headless session, but last known headless socket is no longer alive. Request is dropped!",
247+
process.stderr.write(
248+
"Trying to send to headless session, but last known headless socket is no longer alive. Request is dropped!\n",
233249
);
234250
return Promise.resolve(null);
235251
}

0 commit comments

Comments
 (0)