Skip to content

Commit

Permalink
Merge pull request #16 from VedalAI/game
Browse files Browse the repository at this point in the history
game connection mvp
  • Loading branch information
Govorunb authored Jun 10, 2024
2 parents b3e64ae + 7e5074a commit 43c961e
Show file tree
Hide file tree
Showing 14 changed files with 474 additions and 54 deletions.
27 changes: 17 additions & 10 deletions common/types.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
export enum LiteralTypes {
export const enum LiteralTypes {
String,
Integer,
Float,
Boolean,
Vector
}

type EnumTypeName = string;
export const enum AnnounceType {
DefaultAnnounce,
DefaultSilent,
// e.g. "add signal"
AlwaysAnnounce,
// e.g. "say"/"hint" (because the message itself is the announcement)
AlwaysSilent,
}

type EnumTypeName = string;
type ParamType = LiteralTypes | EnumTypeName;

export type Enum = {
name: EnumTypeName;
values: string[];
};

export type Parameter = TextParam | NumericParam | BooleanParam | EnumParam | VectorParam;
type ParameterBase = {
name: string;
Expand Down Expand Up @@ -59,18 +62,21 @@ export type Redeem = {
id: string;
title: string;
description: string;
args: Parameter[];
announce?: AnnounceType;
moderated?: boolean;

image: string;
price: number;
sku: string;
args: Parameter[];
disabled?: boolean;
hidden?: boolean;
};

export type Config = {
version: number;
enums?: Enum[];
redeems?: Redeem[];
enums?: { [name: string]: string[] };
redeems?: { [id: string]: Redeem };
banned?: string[];
message?: string;
};
Expand All @@ -80,6 +86,7 @@ export type Cart = {
id: string;
sku: string;
args: { [name: string]: any };
announce: boolean;
};

export type IdentifiableCart = Cart & {
Expand Down
2 changes: 2 additions & 0 deletions ebs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-ws": "^5.0.2",
"jsonpack": "^1.1.5",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.10.0",
Expand All @@ -21,6 +22,7 @@
"@types/body-parser": "^1.19.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/express-ws": "^3.0.4",
"@types/jsonpack": "^1.1.6",
"@types/jsonwebtoken": "^9.0.6",
"@types/uuid": "^9.0.8",
Expand Down
29 changes: 7 additions & 22 deletions ebs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { config as dotenv } from "dotenv";
import cors from "cors";
import express from "express";
import expressWs from "express-ws";
import bodyParser from "body-parser";
import mysql from "mysql2/promise";
import { privateApiAuth, publicApiAuth } from "./util/middleware";
import { setupDb } from "./util/db";
import { initDb } from "./util/db";

dotenv();

const port = 3000;

export const app = express();
app.use(cors({ origin: "*" }));
export const { app } = expressWs(express());
app.use(cors({ origin: "*" }))
app.use(bodyParser.json());
app.use("/public/*", publicApiAuth);
app.use("/private/*", privateApiAuth);
Expand All @@ -20,31 +20,16 @@ app.get("/", (_, res) => {
res.send("YOU ARE TRESPASSING ON PRIVATE PROPERTY YOU HAVE 5 SECONDS TO GET OUT OR I WILL CALL THE POLICE");
});

export let db: mysql.Connection;

async function main() {
while (true) {
try {
db = await mysql.createConnection({
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
});
break;
} catch {
console.log("Failed to connect to database. Retrying in 5 seconds...");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}

await setupDb();
await initDb();

app.listen(port, () => {
console.log("Listening on port " + port);

require("./modules/config");
require("./modules/transactions");
require("./modules/game");

});
}

Expand Down
156 changes: 156 additions & 0 deletions ebs/src/modules/game/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Message, MessageType } from "./messages";
import { ResultMessage, HelloMessage, GameMessage } from "./messages.game";
import * as ServerWS from "ws";
import { v4 as uuid } from "uuid";
import { CommandInvocationSource, RedeemMessage, ServerMessage } from "./messages.server";
import { Redeem } from "common/types";

const VERSION = "0.1.0";

type ResultHandler = (result: ResultMessage) => any;

export class GameConnection {
private handshake: boolean = false;
private socket: ServerWS | null = null;
private resultHandlers: ResultHandler[] = [];
private outstandingRedeems: Map<string, RedeemMessage> = new Map<string, RedeemMessage>();

public isConnected() {
return this.socket?.readyState == ServerWS.OPEN;
}
public onResult(handler: ResultHandler) {
this.resultHandlers.push(handler);
}
public setSocket(ws: ServerWS | null) {
if (this.isConnected()) {
this.socket!.close();
}
this.socket = ws;
if (!ws) {
return;
}
ws.on('connection', () => {
this.handshake = false;
})
ws.on('message', async (message) => {
const msgText = message.toString();
let msg: GameMessage;
try {
msg = JSON.parse(msgText);
} catch {
console.error("Could not parse message" + msgText);
return;
}
console.log(`Got message ${JSON.stringify(msg)}`);
this.processMessage(msg);
});
ws.on("close", (code, reason) => {
console.log(`Connection closed with code ${code} and reason ${reason}`);
})
ws.on("error", (error) => {
console.log(`Connection error ${error}`);
})
}
public async processMessage(msg: GameMessage) {
switch (msg.messageType) {
case MessageType.Hello:
this.handshake = true;
const reply = {
...this.makeMessage(MessageType.HelloBack),
allowed: msg.version == VERSION,
}
this.sendMessage(reply);
break;
case MessageType.Ping:
this.sendMessage(this.makeMessage(MessageType.Pong));
break;
case MessageType.Result:
if (!this.outstandingRedeems.has(msg.guid)) {
console.error(`[${msg.guid}] Redeeming untracked ${msg.guid} (either unpaid or more than once)`);
}
for (const handler of this.resultHandlers) {
handler(msg);
}
this.outstandingRedeems.delete(msg.guid);
break;
case MessageType.IngameStateChanged:
this.logMessage(msg, `${MessageType[MessageType.IngameStateChanged]} stub`);
break;
// case MessageType.CommandAvailabilityChanged:
// await this.updateCommandAvailability(msg);
// break;
default:
console.error(`[${msg.guid}] Unknown message type ${msg.messageType}`);
break;
}
}
// private async updateCommandAvailability(msg: CommandAvailabilityChangedMessage) {
// const config = await getConfig();
// if (!config) {
// console.error("Can't change command availability, no config");
// }
// for (const id of msg.becameAvailable) {
// const redeem = config.redeems![id];
// redeem.disabled = false;
// }
// for (const id of msg.becameUnavailable) {
// const redeem = config.redeems![id];
// redeem.disabled = true;
// }
// broadcastConfigRefresh(config);
// }

public sendMessage(msg: ServerMessage) {
if (!this.socket) {
// todo queue unsent messages
console.error(`Tried to send message without a connected socket: ${JSON.stringify(msg)}`);
return;
}
if (!this.handshake) {
console.error(`Tried to send message before handshake was complete: ${JSON.stringify(msg)}`);
return;
}
this.socket.send(JSON.stringify(msg), { binary: false, fin: true }, (err) => {
if (err)
console.error(err);
});
console.log(`Sent message ${JSON.stringify(msg)}`);
}
public makeMessage(type: MessageType, guid?: string): Message {
return {
messageType: type,
guid: guid ?? uuid(),
timestamp: Date.now()
}
}
public redeem(redeem: Redeem, args: {[name: string]: any}, announce: boolean, transactionId: string) {
if (!transactionId) {
console.error(`Tried to redeem without transaction ID`);
return;
}

const msg: RedeemMessage = {
...this.makeMessage(MessageType.Redeem),
guid: transactionId,
source: CommandInvocationSource.Swarm,
command: redeem.id,
title: redeem.title,
announce,
args
} as RedeemMessage;
if (this.outstandingRedeems.has(msg.guid)) {
console.error(`Redeeming ${msg.guid} more than once`);
}
this.outstandingRedeems.set(msg.guid, msg);

if (!this.isConnected()) {
console.error(`Redeemed without active connection`);
}

this.sendMessage(msg);
}

private logMessage(msg: Message, message: string) {
console.log(`[${msg.guid}] ${message}`);
}
}
61 changes: 61 additions & 0 deletions ebs/src/modules/game/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { app } from "../../index";
import { logToDiscord } from "../../util/logger";
import { GameConnection } from "./connection";
import { MessageType } from "./messages";
import { ResultMessage } from "./messages.game";
import { CommandInvocationSource, RedeemMessage } from "./messages.server";

export let connection: GameConnection = new GameConnection();

connection.onResult((res) => {
if (!res.success) {
logToDiscord({
transactionToken: res.guid,
userIdInsecure: null,
important: false,
fields: [
{
header: "Redeem did not succeed",
content: res,
},
],
});
} else {
console.log(`[${res.guid}] Redeem succeeded: ${JSON.stringify(res)}`);
}
})

app.ws("/private/socket", async (ws, req) => {
connection.setSocket(ws);
})

app.post("/private/redeem", async (req, res) => {
//console.log(req.body);
const msg = {
...connection.makeMessage(MessageType.Redeem),
source: CommandInvocationSource.Dev,
...req.body,
} as RedeemMessage;
if (!connection.isConnected()) {
res.status(500).send("Not connected");
return;
}

connection.sendMessage(msg);
res.status(201).send(JSON.stringify(msg));
})

app.post("/private/setresult", async (req, res) => {
//console.log(req.body);
const msg = {
...connection.makeMessage(MessageType.Result),
...req.body,
} as ResultMessage;
if (!connection.isConnected()) {
res.status(500).send("Not connected");
return;
}

connection.processMessage(msg);
res.sendStatus(200);
});
Loading

0 comments on commit 43c961e

Please sign in to comment.