mirror of
https://github.com/odamex/odamex
synced 2026-08-19 02:23:09 -04:00
680 lines
16 KiB
C++
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;
|
|
}
|