simrail.pro/packages/backend/src/util/SimrailClient.ts
Aleksander Wilczyński efd51344f0
fix(backend): main loop stops when simrail api is unresponsive
Signed-off-by: Aleksander Wilczyński <aleks@alekswilc.dev>
2024-12-02 12:04:36 +01:00

311 lines
12 KiB
TypeScript

/*
* Copyright (C) 2024 Aleksander <alekswilc> Wilczyński (aleks@alekswilc.dev)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* See LICENSE for more.
*/
import { EventEmitter } from "node:events";
import { PlayerUtil } from "./PlayerUtil.js";
import { Station, ApiResponse, Server, Train } from "@simrail/types";
import { TMProfile } from "../mongo/profile.js";
export enum SimrailClientEvents
{
StationJoined = "stationJoined",
StationLeft = "stationLeft",
TrainJoined = "trainJoined",
TrainLeft = "trainLeft",
}
export declare interface SimrailClient
{
on(event: SimrailClientEvents.StationJoined, listener: (server: Server, station: Station, player: TMProfile) => void): this;
on(event: SimrailClientEvents.StationLeft, listener: (server: Server, station: Station, player: TMProfile, joinedAt: number) => void): this;
on(event: SimrailClientEvents.TrainJoined, listener: (server: Server, train: Train, player: TMProfile, startDistance: number) => void): this;
on(event: SimrailClientEvents.TrainLeft, listener: (server: Server, train: Train, player: TMProfile, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) => void): this;
//on(event: string, listener: Function): this;
}
export type OccupiedStation = {
SteamId: string;
JoinedAt: number;
}
export type OccupiedTrain = {
SteamId: string;
JoinedAt: number;
StartPlayerDistance: number;
StartPlayerPoints: number;
}
export class SimrailClient extends EventEmitter
{
public stations: Record<Server["ServerCode"], Station[]> = {};
public stationsOccupied: Record<Server["ServerCode"], Record<string, OccupiedStation | null>> = {};
public trains: Record<Server["ServerCode"], Train[]> = {};
public trainsOccupied: Record<Server["ServerCode"], Record<string, OccupiedTrain | null>> = {};
public constructor()
{
super();
this.setup().then(() => {
void this.update(false);
});
}
// todo: full rewrite, rewrite db structure with option to join log to user profile, check for negative values in user profile
// todo: wipe database 13.12.2024
private async setup()
{
if (!await redis.get('last_updated')) {
await redis.json.set('trains_occupied', '$', {});
await redis.json.set('trains', '$', []);
await redis.json.set('stations', '$', []);
await redis.json.set('stations_occupied', '$', {});
}
const lastUpdated = Date.now() - (Number(await redis.get('last_updated')) ?? 0);
if (lastUpdated > 300_000) {
console.log('REDIS: last updated more than > 5 mins');
await redis.json.set('trains_occupied', '$', {});
await redis.json.set('trains', '$', []);
await redis.json.set('stations', '$', []);
await redis.json.set('stations_occupied', '$', {});
}
if (!await redis.json.get('stations'))
redis.json.set('stations', '$', []);
if (!await redis.json.get('trains'))
redis.json.set('trains', '$', []);
if (!await redis.json.get('trains_occupied'))
redis.json.set('trains_occupied', '$', {});
if (!await redis.json.get('stations_occupied'))
redis.json.set('stations_occupied', '$', {});
this.stations = (await redis.json.get('stations') as unknown as SimrailClient['stations']);
this.stationsOccupied = (await redis.json.get('stations_occupied') as unknown as SimrailClient['stationsOccupied']);
this.trains = (await redis.json.get('trains') as unknown as SimrailClient['trains']);
this.trainsOccupied = (await redis.json.get('trains_occupied') as unknown as SimrailClient['trainsOccupied']);
redis.set('last_updated', Date.now().toString());
}
private async processStation(server: Server, stations: ApiResponse<Station>)
{
if (stations.result)
{
if (!this.stations[ server.ServerCode ])
{
this.stations[ server.ServerCode ] = [];
}
if (!this.stationsOccupied[ server.ServerCode ])
{
this.stationsOccupied[ server.ServerCode ] = {};
}
if (!this.stations[ server.ServerCode ].length)
{
this.stations[ server.ServerCode ] = stations.data;
redis.json.set("stations", "$", this.stations);
redis.set('last_updated', Date.now().toString());
}
for (const x of stations.data)
{
const data = this.stations[ server.ServerCode ].find(y => y.Name === x.Name);
if (!data)
{
continue;
}
if (data.DispatchedBy[ 0 ]?.SteamId !== x.DispatchedBy[ 0 ]?.SteamId)
{
if (!data.DispatchedBy[ 0 ]?.SteamId)
{
// join
const date = new Date();
const player = await PlayerUtil.ensurePlayer(x.DispatchedBy[ 0 ]?.SteamId);
this.emit(SimrailClientEvents.StationJoined, server, x, player);
this.stationsOccupied[ server.ServerCode ][ data.Prefix ] = {
SteamId: x.DispatchedBy[ 0 ]?.SteamId,
JoinedAt: date.getTime(),
};
continue;
}
// leave
const player = await PlayerUtil.ensurePlayer(data.DispatchedBy[ 0 ]?.SteamId);
this.emit(SimrailClientEvents.StationLeft, server, x, player, this.stationsOccupied[ server.ServerCode ][ data.Prefix ]?.JoinedAt);
delete this.stationsOccupied[ server.ServerCode ][ data.Prefix ];
}
}
redis.json.set("stations_occupied", "$", this.stationsOccupied);
this.stations[ server.ServerCode ] = stations.data;
redis.json.set("stations", "$", this.stations);
redis.set('last_updated', Date.now().toString());
}
}
private async processTrain(server: Server, trains: ApiResponse<Train>)
{
if (trains.result)
{
if (!this.trains[ server.ServerCode ])
{
this.trains[ server.ServerCode ] = [];
}
if (!this.trainsOccupied[ server.ServerCode ])
{
this.trainsOccupied[ server.ServerCode ] = {};
}
if (!this.trains[ server.ServerCode ].length)
{
this.trains[ server.ServerCode ] = trains.data;
redis.json.set("trains", "$", this.trains);
redis.set('last_updated', Date.now().toString());
return;
}
for (const x of trains.data)
{
const data = this.trains[ server.ServerCode ].find(y => y.id === x.id);
if (!data)
{
continue;
}
if (data.TrainData.ControlledBySteamID !== x.TrainData.ControlledBySteamID)
{
if (!data.TrainData.ControlledBySteamID)
{
if (!x.TrainData.ControlledBySteamID)
{
continue;
}
// join
const date = new Date();
const player = await PlayerUtil.ensurePlayer(x.TrainData.ControlledBySteamID!);
const playerStats = await PlayerUtil.getPlayerStats(x.TrainData.ControlledBySteamID!);
this.emit(SimrailClientEvents.TrainJoined, server, x, player, playerStats?.stats.find(x => x.name === "DISTANCE_M")?.value);
this.trainsOccupied[ server.ServerCode ][ x.TrainNoLocal ] = {
SteamId: x.TrainData.ControlledBySteamID!,
JoinedAt: date.getTime(),
StartPlayerDistance: playerStats?.stats.find(x => x.name === "DISTANCE_M")?.value!,
StartPlayerPoints: playerStats?.stats.find(x => x.name === "SCORE")?.value!,
};
continue;
}
if (!data.TrainData.ControlledBySteamID)
{
continue;
}
const date = new Date();
const player = await PlayerUtil.ensurePlayer(data.TrainData.ControlledBySteamID!);
const playerId = data.TrainData.ControlledBySteamID!;
const trainOccupied = this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ] && JSON.parse(JSON.stringify(this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ])) || null;
setTimeout(() =>
{
PlayerUtil.getPlayerStats(playerId).then(playerStats =>
{
const oldKm = trainOccupied?.StartPlayerDistance ?? 0;
let distance = oldKm ? (playerStats?.stats.find(x => x.name === "DISTANCE_M")?.value ?? 0) - oldKm : 0;
const oldPoints = trainOccupied?.StartPlayerPoints ?? 0;
let points = oldPoints ? (playerStats?.stats.find(x => x.name === "SCORE")?.value ?? 0) - oldPoints : 0;
if (distance < 0) {
console.warn(`Player ${playerId}, Train ${data.TrainNoLocal} - distance < 0`);
distance = 0;
}
if (points < 0) {
console.warn(`Player ${playerId}, Train ${data.TrainNoLocal} - distance < 0`);
points = 0;
}
this.emit(SimrailClientEvents.TrainLeft, server, data, player, trainOccupied?.JoinedAt, date.getTime(), points, distance, x.Vehicles[ 0 ]);
});
}, 30_000);
delete this.trainsOccupied[ server.ServerCode ][ data.TrainNoLocal ];
}
}
this.trains[ server.ServerCode ] = trains.data;
redis.json.set("trains", "$", this.trains);
redis.json.set("trains_occupied", "$", this.trainsOccupied);
redis.set('last_updated', Date.now().toString());
}
}
private async update(needSetup: boolean = false)
{
const servers = (await fetch("https://panel.simrail.eu:8084/servers-open").then(x => x.json()).catch(() => ({ data: [], result: false })) as ApiResponse<Server>)
.data ?? [] //?.filter(x => x.ServerName.includes("Polski")) ?? []; // TODO: remove this in v3
if (!servers.length)
{
console.log("SimrailAPI is down");
await new Promise(res => setTimeout(res, 5000));
await this.update(true);
return;
}
if (needSetup)
{
await this.setup();
}
// TODO: maybe node:worker_threads?
// TODO: check performance
for (const server of servers)
{
const stations = (await fetch('https://panel.simrail.eu:8084/stations-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Station>;
const trains = (await fetch('https://panel.simrail.eu:8084/trains-open?serverCode=' + server.ServerCode).then(x => x.json()).catch(() => ({ result: false }))) as ApiResponse<Train>;
await this.processStation(server, stations);
await this.processTrain(server, trains);
}
await new Promise(res => setTimeout(res, 1000));
await this.update();
}
}