mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Move player position parsing to shared util, fix player ID persistence
This commit is contained in:
1 parent
a36e7bddca
commit
093b71045b
6 files changed
+406
-141
No files matched your search
@@ -25,7 +25,8 @@ import {
|
||||
pixelsToRGBA,
|
||||
parseRecipeTileBinary,
|
||||
ParsedRecipeTile,
|
||||
} from "./tile-utils";
|
||||
} from "./utils/tile-utils";
|
||||
import { parsePlayerPositionsBinary } from "./utils/player-utils";
|
||||
|
||||
const CHUNK_SIZE = 32;
|
||||
const inflateAsync = promisify(zlib.inflate);
|
||||
@@ -113,6 +114,8 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
await this.loadExistingTagContent();
|
||||
// Load existing recipe data for deduplication
|
||||
await this.loadExistingRecipeContent();
|
||||
// Load existing player sessions from disk to maintain ID consistency
|
||||
await this.loadExistingPlayerSessions();
|
||||
|
||||
this.saveInterval = setInterval(() => {
|
||||
this.saveTiles().catch(err => this.logger.error(`Error saving tiles: ${err}`));
|
||||
@@ -1050,17 +1053,18 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
const sessions = this.playerSessions.get(fileKey)!;
|
||||
const activeSessions = this.activePlayerSessions.get(fileKey)!;
|
||||
|
||||
// Assign player ID if this player doesn't have one yet
|
||||
// Assign player ID if this player doesn't have one yet, or reopen their session
|
||||
let playerId: number;
|
||||
if (!sessions.has(player_data.player_name)) {
|
||||
playerId = this.nextPlayerIds.get(fileKey)!;
|
||||
sessions.set(player_data.player_name, playerId);
|
||||
this.nextPlayerIds.set(fileKey, playerId + 1);
|
||||
this.logger.verbose(`Assigned new player ID ${playerId} to ${player_data.player_name} on ${fileKey}`);
|
||||
} else {
|
||||
playerId = sessions.get(player_data.player_name)!;
|
||||
}
|
||||
|
||||
// Track active player session
|
||||
// Reopen/track active player session (handles session reopening after controller restart)
|
||||
activeSessions.add(player_data.player_name);
|
||||
|
||||
// Add the player ID to the data for saving (without modifying the original schema)
|
||||
@@ -1104,4 +1108,53 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
return { positions: null };
|
||||
}
|
||||
}
|
||||
|
||||
private async loadExistingPlayerSessions() {
|
||||
try {
|
||||
if (!await fs.pathExists(this.playerPositionsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await fs.readdir(this.playerPositionsPath);
|
||||
const positionFiles = files.filter(file => file.endsWith(".positions"));
|
||||
|
||||
for (const file of positionFiles) {
|
||||
const fileKey = file.replace(".positions", "");
|
||||
const filePath = path.join(this.playerPositionsPath, file);
|
||||
|
||||
if (!this.playerSessions.has(fileKey)) {
|
||||
this.playerSessions.set(fileKey, new Map());
|
||||
this.nextPlayerIds.set(fileKey, 1); // Start from 1, reserve 0 for unknown
|
||||
// Start with no active sessions - they will be reopened when players send new positions
|
||||
this.activePlayerSessions.set(fileKey, new Set());
|
||||
}
|
||||
|
||||
const sessions = this.playerSessions.get(fileKey)!;
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(filePath);
|
||||
|
||||
// Use shared parsing function
|
||||
const parsed = parsePlayerPositionsBinary(data);
|
||||
|
||||
// Restore the player name -> ID mappings from SessionStart records
|
||||
for (const [playerName, playerId] of parsed.playerSessions) {
|
||||
sessions.set(playerName, playerId);
|
||||
}
|
||||
|
||||
// Set next player ID to be higher than any existing ID
|
||||
if (parsed.maxPlayerId > 0) {
|
||||
this.nextPlayerIds.set(fileKey, parsed.maxPlayerId + 1);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
this.logger.info(`Loaded ${sessions.size} player sessions for ${fileKey}, next ID will be ${this.nextPlayerIds.get(fileKey)}`);
|
||||
} catch (readErr) {
|
||||
this.logger.error(`Failed to read player positions file ${file}: ${readErr}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`Error loading existing player sessions: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@
|
||||
"include": [
|
||||
"web/**/*",
|
||||
"messages.ts",
|
||||
"tile-utils.ts"
|
||||
"utils/tile-utils.ts",
|
||||
"utils/player-utils.ts",
|
||||
"utils/parsing-utils.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist/**/*"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Environment detection
|
||||
export const isNode = typeof Buffer !== "undefined" && typeof process !== "undefined" && process.versions?.node;
|
||||
|
||||
// Cross-platform type for binary data
|
||||
export type BinaryData = Buffer | Uint8Array;
|
||||
|
||||
// Cross-platform helper functions for reading integers
|
||||
export function readUInt8(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt8(offset);
|
||||
}
|
||||
return (data as Uint8Array)[offset];
|
||||
}
|
||||
|
||||
export function readUInt16LE(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt16LE(offset);
|
||||
}
|
||||
const bytes = data as Uint8Array;
|
||||
return bytes[offset] | (bytes[offset + 1] << 8);
|
||||
}
|
||||
|
||||
export function readUInt16BE(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt16BE(offset);
|
||||
}
|
||||
const bytes = data as Uint8Array;
|
||||
return (bytes[offset] << 8) | bytes[offset + 1];
|
||||
}
|
||||
|
||||
export function readUInt32BE(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt32BE(offset);
|
||||
}
|
||||
const bytes = data as Uint8Array;
|
||||
return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];
|
||||
}
|
||||
|
||||
export function sliceData(data: BinaryData, start: number, end: number): BinaryData {
|
||||
return data.slice(start, end);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { BinaryData, sliceData, readUInt8, readUInt16BE, readUInt32BE, isNode } from "./parsing-utils";
|
||||
|
||||
/**
|
||||
* Parsed player position file data
|
||||
*/
|
||||
export interface ParsedPlayerPositions {
|
||||
/** Player name -> player ID mappings from SessionStart records */
|
||||
playerSessions: Map<string, number>;
|
||||
/** Maximum player ID found in the file */
|
||||
maxPlayerId: number;
|
||||
/** All position records in the file */
|
||||
positions: PlayerPositionRecord[];
|
||||
}
|
||||
|
||||
export interface PlayerPositionRecord {
|
||||
type: number;
|
||||
playerId: number;
|
||||
// Position record fields
|
||||
tSec?: number;
|
||||
sec?: number;
|
||||
xTiles?: number;
|
||||
yTiles?: number;
|
||||
// Session record fields
|
||||
tMs?: number;
|
||||
playerName?: string;
|
||||
}
|
||||
|
||||
export interface ParsedPlayerPos {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
sec: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse binary player position file data
|
||||
* @param buf Binary player position data
|
||||
* @returns Parsed player position data with sessions and records
|
||||
*/
|
||||
export function parsePlayerPositionsBinary(buf: BinaryData): ParsedPlayerPositions {
|
||||
const playerSessions = new Map<string, number>();
|
||||
const positions: PlayerPositionRecord[] = [];
|
||||
let maxPlayerId = 0;
|
||||
let offset = 0;
|
||||
|
||||
while (offset < buf.length) {
|
||||
if (offset + 1 > buf.length) { break; }
|
||||
|
||||
const type = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
|
||||
if (type === 0) {
|
||||
// Position record
|
||||
const result = parsePositionRecord(buf, offset);
|
||||
if (!result.success) { break; }
|
||||
|
||||
const record: PlayerPositionRecord = {
|
||||
type: 0,
|
||||
playerId: result.playerId,
|
||||
tSec: result.tSec,
|
||||
sec: result.sec,
|
||||
xTiles: result.xTiles,
|
||||
yTiles: result.yTiles,
|
||||
};
|
||||
|
||||
positions.push(record);
|
||||
if (result.playerId > maxPlayerId) {
|
||||
maxPlayerId = result.playerId;
|
||||
}
|
||||
offset = result.newOffset;
|
||||
|
||||
} else if (type === 1) {
|
||||
// SessionStart record
|
||||
const result = parseSessionStartRecord(buf, offset);
|
||||
if (!result.success) { break; }
|
||||
|
||||
const record: PlayerPositionRecord = {
|
||||
type: 1,
|
||||
playerId: result.playerId,
|
||||
tMs: result.tMs,
|
||||
playerName: result.playerName,
|
||||
};
|
||||
|
||||
positions.push(record);
|
||||
playerSessions.set(result.playerName, result.playerId);
|
||||
if (result.playerId > maxPlayerId) {
|
||||
maxPlayerId = result.playerId;
|
||||
}
|
||||
offset = result.newOffset;
|
||||
|
||||
} else if (type === 2) {
|
||||
// SessionEnd record
|
||||
const result = parseSessionEndRecord(buf, offset);
|
||||
if (!result.success) { break; }
|
||||
|
||||
const record: PlayerPositionRecord = {
|
||||
type: 2,
|
||||
playerId: result.playerId,
|
||||
tMs: result.tMs,
|
||||
};
|
||||
|
||||
positions.push(record);
|
||||
if (result.playerId > maxPlayerId) {
|
||||
maxPlayerId = result.playerId;
|
||||
}
|
||||
offset = result.newOffset;
|
||||
|
||||
} else {
|
||||
// Unknown record type, can't continue parsing safely
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { playerSessions, maxPlayerId, positions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse position record from player position file
|
||||
* @param buf The binary player position data
|
||||
* @param offset Current offset in the data
|
||||
* @returns Processing result with position data and new offset
|
||||
*/
|
||||
function parsePositionRecord(buf: BinaryData, offset: number): {
|
||||
success: boolean;
|
||||
newOffset: number;
|
||||
playerId: number;
|
||||
tSec: number;
|
||||
sec: number;
|
||||
xTiles: number;
|
||||
yTiles: number;
|
||||
} {
|
||||
// Position record: 4 + 4 + 3 + 3 + 2 = 16 bytes
|
||||
if (offset + 16 > buf.length) {
|
||||
return { success: false, newOffset: offset, playerId: 0, tSec: 0, sec: 0, xTiles: 0, yTiles: 0 };
|
||||
}
|
||||
|
||||
const tSec = readUInt32BE(buf, offset);
|
||||
offset += 4;
|
||||
const sec = readUInt32BE(buf, offset);
|
||||
offset += 4;
|
||||
|
||||
// Read x_tiles (3 bytes, signed 24-bit)
|
||||
const xBytes = (readUInt8(buf, offset) << 16) | (readUInt8(buf, offset + 1) << 8) | readUInt8(buf, offset + 2);
|
||||
const xTiles = xBytes > 0x7FFFFF ? xBytes - 0x1000000 : xBytes; // Convert to signed
|
||||
offset += 3;
|
||||
|
||||
// Read y_tiles (3 bytes, signed 24-bit)
|
||||
const yBytes = (readUInt8(buf, offset) << 16) | (readUInt8(buf, offset + 1) << 8) | readUInt8(buf, offset + 2);
|
||||
const yTiles = yBytes > 0x7FFFFF ? yBytes - 0x1000000 : yBytes; // Convert to signed
|
||||
offset += 3;
|
||||
|
||||
const playerId = readUInt16BE(buf, offset);
|
||||
offset += 2;
|
||||
|
||||
return { success: true, newOffset: offset, playerId, tSec, sec, xTiles, yTiles };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SessionStart record from player position file
|
||||
* @param buf The binary player position data
|
||||
* @param offset Current offset in the data
|
||||
* @returns Processing result with session data and new offset
|
||||
*/
|
||||
function parseSessionStartRecord(
|
||||
buf: BinaryData,
|
||||
offset: number
|
||||
): { success: boolean; newOffset: number; playerId: number; tMs: number; playerName: string } {
|
||||
// SessionStart record: 4 + 2 + 1 + n = 7 + n bytes minimum
|
||||
if (offset + 7 > buf.length) {
|
||||
return { success: false, newOffset: offset, playerId: 0, tMs: 0, playerName: "" };
|
||||
}
|
||||
|
||||
const tMs = readUInt32BE(buf, offset);
|
||||
offset += 4;
|
||||
const playerId = readUInt16BE(buf, offset);
|
||||
offset += 2;
|
||||
const nameLen = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
|
||||
if (offset + nameLen > buf.length) {
|
||||
return { success: false, newOffset: offset, playerId, tMs, playerName: "" };
|
||||
}
|
||||
|
||||
const nameBytes = sliceData(buf, offset, offset + nameLen);
|
||||
offset += nameLen;
|
||||
|
||||
// Convert to string using cross-platform method
|
||||
let playerName: string;
|
||||
if (isNode && nameBytes instanceof Buffer) {
|
||||
playerName = nameBytes.toString("utf-8");
|
||||
} else {
|
||||
playerName = new TextDecoder().decode(nameBytes as Uint8Array);
|
||||
}
|
||||
|
||||
return { success: true, newOffset: offset, playerId, tMs, playerName };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SessionEnd record from player position file
|
||||
* @param buf The binary player position data
|
||||
* @param offset Current offset in the data
|
||||
* @returns Processing result with session data and new offset
|
||||
*/
|
||||
function parseSessionEndRecord(
|
||||
buf: BinaryData,
|
||||
offset: number
|
||||
): { success: boolean; newOffset: number; playerId: number; tMs: number } {
|
||||
// SessionEnd record: 4 + 2 = 6 bytes
|
||||
if (offset + 6 > buf.length) {
|
||||
return { success: false, newOffset: offset, playerId: 0, tMs: 0 };
|
||||
}
|
||||
|
||||
const tMs = readUInt32BE(buf, offset);
|
||||
offset += 4;
|
||||
const playerId = readUInt16BE(buf, offset);
|
||||
offset += 2;
|
||||
|
||||
return { success: true, newOffset: offset, playerId, tMs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and deduplicate player positions from binary data
|
||||
* @param buf Binary player position data
|
||||
* @returns Map of player ID to deduplicated position timeline
|
||||
*/
|
||||
export function parseAndDeduplicatePlayerPositions(buf: BinaryData): Map<number, ParsedPlayerPos[]> {
|
||||
// Use the utility function to parse the binary data
|
||||
const parsed: ParsedPlayerPositions = parsePlayerPositionsBinary(buf);
|
||||
|
||||
// Build player timelines from the parsed data
|
||||
const playerTimelines = new Map<number, ParsedPlayerPos[]>();
|
||||
|
||||
// Process all records in order
|
||||
for (const record of parsed.positions) {
|
||||
if (record.type === 0
|
||||
&& record.sec !== undefined
|
||||
&& record.xTiles !== undefined
|
||||
&& record.yTiles !== undefined
|
||||
) {
|
||||
// Position record - convert tile coordinates to world coordinates
|
||||
const name = [...parsed.playerSessions.entries()].find(([, id]) => id === record.playerId)?.[0];
|
||||
if (name) {
|
||||
if (!playerTimelines.has(record.playerId)) {
|
||||
playerTimelines.set(record.playerId, []);
|
||||
}
|
||||
|
||||
// Convert from tiles to world coordinates (tiles are 32x32 world coordinates)
|
||||
const x = record.xTiles * 32;
|
||||
const y = record.yTiles * 32;
|
||||
|
||||
playerTimelines.get(record.playerId)!.push({
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
sec: record.sec,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate consecutive identical positions for each player
|
||||
const deduplicatedTimelines = new Map<number, ParsedPlayerPos[]>();
|
||||
|
||||
for (const [playerId, timeline] of playerTimelines) {
|
||||
if (timeline.length === 0) { continue; }
|
||||
|
||||
const deduplicated: ParsedPlayerPos[] = [];
|
||||
let lastPos = timeline[0];
|
||||
deduplicated.push(lastPos);
|
||||
|
||||
for (let i = 1; i < timeline.length; i++) {
|
||||
const currentPos = timeline[i];
|
||||
|
||||
// Check if position changed (with small tolerance for floating point)
|
||||
const deltaX = Math.abs(currentPos.x - lastPos.x);
|
||||
const deltaY = Math.abs(currentPos.y - lastPos.y);
|
||||
|
||||
if (deltaX > 4 || deltaY > 4) {
|
||||
// Position changed significantly, keep this point
|
||||
deduplicated.push(currentPos);
|
||||
lastPos = currentPos;
|
||||
}
|
||||
// If position is the same, skip this point (deduplication)
|
||||
}
|
||||
|
||||
// Always keep the last position to ensure timeline ends correctly
|
||||
if (timeline.length > 1 && deduplicated[deduplicated.length - 1] !== timeline[timeline.length - 1]) {
|
||||
deduplicated.push(timeline[timeline.length - 1]);
|
||||
}
|
||||
|
||||
deduplicatedTimelines.set(playerId, deduplicated);
|
||||
}
|
||||
|
||||
return deduplicatedTimelines;
|
||||
}
|
||||
@@ -1,47 +1,6 @@
|
||||
import { BinaryData, sliceData, readUInt8, readUInt16LE, readUInt16BE, readUInt32BE, isNode } from "./parsing-utils";
|
||||
import * as zlib from "zlib";
|
||||
|
||||
// Environment detection
|
||||
const isNode = typeof Buffer !== "undefined" && typeof process !== "undefined" && process.versions?.node;
|
||||
|
||||
// Cross-platform type for binary data
|
||||
type BinaryData = Buffer | Uint8Array;
|
||||
|
||||
// Cross-platform helper functions for reading integers
|
||||
function readUInt8(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt8(offset);
|
||||
}
|
||||
return (data as Uint8Array)[offset];
|
||||
}
|
||||
|
||||
function readUInt16LE(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt16LE(offset);
|
||||
}
|
||||
const bytes = data as Uint8Array;
|
||||
return bytes[offset] | (bytes[offset + 1] << 8);
|
||||
}
|
||||
|
||||
function readUInt16BE(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt16BE(offset);
|
||||
}
|
||||
const bytes = data as Uint8Array;
|
||||
return (bytes[offset] << 8) | bytes[offset + 1];
|
||||
}
|
||||
|
||||
function readUInt32BE(data: BinaryData, offset: number): number {
|
||||
if (isNode && data instanceof Buffer) {
|
||||
return data.readUInt32BE(offset);
|
||||
}
|
||||
const bytes = data as Uint8Array;
|
||||
return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];
|
||||
}
|
||||
|
||||
function sliceData(data: BinaryData, start: number, end: number): BinaryData {
|
||||
return data.slice(start, end);
|
||||
}
|
||||
|
||||
// Cross-platform decompression
|
||||
async function inflateData(data: BinaryData): Promise<Buffer | Uint8Array> {
|
||||
if (isNode) {
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
renderTileIncremental,
|
||||
ParsedTileData,
|
||||
parseRecipeTileBinary,
|
||||
} from "../tile-utils";
|
||||
} from "../utils/tile-utils";
|
||||
import { parseAndDeduplicatePlayerPositions, ParsedPlayerPos } from "../utils/player-utils";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -63,7 +64,6 @@ interface TileState {
|
||||
imageData: ImageData;
|
||||
}
|
||||
|
||||
interface ParsedPlayerPos { name: string; x: number; y: number; sec: number; }
|
||||
|
||||
const CHUNK_SIZE = 32; // 32x32 pixels per chunk
|
||||
const TILE_SIZE = 256; // 256x256 pixels per tile (8x8 chunks)
|
||||
@@ -590,106 +590,21 @@ export default function CanvasMinimapPage() {
|
||||
if (!resp.positions) { return null; }
|
||||
const binStr = atob(resp.positions);
|
||||
const buf = new Uint8Array(binStr.length);
|
||||
for (let i = 0; i < binStr.length; i++) { buf[i] = binStr.charCodeAt(i); }
|
||||
for (let i = 0; i < binStr.length; i++) {
|
||||
buf[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
playerPathBufRef.current = buf;
|
||||
|
||||
// Parse and deduplicate positions immediately after loading
|
||||
parseAndDeduplicatePlayerPositions(buf);
|
||||
// Use the utility function to parse and deduplicate player positions
|
||||
const deduplicatedTimelines = parseAndDeduplicatePlayerPositions(buf);
|
||||
|
||||
// Store deduplicated data in cache
|
||||
deduplicatedPlayerPositions.current = deduplicatedTimelines;
|
||||
|
||||
return buf;
|
||||
} catch (_e) { return null; }
|
||||
};
|
||||
|
||||
const parseAndDeduplicatePlayerPositions = (buf: Uint8Array) => {
|
||||
const idToName = new Map<number, string>();
|
||||
const playerTimelines = new Map<number, ParsedPlayerPos[]>();
|
||||
|
||||
const readCoord = (arr: Uint8Array, offset: number): number => {
|
||||
// Read a signed 24-bit coordinate in big-endian format
|
||||
const raw = (arr[offset] << 16) | (arr[offset + 1] << 8) | arr[offset + 2];
|
||||
// Sign-extend from 24 to 32 bits
|
||||
const signed = (raw & 0x800000) ? (raw | 0xFF000000) : raw;
|
||||
return signed;
|
||||
};
|
||||
|
||||
let o = 0;
|
||||
while (o < buf.length) {
|
||||
const type = buf[o]; o += 1;
|
||||
if (type === 0) {
|
||||
if (o + 16 > buf.length) { break; }
|
||||
o += 4; // t_sec epoch not used
|
||||
const sec = (buf[o] << 24) | (buf[o + 1] << 16) | (buf[o + 2] << 8) | buf[o + 3];
|
||||
o += 4;
|
||||
const x = readCoord(buf, o); o += 3;
|
||||
const y = readCoord(buf, o); o += 3;
|
||||
const id = (buf[o] << 8) | buf[o + 1]; o += 2;
|
||||
|
||||
if (idToName.has(id)) {
|
||||
const name = idToName.get(id)!;
|
||||
if (!playerTimelines.has(id)) {
|
||||
playerTimelines.set(id, []);
|
||||
}
|
||||
playerTimelines.get(id)!.push({ name, x, y, sec });
|
||||
}
|
||||
} else if (type === 1) {
|
||||
if (o + 6 > buf.length) { break; }
|
||||
o += 4; // t_ms
|
||||
const id = (buf[o] << 8) | buf[o + 1]; o += 2;
|
||||
const len = buf[o]; o += 1;
|
||||
if (o + len > buf.length) { break; }
|
||||
const name = new TextDecoder().decode(buf.subarray(o, o + len));
|
||||
o += len;
|
||||
idToName.set(id, name);
|
||||
} else if (type === 2) {
|
||||
if (o + 6 > buf.length) { break; }
|
||||
o += 4; // t_ms
|
||||
const id = (buf[o] << 8) | buf[o + 1]; o += 2;
|
||||
// Player disconnect - mark end of timeline for this player
|
||||
if (playerTimelines.has(id)) {
|
||||
// Add a disconnect marker or handle as needed
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate consecutive identical positions for each player
|
||||
const deduplicatedTimelines = new Map<number, ParsedPlayerPos[]>();
|
||||
|
||||
for (const [playerId, timeline] of playerTimelines) {
|
||||
if (timeline.length === 0) { continue; }
|
||||
|
||||
const deduplicated: ParsedPlayerPos[] = [];
|
||||
let lastPos = timeline[0];
|
||||
deduplicated.push(lastPos);
|
||||
|
||||
for (let i = 1; i < timeline.length; i++) {
|
||||
const currentPos = timeline[i];
|
||||
|
||||
// Check if position changed (with small tolerance for floating point)
|
||||
const deltaX = Math.abs(currentPos.x - lastPos.x);
|
||||
const deltaY = Math.abs(currentPos.y - lastPos.y);
|
||||
|
||||
if (deltaX > 4 || deltaY > 4) {
|
||||
// Position changed significantly, keep this point
|
||||
deduplicated.push(currentPos);
|
||||
lastPos = currentPos;
|
||||
}
|
||||
// If position is the same, skip this point (deduplication)
|
||||
}
|
||||
|
||||
// Always keep the last position to ensure timeline ends correctly
|
||||
if (timeline.length > 1 && deduplicated[deduplicated.length - 1] !== timeline[timeline.length - 1]) {
|
||||
deduplicated.push(timeline[timeline.length - 1]);
|
||||
}
|
||||
|
||||
deduplicatedTimelines.set(playerId, deduplicated);
|
||||
}
|
||||
|
||||
// Store deduplicated data in cache
|
||||
deduplicatedPlayerPositions.current = deduplicatedTimelines;
|
||||
};
|
||||
|
||||
const loadPlayerTimestamps = async (
|
||||
allTicks: Set<number>
|
||||
): Promise<number> => {
|
||||
|
||||
Reference in new issue
Block a user