Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some strange weapon prediction behavior #2566

Open
SNMetamorph opened this issue Jul 9, 2019 · 6 comments
Open

Some strange weapon prediction behavior #2566

SNMetamorph opened this issue Jul 9, 2019 · 6 comments

Comments

@SNMetamorph
Copy link

SNMetamorph commented Jul 9, 2019

When I working on my mod, I found out some strange things.
I added this piece of code into PrimaryAttack() method:

void CGlock::PrimaryAttack()
{
    ...
#ifdef CLIENT_DLL
    ALERT(at_console, "glock fire %.4f\n", m_flNextPrimaryAttack);
#endif
    ...
}

When I tried to shoot once time on local listen server, it's ok
I see this in my console:

cl:  glock fire -0.0010

BUT, when I try it in multiplayer or using fakelag, I see this:
With ping 20:

cl:  glock fire -0.0540
cl:  glock fire -0.0410
cl:  glock fire -0.0410
cl:  glock fire -0.0280
cl:  glock fire -0.0280
cl:  glock fire -0.0150
cl:  glock fire -0.0150
cl:  glock fire -0.0010
cl:  glock fire -0.0010

With ping 80:

cl:  glock fire -0.1610
cl:  glock fire -0.1480
cl:  glock fire -0.1480
cl:  glock fire -0.1480
cl:  glock fire -0.1480
cl:  glock fire -0.1480
cl:  glock fire -0.1210
cl:  glock fire -0.1210
cl:  glock fire -0.0940
cl:  glock fire -0.0940
cl:  glock fire -0.0810
cl:  glock fire -0.0810
cl:  glock fire -0.0680
cl:  glock fire -0.0680
cl:  glock fire -0.0680
cl:  glock fire -0.0540
cl:  glock fire -0.0540
cl:  glock fire -0.0280
cl:  glock fire -0.0280
cl:  glock fire -0.0280
cl:  glock fire -0.0010
cl:  glock fire -0.0010

The problem is PrimaryAttack() called multiple times, and the more ping, the more method calls occur.
In my mod this breaks melee combos, when player should perform certain number of punches, but due to multiple calls, punch counter totally breaks.
I tested this on pure HLSDK code, and problem still occur in it.
Also, I tested it on 4554 engine build, and on last Steam engine build (8196), but problem also still occuring.

Why this can occuring? How to fix it?

@SNMetamorph
Copy link
Author

I think this also is a reason of bug, when on client side weapon shoots two times instead of one (manifested as double bullet hole decals and two pulled shells instead one).

@JoelTroch
Copy link
Contributor

Potential duplicate of #1621

@SNMetamorph
Copy link
Author

@SamVanheer do you know something about this problem?

@SamVanheer
Copy link

I looked into weapon prediction event dispatch problems before and i concluded that there were issues in the engine that could cause events to be skipped somehow.

I never managed to figure out what it was exactly that caused it but i suspect the issue there lies with the limit of 64 frames that can be stored for rewinding and playback when resyncing to the server.

As far as events and weapon actions playing multiple times goes this is to be expected. The client will run the same frame multiple times, but animations and sounds are only handled if the engine is telling the client to run functions:

/*
=====================
HUD_PostRunCmd
Client calls this during prediction, after it has moved the player and updated any info changed into to->
time is the current client clock based on prediction
cmd is the command that caused the movement, etc
runfuncs is 1 if this is the first time we've predicted this command. If so, sounds and effects should play, otherwise, they should
be ignored
=====================
*/
void CL_DLLEXPORT HUD_PostRunCmd( struct local_state_s *from, struct local_state_s *to, struct usercmd_s *cmd, int runfuncs, double time, unsigned int random_seed )
{
// RecClPostRunCmd(from, to, cmd, runfuncs, time, random_seed);
g_runfuncs = runfuncs;
#if defined( CLIENT_WEAPONS )
if ( cl_lw && cl_lw->value )
{
HUD_WeaponsPostThink( from, to, cmd, time, random_seed );
}
else
#endif
{
to->client.fov = g_lastFOV;
}
if ( g_irunninggausspred == 1 )
{
Vector forward;
gEngfuncs.pfnAngleVectors( v_angles, forward, NULL, NULL );
to->client.velocity = to->client.velocity - forward * g_flApplyVel * 5;
g_irunninggausspred = false;
}
// All games can use FOV state
g_lastFOV = to->client.fov;
}

The runfuncs parameter is a boolean that informs client code about this. If you really need to run client side logic then check g_runfuncs to see if it's running the first time.

Note that GoldSource's prediction code is very simple and was slapped on after release so it can't handle complex cases. You should always design your code to use server authorative decisions to avoid issues.

If you need to synchronize state you should use weapon_data_t:

halflife/dlls/client.cpp

Lines 1585 to 1649 in c7240b9

int GetWeaponData( struct edict_s *player, struct weapon_data_s *info )
{
#if defined( CLIENT_WEAPONS )
int i;
weapon_data_t *item;
entvars_t *pev = &player->v;
CBasePlayer *pl = dynamic_cast< CBasePlayer *>( CBasePlayer::Instance( pev ) );
CBasePlayerWeapon *gun;
ItemInfo II;
memset( info, 0, 32 * sizeof( weapon_data_t ) );
if ( !pl )
return 1;
// go through all of the weapons and make a list of the ones to pack
for ( i = 0 ; i < MAX_ITEM_TYPES ; i++ )
{
if ( pl->m_rgpPlayerItems[ i ] )
{
// there's a weapon here. Should I pack it?
CBasePlayerItem *pPlayerItem = pl->m_rgpPlayerItems[ i ];
while ( pPlayerItem )
{
gun = dynamic_cast<CBasePlayerWeapon *>( pPlayerItem->GetWeaponPtr() );
if ( gun && gun->UseDecrement() )
{
// Get The ID.
memset( &II, 0, sizeof( II ) );
gun->GetItemInfo( &II );
if ( II.iId >= 0 && II.iId < 32 )
{
item = &info[ II.iId ];
item->m_iId = II.iId;
item->m_iClip = gun->m_iClip;
item->m_flTimeWeaponIdle = max( gun->m_flTimeWeaponIdle, -0.001 );
item->m_flNextPrimaryAttack = max( gun->m_flNextPrimaryAttack, -0.001 );
item->m_flNextSecondaryAttack = max( gun->m_flNextSecondaryAttack, -0.001 );
item->m_fInReload = gun->m_fInReload;
item->m_fInSpecialReload = gun->m_fInSpecialReload;
item->fuser1 = max( gun->pev->fuser1, -0.001 );
item->fuser2 = gun->m_flStartThrow;
item->fuser3 = gun->m_flReleaseThrow;
item->iuser1 = gun->m_chargeReady;
item->iuser2 = gun->m_fInAttack;
item->iuser3 = gun->m_fireState;
// item->m_flPumpTime = max( gun->m_flPumpTime, -0.001 );
}
}
pPlayerItem = pPlayerItem->m_pNext;
}
}
}
#else
memset( info, 0, 32 * sizeof( weapon_data_t ) );
#endif
return 1;
}

/*
=====================
HUD_WeaponsPostThink
Run Weapon firing code on client
=====================
*/
void HUD_WeaponsPostThink( local_state_s *from, local_state_s *to, usercmd_t *cmd, double time, unsigned int random_seed )
{
int i;
int buttonsChanged;
CBasePlayerWeapon *pWeapon = NULL;
CBasePlayerWeapon *pCurrent;
weapon_data_t nulldata, *pfrom, *pto;
static int lasthealth;
memset( &nulldata, 0, sizeof( nulldata ) );
HUD_InitClientWeapons();
// Get current clock
gpGlobals->time = time;
// Fill in data based on selected weapon
// FIXME, make this a method in each weapon? where you pass in an entity_state_t *?
switch ( from->client.m_iId )
{
case WEAPON_CROWBAR:
pWeapon = &g_Crowbar;
break;
case WEAPON_GLOCK:
pWeapon = &g_Glock;
break;
case WEAPON_PYTHON:
pWeapon = &g_Python;
break;
case WEAPON_MP5:
pWeapon = &g_Mp5;
break;
case WEAPON_CROSSBOW:
pWeapon = &g_Crossbow;
break;
case WEAPON_SHOTGUN:
pWeapon = &g_Shotgun;
break;
case WEAPON_RPG:
pWeapon = &g_Rpg;
break;
case WEAPON_GAUSS:
pWeapon = &g_Gauss;
break;
case WEAPON_EGON:
pWeapon = &g_Egon;
break;
case WEAPON_HORNETGUN:
pWeapon = &g_HGun;
break;
case WEAPON_HANDGRENADE:
pWeapon = &g_HandGren;
break;
case WEAPON_SATCHEL:
pWeapon = &g_Satchel;
break;
case WEAPON_TRIPMINE:
pWeapon = &g_Tripmine;
break;
case WEAPON_SNARK:
pWeapon = &g_Snark;
break;
}
// Store pointer to our destination entity_state_t so we can get our origin, etc. from it
// for setting up events on the client
g_finalstate = to;
// If we are running events/etc. go ahead and see if we
// managed to die between last frame and this one
// If so, run the appropriate player killed or spawn function
if ( g_runfuncs )
{
if ( to->client.health <= 0 && lasthealth > 0 )
{
player.Killed( NULL, 0 );
}
else if ( to->client.health > 0 && lasthealth <= 0 )
{
player.Spawn();
}
lasthealth = to->client.health;
}
// We are not predicting the current weapon, just bow out here.
if ( !pWeapon )
return;
for ( i = 0; i < 32; i++ )
{
pCurrent = g_pWpns[ i ];
if ( !pCurrent )
{
continue;
}
pfrom = &from->weapondata[ i ];
pCurrent->m_fInReload = pfrom->m_fInReload;
pCurrent->m_fInSpecialReload = pfrom->m_fInSpecialReload;
// pCurrent->m_flPumpTime = pfrom->m_flPumpTime;
pCurrent->m_iClip = pfrom->m_iClip;
pCurrent->m_flNextPrimaryAttack = pfrom->m_flNextPrimaryAttack;
pCurrent->m_flNextSecondaryAttack = pfrom->m_flNextSecondaryAttack;
pCurrent->m_flTimeWeaponIdle = pfrom->m_flTimeWeaponIdle;
pCurrent->pev->fuser1 = pfrom->fuser1;
pCurrent->m_flStartThrow = pfrom->fuser2;
pCurrent->m_flReleaseThrow = pfrom->fuser3;
pCurrent->m_chargeReady = pfrom->iuser1;
pCurrent->m_fInAttack = pfrom->iuser2;
pCurrent->m_fireState = pfrom->iuser3;
pCurrent->m_iSecondaryAmmoType = (int)from->client.vuser3[ 2 ];
pCurrent->m_iPrimaryAmmoType = (int)from->client.vuser4[ 0 ];
player.m_rgAmmo[ pCurrent->m_iPrimaryAmmoType ] = (int)from->client.vuser4[ 1 ];
player.m_rgAmmo[ pCurrent->m_iSecondaryAmmoType ] = (int)from->client.vuser4[ 2 ];
}
// For random weapon events, use this seed to seed random # generator
player.random_seed = random_seed;
// Get old buttons from previous state.
player.m_afButtonLast = from->playerstate.oldbuttons;
// Which buttsons chave changed
buttonsChanged = (player.m_afButtonLast ^ cmd->buttons); // These buttons have changed this frame
// Debounced button codes for pressed/released
// The changed ones still down are "pressed"
player.m_afButtonPressed = buttonsChanged & cmd->buttons;
// The ones not down are "released"
player.m_afButtonReleased = buttonsChanged & (~cmd->buttons);
// Set player variables that weapons code might check/alter
player.pev->button = cmd->buttons;
player.pev->velocity = from->client.velocity;
player.pev->flags = from->client.flags;
player.pev->deadflag = from->client.deadflag;
player.pev->waterlevel = from->client.waterlevel;
player.pev->maxspeed = from->client.maxspeed;
player.pev->fov = from->client.fov;
player.pev->weaponanim = from->client.weaponanim;
player.pev->viewmodel = from->client.viewmodel;
player.m_flNextAttack = from->client.m_flNextAttack;
player.m_flNextAmmoBurn = from->client.fuser2;
player.m_flAmmoStartCharge = from->client.fuser3;
//Stores all our ammo info, so the client side weapons can use them.
player.ammo_9mm = (int)from->client.vuser1[0];
player.ammo_357 = (int)from->client.vuser1[1];
player.ammo_argrens = (int)from->client.vuser1[2];
player.ammo_bolts = (int)from->client.ammo_nails; //is an int anyways...
player.ammo_buckshot = (int)from->client.ammo_shells;
player.ammo_uranium = (int)from->client.ammo_cells;
player.ammo_hornets = (int)from->client.vuser2[0];
player.ammo_rockets = (int)from->client.ammo_rockets;
// Point to current weapon object
if ( from->client.m_iId )
{
player.m_pActiveItem = g_pWpns[ from->client.m_iId ];
}
if ( player.m_pActiveItem->m_iId == WEAPON_RPG )
{
( ( CRpg * )player.m_pActiveItem)->m_fSpotActive = (int)from->client.vuser2[ 1 ];
( ( CRpg * )player.m_pActiveItem)->m_cActiveRockets = (int)from->client.vuser2[ 2 ];
}
// Don't go firing anything if we have died or are spectating
// Or if we don't have a weapon model deployed
if ( ( player.pev->deadflag != ( DEAD_DISCARDBODY + 1 ) ) &&
!CL_IsDead() && player.pev->viewmodel && !g_iUser1 )
{
if ( player.m_flNextAttack <= 0 )
{
pWeapon->ItemPostFrame();
}
}
// Assume that we are not going to switch weapons
to->client.m_iId = from->client.m_iId;
// Now see if we issued a changeweapon command ( and we're not dead )
if ( cmd->weaponselect && ( player.pev->deadflag != ( DEAD_DISCARDBODY + 1 ) ) )
{
// Switched to a different weapon?
if ( from->weapondata[ cmd->weaponselect ].m_iId == cmd->weaponselect )
{
CBasePlayerWeapon *pNew = g_pWpns[ cmd->weaponselect ];
if ( pNew && ( pNew != pWeapon ) )
{
// Put away old weapon
if (player.m_pActiveItem)
player.m_pActiveItem->Holster( );
player.m_pLastItem = player.m_pActiveItem;
player.m_pActiveItem = pNew;
// Deploy new weapon
if (player.m_pActiveItem)
{
player.m_pActiveItem->Deploy( );
}
// Update weapon id so we can predict things correctly.
to->client.m_iId = cmd->weaponselect;
}
}
}
// Copy in results of prediction code
to->client.viewmodel = player.pev->viewmodel;
to->client.fov = player.pev->fov;
to->client.weaponanim = player.pev->weaponanim;
to->client.m_flNextAttack = player.m_flNextAttack;
to->client.fuser2 = player.m_flNextAmmoBurn;
to->client.fuser3 = player.m_flAmmoStartCharge;
to->client.maxspeed = player.pev->maxspeed;
//HL Weapons
to->client.vuser1[0] = player.ammo_9mm;
to->client.vuser1[1] = player.ammo_357;
to->client.vuser1[2] = player.ammo_argrens;
to->client.ammo_nails = player.ammo_bolts;
to->client.ammo_shells = player.ammo_buckshot;
to->client.ammo_cells = player.ammo_uranium;
to->client.vuser2[0] = player.ammo_hornets;
to->client.ammo_rockets = player.ammo_rockets;
if ( player.m_pActiveItem->m_iId == WEAPON_RPG )
{
from->client.vuser2[ 1 ] = ( ( CRpg * )player.m_pActiveItem)->m_fSpotActive;
from->client.vuser2[ 2 ] = ( ( CRpg * )player.m_pActiveItem)->m_cActiveRockets;
}
// Make sure that weapon animation matches what the game .dll is telling us
// over the wire ( fixes some animation glitches )
if ( g_runfuncs && ( HUD_GetWeaponAnim() != to->client.weaponanim ) )
{
int body = 2;
//Pop the model to body 0.
if ( pWeapon == &g_Tripmine )
body = 0;
//Show laser sight/scope combo
if ( pWeapon == &g_Python && bIsMultiplayer() )
body = 1;
// Force a fixed anim down to viewmodel
HUD_SendWeaponAnim( to->client.weaponanim, body, 1 );
}
for ( i = 0; i < 32; i++ )
{
pCurrent = g_pWpns[ i ];
pto = &to->weapondata[ i ];
if ( !pCurrent )
{
memset( pto, 0, sizeof( weapon_data_t ) );
continue;
}
pto->m_fInReload = pCurrent->m_fInReload;
pto->m_fInSpecialReload = pCurrent->m_fInSpecialReload;
// pto->m_flPumpTime = pCurrent->m_flPumpTime;
pto->m_iClip = pCurrent->m_iClip;
pto->m_flNextPrimaryAttack = pCurrent->m_flNextPrimaryAttack;
pto->m_flNextSecondaryAttack = pCurrent->m_flNextSecondaryAttack;
pto->m_flTimeWeaponIdle = pCurrent->m_flTimeWeaponIdle;
pto->fuser1 = pCurrent->pev->fuser1;
pto->fuser2 = pCurrent->m_flStartThrow;
pto->fuser3 = pCurrent->m_flReleaseThrow;
pto->iuser1 = pCurrent->m_chargeReady;
pto->iuser2 = pCurrent->m_fInAttack;
pto->iuser3 = pCurrent->m_fireState;
// Decrement weapon counters, server does this at same time ( during post think, after doing everything else )
pto->m_flNextReload -= cmd->msec / 1000.0;
pto->m_fNextAimBonus -= cmd->msec / 1000.0;
pto->m_flNextPrimaryAttack -= cmd->msec / 1000.0;
pto->m_flNextSecondaryAttack -= cmd->msec / 1000.0;
pto->m_flTimeWeaponIdle -= cmd->msec / 1000.0;
pto->fuser1 -= cmd->msec / 1000.0;
to->client.vuser3[2] = pCurrent->m_iSecondaryAmmoType;
to->client.vuser4[0] = pCurrent->m_iPrimaryAmmoType;
to->client.vuser4[1] = player.m_rgAmmo[ pCurrent->m_iPrimaryAmmoType ];
to->client.vuser4[2] = player.m_rgAmmo[ pCurrent->m_iSecondaryAmmoType ];
/* if ( pto->m_flPumpTime != -9999 )
{
pto->m_flPumpTime -= cmd->msec / 1000.0;
if ( pto->m_flPumpTime < -0.001 )
pto->m_flPumpTime = -0.001;
}*/
if ( pto->m_fNextAimBonus < -1.0 )
{
pto->m_fNextAimBonus = -1.0;
}
if ( pto->m_flNextPrimaryAttack < -1.0 )
{
pto->m_flNextPrimaryAttack = -1.0;
}
if ( pto->m_flNextSecondaryAttack < -0.001 )
{
pto->m_flNextSecondaryAttack = -0.001;
}
if ( pto->m_flTimeWeaponIdle < -0.001 )
{
pto->m_flTimeWeaponIdle = -0.001;
}
if ( pto->m_flNextReload < -0.001 )
{
pto->m_flNextReload = -0.001;
}
if ( pto->fuser1 < -0.001 )
{
pto->fuser1 = -0.001;
}
}
// m_flNextAttack is now part of the weapons, but is part of the player instead
to->client.m_flNextAttack -= cmd->msec / 1000.0;
if ( to->client.m_flNextAttack < -0.001 )
{
to->client.m_flNextAttack = -0.001;
}
to->client.fuser2 -= cmd->msec / 1000.0;
if ( to->client.fuser2 < -0.001 )
{
to->client.fuser2 = -0.001;
}
to->client.fuser3 -= cmd->msec / 1000.0;
if ( to->client.fuser3 < -0.001 )
{
to->client.fuser3 = -0.001;
}
// Store off the last position from the predicted state.
HUD_SetLastOrg();
// Wipe it so we can't use it after this frame
g_finalstate = NULL;
}

This code is all very ugly since it has to operate within the engine's networking system which can't handle arbitrary data like Source or other modern engines can. I used virtual functions to allow for class-specific networking:
https://github.com/SamVanheer/HLEnhanced/blob/3e763c1083122fd91ae197178f0b97976b224ba8/game/shared/entities/weapons/CBasePlayerWeapon.h#L228-L238
https://github.com/SamVanheer/HLEnhanced/blob/346a9889f7da589f72cc66a71ee1202fc434714a/game/shared/entities/weapons/CHandGrenade.h#L46-L60
https://github.com/SamVanheer/HLEnhanced/blob/346a9889f7da589f72cc66a71ee1202fc434714a/game/server/client.cpp#L881-L921
https://github.com/SamVanheer/HLEnhanced/blob/346a9889f7da589f72cc66a71ee1202fc434714a/game/client/hl/CClientPrediction.cpp#L125-L450

Regardless you will find it difficult to handle stuff on the client side without glitches if it requires multiple sequential events.

@SNMetamorph
Copy link
Author

@SamVanheer do you have some new info about this event double playback issue, please?

@SamVanheer
Copy link

See #1621, that's probably what causes this as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants