Spaces:
Sleeping
Sleeping
File size: 3,277 Bytes
565f6e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | import { ClientInputTuple, ClientInputState } from "./MatchState";
import { FlightCommand } from "../../types";
export class InputSystem {
// Input queues (sessionId -> ClientInputState[])
private inputQueues = new Map<string, ClientInputState[]>();
// Last processed commands for extrapolation
private lastInputs = new Map<string, FlightCommand>();
// Playback state to track jitter buffer initialization
private playbackActive = new Map<string, boolean>();
// Buffer size in ticks (e.g., 3 ticks = ~50ms buffer at 60Hz)
private readonly JITTER_BUFFER_SIZE = 3;
public enqueueInput(sessionId: string, tuple: ClientInputTuple) {
if (!this.inputQueues.has(sessionId)) {
this.inputQueues.set(sessionId, []);
}
const queue = this.inputQueues.get(sessionId)!;
// Convert tuple back to FlightCommand
const [
seq, pitch, roll, yaw, throttleDelta,
boost, airbrake, primaryFire, secondaryFire, flapsCode, gearDeployed
] = tuple;
const flaps = flapsCode === 2 ? "landing" : flapsCode === 1 ? "combat" : "up";
const command: FlightCommand = {
pitch, roll, yaw, throttleDelta,
boost: boost === 1,
airbrake: airbrake === 1,
primaryFire: primaryFire === 1,
secondaryFire: secondaryFire === 1,
flaps,
gearDeployed: gearDeployed === 1
};
queue.push({ seq, command });
// Sort queue by sequence number just in case packets arrived out of order
queue.sort((a, b) => a.seq - b.seq);
// If playback is not active, wait until the buffer fills up to start consuming
if (!this.playbackActive.get(sessionId) && queue.length >= this.JITTER_BUFFER_SIZE) {
this.playbackActive.set(sessionId, true);
}
// Limit queue size to avoid memory leakage and extreme rubber-banding
if (queue.length > 60) {
// Discard oldest
queue.shift();
}
}
public getNextInput(sessionId: string, neutralCommand: FlightCommand): { command: FlightCommand, seq?: number } {
const queue = this.inputQueues.get(sessionId);
if (!queue) {
return { command: neutralCommand };
}
const isActive = this.playbackActive.get(sessionId);
if (!isActive) {
// Buffering phase - hold last input (or neutral) but drop weapons
const last = this.lastInputs.get(sessionId);
const cmd = last ? { ...last, primaryFire: false, secondaryFire: false } : neutralCommand;
return { command: cmd };
}
const nextInput = queue.shift();
if (nextInput) {
this.lastInputs.set(sessionId, nextInput.command);
return { command: nextInput.command, seq: nextInput.seq };
} else {
// Queue under-run: jitter buffer depleted
// Disable playback so we buffer again
this.playbackActive.set(sessionId, false);
const last = this.lastInputs.get(sessionId);
const cmd = last ? { ...last, primaryFire: false, secondaryFire: false } : neutralCommand;
return { command: cmd };
}
}
public getQueueLength(sessionId: string): number {
return this.inputQueues.get(sessionId)?.length || 0;
}
public cleanupPlayer(sessionId: string) {
this.inputQueues.delete(sessionId);
this.lastInputs.delete(sessionId);
this.playbackActive.delete(sessionId);
}
}
|