Replace cheat-detection teleport with server reconciliation

The old PUNKBUSTER check compared client-reported position to server
position and snapped the player back when latency made them diverge,
which felt like getting teleported under any real network conditions.

Replaces that with proper client-side prediction + reconciliation:
client tags each input with a sequence number and keeps an input
buffer; server tracks the last processed sequence and reports its
authoritative position via a per-user inputAck alongside each
worldUpdate. The client only corrects when the actual disagreement
exceeds what the unacked input time can explain — so steady-state
movement runs purely on local physics, and only genuine unexpected
events (collisions, being hit) trigger a smooth blend toward the
server state.

Includes adaptive threshold scaling so high-latency sessions don't
false-positive corrections during normal running.
This commit is contained in:
Jeena 2026-05-11 00:37:42 +00:00
parent e6089687ed
commit 71e4b4e847
9 changed files with 162 additions and 149 deletions

View file

@ -14,6 +14,7 @@ function (Parent, nc, KeyboardAndMouse, Gamepad, pointerLockManager) {
Parent.call(this, me);
this._inputSeq = 0;
this.keyboardAndMouse = new KeyboardAndMouse(this);
this.gamepad = new Gamepad(this);
}
@ -26,34 +27,48 @@ function (Parent, nc, KeyboardAndMouse, Gamepad, pointerLockManager) {
this.gamepad.update();
};
PlayerController.prototype._recordAndSend = function(command) {
this._inputSeq++;
if (this.player.doll && this.player.doll.body) {
var vel = this.player.doll.body.GetLinearVelocity();
this.player.inputBuffer.add({
seq: this._inputSeq,
timestamp: Date.now(),
vx: vel.x,
vy: vel.y
});
}
nc.trigger(nc.ns.client.to.server.gameCommand.send, command, {_seq: this._inputSeq});
};
PlayerController.prototype.moveLeft = function () {
if (!this.isPlayerInputAllowed()) return;
Parent.prototype.moveLeft.call(this);
nc.trigger(nc.ns.client.to.server.gameCommand.send, 'moveLeft');
this._recordAndSend('moveLeft');
}
PlayerController.prototype.moveRight = function () {
if (!this.isPlayerInputAllowed()) return;
Parent.prototype.moveRight.call(this);
nc.trigger(nc.ns.client.to.server.gameCommand.send, 'moveRight');
this._recordAndSend('moveRight');
}
// always allow to stop, to prevent endless running
PlayerController.prototype.stop = function () {
Parent.prototype.stop.call(this);
nc.trigger(nc.ns.client.to.server.gameCommand.send, 'stop');
this._recordAndSend('stop');
}
PlayerController.prototype.jump = function () {
if (!this.isPlayerInputAllowed()) return;
Parent.prototype.jump.call(this);
nc.trigger(nc.ns.client.to.server.gameCommand.send, 'jump');
this._recordAndSend('jump');
}
// always allow to stop.
PlayerController.prototype.jumpStop = function () {
Parent.prototype.jumpStop.call(this);
nc.trigger(nc.ns.client.to.server.gameCommand.send, 'jumpStop');
this._recordAndSend('jumpStop');
}
PlayerController.prototype.setXY = function(x, y) {