og-odamex/common/p_unlag.cpp
2026-01-17 14:53:21 -06:00

680 lines
16 KiB
C++

// Emacs style mode select -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id$
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
// [SL] 2011-05-11 - The Unlagging System is used to apply lag compensation
// for players firing hitscan weapons such as shotguns/chaingun. The end
// result is that players should no longer need to lead their opponents
// with hitscan weapons.
//
// It maintains a history of the positions of every player and the
// floor/ceiling heights of every moving sector. When a player tries to fire
// a hitscan weapon, the system calculates that client's lag X, moves all
// other players (excluding the shooter) to their position X tics ago, fires
// the weapon, then moves all the players back to their original position.
// In the system, this is refered to as 'reconciling' (moving players to a
// prior position) and 'restoring' (moving players back to their proper
// positions).
//
//-----------------------------------------------------------------------------
#include "odamex.h"
#include "m_vectors.h"
#include "p_unlag.h"
#include "p_local.h"
#ifdef _UNLAG_DEBUG_
#include <list>
void SV_SpawnMobj(AActor *mo);
void SV_SendDestroyActor(const AActor *mo);
#endif // _UNLAG_DEBUG_
EXTERN_CVAR(sv_maxunlagtime)
Unlag::SectorHistoryRecord::SectorHistoryRecord()
: sector(NULL), history_size(0),
history_ceilingheight(), history_floorheight(),
backup_ceilingheight(0), backup_floorheight(0)
{
}
Unlag::SectorHistoryRecord::SectorHistoryRecord(sector_t *sec)
: sector(sec), history_size(Unlag::MAX_HISTORY_TICS),
history_ceilingheight(), history_floorheight(),
backup_ceilingheight(0), backup_floorheight(0)
{
if (!sector)
return;
fixed_t ceilingheight = P_CeilingHeight(sector);
fixed_t floorheight = P_FloorHeight(sector);
for (size_t i = 0; i < history_size; i++)
{
history_ceilingheight[i] = ceilingheight;
history_floorheight[i] = floorheight;
}
backup_ceilingheight = ceilingheight;
backup_floorheight = floorheight;
}
//
// Unlag::getInstance
//
// Singleton pattern
// Returns a pointer to the only allowable Unlag object
//
Unlag& Unlag::getInstance()
{
static Unlag instance;
return instance;
}
Unlag::~Unlag()
{
Unlag::reset();
}
//
// Unlag::enabled
//
// Denotes whether a multiplayer game is running on the server.
//
bool Unlag::enabled()
{
return (serverside && multiplayer && !demoplayback);
}
//
// Unlag::movePlayer
//
// Moves the player to the position specified by x, y, z. Should be called
// AFTER Unlag::moveSector so the floor and ceiling heights will be correct.
// Note: this ignores the possibility that two players could share the same
// location.
//
void Unlag::movePlayer(player_t *player, fixed_t x, fixed_t y, fixed_t z)
{
if (!player->mo)
return;
player->mo->SetOrigin(x, y, z);
}
//
// Unlag::moveSector
//
// Moves the ceiling and floor heights to those specified by
// ceilingheight and floorheight respectively
//
void Unlag::moveSector(sector_t *sector, fixed_t ceilingheight,
fixed_t floorheight)
{
P_SetCeilingHeight(sector, ceilingheight);
P_SetFloorHeight(sector, floorheight);
}
//
// Unlag::reconcilePlayerPositions
//
// Moves all of the players except 'shooter' to the position they were
// at 'ticsago' tics before. Players who were not alive at that time
// have their MF_SHOOTABLE flag removed so they do not take damage.
//
// If Unlag::reconcile is true, restore all player positions to their state
// before reconciliation. Restore the MF_SHOOTABLE flag if we changed it.
//
// NOTE: ticsago should be > 0 if we're reconciling and not restoring
//
void Unlag::reconcilePlayerPositions(byte shooter_id, size_t ticsago)
{
for (auto& record : player_history)
{
player_t *player = record.player;
// skip over the player shooting and any spectators
if (player->id == shooter_id || player->spectator || !player->mo)
continue;
fixed_t dest_x, dest_y, dest_z; // position to move player to
if (!reconciled)
{
// record the player's current position, which hasn't yet
// been saved to the history arrays
record.backup_x = player->mo->x;
record.backup_y = player->mo->y;
record.backup_z = player->mo->z;
size_t cur = (gametic - ticsago) % Unlag::MAX_HISTORY_TICS;
dest_x = record.history_x[cur];
dest_y = record.history_y[cur];
dest_z = record.history_z[cur];
record.offset_x = record.backup_x - dest_x;
record.offset_y = record.backup_y - dest_y;
record.offset_z = record.backup_z - dest_z;
if (record.history_size < ticsago)
{
// make the player temporarily unshootable since this player
// was not alive when the shot was fired. Kind of a hack.
record.backup_flags = player->mo->flags;
player->mo->flags &= ~(MF_SHOOTABLE | MF_SOLID);
record.changed_flags = true;
}
#ifdef _UNLAG_DEBUG_
// spawn a marker sprite at the reconciled position for debugging
AActor *mo = new AActor(dest_x, dest_y, dest_z, MT_KEEN);
mo->flags &= ~(MF_SHOOTABLE | MF_SOLID);
mo->health = -187;
SV_SpawnMobj(mo);
#endif // _UNLAG_DEBUG_
}
else
{ // we're moving the player back to proper position
dest_x = record.backup_x;
dest_y = record.backup_y;
dest_z = record.backup_z;
// restore a player's shootability if we removed it previously
if (record.changed_flags)
{
player->mo->flags = record.backup_flags;
record.changed_flags = false;
}
}
movePlayer(player, dest_x, dest_y, dest_z);
}
}
//
// Unlag::reconcileSectorPositions
//
// Moves the ceiling and floor of any sectors considered moveable
// to the positions they were 'ticsago' tics before.
//
// If 'reconciled' is true, restore the ceiling and floors to where they
// were prior to reconciliation.
//
void Unlag::reconcileSectorPositions(size_t ticsago)
{
for (auto& record : sector_history)
{
sector_t *sector = record.sector;
fixed_t dest_ceilingheight, dest_floorheight;
if (!reconciled)
{
// record the player's current position, which hasn't yet
// been saved to the history arrays
record.backup_ceilingheight = P_CeilingHeight(sector);
record.backup_floorheight = P_FloorHeight(sector);
size_t cur = (record.history_size - 1 - ticsago)
% Unlag::MAX_HISTORY_TICS;
dest_ceilingheight = record.history_ceilingheight[cur];
dest_floorheight = record.history_floorheight[cur];
}
else // restore to original positions
{
dest_ceilingheight = record.backup_ceilingheight;
dest_floorheight = record.backup_floorheight;
}
moveSector(sector, dest_ceilingheight, dest_floorheight);
}
}
//
// Unlag::reset
//
// Erases the position history for players and sectors. Unlinks
// all player_t and sector_t objects from this Unlag object.
// Should be called at the begining of each level.
void Unlag::reset()
{
player_history.clear();
sector_history.clear();
player_id_map.clear();
}
//
// Unlag::recordPlayerPositions
//
// Saves the current x, y, z position of all players. History is
// reset every time a player dies, spectates, etc.
//
void Unlag::recordPlayerPositions()
{
if (!Unlag::enabled())
return;
for (auto& record : player_history)
{
player_t *player = record.player;
if (player->playerstate == PST_LIVE &&
!player->spectator && player->mo)
{
record.history_size++;
size_t cur = gametic % Unlag::MAX_HISTORY_TICS;
record.history_x[cur] = player->mo->x;
record.history_y[cur] = player->mo->y;
record.history_z[cur] = player->mo->z;
#ifdef _UNLAG_DEBUG_
DPrintFmt("Unlag ({:03d}): recording player {} position ({}, {})\n",
gametic & 0xFF, player->id,
player->mo->x >> FRACBITS,
player->mo->y >> FRACBITS,
player->mo->z >> FRACBITS);
#endif // _UNLAG_DEBUG_
}
else
{ // reset history for dead, spectating, etc players
record.history_size = 0;
}
}
}
//
// Unlag::recordSectorPositions()
//
// Saves the current ceiling and floor heights of all movable sectors
//
void Unlag::recordSectorPositions()
{
if (!Unlag::enabled())
return;
for (auto& record : sector_history)
{
sector_t *sector = record.sector;
size_t cur = record.history_size++
% Unlag::MAX_HISTORY_TICS;
record.history_ceilingheight[cur] = P_CeilingHeight(sector);
record.history_floorheight[cur] = P_FloorHeight(sector);
}
}
//
// Unlag::refreshRegisteredPlayers
//
// Updates the pointer to player_t in each player history record.
// The address of a player's player_t can change when a player is added to or
// removed from the global 'players' vector. Also recreate the player_id_map.
//
void Unlag::refreshRegisteredPlayers()
{
player_id_map.clear();
for (size_t i=0; i<player_history.size(); i++)
{
byte id = player_history[i].player_id;
player_history[i].player = &idplayer(id);
player_id_map[id] = i;
}
}
//
// Unlag::registerPlayer
//
// Adds a player to the player registry that maintains the history of
// players' positions
//
void Unlag::registerPlayer(byte player_id)
{
if (!Unlag::enabled())
return;
// don't register a bogus player id
if (!validplayer(idplayer(player_id)))
return;
player_history.emplace_back();
player_history.back().player_id = player_id;
player_history.back().history_size = 0;
player_history.back().changed_flags = false;
refreshRegisteredPlayers();
}
//
// Unlag::unregisterPlayer
//
// Removes a player from the player registry that mainins the history
// of players' positions.
//
// NOTE: unregisterPlayer should be called immediately following the call to
// players.erase() to update the pointers to the player objects.
//
void Unlag::unregisterPlayer(byte player_id)
{
if (!Unlag::enabled())
return;
size_t history_index = player_id_map[player_id];
if (history_index >= player_history.size())
return;
player_history.erase(player_history.begin() + history_index);
refreshRegisteredPlayers();
}
//
// Unlag::registerSector
//
// Adds a sector to the sector registry that maintains the history
// of positions for all movable sectors.
//
void Unlag::registerSector(sector_t *sector)
{
if (!Unlag::enabled())
return;
// Check if this sector already is in sector_history
for (const auto& record : sector_history)
{
// note: comparing the pointers to the sector_t objects
if (record.sector == sector)
return;
}
sector_history.push_back(SectorHistoryRecord(sector));
}
//
// Unlag::unregisterSector
//
// Removes a sector from the sector registry that maintains the history
// of positions for all moveable sectors.
//
void Unlag::unregisterSector(sector_t *sector)
{
if (!Unlag::enabled())
return;
for (size_t i=0; i<sector_history.size(); i++)
{
// note: comparing the pointers to the sector_t objects
if (sector_history[i].sector == sector)
{
sector_history.erase(sector_history.begin() + i);
return;
}
}
}
//
// Unlag::reconcile
//
// Temporarily moves all sectors and players to the positions they were
// in when a lagging client (shooter) pressed the fire button on the client's
// end. This allows a client to aim directly at opponents with hitscan
// weapons instead of leading them.
//
void Unlag::reconcile(byte shooter_id)
{
if (!Unlag::enabled())
return;
size_t player_index = player_id_map[shooter_id];
size_t lag = player_history[player_index].current_lag;
#ifdef _UNLAG_DEBUG_
DPrintFmt("Unlag ({:03d}): moving players to their positions at gametic {} ({} tics ago)\n",
gametic & 0xFF, (gametic - lag) & 0xFF, lag);
// remove any other debugging player markers
AActor *mo;
std::list<AActor*> to_destroy;
TThinkerIterator<AActor> iterator;
while ( (mo = iterator.Next() ) )
{
if (mo->type == MT_KEEN && mo->health == -187)
to_destroy.push_back(mo);
}
while (!to_destroy.empty())
{
mo = to_destroy.front();
to_destroy.pop_front();
if (mo)
mo->Destroy();
}
if (lag > Unlag::MAX_HISTORY_TICS)
DPrintFmt("Unlag ({:03d}): player {} has too great of lag ({} tics)\n",
gametic & 0xFF, shooter_id, lag);
#endif // _UNLAG_DEBUG_
if (lag > 0 && lag < Unlag::MAX_HISTORY_TICS)
{
reconcileSectorPositions(lag);
reconcilePlayerPositions(shooter_id, lag);
reconciled = true;
}
}
//
// Unlag::restore
//
// Moves all sectors and players to their original positions before reconcile
// was called.
//
void Unlag::restore(byte shooter_id)
{
if (!Unlag::enabled())
return;
if (reconciled)
{
reconcileSectorPositions(0);
reconcilePlayerPositions(shooter_id, 0);
reconciled = false; // reset after restoring original positions
}
#ifdef _UNLAG_DEBUG_
debugReconciliation(shooter_id);
#endif // _UNLAG_DEBUG_
}
//
// Unlag::setRoundtripDelay
//
// Sets the current_lag member variable for this particular player based on
// the time it takes a message from the server to reach the client and the
// reply to be received. Since lag can spike/have sudden changes, we only
// care about this value at the time a player fires a weapon. The parameter
// svgametic is the server gametic send when the server sends a positional
// update, which is returned to the server when the client sends a ticcmd
// that has the attack button pressed.
void Unlag::setRoundtripDelay(byte player_id, byte svgametic)
{
if (!Unlag::enabled())
return;
size_t maxdelay = TICRATE * sv_maxunlagtime;
if (maxdelay > Unlag::MAX_HISTORY_TICS)
maxdelay = Unlag::MAX_HISTORY_TICS;
size_t delay = ((gametic & 0xFF) + 256 - svgametic) & 0xFF;
size_t player_index = player_id_map[player_id];
player_history[player_index].current_lag = MIN(delay, maxdelay);
#ifdef _UNLAG_DEBUG_
DPrintFmt("Unlag ({:03d}): received gametic {} from player {}, lag = {}\n",
gametic & 0xFF, svgametic, player_id, delay);
#endif // _UNLAG_DEBUG
}
//
// Unlag::getReconciliationOffset
//
// Changes the x, y, z parameters to reflect how much a player was moved
// during reconciliation.
void Unlag::getReconciliationOffset( byte target_id,
fixed_t &x, fixed_t &y, fixed_t &z)
{
x = y = z = 0;
if (!reconciled)
return;
size_t target_index = player_id_map[target_id];
// calculate how far the target was moved during reconciliation
x = player_history[target_index].offset_x;
y = player_history[target_index].offset_y;
z = player_history[target_index].offset_z;
}
//
// Unlag::getCurrentPlayerPosition
//
// Changes the x, y, z parameters to the position of a player that
// was saved prior to reconciliation.
//
void Unlag::getCurrentPlayerPosition( byte player_id,
fixed_t &x, fixed_t &y, fixed_t &z)
{
x = y = z = 0;
size_t cur = player_id_map[player_id];
player_t* player = player_history[cur].player;
if (!player || !player->mo || player->spectator)
return;
if (Unlag::enabled() && reconciled)
{
x = player_history[cur].backup_x;
y = player_history[cur].backup_y;
z = player_history[cur].backup_z;
}
else
{
x = player->mo->x;
y = player->mo->y;
z = player->mo->z;
}
}
//
// Unlag::debugReconciliation
//
// Attempts to determine which tic would have been ideal to use for reconciling
// a target player's position.
void Unlag::debugReconciliation(byte shooter_id)
{
player_t *shooter = &(idplayer(shooter_id));
for (auto& record : player_history)
{
if (record.player->id == shooter_id)
continue;
for (size_t n = 0; n < MAX_HISTORY_TICS; n++)
{
if (n > record.history_size)
break;
size_t cur = (gametic - n) % Unlag::MAX_HISTORY_TICS;
fixed_t x = record.history_x[cur];
fixed_t y = record.history_y[cur];
angle_t angle = P_PointToAngle(shooter->mo->x, shooter->mo->y, x, y);
angle_t deltaangle = angle - shooter->mo->angle < ANG180 ?
angle - shooter->mo->angle :
shooter->mo->angle - angle;
if (deltaangle < 3 * FRACUNIT)
{
DPrintFmt("Unlag ({:03d}): would have hit player {} at gametic {} ({} tics ago)\n",
gametic & 0xFF, record.player->id, (gametic - static_cast<int>(n)) & 0xFF, n);
}
}
}
}
//
// Unlag::clearPlayerHistory
//
// If the player has died, clear their previous positions
//
void Unlag::clearPlayerHistory(byte player_id)
{
if (!Unlag::enabled())
return;
size_t cur = player_id_map[player_id];
if (cur >= player_history.size())
return;
PlayerHistoryRecord& record = player_history[cur];
record.history_size = 0;
}