UNPKG

@krp-races/krp-node-wrapper

Version:

A node.js wrapper for a dedicated or challenge server in kart racing pro.

575 lines (572 loc) 26.1 kB
import { DataType } from '../enums/DataType.mjs'; import { ChallengeType } from '../enums/Event/ChallengeType.mjs'; import { EventType } from '../enums/Event/EventType.mjs'; import { KartStatus } from '../enums/Session/Classification/KartStatus.mjs'; import { RaceStatus } from '../enums/Session/Classification/RaceStatus.mjs'; import { DriverStatusReason } from '../enums/Session/SessionEntry/DriverStatusReason.mjs'; import { DriverStatusState } from '../enums/Session/SessionEntry/DriverStatusState.mjs'; import { SessionState } from '../enums/Session/SessionState.mjs'; import { SessionType } from '../enums/Session/SessionType.mjs'; import { Vec2 } from '../utils/Vec2.mjs'; import { Vec3 } from '../utils/Vec3.mjs'; class LivetimingReader { constructor(lines, data, offset = 2) { this.lines = lines; this.data = data; this.offset = offset; } read() { while (this.offset !== -1) { const type = this.lines[this.offset]; switch (type) { case DataType.EVENT: this.readEvent(); break; case DataType.CHALLENGEDATA: this.readChallengeData(); break; case DataType.ENTRY: case DataType.ENTRYREMOVE: this.readEntry(); break; case DataType.SESSION: case DataType.SESSIONSTATUS: case DataType.WEATHER: case DataType.SESSIONENTRY: case DataType.DRIVERSTATUS: case DataType.BESTLAP: case DataType.LASTLAP: case DataType.PENALTY: case DataType.LAP: case DataType.SPLIT: case DataType.SPEED: case DataType.CLASSIFICATION: this.readSession(); break; case DataType.CONTACT: this.readContact(); break; case DataType.TRACKDATA: case DataType.TRACKSEGMENT: case DataType.TRACKPOSITION: this.readTrackData(); break; } // Determine next offset this.offset = this.lines.findIndex((line, index) => index > this.offset && Object.values(DataType).includes(line)); } return this.data; } readEvent() { const type = this.lines[this.offset + 1]; switch (type) { case EventType.CHALLENGE: this.data.event = { type, name: this.lines[this.offset + 2], track: { name: this.lines[this.offset + 3], length: parseFloat(this.lines[this.offset + 4]), }, allowed: this.lines[this.offset + 5].replace(" + ", "/").split("/"), challengeType: this.lines[this.offset + 6], challengeLength: parseFloat(this.lines[this.offset + 7]), challengeMaxTries: parseInt(this.lines[this.offset + 8]), challengeData: new Map(), }; break; default: this.data.event = { type, name: this.lines[this.offset + 2], track: { name: this.lines[this.offset + 3], length: parseFloat(this.lines[this.offset + 4]), }, allowed: this.lines[this.offset + 5].replace(" + ", "/").split("/"), }; break; } } readChallengeData() { var _a; if (((_a = this.data.event) === null || _a === void 0 ? void 0 : _a.type) !== EventType.CHALLENGE) return; const event = this.data.event; let data; switch (event.challengeType) { case ChallengeType.PRACTICE: data = { name: this.lines[this.offset + 1], kart: { name: this.lines[this.offset + 2], shortName: this.lines[this.offset + 3], }, guid: this.lines[this.offset + 4], extra: this.lines[this.offset + 5], try: parseInt(this.lines[this.offset + 6]), bestLap: parseFloat(this.lines[this.offset + 7]), lapNumber: parseInt(this.lines[this.offset + 8]), totalLaps: parseInt(this.lines[this.offset + 9]), }; break; case ChallengeType.RACE: data = { name: this.lines[this.offset + 1], kart: { name: this.lines[this.offset + 2], shortName: this.lines[this.offset + 3], }, guid: this.lines[this.offset + 4], extra: this.lines[this.offset + 5], try: parseInt(this.lines[this.offset + 6]), }; break; } if (!data) return; event.challengeData.set(data.guid, data); this.data.event = event; } readEntry() { const type = this.lines[this.offset]; const raceNumber = parseInt(this.lines[this.offset + 1]); switch (type) { case DataType.ENTRY: { const entry = { raceNumber, name: this.lines[this.offset + 2], kart: { name: this.lines[this.offset + 3], shortName: this.lines[this.offset + 4], categories: this.lines[this.offset + 5].split("/"), }, guid: this.lines[this.offset + 6], extra: this.lines[this.offset + 7], online: true, }; this.data.entries.set(entry.raceNumber, entry); } break; case DataType.ENTRYREMOVE: { const currentEntry = this.data.entries.get(raceNumber); if (!currentEntry) break; currentEntry.online = false; this.data.entries.set(raceNumber, currentEntry); } break; } } readSession() { const type = this.lines[this.offset]; switch (type) { case DataType.SESSION: { const sessionType = this.readSessionType(this.lines[this.offset + 1]); const session = { type: sessionType.type, count: sessionType.count, state: this.lines[this.offset + 2], length: parseFloat(this.lines[this.offset + 3]), entries: new Map(), }; if (session.type === SessionType.WAITING) this.data.sessions.clear(); this.data.sessions.set(sessionType.combined, session); } break; case DataType.SESSIONSTATUS: { const sessionType = this.readSessionType(this.lines[this.offset + 1]); const currentSession = this.data.sessions.get(sessionType.combined); if (!currentSession) break; currentSession.state = this.lines[this.offset + 2]; this.data.sessions.set(sessionType.combined, currentSession); } break; case DataType.WEATHER: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const weather = { condition: this.lines[this.offset + 1], air: parseFloat(this.lines[this.offset + 2]), track: parseFloat(this.lines[this.offset + 3]), }; currentSession.weather = weather; this.data.sessions.set(sessionKey, currentSession); } break; case DataType.SESSIONENTRY: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const entry = { raceNumber, state: DriverStatusState.NONE, reason: DriverStatusReason.NONE, laps: [], penalties: [], splits: [], speeds: [], }; currentSession.entries.set(raceNumber, entry); } break; case DataType.DRIVERSTATUS: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const state = this.lines[this.offset + 2]; const reason = this.lines[this.offset + 3]; const currentEntry = currentSession.entries.get(raceNumber); if (!currentEntry) break; currentEntry.state = state; currentEntry.reason = reason; currentSession.entries.set(raceNumber, currentEntry); } break; case DataType.BESTLAP: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const sessionTime = parseFloat(this.lines[this.offset + 2]); const lapTime = parseFloat(this.lines[this.offset + 3]); const lapNumber = parseInt(this.lines[this.offset + 4]); const splits = Vec2(parseFloat(this.lines[this.offset + 5]), parseFloat(this.lines[this.offset + 6])); const speed = parseFloat(this.lines[this.offset + 7]); const currentEntry = currentSession.entries.get(raceNumber); if (!currentEntry) break; currentEntry.bestLap = { raceNumber, sessionTime, lapTime, lapNumber, splits, speed, invalid: false, }; currentSession.entries.set(raceNumber, currentEntry); } break; case DataType.LASTLAP: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const sessionTime = parseFloat(this.lines[this.offset + 2]); const lapTime = parseFloat(this.lines[this.offset + 3]); const lapNumber = parseInt(this.lines[this.offset + 4]); const splits = Vec2(parseFloat(this.lines[this.offset + 5]), parseFloat(this.lines[this.offset + 6])); const speed = parseFloat(this.lines[this.offset + 7]); const currentEntry = currentSession.entries.get(raceNumber); if (!currentEntry) break; currentEntry.lastLap = { raceNumber, sessionTime, lapTime, lapNumber, splits, speed, invalid: false, }; currentSession.entries.set(raceNumber, currentEntry); } break; case DataType.PENALTY: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const penaltyNumber = parseInt(this.lines[this.offset + 2]); const type = this.lines[this.offset + 3]; const penalty = parseFloat(this.lines[this.offset + 4]); const offence = this.lines[this.offset + 5]; const currentEntry = currentSession.entries.get(raceNumber); if (!currentEntry) break; currentEntry.penalties.push({ raceNumber, penaltyNumber, type, penalty, offence, }); currentSession.entries.set(raceNumber, currentEntry); } break; case DataType.LAP: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const invalid = parseInt(this.lines[this.offset + 2]) === 1; const sessionTime = parseFloat(this.lines[this.offset + 3]); const lapTime = parseFloat(this.lines[this.offset + 4]); const splits = Vec2(parseFloat(this.lines[this.offset + 5]), parseFloat(this.lines[this.offset + 6])); const speed = parseFloat(this.lines[this.offset + 7]); const currentEntry = currentSession.entries.get(raceNumber); if (!currentEntry) break; currentEntry.laps.push({ raceNumber, sessionTime, lapTime, splits, speed, invalid, }); currentSession.entries.set(raceNumber, currentEntry); } break; case DataType.SPLIT: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const splitNumber = parseInt(this.lines[this.offset + 2]); const splitTime = parseFloat(this.lines[this.offset + 2]); const currentEntry = currentSession.entries.get(raceNumber); if (!currentEntry) break; currentEntry.splits.push({ raceNumber, splitNumber, splitTime, }); currentSession.entries.set(raceNumber, currentEntry); } break; case DataType.SPEED: { const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionKey); if (!currentSession) break; const raceNumber = parseInt(this.lines[this.offset + 1]); const speed = parseFloat(this.lines[this.offset + 2]); const currentEntry = currentSession.entries.get(raceNumber); if (!currentEntry) break; currentEntry.speeds.push({ raceNumber, speed, }); currentSession.entries.set(raceNumber, currentEntry); } break; case DataType.CLASSIFICATION: { const sessionType = this.readSessionType(this.lines[this.offset + 1]); const sessionState = this.lines[this.offset + 2]; const sessionTimer = parseFloat(this.lines[this.offset + 3]); const sessionLength = parseFloat(this.lines[this.offset + 4]); const sessionLap = parseInt(this.lines[this.offset + 5]); const sessionLaps = parseInt(this.lines[this.offset + 6]); const sessionKey = this.getCurrentSessionKey(); if (!sessionKey) break; const currentSession = this.data.sessions.get(sessionType.combined); if (!currentSession) break; const classification = { type: sessionType.type, count: sessionType.count, state: sessionState, timer: sessionTimer, length: sessionLength, lap: sessionLap, laps: sessionLaps, entries: new Map(), }; let entryOffset = this.offset + 7; const isPractise = sessionType.type === SessionType.PRACTICE || sessionType.type === SessionType.QUALIFY || sessionType.type === SessionType.WARMUP; const readEntry = () => { const raceNumber = parseInt(this.lines[entryOffset]); entryOffset++; let entry; if (isPractise) { entry = { raceNumber, lapTime: 0, lapNumber: 0, totalLaps: 0, gap: "--", speed: 0, status: KartStatus.NONE, }; entry.lapTime = parseFloat(this.lines[entryOffset]); entryOffset++; if (entry.lapTime !== 0) { entry.lapNumber = parseInt(this.lines[entryOffset]); entryOffset++; entry.totalLaps = parseInt(this.lines[entryOffset]); entryOffset++; const gap = this.lines[entryOffset]; entry.gap = gap === "--" ? "--" : parseFloat(gap); entryOffset++; entry.speed = parseFloat(this.lines[entryOffset]); entryOffset++; } } else { entry = { raceNumber, raceTime: 0, lapNumber: 0, gap: 0, status: KartStatus.NONE, }; const raceTime = this.lines[entryOffset]; entry.raceTime = Object.values(RaceStatus).includes(raceTime) ? raceTime : parseFloat(raceTime); entryOffset++; if (entry.raceTime !== 0 && typeof entry.raceTime === "number") { entry.lapNumber = parseInt(this.lines[entryOffset]); entryOffset++; const gap = this.lines[entryOffset]; entry.gap = gap === "L" ? "L" : parseFloat(gap); entryOffset++; } } entry.status = this.lines[entryOffset]; entryOffset++; classification.entries.set(raceNumber, entry); }; while (this.lines[entryOffset] !== "") readEntry(); currentSession.classification = classification; this.data.sessions.set(sessionType.combined, currentSession); } break; } } readContact() { this.data.contacts.push({ time: parseFloat(this.lines[this.offset + 1]), raceNumbers: Vec2(parseInt(this.lines[this.offset + 2]), parseInt(this.lines[this.offset + 3])), velocity: parseFloat(this.lines[this.offset + 4]), }); } readTrackData() { const type = this.lines[this.offset]; switch (type) { case DataType.TRACKDATA: { this.data.trackData = { startPosition: parseFloat(this.lines[this.offset + 1]), splitPositions: Vec2(parseFloat(this.lines[this.offset + 2]), parseFloat(this.lines[this.offset + 3])), speedTrapPosition: parseFloat(this.lines[this.offset + 4]), numSegments: parseInt(this.lines[this.offset + 5]), segments: new Map(), positions: new Map(), }; } break; case DataType.TRACKSEGMENT: { if (!this.data.trackData) break; const data = { segmentNumber: parseInt(this.lines[this.offset + 1]), type: parseInt(this.lines[this.offset + 2]), length: parseFloat(this.lines[this.offset + 3]), radius: parseFloat(this.lines[this.offset + 4]), angle: parseFloat(this.lines[this.offset + 5]), startPosition: Vec3(parseFloat(this.lines[this.offset + 6]), parseFloat(this.lines[this.offset + 8]), parseFloat(this.lines[this.offset + 7])), }; this.data.trackData.segments.set(data.segmentNumber, data); } break; case DataType.TRACKPOSITION: { if (!this.data.trackData) break; let entryOffset = this.offset + 1; const readEntry = () => { var _a; if (!this.data.trackData) return; const data = { raceNumber: parseInt(this.lines[entryOffset]), position: Vec3(parseFloat(this.lines[entryOffset + 1]), parseFloat(this.lines[entryOffset + 2]), parseFloat(this.lines[entryOffset + 3])), }; (_a = this.data.trackData) === null || _a === void 0 ? void 0 : _a.positions.set(data.raceNumber, data); entryOffset += 4; }; while (this.lines[entryOffset] !== "") readEntry(); } break; } } getCurrentSessionKey() { const sessions = this.data.sessions; if (sessions.size === 0) return undefined; for (const session of sessions.values()) { if (session.state === SessionState.COMPLETE || session.state === SessionState.NONE) continue; return `${session.type}${session.count ? " " + session.count : ""}`; } return sessions.keys().next().value; } readSessionType(str) { const split = str.toUpperCase().split(" "); return { type: split[0], count: parseInt(split[1]) || undefined, combined: str, }; } } export { LivetimingReader };