From fec84ae109d6a890babe7f142a5490c905174ff1 Mon Sep 17 00:00:00 2001 From: alekswilc Date: Fri, 16 Aug 2024 12:55:09 +0200 Subject: [PATCH] v2.0.0-alpha --- package-lock.json | 15 ++-- package.json | 3 +- src/index.ts | 16 +++- src/modules/stations.ts | 23 +++++- src/types/player.ts | 42 +++++++++- src/util/PlayerUtil.ts | 8 +- src/util/SimrailClient.ts | 166 +++++++++++++++++++++++++++++--------- 7 files changed, 226 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 145cade..c177f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,12 @@ { - "name": "play-history", + "name": "simrail-logs", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "play-history", - "version": "1.0.0", - "license": "ISC", + "name": "simrail-logs", + "license": "AGPL-3.0-only", "dependencies": { "@simrail/types": "^0.0.4", "@types/mongoose": "^5.11.97", @@ -16,7 +15,8 @@ "express": "^4.19.2", "mongoose": "^8.5.1", "redis": "^4.6.15", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "wildcard-match": "^5.1.3" }, "devDependencies": { "@types/express": "^4.17.21", @@ -1598,6 +1598,11 @@ "node": ">=16" } }, + "node_modules/wildcard-match": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.3.tgz", + "integrity": "sha512-a95hPUk+BNzSGLntNXYxsjz2Hooi5oL7xOfJR6CKwSsSALh7vUNuTlzsrZowtYy38JNduYFRVhFv19ocqNOZlg==" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index ff302b1..2268fec 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "express": "^4.19.2", "mongoose": "^8.5.1", "redis": "^4.6.15", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "wildcard-match": "^5.1.3" }, "typings": "types/typings.d.ts" } diff --git a/src/index.ts b/src/index.ts index ac8593a..a3feb35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,8 @@ import { StationsModule } from './modules/stations.js'; import { ApiModule } from './http/server.js'; import mongoose from 'mongoose'; import { IPlayer } from './types/player.js'; -import { Station, Server } from '@simrail/types'; +import { Station, Server, Train } from '@simrail/types'; +import { TrainsModule } from './modules/trains.js'; ; (async () => { @@ -23,14 +24,25 @@ import { Station, Server } from '@simrail/types'; global.client = new SimrailClient(); client.on(SimrailClientEvents.StationJoined, (server: Server, station: Station, player: IPlayer) => { - console.log(`${server.ServerCode} |${station.Name} | ${player.personaname} joined`); + console.log(`${server.ServerCode} | ${station.Name} | ${player.personaname} joined`); }); client.on(SimrailClientEvents.StationLeft, (server: Server, station: Station, player: IPlayer, joinedAt: number) => { console.log(`${server.ServerCode} | ${station.Name} | ${player.personaname} left. | ${joinedAt ? dayjs(joinedAt).fromNow() : 'no time data.'}`); }); + client.on(SimrailClientEvents.TrainLeft, (server: Server, train: Train, player: IPlayer, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) => { + console.log(`${server.ServerCode} | ${train.TrainName} | ${player.personaname} left. | ${joinedAt ? dayjs(joinedAt).fromNow() : 'no time data.'} | + ${vehicle} | ${distance / 1000} | ${points}`); + }); + + client.on(SimrailClientEvents.TrainJoined, (server: Server, train: Train, player: IPlayer, start: number) => { + console.log(`${server.ServerCode} | ${train.TrainName} | ${player.personaname} joined | ${start}`); + }); + + StationsModule.load(); + TrainsModule.load(); ApiModule.load(); })(); diff --git a/src/modules/stations.ts b/src/modules/stations.ts index 093b166..2df9ef2 100644 --- a/src/modules/stations.ts +++ b/src/modules/stations.ts @@ -3,12 +3,33 @@ import { MLog } from '../mongo/logs.js'; import { IPlayer } from '../types/player.js'; import { SimrailClientEvents } from '../util/SimrailClient.js'; import { v4 } from 'uuid'; +import { MProfile } from '../mongo/profile.js'; +import { PlayerUtil } from '../util/PlayerUtil.js'; export class StationsModule { public static load() { - client.on(SimrailClientEvents.StationLeft, (server: Server, station: Station, player: IPlayer, joinedAt: number) => { + client.on(SimrailClientEvents.StationLeft, async (server: Server, station: Station, player: IPlayer, joinedAt: number) => { + const stats = await PlayerUtil.getPlayerStats(player.steamid); const date = new Date(); + if (stats) { + const time = date.getTime() - joinedAt; + + const userProfile = await MProfile.findOne({ steam: player.steamid }) ?? await MProfile.create({ steam: player.steamid, id: v4() }); + if (!userProfile.dispatcherStats) userProfile.dispatcherStats = {}; + + if (userProfile.dispatcherStats[station.Name]) { + userProfile.dispatcherStats[station.Name].time = userProfile.dispatcherStats[station.Name].time + time; + } else { + userProfile.dispatcherStats[station.Name] = { + time + } + } + + console.log(userProfile.dispatcherStats); + + console.log(await MProfile.findOneAndUpdate({ id: userProfile.id }, { dispatcherStats: userProfile.dispatcherStats })) + } MLog.create({ id: v4(), diff --git a/src/types/player.ts b/src/types/player.ts index 8331999..ad81b75 100644 --- a/src/types/player.ts +++ b/src/types/player.ts @@ -21,8 +21,48 @@ export type IPlayer = { locstatecode: string } +export type IPlayerStats = { + "steamID": string, + "gameName": string, + "stats": [ + { + "name": "SCORE", + "value": number + }, + { + "name": "DISPATCHER_TIME", + "value": number + }, + { + "name": "DISTANCE_M", + "value": number + } + ], + "achievements": [ + { + "name": "FINISH_MISSION", + "achieved": 0 | 1 + }, + { + "name": "FINISH_ON_TIME", + "achieved": 0 | 1 + }, + { + "name": "FINISH_NIGHT", + "achieved": 0 | 1 + } + ] + +} + export type IPlayerPayload = { response: { players: IPlayer[] } -} \ No newline at end of file +} + +export type IPlayerStatsPayload = { + playerstats: IPlayerStats +} + + diff --git a/src/util/PlayerUtil.ts b/src/util/PlayerUtil.ts index 8bbafc5..9d7473c 100644 --- a/src/util/PlayerUtil.ts +++ b/src/util/PlayerUtil.ts @@ -1,4 +1,4 @@ -import { IPlayerPayload } from '../types/player.js'; +import { IPlayerPayload, IPlayerStatsPayload } from '../types/player.js'; const STEAM_API_KEY = process.env.STEAM_APIKEY; @@ -8,4 +8,10 @@ export class PlayerUtil { if (!data.response.players) return; return data.response.players[0]; } + + public static async getPlayerStats(steamId: string) { + const data = (await fetch(`http://api.steampowered.com/ISteamUserStats/GetUserStatsForGame/v0002/?appid=1422130&key=${STEAM_API_KEY}&steamid=${steamId}`).then(x => x.json())) as IPlayerStatsPayload; + if (!data.playerstats?.stats) return; + return data.playerstats; + } } \ No newline at end of file diff --git a/src/util/SimrailClient.ts b/src/util/SimrailClient.ts index 957e0d6..b95e10d 100644 --- a/src/util/SimrailClient.ts +++ b/src/util/SimrailClient.ts @@ -2,17 +2,24 @@ import { EventEmitter } from 'node:events'; import { IPlayer } from '../types/player.js'; import { PlayerUtil } from './PlayerUtil.js'; -import { Station, ApiResponse, Server } from '@simrail/types'; +import { Station, ApiResponse, Server, Train } from '@simrail/types'; 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: IPlayer) => void): this; on(event: SimrailClientEvents.StationLeft, listener: (server: Server, station: Station, player: IPlayer, joinedAt: number) => void): this; + + on(event: SimrailClientEvents.TrainJoined, listener: (server: Server, train: Train, player: IPlayer, startDistance: number) => void): this; + on(event: SimrailClientEvents.TrainLeft, listener: (server: Server, train: Train, player: IPlayer, joinedAt: number, leftAt: number, points: number, distance: number, vehicle: string) => void): this; + //on(event: string, listener: Function): this; } @@ -21,10 +28,20 @@ export type OccupiedStation = { JoinedAt: number; } +export type OccupiedTrain = { + SteamId: string; + JoinedAt: number; + StartPlayerDistance: number; + StartPlayerPoints: number; +} + export class SimrailClient extends EventEmitter { public stations: Record = {}; public stationsOccupied: Record> = {}; + public trains: Record = {}; + public trainsOccupied: Record> = {}; + public constructor() { super(); this.setup(); @@ -37,16 +54,28 @@ export class SimrailClient extends EventEmitter { return { player, joinedAt: this.stationsOccupied[name].joinedAt }; } + public getTrain(server: Server['ServerCode'], name: string) { + if (!this.trainsOccupied[server] || !this.trainsOccupied[server][name]) return null; + const player = PlayerUtil.getPlayer(this.trainsOccupied[server][name].SteamId); + return { player, joinedAt: this.trainsOccupied[server][name].JoinedAt, startPlayerDistance: this.trainsOccupied[server][name].StartPlayerDistance }; + } + private async setup() { 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']); } @@ -59,48 +88,113 @@ export class SimrailClient extends EventEmitter { // TODO: check performance servers.forEach(async (server) => { const stations = (await fetch('https://panel.simrail.eu:8084/stations-open?serverCode=' + server.ServerCode).then(x => x.json())) as ApiResponse; - if (!stations.result) return; - - if (!this.stations[server.ServerCode]) this.stations[server.ServerCode] = []; - if (!this.stationsOccupied[server.ServerCode]) this.stationsOccupied[server.ServerCode] = {}; - - if (!this.stations[server.ServerCode].length) { + const trains = (await fetch('https://panel.simrail.eu:8084/trains-open?serverCode=' + server.ServerCode).then(x => x.json())) as ApiResponse; + 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); + return; + } + + stations.data.forEach(async (x) => { + const data = this.stations[server.ServerCode].find(y => y.Name === x.Name); + if (!data) return; + + if (data.DispatchedBy[0]?.SteamId !== x.DispatchedBy[0]?.SteamId) { + if (!data.DispatchedBy[0]?.SteamId) { + // join + const date = new Date(); + const player = await PlayerUtil.getPlayer(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() + }; + + return; + } + // leave + const player = await PlayerUtil.getPlayer(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); - return; } - stations.data.forEach(async (x) => { - const data = this.stations[server.ServerCode].find(y => y.Name === x.Name); - if (!data) return; - - if (data.DispatchedBy[0]?.SteamId !== x.DispatchedBy[0]?.SteamId) { - if (!data.DispatchedBy[0]?.SteamId) { - // join - const date = new Date(); - const player = await PlayerUtil.getPlayer(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() - }; - redis.json.set('stations_occupied', '$', this.stationsOccupied); - - return; - } - // leave - const player = await PlayerUtil.getPlayer(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); + 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); + return; } - }) + + trains.data.forEach(async (x) => { + const data = this.trains[server.ServerCode].find(y => y.id === x.id); + if (!data) return; + + if (data.TrainData.ControlledBySteamID !== x.TrainData.ControlledBySteamID) { + if (!data.TrainData.ControlledBySteamID) { + if (!x.TrainData.ControlledBySteamID) return; + // join + const date = new Date(); + const player = await PlayerUtil.getPlayer(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!, + }; + return; + } + + if (!data.TrainData.ControlledBySteamID) return; + const date = new Date(); + + const player = await PlayerUtil.getPlayer(data.TrainData.ControlledBySteamID!); + const playerId = data.TrainData.ControlledBySteamID!; + const trainOccupied = this.trainsOccupied[server.ServerCode][data.TrainNoLocal]; + + setTimeout(async () => { + const playerStats = await PlayerUtil.getPlayerStats(playerId); + const oldKm = trainOccupied?.StartPlayerDistance ?? 0; + + const distance = oldKm ? (playerStats?.stats.find(x => x.name === 'DISTANCE_M')?.value ?? 0) - oldKm : 0; + + const oldPoints = trainOccupied?.StartPlayerPoints ?? 0; + const points = oldPoints ? (playerStats?.stats.find(x => x.name === 'SCORE')?.value ?? 0) - oldPoints : 0; + + + this.emit(SimrailClientEvents.TrainLeft, server, data, player, trainOccupied?.JoinedAt, date.getTime(), points, distance, x.Vehicles[0]); + }, 60000) + 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); + } - this.stations[server.ServerCode] = stations.data; - redis.json.set('stations', '$', this.stations); }); } } \ No newline at end of file