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

@ -4,9 +4,10 @@ define([
"Lib/Utilities/NotificationCenter",
"Lib/Utilities/Assert",
"Game/Client/Control/PlayerController",
"Game/Client/InputBuffer",
],
function (Parent, Settings, nc, Assert, PlayerController) {
function (Parent, Settings, nc, Assert, PlayerController, InputBuffer) {
"use strict";
@ -19,12 +20,7 @@ function (Parent, Settings, nc, Assert, PlayerController) {
y: 0
};
this.lastServerPositionState = {
p: {
x: 0,
y: 0
}
};
this.inputBuffer = new InputBuffer();
this.arrowMesh = null;
this.createAndAddArrow();
@ -49,50 +45,25 @@ function (Parent, Settings, nc, Assert, PlayerController) {
};
};
Me.prototype.setLastServerPositionState = function(update) {
this.lastServerPositionState = update;
};
Me.prototype.applyReconciliation = function(x, y, vx, vy) {
var currentPos = this.doll.body.GetPosition();
var diffX = x - currentPos.x;
var diffY = y - currentPos.y;
var distance = Math.sqrt(diffX * diffX + diffY * diffY);
// Checks if client should send out its position to server
Me.prototype.isPositionStateOverrideNeeded = function() {
if(!this.doll) {
return false;
if (distance > Settings.RECONCILIATION_SNAP_THRESHOLD) {
// Large error — snap immediately (server-side teleport, respawn, etc.)
this.doll.body.SetPosition({x: x, y: y});
} else {
// Small error — blend toward reconciled position
var factor = Settings.RECONCILIATION_BLEND_FACTOR;
this.doll.body.SetPosition({
x: currentPos.x + diffX * factor,
y: currentPos.y + diffY * factor
});
}
if(this.doll.isAnotherPlayerNearby()) {
return false;
}
var difference = {
x: Math.abs(this.lastServerPositionState.p.x - this.doll.body.GetPosition().x),
y: Math.abs(this.lastServerPositionState.p.y - this.doll.body.GetPosition().y)
};
if(difference.x > Settings.ME_STATE_MAX_DIFFERENCE_METERS ||
difference.y > Settings.ME_STATE_MAX_DIFFERENCE_METERS) {
return true;
}
return false;
};
Me.prototype.getPositionStateOverride = function() {
return {
p: this.doll.body.GetPosition().Copy(),
lv: this.doll.body.GetLinearVelocity().Copy()
};
};
Me.prototype.acceptPositionStateUpdateFromServer = function() {
// gamecontroller should accept me's doll update only when another players doll is nearby.
return this.doll.isAnotherPlayerNearby();
};
Me.prototype.resetPositionState = function(options) {
Assert.number(options.p.x, options.p.y);
Assert.number(options.lv.x, options.lv.y);
this.doll.body.SetPosition(options.p);
this.doll.body.SetLinearVelocity(options.lv);
this.doll.body.SetLinearVelocity({x: vx, y: vy});
};
Me.prototype.createAndAddArrow = function() {