airframe.io / src /game /multiplayer /InputSystem.ts
LATTICEWORKS's picture
feat: integrate Google Earth 3D Tiles and optimize engine hooks
565f6e7
Raw
History Blame Contribute Delete
3.28 kB
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);
}
}