From 768c1bab27d356b51f9a7eb1a9f08fbecd654dde Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:34:25 -0600 Subject: [PATCH 01/14] Add snapshot interpolation and netcode runtime controls --- Classes/IGPlus_InterpSnapshot.uc | 13 + Classes/IGPlus_WeaponImplementationBase.uc | 40 +- Classes/ServerSettings.uc | 11 +- Classes/bbPlayer.uc | 713 ++++++++++++++++++++- 4 files changed, 761 insertions(+), 16 deletions(-) create mode 100644 Classes/IGPlus_InterpSnapshot.uc diff --git a/Classes/IGPlus_InterpSnapshot.uc b/Classes/IGPlus_InterpSnapshot.uc new file mode 100644 index 0000000..27ac854 --- /dev/null +++ b/Classes/IGPlus_InterpSnapshot.uc @@ -0,0 +1,13 @@ +class IGPlus_InterpSnapshot extends Object; + +var vector Loc; +var vector Vel; +var vector RelLoc; +var vector MoverLoc; +var float Time; +var bool bBasedOnMover; +var Actor BaseMover; + +defaultproperties +{ +} diff --git a/Classes/IGPlus_WeaponImplementationBase.uc b/Classes/IGPlus_WeaponImplementationBase.uc index ff1297a..888e28b 100644 --- a/Classes/IGPlus_WeaponImplementationBase.uc +++ b/Classes/IGPlus_WeaponImplementationBase.uc @@ -17,6 +17,22 @@ var int ExplosionWriteIndex; var float CachedTickRate; +function float IGPlus_GetSnapshotInterpRewindMs(Pawn Instigator) { + local bbPlayer bbP; + + if (Instigator == None) + return 0.0; + + bbP = bbPlayer(Instigator); + if (bbP == None || bbP.IGPlus_EnableSnapshotInterpolation == false) + return 0.0; + + if (bbP.zzUTPure == None) + return 0.0; + + return FMax(0.0, bbP.zzUTPure.Settings.SnapshotInterpRewindMs); +} + function PostBeginPlay() { super.PostBeginPlay(); @@ -801,6 +817,7 @@ function SimulateProjectileWithHistory(ST_TranslocatorTarget TTarget, int Ping) local float GameDeltaTime; local float RealDeltaTime; local float SimPing; + local float InterpMs; local float RealTimeRemaining; local float AccumulatedGameTime; local int HistoryInterval; @@ -817,7 +834,8 @@ function SimulateProjectileWithHistory(ST_TranslocatorTarget TTarget, int Ping) PureRef = bbPlayer(TTarget.Instigator).zzUTPure; - SimPing = float(Ping); + InterpMs = IGPlus_GetSnapshotInterpRewindMs(TTarget.Instigator); + SimPing = float(Ping) + InterpMs; // Cap ping compensation if (SimPing > WeaponSettings.PingCompensationMax) @@ -901,6 +919,7 @@ function SimulateProjectileWithHistory(ST_TranslocatorTarget TTarget, int Ping) function SimulateProjectile(Projectile P, int Ping) { local float DeltaTime; local float SimPing; + local float InterpMs; local UTPure PureRef; // Early exit if the projectile is already destroyed @@ -909,7 +928,8 @@ function SimulateProjectile(Projectile P, int Ping) { PureRef = bbPlayer(P.Instigator).zzUTPure; - SimPing = Ping * 0.5; + InterpMs = IGPlus_GetSnapshotInterpRewindMs(P.Instigator); + SimPing = Ping * 0.5 + InterpMs; // Cap ping compensation if (SimPing > WeaponSettings.PingCompensationMax) @@ -940,6 +960,7 @@ function SimulateProjectile(Projectile P, int Ping) { function BatchSimulateProjectiles(Projectile Projectiles[6], int NumProjectiles, int Ping) { local float DeltaTime; local float SimPing; + local float InterpMs; local UTPure PureRef; local int i; local bool bAnyAlive; @@ -949,7 +970,8 @@ function BatchSimulateProjectiles(Projectile Projectiles[6], int NumProjectiles, PureRef = bbPlayer(Projectiles[0].Instigator).zzUTPure; - SimPing = Ping * 0.5; // Simulate only 1-way latency + InterpMs = IGPlus_GetSnapshotInterpRewindMs(Projectiles[0].Instigator); + SimPing = Ping * 0.5 + InterpMs; // Simulate only 1-way latency if (SimPing > WeaponSettings.PingCompensationMax) SimPing = WeaponSettings.PingCompensationMax; @@ -1074,6 +1096,7 @@ simulated function Actor TraceShot(out vector HitLocation, out vector HitNormal, local bbPlayer bbP; local int Ping; local UTPure PureRef; + local float EffectivePing; Ping = 0; @@ -1082,10 +1105,13 @@ simulated function Actor TraceShot(out vector HitLocation, out vector HitNormal, if (bbP != None) { PureRef = bbP.zzUTPure; - Ping = bbP.PingAverage; + EffectivePing = float(bbP.PingAverage) + IGPlus_GetSnapshotInterpRewindMs(PawnOwner); - if (Ping > WSettingsRepl.PingCompensationMax) - Ping = WSettingsRepl.PingCompensationMax; + if (EffectivePing > WSettingsRepl.PingCompensationMax) + EffectivePing = WSettingsRepl.PingCompensationMax; + if (EffectivePing < 0.0) + EffectivePing = 0.0; + Ping = int(EffectivePing); } bWeaponShock = (PawnOwner.Weapon != none && PawnOwner.Weapon.IsA('ShockRifle')); @@ -1116,4 +1142,4 @@ simulated function Actor TraceShotClient(out vector HitLocation, out vector HitN defaultproperties { -} \ No newline at end of file +} diff --git a/Classes/ServerSettings.uc b/Classes/ServerSettings.uc index 56426e3..9124cc3 100644 --- a/Classes/ServerSettings.uc +++ b/Classes/ServerSettings.uc @@ -78,6 +78,9 @@ var config bool bEnablePingCompensatedSpawn; var config bool bEnableJitterBounding; var config float LooseCheckCorrectionFactor; var config float LooseCheckCorrectionFactorOnMover; +var config bool bEnableSnapshotInterpolation; +var config float SnapshotInterpSendHz; +var config float SnapshotInterpRewindMs; var config bool bEnableWarpFix; var config bool bEnableCarcassCollision; var config bool ShowTouchedPackage; @@ -145,6 +148,9 @@ function DumpServerSettings(PlayerPawn P) { DumpSetting(P, "bEnableJitterBounding"); DumpSetting(P, "LooseCheckCorrectionFactor"); DumpSetting(P, "LooseCheckCorrectionFactorOnMover"); + DumpSetting(P, "bEnableSnapshotInterpolation"); + DumpSetting(P, "SnapshotInterpSendHz"); + DumpSetting(P, "SnapshotInterpRewindMs"); DumpSetting(P, "bEnableWarpFix"); DumpSetting(P, "ShowTouchedPackage"); DumpSetting(P, "HitFeedbackMode"); @@ -216,10 +222,13 @@ defaultproperties bEnableJitterBounding=False LooseCheckCorrectionFactor=1.0 LooseCheckCorrectionFactorOnMover=1.0 + bEnableSnapshotInterpolation=False + SnapshotInterpSendHz=30.0 + SnapshotInterpRewindMs=66.0 bEnableWarpFix=True bEnableCarcassCollision=True HitFeedbackMode=HFM_Always bEnableDamageDebugMode=False bEnableDamageDebugConsoleMessages=False bEnableHitboxDebugMode=False -} \ No newline at end of file +} diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 686366f..a13e84c 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -307,11 +307,23 @@ struct IGPlus_WarpFixClient { var float TimeStamp; }; +struct IGPlus_SnapData { + var vector Location; + var vector Velocity; + var vector MoverLocation; + var float ServerTime; + var int Counter; + var bool bHardSnap; + var bool bBasedOnMover; + var Actor BaseMover; +}; + var bool IGPlus_EnableWarpFix; var bool IGPlus_WarpFixUpdate; var float IGPlus_WarpFixDelay; var IGPlus_WarpFix IGPlus_WarpFixData; var IGPlus_WarpFixClient IGPlus_WarpFixClientData; +var IGPlus_SnapData IGPlus_SnapReplicatedData; const IGPlus_LocationOffsetFix_DummyVel = vect(4.56,4.56,0.0); @@ -338,6 +350,30 @@ var IGPlus_CollisionDummy IGPlus_LocationOffsetFix_DummyList; var float LocalExtrapolationOwnPingFactor; var float LocalExtrapolationOtherPingFactor; +var IGPlus_InterpSnapshot IGPlus_SnapInterp_Data[16]; +var int IGPlus_SnapInterp_DataIndex; +var int IGPlus_SnapInterp_Count; +var float IGPlus_SnapInterp_InterpDelay; +var float IGPlus_SnapInterp_LastArrivalTime; +var float IGPlus_SnapInterp_MeanInterval; +var float IGPlus_SnapInterp_JitterEstimate; +var bool IGPlus_SnapInterp_Debug; +var float IGPlus_SnapInterp_DebugNextTime; +var int IGPlus_SnapInterp_LastCounter; +var float IGPlus_SnapInterp_ServerNextTime; +var vector IGPlus_SnapInterp_ServerLastLocation; +var float IGPlus_SnapInterp_ServerLastTime; +var bool IGPlus_SnapInterp_ServerHasLast; +var float IGPlus_SnapInterp_ServerTimeOffset; +var bool IGPlus_SnapInterp_HasClockSync; +var bool IGPlus_SnapInterp_EnabledLast; +var int IGPlus_SnapInterp_DebugHardSnapCount; +var int IGPlus_SnapInterp_DebugInterpCount; +var int IGPlus_SnapInterp_DebugClampCount; +var int IGPlus_SnapInterp_DebugFailCount; +var int IGPlus_SnapInterp_DebugExtrapolationCount; +var int IGPlus_SnapInterp_DebugSnapCount; + var bool IGPlus_AlwaysRenderFlagCarrier; var bool IGPlus_AlwaysRenderDroppedFlags; var bool IGPlus_InitFlagSprites; @@ -348,6 +384,7 @@ var bool IGPlus_UseFastWeaponSwitch; var bool IGPlus_DeferredWeaponSwitch; var bool IGPlus_EnableInputReplication; +var bool IGPlus_EnableSnapshotInterpolation; var bool IGPlus_PressedJumpSave; var IGPlus_SavedInputChain IGPlus_SavedInputChain; var IGPlus_DataBuffer IGPlus_InputReplicationBuffer; @@ -429,7 +466,9 @@ replication unreliable if ( Role == ROLE_Authority ) DuckFractionRepl, IGPlus_AdditionalReplicationInfo, - IGPlus_WarpFixData; + IGPlus_WarpFixData, + IGPlus_EnableSnapshotInterpolation, + IGPlus_SnapReplicatedData; unreliable if ( bDrawDebugData && RemoteRole == ROLE_AutonomousProxy ) clientForcedPosition, @@ -519,6 +558,8 @@ replication IGPlus_ForcedSettingsOK, IGPlus_ForcedSettings_InitOK, PrintWeaponState, + IGPlus_ServerNetcodeHelp, + IGPlus_ServerSetNetcodeSetting, ServerSetDodgeSettings, xxExplodeOther, xxNN_AltFire, @@ -679,6 +720,8 @@ simulated event Destroyed() { if (Role == ROLE_SimulatedProxy && IGPlus_LocationOffsetFix_CollisionDummy != none) { IGPlus_LocationOffsetFix_DestroyCollisionDummy(); } + if (Role == ROLE_SimulatedProxy) + IGPlus_SnapInterp_Reset(); if (bTraceInput && IGPlus_InputLogFile != none) IGPlus_InputLogFile.StopLog(); @@ -1212,6 +1255,7 @@ event Possess() IGPlus_AlwaysRenderDroppedFlags = zzUTPure.Settings.bAlwaysRenderDroppedFlags; IGPlus_UseFastWeaponSwitch = zzUTPure.Settings.bUseFastWeaponSwitch; IGPlus_EnableInputReplication = zzUTPure.Settings.bEnableInputReplication; + IGPlus_EnableSnapshotInterpolation = zzUTPure.Settings.bEnableSnapshotInterpolation; bEnableDamageDebugMode = zzUTPure.Settings.bEnableDamageDebugMode; bEnableDamageDebugConsoleMessages = zzUTPure.Settings.bEnableDamageDebugConsoleMessages; @@ -6373,14 +6417,73 @@ function Died(pawn Killer, name damageType, vector HitLocation) } event ServerTick(float DeltaTime) { + local bool bSendSnapshot; + local bool bHardSnap; + local bool bOnMover; + local float SnapshotDeltaTime; + local float SnapshotSendHz; + local float SnapshotInterval; + AverageServerDeltaTime = (AverageServerDeltaTime*99 + DeltaTime) * 0.01; IGPlus_ProcessRemoteMovement(); xxRememberPosition(); - if (IGPlus_WarpFixUpdate && IGPlus_EnableWarpFix) { - IGPlus_WarpFixData.OldLocation = Location; - IGPlus_WarpFixData.Counter += 1; + if (IGPlus_EnableSnapshotInterpolation) { + SnapshotSendHz = 30.0; + if (zzUTPure != None && zzUTPure.Settings.SnapshotInterpSendHz > 0.0) + SnapshotSendHz = zzUTPure.Settings.SnapshotInterpSendHz; + SnapshotInterval = 1.0 / FMax(SnapshotSendHz, 1.0); + + if (IGPlus_SnapInterp_ServerNextTime <= 0) + IGPlus_SnapInterp_ServerNextTime = Level.TimeSeconds; + + bSendSnapshot = Level.TimeSeconds >= IGPlus_SnapInterp_ServerNextTime; + if (bSendSnapshot) { + bHardSnap = false; + if (IGPlus_SnapInterp_ServerHasLast) { + SnapshotDeltaTime = FMax(Level.TimeSeconds - IGPlus_SnapInterp_ServerLastTime, 0.016); + if (VSize(Location - IGPlus_SnapInterp_ServerLastLocation) > 3000.0 * SnapshotDeltaTime) + bHardSnap = true; + } else { + bHardSnap = true; + } + + bOnMover = Mover(Base) != none; + + IGPlus_SnapReplicatedData.Location = Location; + IGPlus_SnapReplicatedData.Velocity = Velocity; + IGPlus_SnapReplicatedData.ServerTime = Level.TimeSeconds; + IGPlus_SnapReplicatedData.Counter += 1; + IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; + IGPlus_SnapReplicatedData.bBasedOnMover = bOnMover; + if (bOnMover) { + IGPlus_SnapReplicatedData.MoverLocation = Base.Location; + IGPlus_SnapReplicatedData.BaseMover = Base; + } else { + IGPlus_SnapReplicatedData.MoverLocation = vect(0,0,0); + IGPlus_SnapReplicatedData.BaseMover = None; + } + + IGPlus_SnapInterp_ServerLastLocation = Location; + IGPlus_SnapInterp_ServerLastTime = Level.TimeSeconds; + IGPlus_SnapInterp_ServerHasLast = true; + + while (IGPlus_SnapInterp_ServerNextTime <= Level.TimeSeconds) + IGPlus_SnapInterp_ServerNextTime += SnapshotInterval; + } + + // Snapshot interpolation uses a fixed send cadence and does not + // depend on per-move WarpFix update flags. IGPlus_WarpFixUpdate = false; + } else { + IGPlus_SnapInterp_ServerNextTime = 0; + IGPlus_SnapInterp_ServerHasLast = false; + + if (IGPlus_WarpFixUpdate && IGPlus_EnableWarpFix) { + IGPlus_WarpFixData.OldLocation = Location; + IGPlus_WarpFixData.Counter += 1; + IGPlus_WarpFixUpdate = false; + } } if (DelayedNavPoint != none) { @@ -8148,6 +8251,8 @@ function IGPlus_ApplyWarpFix(PlayerReplicationInfo PRI) { return; P = bbPlayer(PRI.Owner); + if (P.IGPlus_EnableSnapshotInterpolation) + return; if (Level.Pauser != "") { P.IGPlus_WarpFixClientData.TimeStamp = Level.TimeSeconds; @@ -8499,7 +8604,10 @@ function IGPlus_LocationOffsetFix_AfterAll(float DeltaTime) { if (P == none) continue; if (P.Role != ROLE_SimulatedProxy) continue; - P.IGPlus_LocationOffsetFix_After(DeltaTime); + if (P.IGPlus_EnableSnapshotInterpolation) + P.IGPlus_SnapInterp_After(DeltaTime); + else + P.IGPlus_LocationOffsetFix_After(DeltaTime); } } @@ -8514,8 +8622,16 @@ function IGPlus_LocationOffsetFix_AfterAll(float DeltaTime) { simulated event Tick(float DeltaTime) { super.Tick(DeltaTime); - if (Settings.bEnableLocationOffsetFix || IGPlus_LocationOffsetFix_Moved) + if (Role == ROLE_SimulatedProxy && IGPlus_SnapInterp_EnabledLast != IGPlus_EnableSnapshotInterpolation) { + IGPlus_SnapInterp_Reset(); + } + + if (IGPlus_EnableSnapshotInterpolation) { + if (IGPlus_LocationOffsetFix_Moved) + IGPlus_SnapInterp_After(DeltaTime); + } else if (Settings.bEnableLocationOffsetFix || IGPlus_LocationOffsetFix_Moved) { IGPlus_LocationOffsetFix_After(DeltaTime); + } } simulated function IGPlus_LocationOffsetFix_Restore() { @@ -8640,6 +8756,420 @@ simulated function bool IGPlus_LocationOffsetFix_IsOnGround(out vector HitNormal && (HitNormal.Z >= 0.7); } +simulated function Actor IGPlus_SnapInterp_FindNearestMover(vector WorldPos) { + local Mover M; + local Mover BestMover; + local float BestDist; + local float Dist; + + BestMover = None; + BestDist = 999999.0; + + foreach AllActors(class'Mover', M) { + Dist = VSize(M.Location - WorldPos); + if (Dist < BestDist) { + BestDist = Dist; + BestMover = M; + } + } + + return BestMover; +} + +simulated function IGPlus_SnapInterp_Push( + vector Pos, + vector Vel, + float SnapTime, + float ArrivalTime, + optional bool bFromServerMoverState, + optional bool bBasedOnMover, + optional vector BaseMoverLocation, + optional Actor BaseMoverActor +) { + local int BufSize; + local int PrevIdx; + local float Interval; + local IGPlus_InterpSnapshot Snap; + local IGPlus_InterpSnapshot PrevSnap; + + BufSize = arraycount(IGPlus_SnapInterp_Data); + + if (IGPlus_SnapInterp_Data[IGPlus_SnapInterp_DataIndex] == None) + IGPlus_SnapInterp_Data[IGPlus_SnapInterp_DataIndex] = new class'IGPlus_InterpSnapshot'; + + Snap = IGPlus_SnapInterp_Data[IGPlus_SnapInterp_DataIndex]; + Snap.Loc = Pos; + Snap.Vel = Vel; + Snap.Time = SnapTime; + if (bFromServerMoverState) { + Snap.bBasedOnMover = bBasedOnMover; + if (Snap.bBasedOnMover) { + Snap.MoverLoc = BaseMoverLocation; + Snap.RelLoc = Pos - BaseMoverLocation; + Snap.BaseMover = BaseMoverActor; + if (Snap.BaseMover == None) { + Snap.bBasedOnMover = false; + Snap.RelLoc = vect(0,0,0); + Snap.MoverLoc = vect(0,0,0); + } + } else { + Snap.RelLoc = vect(0,0,0); + Snap.MoverLoc = vect(0,0,0); + Snap.BaseMover = None; + } + } else { + Snap.bBasedOnMover = Mover(Base) != None; + if (Snap.bBasedOnMover) { + Snap.MoverLoc = Base.Location; + Snap.RelLoc = Pos - Base.Location; + Snap.BaseMover = Base; + } else { + Snap.RelLoc = vect(0,0,0); + Snap.MoverLoc = vect(0,0,0); + Snap.BaseMover = None; + } + } + + // Teleport detection: if distance to previous snapshot is unreasonably large, reset + if (IGPlus_SnapInterp_Count > 0) { + PrevIdx = (IGPlus_SnapInterp_DataIndex - 1 + BufSize) % BufSize; + PrevSnap = IGPlus_SnapInterp_Data[PrevIdx]; + if (PrevSnap != None && VSize(Pos - PrevSnap.Loc) > 3000.0 * FMax(SnapTime - PrevSnap.Time, 0.016)) { + IGPlus_SnapInterp_Reset(); + IGPlus_SnapInterp_Data[0] = Snap; + IGPlus_SnapInterp_DataIndex = 1 % BufSize; + IGPlus_SnapInterp_Count = 1; + IGPlus_SnapInterp_LastArrivalTime = ArrivalTime; + return; + } + } + + IGPlus_SnapInterp_DataIndex = (IGPlus_SnapInterp_DataIndex + 1) % BufSize; + if (IGPlus_SnapInterp_Count < BufSize) + IGPlus_SnapInterp_Count++; + + // Update adaptive interpolation delay + if (IGPlus_SnapInterp_LastArrivalTime > 0) { + Interval = ArrivalTime - IGPlus_SnapInterp_LastArrivalTime; + if (Interval > 0 && Interval < 1.0) { + IGPlus_SnapInterp_MeanInterval += 0.1 * (Interval - IGPlus_SnapInterp_MeanInterval); + IGPlus_SnapInterp_JitterEstimate += 0.1 * (Abs(Interval - IGPlus_SnapInterp_MeanInterval) - IGPlus_SnapInterp_JitterEstimate); + IGPlus_SnapInterp_InterpDelay = FMax(2.0 * IGPlus_SnapInterp_MeanInterval + 2.0 * IGPlus_SnapInterp_JitterEstimate, 0.020); + IGPlus_SnapInterp_InterpDelay = FMin(IGPlus_SnapInterp_InterpDelay, 0.200); + } + } + IGPlus_SnapInterp_LastArrivalTime = ArrivalTime; +} + +simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vector OutPos, out vector OutVel, out byte InterpMode) { + local int BufSize; + local int Idx, Scans; + local IGPlus_InterpSnapshot OlderSnap; + local IGPlus_InterpSnapshot NewerSnap; + local float TimeDelta, Alpha; + local float ExtrapolationTime; + local vector MoverOffset; + + InterpMode = 0; + + if (IGPlus_SnapInterp_Count < 1) + return false; + + BufSize = arraycount(IGPlus_SnapInterp_Data); + Idx = (IGPlus_SnapInterp_DataIndex - 1 + BufSize) % BufSize; + NewerSnap = None; + + for (Scans = 0; Scans < IGPlus_SnapInterp_Count; Scans++) { + OlderSnap = IGPlus_SnapInterp_Data[Idx]; + + if (OlderSnap != None && OlderSnap.Time > 0) { + if (OlderSnap.Time <= RenderTime) { + if (NewerSnap != None) { + TimeDelta = NewerSnap.Time - OlderSnap.Time; + if (TimeDelta > 0.001) { + Alpha = (RenderTime - OlderSnap.Time) / TimeDelta; + if (Alpha > 1.0) Alpha = 1.0; + + // World-space base interpolation + OutPos = OlderSnap.Loc + (NewerSnap.Loc - OlderSnap.Loc) * Alpha; + + // Mover compensation: shifts each on-mover endpoint from its + // historical server-sampled mover position to the live client + // mover position. Alpha-weighting naturally fades the offset + // during mount/dismount transitions, avoiding velocity pops. + if (OlderSnap.bBasedOnMover && OlderSnap.BaseMover != None) { + MoverOffset = OlderSnap.BaseMover.Location - OlderSnap.MoverLoc; + if (VSize(MoverOffset) < 256.0) + OutPos += MoverOffset * (1.0 - Alpha); + } + if (NewerSnap.bBasedOnMover && NewerSnap.BaseMover != None) { + MoverOffset = NewerSnap.BaseMover.Location - NewerSnap.MoverLoc; + if (VSize(MoverOffset) < 256.0) + OutPos += MoverOffset * Alpha; + } + + OutVel = OlderSnap.Vel + (NewerSnap.Vel - OlderSnap.Vel) * Alpha; + InterpMode = 1; + return true; + } + } + ExtrapolationTime = FClamp(RenderTime - OlderSnap.Time, 0.0, 0.050); + if (OlderSnap.bBasedOnMover && OlderSnap.BaseMover != None) { + MoverOffset = OlderSnap.BaseMover.Location - OlderSnap.MoverLoc; + if (VSize(MoverOffset) < 256.0) + OutPos = OlderSnap.BaseMover.Location + OlderSnap.RelLoc; + else + OutPos = OlderSnap.Loc + OlderSnap.Vel * ExtrapolationTime; + } else { + OutPos = OlderSnap.Loc + OlderSnap.Vel * ExtrapolationTime; + } + OutVel = OlderSnap.Vel; + InterpMode = 2; + return true; + } + NewerSnap = OlderSnap; + } + + Idx = (Idx - 1 + BufSize) % BufSize; + } + + if (NewerSnap != None) { + if (NewerSnap.bBasedOnMover && NewerSnap.BaseMover != None) { + MoverOffset = NewerSnap.BaseMover.Location - NewerSnap.MoverLoc; + if (VSize(MoverOffset) < 256.0) + OutPos = NewerSnap.BaseMover.Location + NewerSnap.RelLoc; + else + OutPos = NewerSnap.Loc; + } else { + OutPos = NewerSnap.Loc; + } + OutVel = NewerSnap.Vel; + InterpMode = 3; + return true; + } + + return false; +} + +simulated function IGPlus_SnapInterp_Reset() { + local int i; + + for (i = 0; i < arraycount(IGPlus_SnapInterp_Data); i++) + IGPlus_SnapInterp_Data[i] = None; + + IGPlus_SnapInterp_DataIndex = 0; + IGPlus_SnapInterp_Count = 0; + IGPlus_SnapInterp_InterpDelay = 0.050; + IGPlus_SnapInterp_LastArrivalTime = 0; + IGPlus_SnapInterp_MeanInterval = 0; + IGPlus_SnapInterp_JitterEstimate = 0; + IGPlus_SnapInterp_LastCounter = 0; + IGPlus_SnapInterp_HasClockSync = false; + IGPlus_SnapInterp_ServerTimeOffset = 0; + IGPlus_SnapInterp_EnabledLast = IGPlus_EnableSnapshotInterpolation; +} + +simulated function IGPlus_SnapInterp_After(float DeltaTime) { + local bool bReplicatedLocation; + local int RepCounter; + local byte InterpMode; + local float RenderTime; + local float ArrivalTime; + local float SnapshotTime; + local vector OutPos; + local vector OutVel; + local vector FloorSnapLoc; + local bbPlayer LP; + local string TargetName; + local string DebugMode; + local vector FloorHitLoc; + local vector FloorHitNorm; + local Actor FloorActor; + local vector TraceExtent; + local int DebugSnapHz; + local int DebugExtrap; + local int DebugInterp; + local int DebugClamp; + local int DebugFail; + local int DebugHardSnap; + local int ModeTotal; + local int InterpPct; + local int ExtrapPct; + local int DebugAgeMs; + local int NewestIdx; + local IGPlus_InterpSnapshot NewestSnap; + + if (IGPlus_LocationOffsetFix_Moved == false) + return; + + if (IGPlus_LocationOffsetFix_CollisionDummy != none) { + IGPlus_LocationOffsetFix_CollisionDummy.bCollideWorld = false; + IGPlus_LocationOffsetFix_CollisionDummy.SetCollision(false, false, false); + } + + RepCounter = IGPlus_SnapReplicatedData.Counter; + bReplicatedLocation = (RepCounter > IGPlus_SnapInterp_LastCounter) + || (IGPlus_SnapInterp_LastCounter == 0 && RepCounter != 0); + + if (bReplicatedLocation) { + ArrivalTime = Level.TimeSeconds; + // Use arrival time for interpolation to avoid server clock quantization issues. + SnapshotTime = ArrivalTime; + IGPlus_SnapInterp_HasClockSync = false; + IGPlus_SnapInterp_ServerTimeOffset = 0; + + if (IGPlus_SnapReplicatedData.bHardSnap) { + IGPlus_SnapInterp_DebugHardSnapCount += 1; + IGPlus_SnapInterp_Reset(); + } + + IGPlus_SnapInterp_LastCounter = RepCounter; + IGPlus_SnapInterp_DebugSnapCount += 1; + IGPlus_SnapInterp_Push( + IGPlus_SnapReplicatedData.Location, + IGPlus_SnapReplicatedData.Velocity, + SnapshotTime, + ArrivalTime, + true, + IGPlus_SnapReplicatedData.bBasedOnMover, + IGPlus_SnapReplicatedData.MoverLocation, + IGPlus_SnapReplicatedData.BaseMover); + } + + if (IGPlus_SnapInterp_Count < 2) { + if (bReplicatedLocation) { + bCollideWorld = false; + SetLocation(IGPlus_SnapReplicatedData.Location); + bCollideWorld = true; + } else { + bCollideWorld = false; + SetLocation(IGPlus_LocationOffsetFix_OldLocation); + bCollideWorld = true; + } + IGPlus_LocationOffsetFix_Moved = false; + return; + } + + if (IGPlus_SnapInterp_HasClockSync) + RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_ServerTimeOffset - IGPlus_SnapInterp_InterpDelay; + else + RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_InterpDelay; + + if (IGPlus_SnapInterp_Interpolate(RenderTime, OutPos, OutVel, InterpMode)) { + bCollideWorld = false; + SetLocation(OutPos); + bCollideWorld = true; + Velocity = OutVel; + if (InterpMode == 1) + IGPlus_SnapInterp_DebugInterpCount += 1; + else if (InterpMode == 2) + IGPlus_SnapInterp_DebugExtrapolationCount += 1; + else if (InterpMode == 3) + IGPlus_SnapInterp_DebugClampCount += 1; + } else { + bCollideWorld = false; + SetLocation(IGPlus_LocationOffsetFix_OldLocation); + bCollideWorld = true; + IGPlus_SnapInterp_DebugFailCount += 1; + } + + // Floor-snap: SetLocation with bCollideWorld=false bypasses UE1 floor tracing. + // Use an extent trace so slope/ramp contact uses cylinder geometry and + // returns a center position we can snap to directly. + // Do not floor-snap while moving upward, otherwise jump ascent can look like + // discrete vertical snapping for simulated proxies. + if (!bCanFly && !Region.Zone.bWaterZone && Velocity.Z <= 0.0) { + TraceExtent.X = CollisionRadius; + TraceExtent.Y = CollisionRadius; + TraceExtent.Z = CollisionHeight; + FloorActor = Trace(FloorHitLoc, FloorHitNorm, + Location - vect(0,0,1) * (CollisionHeight + MaxStepHeight + 2), + Location + vect(0,0,1) * (MaxStepHeight + 2), + false, + TraceExtent); + if (FloorActor != None && FloorHitNorm.Z >= 0.7 && Mover(FloorActor) == None) { + // Preserve interpolated X/Y motion. Only correct Z when the proxy + // is slightly below the floor to avoid movement snapping. + if (FloorHitLoc.Z > Location.Z + 0.01 && FloorHitLoc.Z - Location.Z <= MaxStepHeight + 2) { + FloorSnapLoc = Location; + FloorSnapLoc.Z = FloorHitLoc.Z; + bCollideWorld = false; + SetLocation(FloorSnapLoc); + bCollideWorld = true; + } + } + } + + LP = bbPlayer(GetLocalPlayer()); + if (Level.NetMode == NM_Client && + LP != none && + LP.IGPlus_SnapInterp_Debug && + (LP.ViewTarget == self || LP.ViewTarget == LP || LP.ViewTarget == none) && + Level.TimeSeconds >= LP.IGPlus_SnapInterp_DebugNextTime + ) { + LP.IGPlus_SnapInterp_DebugNextTime = Level.TimeSeconds + 1.0; + TargetName = "Unknown"; + if (PlayerReplicationInfo != none) + TargetName = PlayerReplicationInfo.PlayerName; + if (LP.ViewTarget == self) + DebugMode = "spectate"; + else + DebugMode = "play"; + + DebugSnapHz = IGPlus_SnapInterp_DebugSnapCount; + DebugExtrap = IGPlus_SnapInterp_DebugExtrapolationCount; + DebugInterp = IGPlus_SnapInterp_DebugInterpCount; + DebugClamp = IGPlus_SnapInterp_DebugClampCount; + DebugFail = IGPlus_SnapInterp_DebugFailCount; + DebugHardSnap = IGPlus_SnapInterp_DebugHardSnapCount; + ModeTotal = DebugInterp + DebugExtrap; + if (ModeTotal > 0) { + InterpPct = int(100.0 * float(DebugInterp) / float(ModeTotal)); + ExtrapPct = int(100.0 * float(DebugExtrap) / float(ModeTotal)); + } else { + InterpPct = 0; + ExtrapPct = 0; + } + DebugAgeMs = 0; + if (IGPlus_SnapInterp_Count > 0) { + NewestIdx = (IGPlus_SnapInterp_DataIndex - 1 + arraycount(IGPlus_SnapInterp_Data)) % arraycount(IGPlus_SnapInterp_Data); + NewestSnap = IGPlus_SnapInterp_Data[NewestIdx]; + if (NewestSnap != None && NewestSnap.Time > 0) + DebugAgeMs = int((RenderTime - NewestSnap.Time) * 1000.0); + } + + LP.ClientMessage( + "SnapInterp target="$TargetName$ + " mode="$DebugMode$ + " count="$IGPlus_SnapInterp_Count$ + " delayMs="$int(IGPlus_SnapInterp_InterpDelay * 1000.0)$ + " snapHz="$DebugSnapHz$ + " ageMs="$DebugAgeMs$ + " clockOffMs="$int(IGPlus_SnapInterp_ServerTimeOffset * 1000.0)$ + " hardSnap="$DebugHardSnap$ + " extrap="$DebugExtrap$ + " interpPct="$InterpPct$ + " extrapPct="$ExtrapPct$ + " clamp="$DebugClamp$ + " fail="$DebugFail + ); + + IGPlus_SnapInterp_DebugSnapCount = 0; + IGPlus_SnapInterp_DebugExtrapolationCount = 0; + IGPlus_SnapInterp_DebugInterpCount = 0; + IGPlus_SnapInterp_DebugClampCount = 0; + IGPlus_SnapInterp_DebugFailCount = 0; + IGPlus_SnapInterp_DebugHardSnapCount = 0; + } + + IGPlus_LocationOffsetFix_Moved = false; + + if (IGPlus_LocationOffsetFix_FootstepQueued) { + FootStepping(); + IGPlus_LocationOffsetFix_FootstepQueued = false; + } +} + simulated function bool IGPlus_LocationOffsetFix_IsOnMover() { local Actor HitActor; local vector HitLocation; @@ -8714,7 +9244,9 @@ function IGPlus_LocationOffsetFix_TickBefore() { if (P.bDeleteMe) continue; if (P.Role != ROLE_SimulatedProxy) continue; - if (Settings.bEnableLocationOffsetFix) + if (P.IGPlus_EnableSnapshotInterpolation) + P.IGPlus_LocationOffsetFix_Before(); + else if (Settings.bEnableLocationOffsetFix) P.IGPlus_LocationOffsetFix_Before(); else if (P.IGPlus_LocationOffsetFix_ShouldRunMoverOnly()) P.IGPlus_LocationOffsetFix_Before(); @@ -10530,6 +11062,171 @@ exec function PrintWeaponState() { if (Weapon != none) ClientMessage(Weapon.Name@Weapon.GetStateName()); } +exec function ToggleSnapInterpDebug() { + IGPlus_SnapInterp_Debug = !IGPlus_SnapInterp_Debug; + IGPlus_SnapInterp_DebugNextTime = 0; + + if (IGPlus_SnapInterp_Debug) + ClientMessage("SnapInterp debug enabled (shows while playing and spectating)"); + else + ClientMessage("SnapInterp debug disabled"); +} + +exec function bEnableLoosePositionCheck(optional string Value) { + IGPlus_SetNetcodeSetting("bEnableLoosePositionCheck", Value); +} + +exec function LooseCheckCorrectionFactor(optional string Value) { + IGPlus_SetNetcodeSetting("LooseCheckCorrectionFactor", Value); +} + +exec function LooseCheckCorrectionFactorOnMover(optional string Value) { + IGPlus_SetNetcodeSetting("LooseCheckCorrectionFactorOnMover", Value); +} + +exec function bEnableInputReplication(optional string Value) { + IGPlus_SetNetcodeSetting("bEnableInputReplication", Value); +} + +exec function bEnableSnapshotInterpolation(optional string Value) { + IGPlus_SetNetcodeSetting("bEnableSnapshotInterpolation", Value); +} + +exec function SnapshotInterpSendHz(optional string Value) { + IGPlus_SetNetcodeSetting("SnapshotInterpSendHz", Value); +} + +exec function SnapshotInterpRewindMs(optional string Value) { + IGPlus_SetNetcodeSetting("SnapshotInterpRewindMs", Value); +} + +exec function MaxJitterTime(optional string Value) { + IGPlus_SetNetcodeSetting("MaxJitterTime", Value); +} + +exec function bEnableJitterBounding(optional string Value) { + IGPlus_SetNetcodeSetting("bEnableJitterBounding", Value); +} + +exec function NetcodeHelp() { + if (Role < ROLE_Authority) { + IGPlus_ServerNetcodeHelp(); + return; + } + + IGPlus_PrintNetcodeHelp(); +} + +function IGPlus_ServerNetcodeHelp() { + IGPlus_PrintNetcodeHelp(); +} + +function IGPlus_PrintNetcodeHelp() { + local ServerSettings S; + + S = None; + if (zzUTPure != None) + S = zzUTPure.Settings; + + ClientMessage("Netcode settings (use ' '):"); + if (S == None) { + ClientMessage("Settings unavailable"); + return; + } + + ClientMessage("bEnableLoosePositionCheck="$S.GetPropertyText("bEnableLoosePositionCheck")); + ClientMessage("LooseCheckCorrectionFactor="$S.GetPropertyText("LooseCheckCorrectionFactor")); + ClientMessage("LooseCheckCorrectionFactorOnMover="$S.GetPropertyText("LooseCheckCorrectionFactorOnMover")); + ClientMessage("bEnableInputReplication="$S.GetPropertyText("bEnableInputReplication")); + ClientMessage("bEnableSnapshotInterpolation="$S.GetPropertyText("bEnableSnapshotInterpolation")); + ClientMessage("SnapshotInterpSendHz="$S.GetPropertyText("SnapshotInterpSendHz")); + ClientMessage("SnapshotInterpRewindMs="$S.GetPropertyText("SnapshotInterpRewindMs")); + ClientMessage("MaxJitterTime="$S.GetPropertyText("MaxJitterTime")); + ClientMessage("bEnableJitterBounding="$S.GetPropertyText("bEnableJitterBounding")); +} + +exec function IGPlus_SetNetcodeSetting(string Key, optional string Value) { + if (Role < ROLE_Authority) { + IGPlus_ServerSetNetcodeSetting(Key, Value); + return; + } + + if (bAdmin == false) { + ClientMessage("Admin only"); + return; + } + + IGPlus_ApplyNetcodeSetting(Key, Value); +} + +function IGPlus_ServerSetNetcodeSetting(string Key, string Value) { + if (bAdmin == false) { + ClientMessage("Admin only"); + return; + } + + IGPlus_ApplyNetcodeSetting(Key, Value); +} + +function string IGPlus_NormalizeNetcodeKey(string Key) { + if (Key ~= "bEnableLoosePositionCheck") return "bEnableLoosePositionCheck"; + if (Key ~= "LooseCheckCorrectionFactor") return "LooseCheckCorrectionFactor"; + if (Key ~= "LooseCheckCorrectionFactorOnMover") return "LooseCheckCorrectionFactorOnMover"; + if (Key ~= "bEnableInputReplication") return "bEnableInputReplication"; + if (Key ~= "bEnableSnapshotInterpolation") return "bEnableSnapshotInterpolation"; + if (Key ~= "SnapshotInterpSendHz") return "SnapshotInterpSendHz"; + if (Key ~= "SnapshotInterpRewindMs") return "SnapshotInterpRewindMs"; + if (Key ~= "MaxJitterTime") return "MaxJitterTime"; + if (Key ~= "bEnableJitterBounding") return "bEnableJitterBounding"; + return ""; +} + +function IGPlus_ApplyNetcodeSettingsToPlayers() { + local Pawn P; + local bbPlayer BP; + local bool bOldSnap; + + for (P = Level.PawnList; P != None; P = P.NextPawn) { + BP = bbPlayer(P); + if (BP == None) + continue; + + BP.IGPlus_EnableInputReplication = zzUTPure.Settings.bEnableInputReplication; + bOldSnap = BP.IGPlus_EnableSnapshotInterpolation; + BP.IGPlus_EnableSnapshotInterpolation = zzUTPure.Settings.bEnableSnapshotInterpolation; + + if (bOldSnap != BP.IGPlus_EnableSnapshotInterpolation) { + BP.IGPlus_SnapInterp_ServerNextTime = 0; + BP.IGPlus_SnapInterp_ServerHasLast = false; + } + } +} + +function IGPlus_ApplyNetcodeSetting(string Key, string Value) { + local string NormalKey; + + if (zzUTPure == None || zzUTPure.Settings == None) { + ClientMessage("No server settings loaded"); + return; + } + + NormalKey = IGPlus_NormalizeNetcodeKey(Key); + if (NormalKey == "") { + ClientMessage("Unknown netcode setting: " $ Key); + return; + } + + if (Value == "") { + ClientMessage(NormalKey $ "=" $ zzUTPure.Settings.GetPropertyText(NormalKey)); + return; + } + + zzUTPure.Settings.SetPropertyText(NormalKey, Value); + zzUTPure.Settings.SaveConfig(); + IGPlus_ApplyNetcodeSettingsToPlayers(); + ClientMessage(NormalKey $ "=" $ zzUTPure.Settings.GetPropertyText(NormalKey)); +} + exec function DrawServerLocation() { IGPlus_LocationOffsetFix_DrawServerLocation = !IGPlus_LocationOffsetFix_DrawServerLocation; } @@ -10591,4 +11288,4 @@ defaultproperties IGPlus_LocationOffsetFix_PredCompatMode=True IGPlus_EnableInputReplication=True -} \ No newline at end of file +} From 639c8197d75d206affd7760925218fee1e8364bb Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:55:43 -0600 Subject: [PATCH 02/14] Add snapshot rotation interpolation for proxies --- Classes/IGPlus_InterpSnapshot.uc | 1 + Classes/bbPlayer.uc | 51 ++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Classes/IGPlus_InterpSnapshot.uc b/Classes/IGPlus_InterpSnapshot.uc index 27ac854..fd71b8c 100644 --- a/Classes/IGPlus_InterpSnapshot.uc +++ b/Classes/IGPlus_InterpSnapshot.uc @@ -4,6 +4,7 @@ var vector Loc; var vector Vel; var vector RelLoc; var vector MoverLoc; +var rotator Rot; var float Time; var bool bBasedOnMover; var Actor BaseMover; diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index a13e84c..113662b 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -310,6 +310,7 @@ struct IGPlus_WarpFixClient { struct IGPlus_SnapData { var vector Location; var vector Velocity; + var rotator Rotation; var vector MoverLocation; var float ServerTime; var int Counter; @@ -6452,6 +6453,8 @@ event ServerTick(float DeltaTime) { IGPlus_SnapReplicatedData.Location = Location; IGPlus_SnapReplicatedData.Velocity = Velocity; + IGPlus_SnapReplicatedData.Rotation = Rotation; + IGPlus_SnapReplicatedData.Rotation.Roll = 0; IGPlus_SnapReplicatedData.ServerTime = Level.TimeSeconds; IGPlus_SnapReplicatedData.Counter += 1; IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; @@ -8779,6 +8782,7 @@ simulated function Actor IGPlus_SnapInterp_FindNearestMover(vector WorldPos) { simulated function IGPlus_SnapInterp_Push( vector Pos, vector Vel, + rotator SnapRot, float SnapTime, float ArrivalTime, optional bool bFromServerMoverState, @@ -8800,6 +8804,8 @@ simulated function IGPlus_SnapInterp_Push( Snap = IGPlus_SnapInterp_Data[IGPlus_SnapInterp_DataIndex]; Snap.Loc = Pos; Snap.Vel = Vel; + Snap.Rot = SnapRot; + Snap.Rot.Roll = 0; Snap.Time = SnapTime; if (bFromServerMoverState) { Snap.bBasedOnMover = bBasedOnMover; @@ -8861,7 +8867,28 @@ simulated function IGPlus_SnapInterp_Push( IGPlus_SnapInterp_LastArrivalTime = ArrivalTime; } -simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vector OutPos, out vector OutVel, out byte InterpMode) { +simulated function int IGPlus_SnapInterp_LerpAxis(int A, int B, float Alpha) { + local int AS; + local int BS; + local int Delta; + local int Out; + + AS = Utils.RotU2S(A); + BS = Utils.RotU2S(B); + Delta = ((BS - AS + 32768) & 65535) - 32768; + Out = AS + int(float(Delta) * Alpha); + return Utils.RotS2U(Out); +} + +simulated function rotator IGPlus_SnapInterp_LerpRot(rotator A, rotator B, float Alpha) { + local rotator R; + R.Yaw = IGPlus_SnapInterp_LerpAxis(A.Yaw, B.Yaw, Alpha); + R.Pitch = IGPlus_SnapInterp_LerpAxis(A.Pitch, B.Pitch, Alpha); + R.Roll = 0; + return R; +} + +simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vector OutPos, out vector OutVel, out rotator OutRot, out byte InterpMode) { local int BufSize; local int Idx, Scans; local IGPlus_InterpSnapshot OlderSnap; @@ -8909,6 +8936,7 @@ simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vect } OutVel = OlderSnap.Vel + (NewerSnap.Vel - OlderSnap.Vel) * Alpha; + OutRot = IGPlus_SnapInterp_LerpRot(OlderSnap.Rot, NewerSnap.Rot, Alpha); InterpMode = 1; return true; } @@ -8924,6 +8952,8 @@ simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vect OutPos = OlderSnap.Loc + OlderSnap.Vel * ExtrapolationTime; } OutVel = OlderSnap.Vel; + OutRot = OlderSnap.Rot; + OutRot.Roll = 0; InterpMode = 2; return true; } @@ -8944,6 +8974,8 @@ simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vect OutPos = NewerSnap.Loc; } OutVel = NewerSnap.Vel; + OutRot = NewerSnap.Rot; + OutRot.Roll = 0; InterpMode = 3; return true; } @@ -8978,6 +9010,7 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { local float SnapshotTime; local vector OutPos; local vector OutVel; + local rotator OutRot; local vector FloorSnapLoc; local bbPlayer LP; local string TargetName; @@ -8995,6 +9028,8 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { local int ModeTotal; local int InterpPct; local int ExtrapPct; + local int DebugYaw; + local int DebugPitch; local int DebugAgeMs; local int NewestIdx; local IGPlus_InterpSnapshot NewestSnap; @@ -9028,6 +9063,7 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { IGPlus_SnapInterp_Push( IGPlus_SnapReplicatedData.Location, IGPlus_SnapReplicatedData.Velocity, + IGPlus_SnapReplicatedData.Rotation, SnapshotTime, ArrivalTime, true, @@ -9041,6 +9077,10 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { bCollideWorld = false; SetLocation(IGPlus_SnapReplicatedData.Location); bCollideWorld = true; + OutRot = IGPlus_SnapReplicatedData.Rotation; + OutRot.Roll = 0; + SetRotation(OutRot); + DesiredRotation = OutRot; } else { bCollideWorld = false; SetLocation(IGPlus_LocationOffsetFix_OldLocation); @@ -9055,10 +9095,12 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { else RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_InterpDelay; - if (IGPlus_SnapInterp_Interpolate(RenderTime, OutPos, OutVel, InterpMode)) { + if (IGPlus_SnapInterp_Interpolate(RenderTime, OutPos, OutVel, OutRot, InterpMode)) { bCollideWorld = false; SetLocation(OutPos); bCollideWorld = true; + SetRotation(OutRot); + DesiredRotation = OutRot; Velocity = OutVel; if (InterpMode == 1) IGPlus_SnapInterp_DebugInterpCount += 1; @@ -9138,6 +9180,9 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { DebugAgeMs = int((RenderTime - NewestSnap.Time) * 1000.0); } + DebugYaw = Utils.RotU2S(Rotation.Yaw); + DebugPitch = Utils.RotU2S(Rotation.Pitch); + LP.ClientMessage( "SnapInterp target="$TargetName$ " mode="$DebugMode$ @@ -9150,6 +9195,8 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { " extrap="$DebugExtrap$ " interpPct="$InterpPct$ " extrapPct="$ExtrapPct$ + " yaw="$DebugYaw$ + " pitch="$DebugPitch$ " clamp="$DebugClamp$ " fail="$DebugFail ); From 59b8b95c542d2f196a2101379060ea966fcdc0c2 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:46:32 -0600 Subject: [PATCH 03/14] Refine mover-exit CAP smoothing and simplify logic --- Classes/bbPlayer.uc | 133 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 113662b..9bebd4e 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -86,6 +86,21 @@ var float NextRealCAPTime; var decoration carriedFlag; var WeaponSettingsRepl WSettings; +const IGPLUS_MOVER_EXIT_GRACE_BASE = 0.10; +const IGPLUS_MOVER_EXIT_GRACE_MIN = 0.16; +const IGPLUS_MOVER_EXIT_GRACE_MAX = 0.35; +const IGPLUS_MOVER_GRACE_MEDIUM_UNITS = 80.0; +const IGPLUS_MOVER_GRACE_HARD_UNITS = 120.0; +const IGPLUS_MOVER_GRACE_ADAPT_SCALE = 0.20; +const IGPLUS_MOVER_GRACE_ADAPT_MAX = 20.0; +const IGPLUS_MOVER_GRACE_DEBOUNCE = 3; + +var float IGPlus_MoverExitGraceUntil; +var bool IGPlus_WasOnMover; +var int IGPlus_MoverExitGraceDebounceCount; +var float IGPlus_MoverExitGraceMediumErrorSq; +var float IGPlus_MoverExitGraceHardErrorSq; + // HUD stuff var Mutator zzHudMutes[50]; // Accepted Hud Mutators @@ -2926,6 +2941,60 @@ function IGPlus_ApplyServerMove(IGPlus_ServerMove SM) { IGPlus_WantCAP = IGPlus_IsCAPNecessary(); } +function float IGPlus_GetHalfPingSeconds() { + // Prefer our smoothed ping estimate for stable mover-exit behavior. + if (PingAverage > 0) + return PingAverage * 0.0005 * Level.TimeDilation; + + if (PlayerReplicationInfo != None) + return PlayerReplicationInfo.Ping * 0.0005 * Level.TimeDilation; + + return 0.0; +} + +function float IGPlus_GetMoverExitGraceDuration() { + return FClamp( + (IGPLUS_MOVER_EXIT_GRACE_BASE * Level.TimeDilation) + IGPlus_GetHalfPingSeconds(), + IGPLUS_MOVER_EXIT_GRACE_MIN * Level.TimeDilation, + IGPLUS_MOVER_EXIT_GRACE_MAX * Level.TimeDilation + ); +} + +function vector IGPlus_GetMoverAdjustedClientLocation(vector ClientLoc, Actor ClientBase) { + local ST_MoverDummy MD; + local float TargetTimeStamp; + local vector BaseLoc; + + BaseLoc = vect(0,0,0); + + if (ClientBase == None) + return ClientLoc; + + if (zzUTPure != None && Mover(ClientBase) != None) { + MD = zzUTPure.FindMoverDummy(Mover(ClientBase)); + if (MD != None) { + TargetTimeStamp = Level.TimeSeconds - IGPlus_GetHalfPingSeconds(); + if (TargetTimeStamp < 0) + TargetTimeStamp = 0; + BaseLoc = MD.GetHistoricalLocation(TargetTimeStamp); + } else { + BaseLoc = ClientBase.Location; + } + } else { + BaseLoc = ClientBase.Location; + } + + return ClientLoc + BaseLoc; +} + +simulated function IGPlus_ResetMoverGraceState() { + IGPlus_MoverExitGraceUntil = 0; + IGPlus_WasOnMover = false; + IGPlus_MoverExitGraceDebounceCount = 0; + IGPlus_MoverExitGraceMediumErrorSq = 0; + IGPlus_MoverExitGraceHardErrorSq = 0; +} + function IGPlus_CheckClientError() { local vector ClientLoc; local vector ClientVel; @@ -2936,6 +3005,16 @@ function IGPlus_CheckClientError() { local float ClientLocError; local float MaxLocError; local bool bForceUpdate; + local bool bClientOnMover; + local bool bGraceActive; + local Actor EffectiveServerBase; + local float GraceMediumError; + local float GraceHardError; + local float GraceAdaptiveMargin; + local float HalfPingSeconds; + local float ClientSpeed; + local int GraceDebounceThreshold; + local bool bGraceForceCAP; if (bHaveReceivedServerMove == false) return; @@ -2944,8 +3023,10 @@ function IGPlus_CheckClientError() { ClientVel = LastServerMoveParams.Velocity; ClientPhysics = LastServerMoveParams.Physics; ClientLocAbs = ClientLoc; - if (LastServerMoveParams.Base != none) - ClientLocAbs += LastServerMoveParams.Base.Location; + bClientOnMover = Mover(Base) != None; + EffectiveServerBase = LastServerMoveParams.Base; + + ClientLocAbs = IGPlus_GetMoverAdjustedClientLocation(ClientLoc, EffectiveServerBase); ClientTlocCounter = LastServerMoveParams.TlocCounter; LocDelta = Location - ClientLocAbs; @@ -2955,6 +3036,49 @@ function IGPlus_CheckClientError() { // Calculate how far off we allow the client to be from the predicted position MaxLocError = 3.0; + if (IGPlus_WasOnMover && !bClientOnMover && Physics == PHYS_Falling) { + // Keep a short post-mover grace to absorb lift-exit desync spikes. + IGPlus_MoverExitGraceUntil = Level.TimeSeconds + IGPlus_GetMoverExitGraceDuration(); + IGPlus_MoverExitGraceDebounceCount = 0; + ClientSpeed = VSize(ClientVel); + HalfPingSeconds = IGPlus_GetHalfPingSeconds(); + + // Lock thresholds for this grace window to avoid per-tick jitter in decisions. + GraceAdaptiveMargin = FClamp(ClientSpeed * HalfPingSeconds * IGPLUS_MOVER_GRACE_ADAPT_SCALE, 0.0, IGPLUS_MOVER_GRACE_ADAPT_MAX); + GraceMediumError = IGPLUS_MOVER_GRACE_MEDIUM_UNITS + GraceAdaptiveMargin; + GraceHardError = IGPLUS_MOVER_GRACE_HARD_UNITS + GraceAdaptiveMargin; + IGPlus_MoverExitGraceMediumErrorSq = GraceMediumError * GraceMediumError; + IGPlus_MoverExitGraceHardErrorSq = GraceHardError * GraceHardError; + } + + bGraceActive = Level.TimeSeconds < IGPlus_MoverExitGraceUntil; + if (bGraceActive) { + GraceDebounceThreshold = IGPLUS_MOVER_GRACE_DEBOUNCE; + bGraceForceCAP = false; + + if (ClientLocError <= IGPlus_MoverExitGraceMediumErrorSq) { + IGPlus_MoverExitGraceDebounceCount = 0; + } else if (ClientLocError <= IGPlus_MoverExitGraceHardErrorSq) { + IGPlus_MoverExitGraceDebounceCount++; + if (IGPlus_MoverExitGraceDebounceCount >= GraceDebounceThreshold) { + bGraceForceCAP = true; + IGPlus_MoverExitGraceDebounceCount = 0; + } + } else { + bGraceForceCAP = true; + IGPlus_MoverExitGraceDebounceCount = 0; + } + + if (!bGraceForceCAP) { + clientLastUpdateTime = ServerTimeStamp; + ClearLastServerMoveParams(); + IGPlus_WasOnMover = bClientOnMover; + return; + } + } else { + IGPlus_MoverExitGraceDebounceCount = 0; + } + clientLastUpdateTime = ServerTimeStamp; ClearLastServerMoveParams(); @@ -2962,6 +3086,8 @@ function IGPlus_CheckClientError() { (ClientLocError > MaxLocError && ServerTimeStamp >= NextRealCAPTime) || (ClientTlocCounter != TlocCounter && IGPlus_NotifiedTranslocate == false); + IGPlus_WasOnMover = bClientOnMover; + debugClientForceUpdate = bForceUpdate; if (bForceUpdate) { @@ -6641,6 +6767,7 @@ state FeigningDeath { zzbForceUpdate = true; zzIgnoreUpdateUntil = 0; + IGPlus_ResetMoverGraceState(); Super.EndState(); } } @@ -7267,6 +7394,7 @@ function xxServerSetReadyToPlay() { zzbForceUpdate = true; zzIgnoreUpdateUntil = 0; + IGPlus_ResetMoverGraceState(); PlayerRestartState = 'PlayerWarmup'; GotoState('PlayerWarmup'); @@ -7566,6 +7694,7 @@ state Dying bJumpStatus = false; zzIgnoreUpdateUntil = 0; + IGPlus_ResetMoverGraceState(); if (zzClientTTarget != None) zzClientTTarget.Destroy(); From c613b757ac94ccfbcb2ee0b8ed6964bab6ecae77 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:38:27 -0600 Subject: [PATCH 04/14] Fix mover jump desync and remove dead mover-grace code --- Classes/bbPlayer.uc | 166 ++++---------------------------------------- 1 file changed, 13 insertions(+), 153 deletions(-) diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 9bebd4e..1be7b7d 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -86,22 +86,6 @@ var float NextRealCAPTime; var decoration carriedFlag; var WeaponSettingsRepl WSettings; -const IGPLUS_MOVER_EXIT_GRACE_BASE = 0.10; -const IGPLUS_MOVER_EXIT_GRACE_MIN = 0.16; -const IGPLUS_MOVER_EXIT_GRACE_MAX = 0.35; -const IGPLUS_MOVER_GRACE_MEDIUM_UNITS = 80.0; -const IGPLUS_MOVER_GRACE_HARD_UNITS = 120.0; -const IGPLUS_MOVER_GRACE_ADAPT_SCALE = 0.20; -const IGPLUS_MOVER_GRACE_ADAPT_MAX = 20.0; -const IGPLUS_MOVER_GRACE_DEBOUNCE = 3; - -var float IGPlus_MoverExitGraceUntil; -var bool IGPlus_WasOnMover; -var int IGPlus_MoverExitGraceDebounceCount; -var float IGPlus_MoverExitGraceMediumErrorSq; -var float IGPlus_MoverExitGraceHardErrorSq; - - // HUD stuff var Mutator zzHudMutes[50]; // Accepted Hud Mutators var Mutator zzWaitMutes[50]; // Hud Mutes waiting to be accepted @@ -2052,7 +2036,8 @@ simulated function xxPureCAP(float TimeStamp, name newState, int MiscData, vecto { local Decoration Carried; local vector OldLoc; - local bool bOldCollWorld, bOldActCol, bOldBlockAct, bOldBlockPlayer; + local bool bOldCollWorld; + local bool bOldActCol, bOldBlockAct, bOldBlockPlayer; if (bDeleteMe) return; @@ -2941,60 +2926,6 @@ function IGPlus_ApplyServerMove(IGPlus_ServerMove SM) { IGPlus_WantCAP = IGPlus_IsCAPNecessary(); } -function float IGPlus_GetHalfPingSeconds() { - // Prefer our smoothed ping estimate for stable mover-exit behavior. - if (PingAverage > 0) - return PingAverage * 0.0005 * Level.TimeDilation; - - if (PlayerReplicationInfo != None) - return PlayerReplicationInfo.Ping * 0.0005 * Level.TimeDilation; - - return 0.0; -} - -function float IGPlus_GetMoverExitGraceDuration() { - return FClamp( - (IGPLUS_MOVER_EXIT_GRACE_BASE * Level.TimeDilation) + IGPlus_GetHalfPingSeconds(), - IGPLUS_MOVER_EXIT_GRACE_MIN * Level.TimeDilation, - IGPLUS_MOVER_EXIT_GRACE_MAX * Level.TimeDilation - ); -} - -function vector IGPlus_GetMoverAdjustedClientLocation(vector ClientLoc, Actor ClientBase) { - local ST_MoverDummy MD; - local float TargetTimeStamp; - local vector BaseLoc; - - BaseLoc = vect(0,0,0); - - if (ClientBase == None) - return ClientLoc; - - if (zzUTPure != None && Mover(ClientBase) != None) { - MD = zzUTPure.FindMoverDummy(Mover(ClientBase)); - if (MD != None) { - TargetTimeStamp = Level.TimeSeconds - IGPlus_GetHalfPingSeconds(); - if (TargetTimeStamp < 0) - TargetTimeStamp = 0; - BaseLoc = MD.GetHistoricalLocation(TargetTimeStamp); - } else { - BaseLoc = ClientBase.Location; - } - } else { - BaseLoc = ClientBase.Location; - } - - return ClientLoc + BaseLoc; -} - -simulated function IGPlus_ResetMoverGraceState() { - IGPlus_MoverExitGraceUntil = 0; - IGPlus_WasOnMover = false; - IGPlus_MoverExitGraceDebounceCount = 0; - IGPlus_MoverExitGraceMediumErrorSq = 0; - IGPlus_MoverExitGraceHardErrorSq = 0; -} - function IGPlus_CheckClientError() { local vector ClientLoc; local vector ClientVel; @@ -3005,16 +2936,6 @@ function IGPlus_CheckClientError() { local float ClientLocError; local float MaxLocError; local bool bForceUpdate; - local bool bClientOnMover; - local bool bGraceActive; - local Actor EffectiveServerBase; - local float GraceMediumError; - local float GraceHardError; - local float GraceAdaptiveMargin; - local float HalfPingSeconds; - local float ClientSpeed; - local int GraceDebounceThreshold; - local bool bGraceForceCAP; if (bHaveReceivedServerMove == false) return; @@ -3023,10 +2944,8 @@ function IGPlus_CheckClientError() { ClientVel = LastServerMoveParams.Velocity; ClientPhysics = LastServerMoveParams.Physics; ClientLocAbs = ClientLoc; - bClientOnMover = Mover(Base) != None; - EffectiveServerBase = LastServerMoveParams.Base; - - ClientLocAbs = IGPlus_GetMoverAdjustedClientLocation(ClientLoc, EffectiveServerBase); + if (LastServerMoveParams.Base != none) + ClientLocAbs += LastServerMoveParams.Base.Location; ClientTlocCounter = LastServerMoveParams.TlocCounter; LocDelta = Location - ClientLocAbs; @@ -3036,49 +2955,6 @@ function IGPlus_CheckClientError() { // Calculate how far off we allow the client to be from the predicted position MaxLocError = 3.0; - if (IGPlus_WasOnMover && !bClientOnMover && Physics == PHYS_Falling) { - // Keep a short post-mover grace to absorb lift-exit desync spikes. - IGPlus_MoverExitGraceUntil = Level.TimeSeconds + IGPlus_GetMoverExitGraceDuration(); - IGPlus_MoverExitGraceDebounceCount = 0; - ClientSpeed = VSize(ClientVel); - HalfPingSeconds = IGPlus_GetHalfPingSeconds(); - - // Lock thresholds for this grace window to avoid per-tick jitter in decisions. - GraceAdaptiveMargin = FClamp(ClientSpeed * HalfPingSeconds * IGPLUS_MOVER_GRACE_ADAPT_SCALE, 0.0, IGPLUS_MOVER_GRACE_ADAPT_MAX); - GraceMediumError = IGPLUS_MOVER_GRACE_MEDIUM_UNITS + GraceAdaptiveMargin; - GraceHardError = IGPLUS_MOVER_GRACE_HARD_UNITS + GraceAdaptiveMargin; - IGPlus_MoverExitGraceMediumErrorSq = GraceMediumError * GraceMediumError; - IGPlus_MoverExitGraceHardErrorSq = GraceHardError * GraceHardError; - } - - bGraceActive = Level.TimeSeconds < IGPlus_MoverExitGraceUntil; - if (bGraceActive) { - GraceDebounceThreshold = IGPLUS_MOVER_GRACE_DEBOUNCE; - bGraceForceCAP = false; - - if (ClientLocError <= IGPlus_MoverExitGraceMediumErrorSq) { - IGPlus_MoverExitGraceDebounceCount = 0; - } else if (ClientLocError <= IGPlus_MoverExitGraceHardErrorSq) { - IGPlus_MoverExitGraceDebounceCount++; - if (IGPlus_MoverExitGraceDebounceCount >= GraceDebounceThreshold) { - bGraceForceCAP = true; - IGPlus_MoverExitGraceDebounceCount = 0; - } - } else { - bGraceForceCAP = true; - IGPlus_MoverExitGraceDebounceCount = 0; - } - - if (!bGraceForceCAP) { - clientLastUpdateTime = ServerTimeStamp; - ClearLastServerMoveParams(); - IGPlus_WasOnMover = bClientOnMover; - return; - } - } else { - IGPlus_MoverExitGraceDebounceCount = 0; - } - clientLastUpdateTime = ServerTimeStamp; ClearLastServerMoveParams(); @@ -3086,8 +2962,6 @@ function IGPlus_CheckClientError() { (ClientLocError > MaxLocError && ServerTimeStamp >= NextRealCAPTime) || (ClientTlocCounter != TlocCounter && IGPlus_NotifiedTranslocate == false); - IGPlus_WasOnMover = bClientOnMover; - debugClientForceUpdate = bForceUpdate; if (bForceUpdate) { @@ -3119,6 +2993,8 @@ function bool IGPlus_IsCAPNecessary() { local bool bCanTraceNewLoc; local bool bMovedToNewLoc; local bool bMoverLooseCheckContext; + local bool bAppliedQueuedMomentum; + local bool bPreserveServerVelocity; local Decoration Carried; local vector OldLoc; local float LooseCheckCorrectionFactor; @@ -3139,10 +3015,12 @@ function bool IGPlus_IsCAPNecessary() { debugClientLocError = ClientLocError; // Apply momentum that the client never got around to + bAppliedQueuedMomentum = false; while(LastAddVelocityAppliedIndex != LastAddVelocityIndex && AddVelocityCalls[LastAddVelocityAppliedIndex].TimeStamp < ServerTimeStamp) { IGPlus_ApplyMomentum(AddVelocityCalls[LastAddVelocityAppliedIndex].Momentum); AddVelocityCalls[LastAddVelocityAppliedIndex].Momentum = vect(0,0,0); zzbForceUpdate = true; + bAppliedQueuedMomentum = true; LastAddVelocityAppliedIndex = (LastAddVelocityAppliedIndex+1) & 0xF; } @@ -3211,12 +3089,15 @@ function bool IGPlus_IsCAPNecessary() { if (LooseCheckCorrectionFactor < 1.0) ClientLocAbs = Location + (ClientLocAbs - Location) * LooseCheckCorrectionFactor; + // Keep server-side knockback velocity authoritative for a short window. + bPreserveServerVelocity = bAppliedQueuedMomentum || (zzIgnoreUpdateUntil >= ServerTimeStamp); + bCanTraceNewLoc = FastTrace(ClientLocAbs); if (bCanTraceNewLoc) { clientForcedPosition = ClientLocAbs; zzLastClientErr = 0; bMovedToNewLoc = xxNewMoveSmooth(ClientLocAbs); - if (bMovedToNewLoc && ClientPhysics == Physics) + if (bMovedToNewLoc && ClientPhysics == Physics && !bPreserveServerVelocity) Velocity = ClientVel; } if (bCanTraceNewLoc == false) { @@ -3224,7 +3105,7 @@ function bool IGPlus_IsCAPNecessary() { OldLoc = Location; bCanTeleport = false; - if (SetLocation(ClientLocAbs) && ClientPhysics == Physics) + if (SetLocation(ClientLocAbs) && ClientPhysics == Physics && !bPreserveServerVelocity) Velocity = ClientVel; bCanTeleport = true; @@ -3583,9 +3464,6 @@ function ServerApplyInput(float RefTimeStamp, int NumBits, ReplBuffer B) { local float DeltaTime; local float ServerDeltaTime; local float LostTime; - local Mover PreReplayMover; - local ST_MoverDummy MD; - local vector MoverCompensation; if (Role < ROLE_Authority) { zzbLogoDone = True; @@ -3662,8 +3540,6 @@ function ServerApplyInput(float RefTimeStamp, int NumBits, ReplBuffer B) { if (LostTime > 0.001) SimMoveAutonomous(LostTime); - PreReplayMover = Mover(Base); - while(Old.Next != none) { PlayBackInput(Old, Old.Next); if (bTraceInput && IGPlus_InputLogFile != none) @@ -3671,19 +3547,6 @@ function ServerApplyInput(float RefTimeStamp, int NumBits, ReplBuffer B) { Old = Old.Next; } - // Compensate mover position desync: the server's mover is further along - // than where the client saw it when they jumped. Adjust the player's - // position to match the trajectory the client actually experienced. - if (PreReplayMover != None && Mover(Base) != PreReplayMover && PlayerReplicationInfo != None) { - MD = zzUTPure.FindMoverDummy(PreReplayMover); - if (MD != None) { - MoverCompensation = PreReplayMover.Location - - MD.GetHistoricalLocation(Level.TimeSeconds - PlayerReplicationInfo.Ping * 0.0005 * Level.TimeDilation); - if (VSize(MoverCompensation) > 0.1) - SetLocation(Location - MoverCompensation); - } - } - // clean up IGPlus_SavedInputChain.RemoveOutdatedNodes(Old.TimeStamp); @@ -6767,7 +6630,6 @@ state FeigningDeath { zzbForceUpdate = true; zzIgnoreUpdateUntil = 0; - IGPlus_ResetMoverGraceState(); Super.EndState(); } } @@ -7394,7 +7256,6 @@ function xxServerSetReadyToPlay() { zzbForceUpdate = true; zzIgnoreUpdateUntil = 0; - IGPlus_ResetMoverGraceState(); PlayerRestartState = 'PlayerWarmup'; GotoState('PlayerWarmup'); @@ -7694,7 +7555,6 @@ state Dying bJumpStatus = false; zzIgnoreUpdateUntil = 0; - IGPlus_ResetMoverGraceState(); if (zzClientTTarget != None) zzClientTTarget.Destroy(); From a287a9eb78305df5ac5e95e8baeeaca0053c68d1 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:13:06 -0600 Subject: [PATCH 05/14] Clean up and isolate snapshot interpolation gating --- Classes/bbPlayer.uc | 62 +++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 1be7b7d..a75b1fe 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -311,7 +311,6 @@ struct IGPlus_SnapData { var vector Velocity; var rotator Rotation; var vector MoverLocation; - var float ServerTime; var int Counter; var bool bHardSnap; var bool bBasedOnMover; @@ -364,8 +363,6 @@ var float IGPlus_SnapInterp_ServerNextTime; var vector IGPlus_SnapInterp_ServerLastLocation; var float IGPlus_SnapInterp_ServerLastTime; var bool IGPlus_SnapInterp_ServerHasLast; -var float IGPlus_SnapInterp_ServerTimeOffset; -var bool IGPlus_SnapInterp_HasClockSync; var bool IGPlus_SnapInterp_EnabledLast; var int IGPlus_SnapInterp_DebugHardSnapCount; var int IGPlus_SnapInterp_DebugInterpCount; @@ -2529,7 +2526,12 @@ function ClearLastServerMoveParams() { function IGPlus_ProcessRemoteMovement() { if (IGPlus_DeferredWeaponSwitch) { - if (IGPlus_UseFastWeaponSwitch && PendingWeapon != None) + // Deferred fast switch is loose-check-only. If loose check is disabled, + // clear any stale deferred switch flag without applying it. + if (zzUTPure != None && + zzUTPure.Settings.bEnableLoosePositionCheck && + IGPlus_UseFastWeaponSwitch && + PendingWeapon != None) ChangedWeapon(); IGPlus_DeferredWeaponSwitch = false; } @@ -2796,7 +2798,8 @@ function IGPlus_ApplyServerMove(IGPlus_ServerMove SM) { } if (IGPlus_UseFastWeaponSwitch && PendingWeapon != None) { - if (zzUTPure.Settings.bEnableLoosePositionCheck) + // Defer only for loose-check path to avoid changing standard/input behavior. + if (zzUTPure != None && zzUTPure.Settings.bEnableLoosePositionCheck) IGPlus_DeferredWeaponSwitch = true; else ChangedWeapon(); @@ -6444,7 +6447,6 @@ event ServerTick(float DeltaTime) { IGPlus_SnapReplicatedData.Velocity = Velocity; IGPlus_SnapReplicatedData.Rotation = Rotation; IGPlus_SnapReplicatedData.Rotation.Roll = 0; - IGPlus_SnapReplicatedData.ServerTime = Level.TimeSeconds; IGPlus_SnapReplicatedData.Counter += 1; IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; IGPlus_SnapReplicatedData.bBasedOnMover = bOnMover; @@ -8231,6 +8233,15 @@ function ApplyBrightskins(PlayerReplicationInfo PRI) { P.Weapon.bUnlit = false; } +function bool IGPlus_ShouldUseSnapshotInterpForProxy(optional bbPlayer P) { + if (P == none) + P = self; + + return P != none && + P.Role == ROLE_SimulatedProxy && + P.IGPlus_EnableSnapshotInterpolation; +} + function IGPlus_ApplyWarpFix(PlayerReplicationInfo PRI) { local bbPlayer P; if (PRI == PlayerReplicationInfo) @@ -8243,7 +8254,7 @@ function IGPlus_ApplyWarpFix(PlayerReplicationInfo PRI) { return; P = bbPlayer(PRI.Owner); - if (P.IGPlus_EnableSnapshotInterpolation) + if (IGPlus_ShouldUseSnapshotInterpForProxy(P)) return; if (Level.Pauser != "") { @@ -8596,7 +8607,7 @@ function IGPlus_LocationOffsetFix_AfterAll(float DeltaTime) { if (P == none) continue; if (P.Role != ROLE_SimulatedProxy) continue; - if (P.IGPlus_EnableSnapshotInterpolation) + if (IGPlus_ShouldUseSnapshotInterpForProxy(P)) P.IGPlus_SnapInterp_After(DeltaTime); else P.IGPlus_LocationOffsetFix_After(DeltaTime); @@ -8614,11 +8625,12 @@ function IGPlus_LocationOffsetFix_AfterAll(float DeltaTime) { simulated event Tick(float DeltaTime) { super.Tick(DeltaTime); + // Reset on both enable and disable transitions for simulated proxies. if (Role == ROLE_SimulatedProxy && IGPlus_SnapInterp_EnabledLast != IGPlus_EnableSnapshotInterpolation) { IGPlus_SnapInterp_Reset(); } - if (IGPlus_EnableSnapshotInterpolation) { + if (IGPlus_ShouldUseSnapshotInterpForProxy()) { if (IGPlus_LocationOffsetFix_Moved) IGPlus_SnapInterp_After(DeltaTime); } else if (Settings.bEnableLocationOffsetFix || IGPlus_LocationOffsetFix_Moved) { @@ -8748,26 +8760,6 @@ simulated function bool IGPlus_LocationOffsetFix_IsOnGround(out vector HitNormal && (HitNormal.Z >= 0.7); } -simulated function Actor IGPlus_SnapInterp_FindNearestMover(vector WorldPos) { - local Mover M; - local Mover BestMover; - local float BestDist; - local float Dist; - - BestMover = None; - BestDist = 999999.0; - - foreach AllActors(class'Mover', M) { - Dist = VSize(M.Location - WorldPos); - if (Dist < BestDist) { - BestDist = Dist; - BestMover = M; - } - } - - return BestMover; -} - simulated function IGPlus_SnapInterp_Push( vector Pos, vector Vel, @@ -8985,8 +8977,6 @@ simulated function IGPlus_SnapInterp_Reset() { IGPlus_SnapInterp_MeanInterval = 0; IGPlus_SnapInterp_JitterEstimate = 0; IGPlus_SnapInterp_LastCounter = 0; - IGPlus_SnapInterp_HasClockSync = false; - IGPlus_SnapInterp_ServerTimeOffset = 0; IGPlus_SnapInterp_EnabledLast = IGPlus_EnableSnapshotInterpolation; } @@ -9039,8 +9029,6 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { ArrivalTime = Level.TimeSeconds; // Use arrival time for interpolation to avoid server clock quantization issues. SnapshotTime = ArrivalTime; - IGPlus_SnapInterp_HasClockSync = false; - IGPlus_SnapInterp_ServerTimeOffset = 0; if (IGPlus_SnapReplicatedData.bHardSnap) { IGPlus_SnapInterp_DebugHardSnapCount += 1; @@ -9079,10 +9067,7 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { return; } - if (IGPlus_SnapInterp_HasClockSync) - RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_ServerTimeOffset - IGPlus_SnapInterp_InterpDelay; - else - RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_InterpDelay; + RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_InterpDelay; if (IGPlus_SnapInterp_Interpolate(RenderTime, OutPos, OutVel, OutRot, InterpMode)) { bCollideWorld = false; @@ -9179,7 +9164,6 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { " delayMs="$int(IGPlus_SnapInterp_InterpDelay * 1000.0)$ " snapHz="$DebugSnapHz$ " ageMs="$DebugAgeMs$ - " clockOffMs="$int(IGPlus_SnapInterp_ServerTimeOffset * 1000.0)$ " hardSnap="$DebugHardSnap$ " extrap="$DebugExtrap$ " interpPct="$InterpPct$ @@ -9280,7 +9264,7 @@ function IGPlus_LocationOffsetFix_TickBefore() { if (P.bDeleteMe) continue; if (P.Role != ROLE_SimulatedProxy) continue; - if (P.IGPlus_EnableSnapshotInterpolation) + if (IGPlus_ShouldUseSnapshotInterpForProxy(P)) P.IGPlus_LocationOffsetFix_Before(); else if (Settings.bEnableLocationOffsetFix) P.IGPlus_LocationOffsetFix_Before(); From ef6198fdc76758d68f66f75f2d74844728036768 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:49:03 -0600 Subject: [PATCH 06/14] Add IG+ server settings tab with admin auth and settings editor --- Classes/ClientSettings.uc | 1 + Classes/IGPlus_ServerSettingsContent.uc | 1307 +++++++++++++++++++++++ Classes/IGPlus_SettingsContent.uc | 26 +- Classes/IGPlus_SettingsScroll.uc | 120 ++- Classes/UTPure.uc | 208 +++- Classes/bbPlayer.uc | 106 ++ 6 files changed, 1753 insertions(+), 15 deletions(-) create mode 100644 Classes/IGPlus_ServerSettingsContent.uc diff --git a/Classes/ClientSettings.uc b/Classes/ClientSettings.uc index 81616a5..12d8ef6 100644 --- a/Classes/ClientSettings.uc +++ b/Classes/ClientSettings.uc @@ -143,6 +143,7 @@ var config bool bSniperUseClientSideAnimations; var config bool bTranslocatorUseClientSideAnimations; var config float MenuX, MenuY, MenuWidth, MenuHeight; +var config string ServerAdminPasswords[16]; var Sound DefaultHitSound[16]; var Sound LoadedHitSound[16]; diff --git a/Classes/IGPlus_ServerSettingsContent.uc b/Classes/IGPlus_ServerSettingsContent.uc new file mode 100644 index 0000000..6fbe9c8 --- /dev/null +++ b/Classes/IGPlus_ServerSettingsContent.uc @@ -0,0 +1,1307 @@ +class IGPlus_ServerSettingsContent extends UMenuPageWindow; + +enum EEditControlType { + ECT_Text, + ECT_Integer, + ECT_Real +}; + +var UWindowLabelControl Lbl_Header; +var localized string HeaderText; + +var UWindowLabelControl Lbl_Status; +var localized string StatusAdminText; +var localized string StatusLoginText; +var localized string StatusLoadingText; +var localized string StatusLoginPendingText; + +var UWindowLabelControl Lbl_MoreInformation; +var localized string MoreInformationText; + +var UWindowLabelControl Lbl_Login; +var localized string LoginText; + +var IGPlus_ComboBox Cmb_AdminPassword; +var localized string AdminPasswordText; +var localized string AdminPasswordHelp; +var IGPlus_Button Btn_DeletePassword; +var localized string DeletePasswordButtonText; +var localized string DeletePasswordButtonHelp; + +var IGPlus_Button Btn_AdminAuth; +var localized string LoginButtonText; +var localized string LoginButtonHelp; +var localized string LogoutButtonText; +var localized string LogoutButtonHelp; + +var localized string LoginRequiredText; + +var UWindowLabelControl Lbl_General; +var localized string GeneralText; +var UWindowCheckbox Chk_bAutoPause; +var localized string bAutoPauseText; +var localized string bAutoPauseHelp; +var IGPlus_EditControl Edit_PauseTotalTime; +var localized string PauseTotalTimeText; +var localized string PauseTotalTimeHelp; +var IGPlus_EditControl Edit_PauseTime; +var localized string PauseTimeText; +var localized string PauseTimeHelp; +var UWindowCheckbox Chk_bForceDemo; +var localized string bForceDemoText; +var localized string bForceDemoHelp; +var UWindowCheckbox Chk_bRestrictTrading; +var localized string bRestrictTradingText; +var localized string bRestrictTradingHelp; +var IGPlus_EditControl Edit_MaxTradeTimeMargin; +var localized string MaxTradeTimeMarginText; +var localized string MaxTradeTimeMarginHelp; +var IGPlus_EditControl Edit_TradePingMargin; +var localized string TradePingMarginText; +var localized string TradePingMarginHelp; +var IGPlus_EditControl Edit_KillCamDelay; +var localized string KillCamDelayText; +var localized string KillCamDelayHelp; +var IGPlus_EditControl Edit_KillCamDuration; +var localized string KillCamDurationText; +var localized string KillCamDurationHelp; +var IGPlus_ComboBox Cmb_BrightskinMode; +var localized string BrightskinModeText; +var localized string BrightskinModeHelp; +var localized string BrightskinModeDisabled; +var localized string BrightskinModeUnlit; +var IGPlus_EditControl Edit_PlayerScale; +var localized string PlayerScaleText; +var localized string PlayerScaleHelp; +var UWindowCheckbox Chk_bAlwaysRenderFlagCarrier; +var localized string bAlwaysRenderFlagCarrierText; +var localized string bAlwaysRenderFlagCarrierHelp; +var UWindowCheckbox Chk_bAlwaysRenderDroppedFlags; +var localized string bAlwaysRenderDroppedFlagsText; +var localized string bAlwaysRenderDroppedFlagsHelp; +var IGPlus_ComboBox Cmb_HitFeedbackMode; +var localized string HitFeedbackModeText; +var localized string HitFeedbackModeHelp; +var localized string HitFeedbackModeDisabled; +var localized string HitFeedbackModeVisibleOnly; +var localized string HitFeedbackModeAlways; +var localized string bEnablePingCompensatedSpawnHelp; + +var UWindowLabelControl Lbl_Movement; +var localized string MovementText; +var UWindowCheckbox Chk_bJumpingPreservesMomentum; +var localized string bJumpingPreservesMomentumText; +var localized string bJumpingPreservesMomentumHelp; +var UWindowCheckbox Chk_bOldLandingMomentum; +var localized string bOldLandingMomentumText; +var localized string bOldLandingMomentumHelp; +var UWindowCheckbox Chk_bEnableSingleButtonDodge; +var localized string bEnableSingleButtonDodgeText; +var localized string bEnableSingleButtonDodgeHelp; +var UWindowCheckbox Chk_bUseFlipAnimation; +var localized string bUseFlipAnimationText; +var localized string bUseFlipAnimationHelp; +var UWindowCheckbox Chk_bEnableWallDodging; +var localized string bEnableWallDodgingText; +var localized string bEnableWallDodgingHelp; +var UWindowCheckbox Chk_bDodgePreserveZMomentum; +var localized string bDodgePreserveZMomentumText; +var localized string bDodgePreserveZMomentumHelp; +var IGPlus_EditControl Edit_MaxMultiDodges; +var localized string MaxMultiDodgesText; +var localized string MaxMultiDodgesHelp; +var UWindowCheckbox Chk_bEnablePingCompensatedSpawn; +var localized string bEnablePingCompensatedSpawnText; + +var UWindowLabelControl Lbl_Networking; +var localized string NetworkingText; +var IGPlus_EditControl Edit_MaxPosError; +var localized string MaxPosErrorText; +var localized string MaxPosErrorHelp; +var IGPlus_EditControl Edit_MaxHitError; +var localized string MaxHitErrorText; +var localized string MaxHitErrorHelp; +var IGPlus_EditControl Edit_MaxJitterTime; +var localized string MaxJitterTimeText; +var localized string MaxJitterTimeHelp; +var IGPlus_EditControl Edit_WarpFixDelay; +var localized string WarpFixDelayText; +var localized string WarpFixDelayHelp; +var IGPlus_EditControl Edit_FireTimeout; +var localized string FireTimeoutText; +var localized string FireTimeoutHelp; +var IGPlus_EditControl Edit_MinNetUpdateRate; +var localized string MinNetUpdateRateText; +var localized string MinNetUpdateRateHelp; +var IGPlus_EditControl Edit_MaxNetUpdateRate; +var localized string MaxNetUpdateRateText; +var localized string MaxNetUpdateRateHelp; +var UWindowCheckbox Chk_bEnableInputReplication; +var localized string bEnableInputReplicationText; +var localized string bEnableInputReplicationHelp; +var UWindowCheckbox Chk_bEnableServerExtrapolation; +var localized string bEnableServerExtrapolationText; +var localized string bEnableServerExtrapolationHelp; +var UWindowCheckbox Chk_bEnableServerPacketReordering; +var localized string bEnableServerPacketReorderingText; +var localized string bEnableServerPacketReorderingHelp; +var UWindowCheckbox Chk_bEnableLoosePositionCheck; +var localized string bEnableLoosePositionCheckText; +var localized string bEnableLoosePositionCheckHelp; +var UWindowCheckbox Chk_bPlayersAlwaysRelevant; +var localized string bPlayersAlwaysRelevantText; +var localized string bPlayersAlwaysRelevantHelp; +var UWindowCheckbox Chk_bEnableJitterBounding; +var localized string bEnableJitterBoundingText; +var localized string bEnableJitterBoundingHelp; +var IGPlus_EditControl Edit_LooseCheckCorrectionFactor; +var localized string LooseCheckCorrectionFactorText; +var localized string LooseCheckCorrectionFactorHelp; +var IGPlus_EditControl Edit_LooseCheckCorrectionFactorOnMover; +var localized string LooseCheckCorrectionFactorOnMoverText; +var localized string LooseCheckCorrectionFactorOnMoverHelp; +var UWindowCheckbox Chk_bEnableSnapshotInterpolation; +var localized string bEnableSnapshotInterpolationText; +var localized string bEnableSnapshotInterpolationHelp; +var IGPlus_EditControl Edit_SnapshotInterpSendHz; +var localized string SnapshotInterpSendHzText; +var localized string SnapshotInterpSendHzHelp; +var IGPlus_EditControl Edit_SnapshotInterpRewindMs; +var localized string SnapshotInterpRewindMsText; +var localized string SnapshotInterpRewindMsHelp; +var UWindowCheckbox Chk_bEnableWarpFix; +var localized string bEnableWarpFixText; +var localized string bEnableWarpFixHelp; + +var UWindowLabelControl Lbl_Debug; +var localized string DebugText; +var UWindowCheckbox Chk_ShowTouchedPackage; +var localized string ShowTouchedPackageText; +var localized string ShowTouchedPackageHelp; +var UWindowCheckbox Chk_bEnableDamageDebugMode; +var localized string bEnableDamageDebugModeText; +var localized string bEnableDamageDebugModeHelp; +var UWindowCheckbox Chk_bEnableDamageDebugConsoleMessages; +var localized string bEnableDamageDebugConsoleMessagesText; +var localized string bEnableDamageDebugConsoleMessagesHelp; +var UWindowCheckbox Chk_bEnableHitboxDebugMode; +var localized string bEnableHitboxDebugModeText; +var localized string bEnableHitboxDebugModeHelp; + +var float PaddingX; +var float PaddingY; +var float LineSpacing; +var float SeparatorSpacing; +var float ControlOffset; +var bool bLastAdminState; +var bool bLoadSucceeded; +var bool bPendingAdminLogin; +var float LoginPendingUntilTime; +var float NextRefreshRequestTime; + +function ClientSettings FindSettingsObject() { + local bbPlayer P; + local bbCHSpectator S; + + if (GetPlayerOwner().IsA('bbPlayer') && bbPlayer(GetPlayerOwner()).Settings != none) + return bbPlayer(GetPlayerOwner()).Settings; + + if (GetPlayerOwner().IsA('bbCHSpectator') && bbCHSpectator(GetPlayerOwner()).Settings != none) + return bbCHSpectator(GetPlayerOwner()).Settings; + + foreach GetPlayerOwner().AllActors(class'bbPlayer', P) + if (P.Settings != none) + return P.Settings; + + foreach GetPlayerOwner().AllActors(class'bbCHSpectator', S) + if (S.Settings != none) + return S.Settings; + + return none; +} + +function PlayerPawn ResolveOwnerPawn() { + return GetPlayerOwner(); +} + +function bbPlayer ResolveOwnerBBPlayer() { + return bbPlayer(ResolveOwnerPawn()); +} + +function ServerSettings FindServerSettingsObject() { + local bbPlayer P; + + P = ResolveOwnerBBPlayer(); + if (P == none) + return none; + + return P.IGPlus_GetServerSettingsObject(); +} + +function ResetLocalServerSettingsCache() { + local bbPlayer P; + + P = ResolveOwnerBBPlayer(); + if (P == none) + return; + + P.IGPlus_ServerSettingsInit(); +} + +function bool AreServerSettingsLoaded() { + local bbPlayer P; + + P = ResolveOwnerBBPlayer(); + if (P == none) + return false; + + return P.IGPlus_ServerSettingsMenuLoaded; +} + +function bool HasServerAdminAccess() { + local bbPlayer P; + + P = ResolveOwnerBBPlayer(); + if (P != none) + return P.IGPlus_ServerSettingsMenuCanEdit; + + return IsAdmin(); +} + +function float GetNowTimeSeconds() { + local LevelInfo L; + + L = GetLevel(); + if (L == none) + return 0; + + return L.TimeSeconds; +} + +function bool IsAdmin() { + local PlayerPawn P; + + P = ResolveOwnerPawn(); + if (P == none) + return false; + + if (P.bAdmin) + return true; + + if (P.PlayerReplicationInfo != none && P.PlayerReplicationInfo.bAdmin) + return true; + + return false; +} + +function RequestServerSettings(optional bool bForce) { + local bbPlayer P; + local float NowTime; + + NowTime = GetNowTimeSeconds(); + if (bForce == false && NowTime < NextRefreshRequestTime) + return; + + P = ResolveOwnerBBPlayer(); + if (P == none) + return; + + P.IGPlus_ServerRequestSettings(); + NextRefreshRequestTime = NowTime + 1.0; +} + +function SetStatusText(string Text) { + Lbl_Status.SetText(Text); +} + +function UpdateStatusText() { + if (HasServerAdminAccess() == false) { + if (bPendingAdminLogin && GetNowTimeSeconds() <= LoginPendingUntilTime) + SetStatusText(StatusLoginPendingText); + else + SetStatusText(StatusLoginText); + } else if (AreServerSettingsLoaded() == false) + SetStatusText(StatusLoadingText); + else + SetStatusText(StatusAdminText); +} + +function UpdateAuthButton() { + if (Btn_AdminAuth == none) + return; + + if (HasServerAdminAccess()) { + Btn_AdminAuth.SetText(LogoutButtonText); + Btn_AdminAuth.SetHelpText(LogoutButtonHelp); + } else { + Btn_AdminAuth.SetText(LoginButtonText); + Btn_AdminAuth.SetHelpText(LoginButtonHelp); + } +} + +function string BoolToString(bool B) { + if (B) + return "True"; + return "False"; +} + +function SendMutateCommand(string Cmd) { + local PlayerPawn P; + + P = ResolveOwnerPawn(); + if (P == none) + return; + + P.Mutate(Cmd); +} + +function SendServerSetting(string Key, string Value, optional bool bSendFeedback) { + if (bSendFeedback) + SendMutateCommand("IGPlusServerSet "$Key$" "$Value); + else + SendMutateCommand("IGPlusServerSetSilent "$Key$" "$Value); +} + +function bool AreServerSettingValuesEqual(string ValueA, string ValueB) { + return Caps(class'StringUtils'.static.Trim(ValueA)) == Caps(class'StringUtils'.static.Trim(ValueB)); +} + +function bool SaveServerSettingIfChanged(ServerSettings S, string Key, string Value) { + local string CurrentValue; + + if (S != none) + CurrentValue = S.GetPropertyText(Key); + + if (S == none || AreServerSettingValuesEqual(CurrentValue, Value) == false) { + // Use the non-silent command only for changed values so admins get a concise confirmation. + SendServerSetting(Key, Value, true); + if (S != none) + S.SetPropertyText(Key, Value); + return true; + } + + return false; +} + +function string HitFeedbackModeIndexToValue(int Index) { + switch (Clamp(Index, 0, 2)) { + case 0: return "HFM_Disabled"; + case 1: return "HFM_VisibleOnly"; + case 2: return "HFM_Always"; + } + + return "HFM_Disabled"; +} + +function PopulatePasswordHistory() { + local ClientSettings Settings; + local int i; + local string FirstPassword; + + if (Cmb_AdminPassword == none) + return; + + Settings = FindSettingsObject(); + Cmb_AdminPassword.Clear(); + if (Settings == none) + return; + + for (i = 0; i < arraycount(Settings.ServerAdminPasswords); i++) { + if (Settings.ServerAdminPasswords[i] != "") { + Cmb_AdminPassword.AddItem(Settings.ServerAdminPasswords[i]); + if (FirstPassword == "") + FirstPassword = Settings.ServerAdminPasswords[i]; + } + } + + if (FirstPassword != "") + Cmb_AdminPassword.SetValue(FirstPassword); + else + Cmb_AdminPassword.SetValue(""); +} + +function SavePasswordHistory(string Password) { + local ClientSettings Settings; + local int i; + local int FoundIndex; + + if (Password == "") + return; + + Settings = FindSettingsObject(); + if (Settings == none) + return; + + FoundIndex = -1; + for (i = 0; i < arraycount(Settings.ServerAdminPasswords); i++) { + if (Settings.ServerAdminPasswords[i] == Password) { + FoundIndex = i; + break; + } + } + + if (FoundIndex >= 0) { + for (i = FoundIndex + 1; i < arraycount(Settings.ServerAdminPasswords); i++) + Settings.ServerAdminPasswords[i - 1] = Settings.ServerAdminPasswords[i]; + } + + for (i = arraycount(Settings.ServerAdminPasswords) - 1; i > 0; i--) + Settings.ServerAdminPasswords[i] = Settings.ServerAdminPasswords[i - 1]; + + Settings.ServerAdminPasswords[0] = Password; + Settings.SaveConfig(); + + PopulatePasswordHistory(); + if (Cmb_AdminPassword != none) + Cmb_AdminPassword.SetValue(Password); +} + +function DeletePasswordFromHistory(string Password) { + local ClientSettings Settings; + local int i; + local int WriteIndex; + local string CurrentPassword; + + if (Password == "") + return; + + Settings = FindSettingsObject(); + if (Settings == none) + return; + + WriteIndex = 0; + for (i = 0; i < arraycount(Settings.ServerAdminPasswords); i++) { + CurrentPassword = Settings.ServerAdminPasswords[i]; + if (CurrentPassword != "" && CurrentPassword != Password) { + Settings.ServerAdminPasswords[WriteIndex] = CurrentPassword; + WriteIndex++; + } + } + + for (i = WriteIndex; i < arraycount(Settings.ServerAdminPasswords); i++) + Settings.ServerAdminPasswords[i] = ""; + + Settings.SaveConfig(); + PopulatePasswordHistory(); +} + +function DeletePasswordFromUI() { + local string Password; + + if (Cmb_AdminPassword == none) + return; + + Password = class'StringUtils'.static.Trim(Cmb_AdminPassword.GetValue()); + DeletePasswordFromHistory(Password); +} + +function SaveServerSettings() { + local ServerSettings S; + + S = FindServerSettingsObject(); + + SaveServerSettingIfChanged(S, "bAutoPause", BoolToString(Chk_bAutoPause.bChecked)); + SaveServerSettingIfChanged(S, "PauseTotalTime", Edit_PauseTotalTime.GetValue()); + SaveServerSettingIfChanged(S, "PauseTime", Edit_PauseTime.GetValue()); + SaveServerSettingIfChanged(S, "bForceDemo", BoolToString(Chk_bForceDemo.bChecked)); + SaveServerSettingIfChanged(S, "bRestrictTrading", BoolToString(Chk_bRestrictTrading.bChecked)); + SaveServerSettingIfChanged(S, "MaxTradeTimeMargin", Edit_MaxTradeTimeMargin.GetValue()); + SaveServerSettingIfChanged(S, "TradePingMargin", Edit_TradePingMargin.GetValue()); + SaveServerSettingIfChanged(S, "KillCamDelay", Edit_KillCamDelay.GetValue()); + SaveServerSettingIfChanged(S, "KillCamDuration", Edit_KillCamDuration.GetValue()); + SaveServerSettingIfChanged(S, "BrightskinMode", string(Cmb_BrightskinMode.GetSelectedIndex())); + SaveServerSettingIfChanged(S, "PlayerScale", Edit_PlayerScale.GetValue()); + SaveServerSettingIfChanged(S, "bAlwaysRenderFlagCarrier", BoolToString(Chk_bAlwaysRenderFlagCarrier.bChecked)); + SaveServerSettingIfChanged(S, "bAlwaysRenderDroppedFlags", BoolToString(Chk_bAlwaysRenderDroppedFlags.bChecked)); + SaveServerSettingIfChanged(S, "HitFeedbackMode", HitFeedbackModeIndexToValue(Cmb_HitFeedbackMode.GetSelectedIndex())); + SaveServerSettingIfChanged(S, "bEnablePingCompensatedSpawn", BoolToString(Chk_bEnablePingCompensatedSpawn.bChecked)); + + SaveServerSettingIfChanged(S, "bJumpingPreservesMomentum", BoolToString(Chk_bJumpingPreservesMomentum.bChecked)); + SaveServerSettingIfChanged(S, "bOldLandingMomentum", BoolToString(Chk_bOldLandingMomentum.bChecked)); + SaveServerSettingIfChanged(S, "bEnableSingleButtonDodge", BoolToString(Chk_bEnableSingleButtonDodge.bChecked)); + SaveServerSettingIfChanged(S, "bUseFlipAnimation", BoolToString(Chk_bUseFlipAnimation.bChecked)); + SaveServerSettingIfChanged(S, "bEnableWallDodging", BoolToString(Chk_bEnableWallDodging.bChecked)); + SaveServerSettingIfChanged(S, "bDodgePreserveZMomentum", BoolToString(Chk_bDodgePreserveZMomentum.bChecked)); + SaveServerSettingIfChanged(S, "MaxMultiDodges", Edit_MaxMultiDodges.GetValue()); + + SaveServerSettingIfChanged(S, "MaxPosError", Edit_MaxPosError.GetValue()); + SaveServerSettingIfChanged(S, "MaxHitError", Edit_MaxHitError.GetValue()); + SaveServerSettingIfChanged(S, "FireTimeout", Edit_FireTimeout.GetValue()); + SaveServerSettingIfChanged(S, "MinNetUpdateRate", Edit_MinNetUpdateRate.GetValue()); + SaveServerSettingIfChanged(S, "MaxNetUpdateRate", Edit_MaxNetUpdateRate.GetValue()); + SaveServerSettingIfChanged(S, "bEnableServerExtrapolation", BoolToString(Chk_bEnableServerExtrapolation.bChecked)); + SaveServerSettingIfChanged(S, "bEnableServerPacketReordering", BoolToString(Chk_bEnableServerPacketReordering.bChecked)); + SaveServerSettingIfChanged(S, "bPlayersAlwaysRelevant", BoolToString(Chk_bPlayersAlwaysRelevant.bChecked)); + SaveServerSettingIfChanged(S, "bEnableJitterBounding", BoolToString(Chk_bEnableJitterBounding.bChecked)); + SaveServerSettingIfChanged(S, "MaxJitterTime", Edit_MaxJitterTime.GetValue()); + SaveServerSettingIfChanged(S, "bEnableInputReplication", BoolToString(Chk_bEnableInputReplication.bChecked)); + SaveServerSettingIfChanged(S, "bEnableSnapshotInterpolation", BoolToString(Chk_bEnableSnapshotInterpolation.bChecked)); + SaveServerSettingIfChanged(S, "SnapshotInterpSendHz", Edit_SnapshotInterpSendHz.GetValue()); + SaveServerSettingIfChanged(S, "SnapshotInterpRewindMs", Edit_SnapshotInterpRewindMs.GetValue()); + SaveServerSettingIfChanged(S, "bEnableLoosePositionCheck", BoolToString(Chk_bEnableLoosePositionCheck.bChecked)); + SaveServerSettingIfChanged(S, "LooseCheckCorrectionFactor", Edit_LooseCheckCorrectionFactor.GetValue()); + SaveServerSettingIfChanged(S, "LooseCheckCorrectionFactorOnMover", Edit_LooseCheckCorrectionFactorOnMover.GetValue()); + SaveServerSettingIfChanged(S, "bEnableWarpFix", BoolToString(Chk_bEnableWarpFix.bChecked)); + SaveServerSettingIfChanged(S, "WarpFixDelay", Edit_WarpFixDelay.GetValue()); + + SaveServerSettingIfChanged(S, "ShowTouchedPackage", BoolToString(Chk_ShowTouchedPackage.bChecked)); + SaveServerSettingIfChanged(S, "bEnableDamageDebugMode", BoolToString(Chk_bEnableDamageDebugMode.bChecked)); + SaveServerSettingIfChanged(S, "bEnableDamageDebugConsoleMessages", BoolToString(Chk_bEnableDamageDebugConsoleMessages.bChecked)); + SaveServerSettingIfChanged(S, "bEnableHitboxDebugMode", BoolToString(Chk_bEnableHitboxDebugMode.bChecked)); +} + +function AdminLoginFromUI() { + local string Password; + local PlayerPawn P; + + P = ResolveOwnerPawn(); + if (P == none) + return; + + Password = class'StringUtils'.static.Trim(Cmb_AdminPassword.GetValue()); + if (Password == "") { + SetStatusText(LoginRequiredText); + return; + } + + SavePasswordHistory(Password); + P.AdminLogin(Password); + bLoadSucceeded = false; + bPendingAdminLogin = true; + LoginPendingUntilTime = GetNowTimeSeconds() + 6.0; + ResetLocalServerSettingsCache(); + RequestServerSettings(true); +} + +function AdminLogoutFromUI() { + local PlayerPawn P; + + P = ResolveOwnerPawn(); + if (P == none) + return; + + P.AdminLogout(); + bLoadSucceeded = false; + bPendingAdminLogin = false; + ResetLocalServerSettingsCache(); + RequestServerSettings(true); +} + +function UWindowCheckbox CreateCheckbox(string T, optional string HT) { + local UWindowCheckbox Chk; + + Chk = UWindowCheckbox(CreateControl(class'IGPlus_Checkbox', PaddingX, ControlOffset, 200, 1)); + Chk.SetText(T); + Chk.SetHelpText(HT); + Chk.ToolTipString = HT; + Chk.SetFont(F_Normal); + Chk.Align = TA_Left; + ControlOffset += LineSpacing; + + return Chk; +} + +function IGPlus_EditControl CreateEdit( + EEditControlType ECT, + string T, + optional string HT, + optional int MaxLength, + optional float EditBoxWidth +) { + local IGPlus_EditControl Edit; + + Edit = IGPlus_EditControl(CreateControl(class'IGPlus_EditControl', PaddingX, ControlOffset, 200, 1)); + Edit.SetText(T); + Edit.SetHelpText(HT); + Edit.SetFont(F_Normal); + Edit.Align = TA_Left; + if (MaxLength > 0) + Edit.SetMaxLength(MaxLength); + + if (EditBoxWidth > 0) { + Edit.EditBoxWidthFraction = 0.5; + Edit.EditBoxMinWidth = EditBoxWidth; + Edit.EditBoxMaxWidth = EditBoxWidth; + } + + switch(ECT) { + case ECT_Text: + Edit.SetNumericOnly(false); + Edit.SetNumericFloat(false); + break; + case ECT_Integer: + Edit.SetNumericOnly(true); + Edit.SetNumericFloat(false); + break; + case ECT_Real: + Edit.SetNumericOnly(true); + Edit.SetNumericFloat(true); + break; + } + + ControlOffset += LineSpacing; + + return Edit; +} + +function UWindowLabelControl CreateLabel(string T, optional string HT) { + local UWindowLabelControl Lbl; + + Lbl = UWindowLabelControl(CreateControl(class'IGPlus_Label', PaddingX, ControlOffset, 200, 1)); + Lbl.SetText(T); + Lbl.SetHelpText(HT); + Lbl.SetFont(F_Normal); + Lbl.Align = TA_Left; + ControlOffset += LineSpacing; + + return Lbl; +} + +function UWindowLabelControl CreateSeparator(string T, optional string HT) { + local UWindowLabelControl Lbl; + + if (ControlOffset > PaddingY) + ControlOffset += (SeparatorSpacing - LineSpacing); + + Lbl = UWindowLabelControl(CreateControl(class'IGPlus_Separator', PaddingX, ControlOffset, 200, 1)); + Lbl.SetText(T); + Lbl.SetHelpText(HT); + Lbl.SetFont(F_Normal); + Lbl.Align = TA_Left; + ControlOffset += LineSpacing; + + return Lbl; +} + +function IGPlus_ComboBox CreateComboBox( + string T, + optional string HT, + optional bool bCanEdit, + optional float EditBoxWidth +) { + local IGPlus_ComboBox Cmb; + + Cmb = IGPlus_ComboBox(CreateControl(class'IGPlus_ComboBox', PaddingX, ControlOffset, 200, 1)); + Cmb.SetText(T); + Cmb.SetHelpText(HT); + Cmb.SetFont(F_Normal); + Cmb.Align = TA_Left; + Cmb.SetEditable(bCanEdit); + + if (EditBoxWidth > 0) { + Cmb.EditBoxWidthFraction = 0.5; + Cmb.EditBoxMinWidth = EditBoxWidth; + Cmb.EditBoxMaxWidth = EditBoxWidth; + } + + ControlOffset += LineSpacing; + + return Cmb; +} + +function IGPlus_Button CreateButton(string T, optional string HT) { + local IGPlus_Button Btn; + + Btn = IGPlus_Button(CreateControl(class'IGPlus_Button', PaddingX, ControlOffset, 200, 1)); + Btn.SetText(T); + Btn.SetHelpText(HT); + Btn.Align = TA_Left; + ControlOffset += LineSpacing; + + return Btn; +} + +function LayoutControl(UWindowDialogControl C, bool bVisible, float Width, out float Y) { + local float Height; + + if (C == none) + return; + + if (bVisible) { + if (C.bWindowVisible == false) + C.ShowWindow(); + C.WinLeft = PaddingX; + C.WinTop = Y; + Height = FMax(C.WinHeight, 1); + C.SetSize(Width, Height); + Y += LineSpacing; + return; + } + + if (C.bWindowVisible) + C.HideWindow(); +} + +function LayoutPasswordControls(Canvas C, bool bVisible, float Width, out float Y) { + local float Height; + local float Gap; + local float DeleteButtonWidth; + local float ComboWidth; + + if (Cmb_AdminPassword == none || Btn_DeletePassword == none) + return; + + if (bVisible) { + if (Cmb_AdminPassword.bWindowVisible == false) + Cmb_AdminPassword.ShowWindow(); + if (Btn_DeletePassword.bWindowVisible == false) + Btn_DeletePassword.ShowWindow(); + + Gap = 4; + DeleteButtonWidth = 20; + ComboWidth = FMax(Width - DeleteButtonWidth - Gap, 40); + ConfigurePasswordCombo(C, ComboWidth); + + Cmb_AdminPassword.WinLeft = PaddingX; + Cmb_AdminPassword.WinTop = Y; + Height = FMax(Cmb_AdminPassword.WinHeight, 1); + Cmb_AdminPassword.SetSize(ComboWidth, Height); + + Btn_DeletePassword.WinLeft = PaddingX + ComboWidth + Gap; + Btn_DeletePassword.WinTop = Y; + Height = FMax(Btn_DeletePassword.WinHeight, 1); + Btn_DeletePassword.SetSize(DeleteButtonWidth, Height); + + Y += LineSpacing; + return; + } + + if (Cmb_AdminPassword.bWindowVisible) + Cmb_AdminPassword.HideWindow(); + if (Btn_DeletePassword.bWindowVisible) + Btn_DeletePassword.HideWindow(); +} + +function ConfigurePasswordCombo(Canvas C, float ControlWidth) { + local float LabelWidth; + local float LabelHeight; + local float MaxWidth; + + if (Cmb_AdminPassword == none) + return; + + Cmb_AdminPassword.TextSize(C, Cmb_AdminPassword.Text, LabelWidth, LabelHeight); + + // Keep the editable area as wide as possible while preserving label visibility. + MaxWidth = FMax(ControlWidth - LabelWidth - 8, 40); + Cmb_AdminPassword.EditBoxMinWidth = FMin(70, MaxWidth); + Cmb_AdminPassword.EditBoxMaxWidth = MaxWidth; + Cmb_AdminPassword.EditBoxWidth = FClamp( + ControlWidth * Cmb_AdminPassword.EditBoxWidthFraction, + Cmb_AdminPassword.EditBoxMinWidth, + Cmb_AdminPassword.EditBoxMaxWidth + ); +} + +function float GetLabeledInputMaxWidth(UWindowDialogControl Control, Canvas C, float ControlWidth, optional float MinWidth) { + local float LabelWidth; + local float LabelHeight; + + if (Control == none) + return 0; + + if (MinWidth <= 0) + MinWidth = 40; + + Control.TextSize(C, Control.Text, LabelWidth, LabelHeight); + return FMax(ControlWidth - LabelWidth - 8, MinWidth); +} + +function ConfigureFixedWidthEdit(IGPlus_EditControl Edit, Canvas C, float ControlWidth, float PreferredWidth) { + local float MaxWidth; + + if (Edit == none) + return; + + MaxWidth = GetLabeledInputMaxWidth(Edit, C, ControlWidth, 40); + PreferredWidth = FClamp(PreferredWidth, 40, MaxWidth); + Edit.EditBoxMinWidth = PreferredWidth; + Edit.EditBoxMaxWidth = PreferredWidth; + Edit.EditBoxWidth = PreferredWidth; +} + +function ConfigureFixedWidthCombo(IGPlus_ComboBox Cmb, Canvas C, float ControlWidth, float PreferredWidth) { + local float MaxWidth; + + if (Cmb == none) + return; + + MaxWidth = GetLabeledInputMaxWidth(Cmb, C, ControlWidth, 40); + PreferredWidth = FClamp(PreferredWidth, 40, MaxWidth); + Cmb.EditBoxMinWidth = PreferredWidth; + Cmb.EditBoxMaxWidth = PreferredWidth; + Cmb.EditBoxWidth = PreferredWidth; +} + +function ConfigureResponsiveServerControls(Canvas C, float ControlWidth) { + ConfigureFixedWidthCombo(Cmb_BrightskinMode, C, ControlWidth, 180); + ConfigureFixedWidthCombo(Cmb_HitFeedbackMode, C, ControlWidth, 180); + + ConfigureFixedWidthEdit(Edit_PauseTotalTime, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_PauseTime, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_MaxTradeTimeMargin, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_TradePingMargin, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_KillCamDelay, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_KillCamDuration, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_PlayerScale, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_MaxMultiDodges, C, ControlWidth, 80); + + ConfigureFixedWidthEdit(Edit_MaxPosError, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_MaxHitError, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_FireTimeout, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_MinNetUpdateRate, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_MaxNetUpdateRate, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_MaxJitterTime, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_SnapshotInterpSendHz, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_SnapshotInterpRewindMs, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_LooseCheckCorrectionFactor, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_LooseCheckCorrectionFactorOnMover, C, ControlWidth, 80); + ConfigureFixedWidthEdit(Edit_WarpFixDelay, C, ControlWidth, 80); +} + +function LoadServerSettings() { + local ServerSettings S; + + S = FindServerSettingsObject(); + if (S == none) + return; + + Chk_bAutoPause.bChecked = S.bAutoPause; + Edit_PauseTotalTime.SetValue(string(S.PauseTotalTime)); + Edit_PauseTime.SetValue(string(S.PauseTime)); + Chk_bForceDemo.bChecked = S.bForceDemo; + Chk_bRestrictTrading.bChecked = S.bRestrictTrading; + Edit_MaxTradeTimeMargin.SetValue(string(S.MaxTradeTimeMargin)); + Edit_TradePingMargin.SetValue(string(S.TradePingMargin)); + Edit_KillCamDelay.SetValue(string(S.KillCamDelay)); + Edit_KillCamDuration.SetValue(string(S.KillCamDuration)); + Cmb_BrightskinMode.SetSelectedIndex(Clamp(int(S.BrightskinMode), 0, 1)); + Edit_PlayerScale.SetValue(string(S.PlayerScale)); + Chk_bAlwaysRenderFlagCarrier.bChecked = S.bAlwaysRenderFlagCarrier; + Chk_bAlwaysRenderDroppedFlags.bChecked = S.bAlwaysRenderDroppedFlags; + Cmb_HitFeedbackMode.SetSelectedIndex(Clamp(int(S.HitFeedbackMode), 0, 2)); + + Chk_bJumpingPreservesMomentum.bChecked = S.bJumpingPreservesMomentum; + Chk_bOldLandingMomentum.bChecked = S.bOldLandingMomentum; + Chk_bEnableSingleButtonDodge.bChecked = S.bEnableSingleButtonDodge; + Chk_bUseFlipAnimation.bChecked = S.bUseFlipAnimation; + Chk_bEnableWallDodging.bChecked = S.bEnableWallDodging; + Chk_bDodgePreserveZMomentum.bChecked = S.bDodgePreserveZMomentum; + Edit_MaxMultiDodges.SetValue(string(S.MaxMultiDodges)); + Chk_bEnablePingCompensatedSpawn.bChecked = S.bEnablePingCompensatedSpawn; + + Edit_MaxPosError.SetValue(string(S.MaxPosError)); + Edit_MaxHitError.SetValue(string(S.MaxHitError)); + Edit_MaxJitterTime.SetValue(string(S.MaxJitterTime)); + Edit_WarpFixDelay.SetValue(string(S.WarpFixDelay)); + Edit_FireTimeout.SetValue(string(S.FireTimeout)); + Edit_MinNetUpdateRate.SetValue(string(S.MinNetUpdateRate)); + Edit_MaxNetUpdateRate.SetValue(string(S.MaxNetUpdateRate)); + Chk_bEnableInputReplication.bChecked = S.bEnableInputReplication; + Chk_bEnableServerExtrapolation.bChecked = S.bEnableServerExtrapolation; + Chk_bEnableServerPacketReordering.bChecked = S.bEnableServerPacketReordering; + Chk_bEnableLoosePositionCheck.bChecked = S.bEnableLoosePositionCheck; + Chk_bPlayersAlwaysRelevant.bChecked = S.bPlayersAlwaysRelevant; + Chk_bEnableJitterBounding.bChecked = S.bEnableJitterBounding; + Edit_LooseCheckCorrectionFactor.SetValue(string(S.LooseCheckCorrectionFactor)); + Edit_LooseCheckCorrectionFactorOnMover.SetValue(string(S.LooseCheckCorrectionFactorOnMover)); + Chk_bEnableSnapshotInterpolation.bChecked = S.bEnableSnapshotInterpolation; + Edit_SnapshotInterpSendHz.SetValue(string(S.SnapshotInterpSendHz)); + Edit_SnapshotInterpRewindMs.SetValue(string(S.SnapshotInterpRewindMs)); + Chk_bEnableWarpFix.bChecked = S.bEnableWarpFix; + + Chk_ShowTouchedPackage.bChecked = S.ShowTouchedPackage; + Chk_bEnableDamageDebugMode.bChecked = S.bEnableDamageDebugMode; + Chk_bEnableDamageDebugConsoleMessages.bChecked = S.bEnableDamageDebugConsoleMessages; + Chk_bEnableHitboxDebugMode.bChecked = S.bEnableHitboxDebugMode; + + bLoadSucceeded = true; +} + +function Created() { + super.Created(); + + ControlOffset = PaddingY; + + Lbl_Header = CreateLabel(HeaderText); + Lbl_Header.Align = TA_Center; + Lbl_Header.SetFont(F_Bold); + + Lbl_Status = CreateLabel(""); + Lbl_Status.Align = TA_Center; + + Lbl_MoreInformation = CreateLabel(MoreInformationText); + Lbl_MoreInformation.Align = TA_Center; + Lbl_MoreInformation.SetFont(F_Bold); + + Lbl_Login = CreateSeparator(LoginText); + Cmb_AdminPassword = CreateComboBox(AdminPasswordText, AdminPasswordHelp, true); + Cmb_AdminPassword.SetMaxLength(64); + Cmb_AdminPassword.SetNumericOnly(false); + Cmb_AdminPassword.EditBoxWidthFraction = 0.85; + Cmb_AdminPassword.EditBoxMinWidth = 70; + Cmb_AdminPassword.EditBoxMaxWidth = 65535; + Btn_DeletePassword = CreateButton(DeletePasswordButtonText, DeletePasswordButtonHelp); + ControlOffset -= LineSpacing; + Btn_AdminAuth = CreateButton(LoginButtonText, LoginButtonHelp); + + Lbl_General = CreateSeparator(GeneralText); + Chk_bAutoPause = CreateCheckbox(bAutoPauseText, bAutoPauseHelp); + Edit_PauseTotalTime = CreateEdit(ECT_Integer, PauseTotalTimeText, PauseTotalTimeHelp, 6, 80); + Edit_PauseTime = CreateEdit(ECT_Integer, PauseTimeText, PauseTimeHelp, 6, 80); + Chk_bForceDemo = CreateCheckbox(bForceDemoText, bForceDemoHelp); + Chk_bRestrictTrading = CreateCheckbox(bRestrictTradingText, bRestrictTradingHelp); + Edit_MaxTradeTimeMargin = CreateEdit(ECT_Real, MaxTradeTimeMarginText, MaxTradeTimeMarginHelp, 16, 80); + Edit_TradePingMargin = CreateEdit(ECT_Real, TradePingMarginText, TradePingMarginHelp, 16, 80); + Edit_KillCamDelay = CreateEdit(ECT_Real, KillCamDelayText, KillCamDelayHelp, 16, 80); + Edit_KillCamDuration = CreateEdit(ECT_Real, KillCamDurationText, KillCamDurationHelp, 16, 80); + Cmb_BrightskinMode = CreateComboBox(BrightskinModeText, BrightskinModeHelp, false, 180); + Cmb_BrightskinMode.AddItem(BrightskinModeDisabled); + Cmb_BrightskinMode.AddItem(BrightskinModeUnlit); + Edit_PlayerScale = CreateEdit(ECT_Real, PlayerScaleText, PlayerScaleHelp, 16, 80); + Chk_bAlwaysRenderFlagCarrier = CreateCheckbox(bAlwaysRenderFlagCarrierText, bAlwaysRenderFlagCarrierHelp); + Chk_bAlwaysRenderDroppedFlags = CreateCheckbox(bAlwaysRenderDroppedFlagsText, bAlwaysRenderDroppedFlagsHelp); + Cmb_HitFeedbackMode = CreateComboBox(HitFeedbackModeText, HitFeedbackModeHelp, false, 180); + Cmb_HitFeedbackMode.AddItem(HitFeedbackModeDisabled); + Cmb_HitFeedbackMode.AddItem(HitFeedbackModeVisibleOnly); + Cmb_HitFeedbackMode.AddItem(HitFeedbackModeAlways); + Chk_bEnablePingCompensatedSpawn = CreateCheckbox(bEnablePingCompensatedSpawnText, bEnablePingCompensatedSpawnHelp); + + Lbl_Movement = CreateSeparator(MovementText); + Chk_bJumpingPreservesMomentum = CreateCheckbox(bJumpingPreservesMomentumText, bJumpingPreservesMomentumHelp); + Chk_bOldLandingMomentum = CreateCheckbox(bOldLandingMomentumText, bOldLandingMomentumHelp); + Chk_bEnableSingleButtonDodge = CreateCheckbox(bEnableSingleButtonDodgeText, bEnableSingleButtonDodgeHelp); + Chk_bUseFlipAnimation = CreateCheckbox(bUseFlipAnimationText, bUseFlipAnimationHelp); + Chk_bEnableWallDodging = CreateCheckbox(bEnableWallDodgingText, bEnableWallDodgingHelp); + Chk_bDodgePreserveZMomentum = CreateCheckbox(bDodgePreserveZMomentumText, bDodgePreserveZMomentumHelp); + Edit_MaxMultiDodges = CreateEdit(ECT_Integer, MaxMultiDodgesText, MaxMultiDodgesHelp, 6, 80); + + Lbl_Networking = CreateSeparator(NetworkingText); + Edit_MaxPosError = CreateEdit(ECT_Integer, MaxPosErrorText, MaxPosErrorHelp, 8, 80); + Edit_MaxHitError = CreateEdit(ECT_Integer, MaxHitErrorText, MaxHitErrorHelp, 8, 80); + Edit_FireTimeout = CreateEdit(ECT_Real, FireTimeoutText, FireTimeoutHelp, 16, 80); + Edit_MinNetUpdateRate = CreateEdit(ECT_Real, MinNetUpdateRateText, MinNetUpdateRateHelp, 16, 80); + Edit_MaxNetUpdateRate = CreateEdit(ECT_Real, MaxNetUpdateRateText, MaxNetUpdateRateHelp, 16, 80); + Chk_bEnableServerExtrapolation = CreateCheckbox(bEnableServerExtrapolationText, bEnableServerExtrapolationHelp); + Chk_bEnableServerPacketReordering = CreateCheckbox(bEnableServerPacketReorderingText, bEnableServerPacketReorderingHelp); + Chk_bPlayersAlwaysRelevant = CreateCheckbox(bPlayersAlwaysRelevantText, bPlayersAlwaysRelevantHelp); + Chk_bEnableJitterBounding = CreateCheckbox(bEnableJitterBoundingText, bEnableJitterBoundingHelp); + Edit_MaxJitterTime = CreateEdit(ECT_Real, MaxJitterTimeText, MaxJitterTimeHelp, 16, 80); + Chk_bEnableInputReplication = CreateCheckbox(bEnableInputReplicationText, bEnableInputReplicationHelp); + Chk_bEnableSnapshotInterpolation = CreateCheckbox(bEnableSnapshotInterpolationText, bEnableSnapshotInterpolationHelp); + Edit_SnapshotInterpSendHz = CreateEdit(ECT_Real, SnapshotInterpSendHzText, SnapshotInterpSendHzHelp, 16, 80); + Edit_SnapshotInterpRewindMs = CreateEdit(ECT_Real, SnapshotInterpRewindMsText, SnapshotInterpRewindMsHelp, 16, 80); + Chk_bEnableLoosePositionCheck = CreateCheckbox(bEnableLoosePositionCheckText, bEnableLoosePositionCheckHelp); + Edit_LooseCheckCorrectionFactor = CreateEdit(ECT_Real, LooseCheckCorrectionFactorText, LooseCheckCorrectionFactorHelp, 16, 80); + Edit_LooseCheckCorrectionFactorOnMover = CreateEdit(ECT_Real, LooseCheckCorrectionFactorOnMoverText, LooseCheckCorrectionFactorOnMoverHelp, 16, 80); + Chk_bEnableWarpFix = CreateCheckbox(bEnableWarpFixText, bEnableWarpFixHelp); + Edit_WarpFixDelay = CreateEdit(ECT_Real, WarpFixDelayText, WarpFixDelayHelp, 16, 80); + + Lbl_Debug = CreateSeparator(DebugText); + Chk_ShowTouchedPackage = CreateCheckbox(ShowTouchedPackageText, ShowTouchedPackageHelp); + Chk_bEnableDamageDebugMode = CreateCheckbox(bEnableDamageDebugModeText, bEnableDamageDebugModeHelp); + Chk_bEnableDamageDebugConsoleMessages = CreateCheckbox(bEnableDamageDebugConsoleMessagesText, bEnableDamageDebugConsoleMessagesHelp); + Chk_bEnableHitboxDebugMode = CreateCheckbox(bEnableHitboxDebugModeText, bEnableHitboxDebugModeHelp); + + ControlOffset += PaddingY - 4; + + Load(); +} + +function AfterCreate() { + super.AfterCreate(); + + DesiredWidth = 180; + DesiredHeight = ControlOffset; +} + +function BeforePaint(Canvas C, float X, float Y) { + local float WndWidth; + local float Top; + local bool bAdmin; + local bool bShowSettings; + + super.BeforePaint(C, X, Y); + + if (bPendingAdminLogin && GetNowTimeSeconds() > LoginPendingUntilTime) + bPendingAdminLogin = false; + + if (bPendingAdminLogin || AreServerSettingsLoaded() == false) + RequestServerSettings(); + + bAdmin = HasServerAdminAccess(); + if (bAdmin) + bPendingAdminLogin = false; + if (bLastAdminState != bAdmin) { + bLastAdminState = bAdmin; + bLoadSucceeded = false; + } + + if (bAdmin && AreServerSettingsLoaded() && bLoadSucceeded == false) + LoadServerSettings(); + + UpdateStatusText(); + UpdateAuthButton(); + if (Btn_DeletePassword != none && Cmb_AdminPassword != none) + Btn_DeletePassword.bDisabled = (class'StringUtils'.static.Trim(Cmb_AdminPassword.GetValue()) == ""); + + WndWidth = WinWidth - 2*PaddingX; + ConfigureResponsiveServerControls(C, WndWidth); + Top = PaddingY; + bShowSettings = bAdmin && AreServerSettingsLoaded(); + + LayoutControl(Lbl_Header, true, WndWidth, Top); + LayoutControl(Lbl_Status, true, WndWidth, Top); + LayoutControl(Lbl_MoreInformation, bAdmin, WndWidth, Top); + LayoutControl(Btn_AdminAuth, true, WndWidth, Top); + + LayoutControl(Lbl_Login, !bAdmin, WndWidth, Top); + LayoutPasswordControls(C, !bAdmin, WndWidth, Top); + + LayoutControl(Lbl_General, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bAutoPause, bShowSettings, WndWidth, Top); + LayoutControl(Edit_PauseTotalTime, bShowSettings, WndWidth, Top); + LayoutControl(Edit_PauseTime, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bForceDemo, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bRestrictTrading, bShowSettings, WndWidth, Top); + LayoutControl(Edit_MaxTradeTimeMargin, bShowSettings, WndWidth, Top); + LayoutControl(Edit_TradePingMargin, bShowSettings, WndWidth, Top); + LayoutControl(Edit_KillCamDelay, bShowSettings, WndWidth, Top); + LayoutControl(Edit_KillCamDuration, bShowSettings, WndWidth, Top); + LayoutControl(Cmb_BrightskinMode, bShowSettings, WndWidth, Top); + LayoutControl(Edit_PlayerScale, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bAlwaysRenderFlagCarrier, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bAlwaysRenderDroppedFlags, bShowSettings, WndWidth, Top); + LayoutControl(Cmb_HitFeedbackMode, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnablePingCompensatedSpawn, bShowSettings, WndWidth, Top); + + LayoutControl(Lbl_Movement, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bJumpingPreservesMomentum, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bOldLandingMomentum, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableSingleButtonDodge, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bUseFlipAnimation, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableWallDodging, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bDodgePreserveZMomentum, bShowSettings, WndWidth, Top); + LayoutControl(Edit_MaxMultiDodges, bShowSettings, WndWidth, Top); + + LayoutControl(Lbl_Networking, bShowSettings, WndWidth, Top); + LayoutControl(Edit_MaxPosError, bShowSettings, WndWidth, Top); + LayoutControl(Edit_MaxHitError, bShowSettings, WndWidth, Top); + LayoutControl(Edit_FireTimeout, bShowSettings, WndWidth, Top); + LayoutControl(Edit_MinNetUpdateRate, bShowSettings, WndWidth, Top); + LayoutControl(Edit_MaxNetUpdateRate, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableServerExtrapolation, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableServerPacketReordering, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bPlayersAlwaysRelevant, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableJitterBounding, bShowSettings, WndWidth, Top); + LayoutControl(Edit_MaxJitterTime, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableInputReplication, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableSnapshotInterpolation, bShowSettings, WndWidth, Top); + LayoutControl(Edit_SnapshotInterpSendHz, bShowSettings, WndWidth, Top); + LayoutControl(Edit_SnapshotInterpRewindMs, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableLoosePositionCheck, bShowSettings, WndWidth, Top); + LayoutControl(Edit_LooseCheckCorrectionFactor, bShowSettings, WndWidth, Top); + LayoutControl(Edit_LooseCheckCorrectionFactorOnMover, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableWarpFix, bShowSettings, WndWidth, Top); + LayoutControl(Edit_WarpFixDelay, bShowSettings, WndWidth, Top); + + LayoutControl(Lbl_Debug, bShowSettings, WndWidth, Top); + LayoutControl(Chk_ShowTouchedPackage, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableDamageDebugMode, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableDamageDebugConsoleMessages, bShowSettings, WndWidth, Top); + LayoutControl(Chk_bEnableHitboxDebugMode, bShowSettings, WndWidth, Top); + + DesiredHeight = Top + PaddingY; +} + +function Notify(UWindowDialogControl C, byte E) { + super.Notify(C, E); + + if (E == DE_Click && C == Btn_AdminAuth) { + if (HasServerAdminAccess()) + AdminLogoutFromUI(); + else + AdminLoginFromUI(); + } else if (E == DE_Click && C == Btn_DeletePassword) { + DeletePasswordFromUI(); + } +} + +function Load() { + bLastAdminState = HasServerAdminAccess(); + bLoadSucceeded = false; + bPendingAdminLogin = false; + ResetLocalServerSettingsCache(); + RequestServerSettings(true); + PopulatePasswordHistory(); + UpdateStatusText(); + UpdateAuthButton(); +} + +function Save() { + if (HasServerAdminAccess() == false) { + bPendingAdminLogin = false; + SetStatusText(StatusLoginText); + return; + } + + if (AreServerSettingsLoaded() == false) { + ResetLocalServerSettingsCache(); + RequestServerSettings(true); + return; + } + + SaveServerSettings(); + bPendingAdminLogin = false; + SetStatusText(StatusAdminText); +} + +function SaveConfigs() { + local ClientSettings Settings; + local UWindowWindow Dialog; + + super.SaveConfigs(); + + Settings = FindSettingsObject(); + if (Settings == none) + return; + + Dialog = GetParent(class'UWindowFramedWindow'); + if (Dialog == none) + return; + + Settings.MenuX = Dialog.WinLeft; + Settings.MenuY = Dialog.WinTop; + Settings.MenuWidth = Dialog.WinWidth; + Settings.MenuHeight = Dialog.WinHeight; + Settings.SaveConfig(); +} + +defaultproperties +{ + HeaderText="Server Settings" + StatusAdminText="Admin access granted. Edit settings and click Save." + StatusLoginText="Admin access required. Enter password and click Login." + StatusLoadingText="Loading server settings..." + StatusLoginPendingText="Login requested..." + MoreInformationText="Right-click on settings to get more information" + + LoginText="Admin Login" + AdminPasswordText="Password" + AdminPasswordHelp="Enter password" + DeletePasswordButtonText="X" + DeletePasswordButtonHelp="Delete this password from saved history" + LoginButtonText="Login" + LoginButtonHelp="Log in as admin" + LogoutButtonText="Logout" + LogoutButtonHelp="Log out of admin account" + LoginRequiredText="Please enter admin password first." + + GeneralText="General" + bAutoPauseText="Enable Auto Pause" + bAutoPauseHelp="If checked, teams can use match pause functionality in tournament mode" + PauseTotalTimeText="Total Pause Time" + PauseTotalTimeHelp="Total pause time in seconds available to each team" + PauseTimeText="Pause Time" + PauseTimeHelp="Maximum duration in seconds of a single pause" + bForceDemoText="Force Demo Recording" + bForceDemoHelp="If checked, players are forced to record demos" + bRestrictTradingText="Restrict Trading" + bRestrictTradingHelp="If checked, trade kills are only accepted within limited timing margins" + MaxTradeTimeMarginText="Max Trade Time Margin" + MaxTradeTimeMarginHelp="Maximum allowed trade window in seconds" + TradePingMarginText="Trade Ping Margin" + TradePingMarginHelp="Additional trade allowance scaled by player ping" + KillCamDelayText="Kill Cam Delay" + KillCamDelayHelp="Delay in seconds before kill cam starts" + KillCamDurationText="Kill Cam Duration" + KillCamDurationHelp="How long kill cam runs in seconds" + BrightskinModeText="Brightskin Mode" + BrightskinModeHelp="Select how player skins are rendered on clients" + BrightskinModeDisabled="Disabled" + BrightskinModeUnlit="Unlit" + PlayerScaleText="Player Scale" + PlayerScaleHelp="Scale multiplier applied to player models" + bAlwaysRenderFlagCarrierText="Always Render Flag Carrier" + bAlwaysRenderFlagCarrierHelp="If checked, flag carriers are always rendered" + bAlwaysRenderDroppedFlagsText="Always Render Dropped Flags" + bAlwaysRenderDroppedFlagsHelp="If checked, dropped flags are always rendered" + HitFeedbackModeText="Hit Feedback Mode" + HitFeedbackModeHelp="Controls when hit feedback events are allowed" + HitFeedbackModeDisabled="Disabled" + HitFeedbackModeVisibleOnly="Visible Only" + HitFeedbackModeAlways="Always" + bEnablePingCompensatedSpawnHelp="If checked, enables ping-compensated spawn behavior" + + MovementText="Movement" + bJumpingPreservesMomentumText="Jumping Preserves Momentum" + bJumpingPreservesMomentumHelp="If checked, jumping keeps horizontal momentum more aggressively" + bOldLandingMomentumText="Old Landing Momentum" + bOldLandingMomentumHelp="If checked, uses legacy landing momentum handling" + bEnableSingleButtonDodgeText="Enable Single Button Dodge" + bEnableSingleButtonDodgeHelp="If checked, allows dodging from single key press input" + bUseFlipAnimationText="Use Flip Animation" + bUseFlipAnimationHelp="If checked, uses flip animation for dodge-style movement" + bEnableWallDodgingText="Enable Wall Dodging" + bEnableWallDodgingHelp="If checked, allows players to wall dodge" + bDodgePreserveZMomentumText="Preserve Dodge Z Momentum" + bDodgePreserveZMomentumHelp="If checked, dodge keeps more vertical momentum" + MaxMultiDodgesText="Max Multi Dodges" + MaxMultiDodgesHelp="Maximum allowed chained dodges before touching ground" + bEnablePingCompensatedSpawnText="Enable Ping Compensated Spawn" + + NetworkingText="Networking" + MaxPosErrorText="Max Position Error" + MaxPosErrorHelp="Maximum position error before server correction is forced" + MaxHitErrorText="Max Hit Error" + MaxHitErrorHelp="Maximum tolerated hit registration error" + MaxJitterTimeText="Max Jitter Time" + MaxJitterTimeHelp="Maximum jitter time in seconds used for movement correction" + WarpFixDelayText="Warp Fix Delay" + WarpFixDelayHelp="Delay in seconds before warp-fix smoothing is applied" + FireTimeoutText="Fire Timeout" + FireTimeoutHelp="Time in seconds before queued fire input expires" + MinNetUpdateRateText="Min Net Update Rate" + MinNetUpdateRateHelp="Minimum per-client update rate the server allows" + MaxNetUpdateRateText="Max Net Update Rate" + MaxNetUpdateRateHelp="Maximum per-client update rate the server allows" + bEnableInputReplicationText="Enable Input Replication" + bEnableInputReplicationHelp="If checked, replicates movement input data for validation" + bEnableServerExtrapolationText="Enable Server Extrapolation" + bEnableServerExtrapolationHelp="If checked, server extrapolates movement between updates" + bEnableServerPacketReorderingText="Enable Packet Reordering" + bEnableServerPacketReorderingHelp="If checked, server tries to reorder delayed movement packets" + bEnableLoosePositionCheckText="Enable Loose Position Check" + bEnableLoosePositionCheckHelp="If checked, uses looser position validation rules" + bPlayersAlwaysRelevantText="Players Always Relevant" + bPlayersAlwaysRelevantHelp="If checked, players stay network relevant regardless of distance" + bEnableJitterBoundingText="Enable Jitter Bounding" + bEnableJitterBoundingHelp="If checked, bounds movement jitter spikes" + LooseCheckCorrectionFactorText="Loose Check Correction Factor" + LooseCheckCorrectionFactorHelp="Correction strength used by loose position checks" + LooseCheckCorrectionFactorOnMoverText="Loose Check Correction On Mover" + LooseCheckCorrectionFactorOnMoverHelp="Loose check correction strength while standing on movers" + bEnableSnapshotInterpolationText="Enable Snapshot Interpolation" + bEnableSnapshotInterpolationHelp="If checked, clients smooth movement using snapshot interpolation" + SnapshotInterpSendHzText="Snapshot Send Hz" + SnapshotInterpSendHzHelp="Snapshot transmission frequency in Hz" + SnapshotInterpRewindMsText="Snapshot Rewind (ms)" + SnapshotInterpRewindMsHelp="Interpolation rewind delay in milliseconds" + bEnableWarpFixText="Enable Warp Fix" + bEnableWarpFixHelp="If checked, enables warp correction smoothing" + + DebugText="Debug" + ShowTouchedPackageText="Show Touched Package" + ShowTouchedPackageHelp="If checked, logs touched-package details for diagnostics" + bEnableDamageDebugModeText="Enable Damage Debug Mode" + bEnableDamageDebugModeHelp="If checked, enables server-side damage debug tracing" + bEnableDamageDebugConsoleMessagesText="Damage Debug Console Messages" + bEnableDamageDebugConsoleMessagesHelp="If checked, prints damage debug messages to client consoles" + bEnableHitboxDebugModeText="Enable Hitbox Debug Mode" + bEnableHitboxDebugModeHelp="If checked, enables hitbox debug diagnostics" + + PaddingX=20 + PaddingY=12 + LineSpacing=22 + SeparatorSpacing=30 +} diff --git a/Classes/IGPlus_SettingsContent.uc b/Classes/IGPlus_SettingsContent.uc index 17acb3c..f35abb1 100644 --- a/Classes/IGPlus_SettingsContent.uc +++ b/Classes/IGPlus_SettingsContent.uc @@ -2,6 +2,9 @@ class IGPlus_SettingsContent extends UMenuPageWindow; var ClientSettings Settings; +var UWindowLabelControl Lbl_Header; +var localized string HeaderText; + var UWindowLabelControl Lbl_MoreInformation; var localized string MoreInformationText; @@ -842,6 +845,10 @@ function Created() { ControlOffset = PaddingY; ControlWidth = WinWidth - 2*PaddingX; + Lbl_Header = CreateLabel(HeaderText); + Lbl_Header.Align = TA_Center; + Lbl_Header.SetFont(F_Bold); + Lbl_MoreInformation = CreateLabel(MoreInformationText); Lbl_MoreInformation.Align = TA_Center; Lbl_MoreInformation.SetFont(F_Bold); @@ -1290,6 +1297,7 @@ function SaveConfigs() { defaultproperties { + HeaderText="Client Settings" MoreInformationText="Right-click on settings to get more information" GeneralText="General" @@ -1596,31 +1604,31 @@ defaultproperties ClientSideAnimationsText="Ping Compensation Settings" - BioUseClientSideAnimationsText="Use Client-Side Animations for Bio Rifle" + BioUseClientSideAnimationsText="Client-Side Animations Bio Rifle" BioUseClientSideAnimationsHelp="If checked, use client-side animations for Bio Rifle" - ShockBeamUseClientSideAnimationsText="Use Client-Side Animations for Shock Beam" + ShockBeamUseClientSideAnimationsText="Client-Side Animations Shock Beam" ShockBeamUseClientSideAnimationsHelp="If checked, use client-side animations for Shock Beam" - ShockProjectileUseClientSideAnimationsText="Use Client-Side Animations for Shock Projectile" + ShockProjectileUseClientSideAnimationsText="Client-Side Animations Shock Projectile" ShockProjectileUseClientSideAnimationsHelp="If checked, use client-side animations for Shock Projectile" - PulseUseClientSideAnimationsText="Use Client-Side Animations for Pulse Rifle" + PulseUseClientSideAnimationsText="Client-Side Animations Pulse Rifle" PulseUseClientSideAnimationsHelp="If checked, use client-side animations for Pulse Rifle" - RipperUseClientSideAnimationsText="Use Client-Side Animations for Ripper" + RipperUseClientSideAnimationsText="Client-Side Animations Ripper" RipperUseClientSideAnimationsHelp="If checked, use client-side animations for Ripper" - FlakUseClientSideAnimationsText="Use Client-Side Animations for Flak Cannon" + FlakUseClientSideAnimationsText="Client-Side Animations Flak Cannon" FlakUseClientSideAnimationsHelp="If checked, use client-side animations for Flak Cannon" - RocketUseClientSideAnimationsText="Use Client-Side Animations for Rocket Launcher" + RocketUseClientSideAnimationsText="Client-Side Animations Rocket Launcher" RocketUseClientSideAnimationsHelp="If checked, use client-side animations for Rocket Launcher" - SniperUseClientSideAnimationsText="Use Client-Side Animations for Sniper Rifle" + SniperUseClientSideAnimationsText="Client-Side Animations Sniper Rifle" SniperUseClientSideAnimationsHelp="If checked, use client-side animations for Sniper Rifle" - TranslocatorUseClientSideAnimationsText="Use Client-Side Animations for Translocator" + TranslocatorUseClientSideAnimationsText="Client-Side Animations Translocator" TranslocatorUseClientSideAnimationsHelp="If checked, use client-side animations for Translocator" PaddingX=20 diff --git a/Classes/IGPlus_SettingsScroll.uc b/Classes/IGPlus_SettingsScroll.uc index eee8617..cac8a49 100644 --- a/Classes/IGPlus_SettingsScroll.uc +++ b/Classes/IGPlus_SettingsScroll.uc @@ -1,16 +1,97 @@ class IGPlus_SettingsScroll extends UWindowScrollingDialogClient; +enum ESettingsTab { + ST_Client, + ST_Server +}; + var UWindowSmallButton Btn_Close; var UWindowSmallButton Btn_Save; +var UWindowSmallButton Btn_ClientTab; +var UWindowSmallButton Btn_ServerTab; var localized string SaveButtonText; var localized string SaveButtonToolTip; +var localized string ClientTabText; +var localized string ClientTabToolTip; +var localized string ServerTabText; +var localized string ServerTabToolTip; + +var class ClientTabClass; +var class ServerTabClass; +var ESettingsTab ActiveTab; +var UWindowDialogClientWindow ClientTabArea; +var UWindowDialogClientWindow ServerTabArea; var float FixedPaddingX; var float FixedPaddingY; +function EnsureServerTabArea() { + if (ServerTabArea != none) + return; + + if (ServerTabClass == none) + return; + + ServerTabArea = UWindowDialogClientWindow(CreateWindow(ServerTabClass, 0, 0, WinWidth, WinHeight, OwnerWindow)); + if (ServerTabArea != none) + ServerTabArea.HideWindow(); +} + +function SetActiveTab(ESettingsTab NewTab) { + if (NewTab == ActiveTab && ClientArea != none) + return; + + EnsureServerTabArea(); + + ActiveTab = NewTab; + if (ActiveTab == ST_Server && ServerTabArea != none) { + if (ClientTabArea != none && ClientTabArea.bWindowVisible) + ClientTabArea.HideWindow(); + if (ServerTabArea.bWindowVisible == false) + ServerTabArea.ShowWindow(); + ClientArea = ServerTabArea; + } else { + if (ServerTabArea != none && ServerTabArea.bWindowVisible) + ServerTabArea.HideWindow(); + if (ClientTabArea != none && ClientTabArea.bWindowVisible == false) + ClientTabArea.ShowWindow(); + ClientArea = ClientTabArea; + ActiveTab = ST_Client; + } + + if (Btn_ClientTab != none) + Btn_ClientTab.bDisabled = (ActiveTab == ST_Client); + if (Btn_ServerTab != none) + Btn_ServerTab.bDisabled = (ActiveTab == ST_Server); + + Load(); +} + function Created() { super.Created(); + Btn_ClientTab = UWindowSmallButton(FixedArea.CreateControl( + class'UWindowSmallButton', + FixedPaddingX, + FixedPaddingY, + 32, + 16 + )); + Btn_ClientTab.SetText(ClientTabText); + Btn_ClientTab.ToolTipString = ClientTabToolTip; + Btn_ClientTab.Register(self); + + Btn_ServerTab = UWindowSmallButton(FixedArea.CreateControl( + class'UWindowSmallButton', + FixedPaddingX + 40, + FixedPaddingY, + 32, + 16 + )); + Btn_ServerTab.SetText(ServerTabText); + Btn_ServerTab.ToolTipString = ServerTabToolTip; + Btn_ServerTab.Register(self); + Btn_Save = UWindowSmallButton(FixedArea.CreateControl( class'UWindowSmallButton', FixedArea.WinWidth-FixedPaddingX-72, @@ -31,21 +112,38 @@ function Created() { )); FixedArea.WinHeight = 2*FixedPaddingY + 16; + + ClientTabArea = ClientArea; + EnsureServerTabArea(); + + ActiveTab = ST_Client; + SetActiveTab(ST_Client); } function Notify(UWindowDialogControl C, byte E) { Super.Notify(C, E); - if (E == DE_Click && C == Btn_Save) { + if (E == DE_Click && C == Btn_ClientTab) + SetActiveTab(ST_Client); + else if (E == DE_Click && C == Btn_ServerTab) + SetActiveTab(ST_Server); + else if (E == DE_Click && C == Btn_Save) { Save(); - Load(); + if (IGPlus_SettingsContent(ClientArea) != none) + Load(); } } function BeforePaint(Canvas C, float X, float Y) { super.BeforePaint(C, X, Y); + Btn_ClientTab.AutoWidth(C); + Btn_ClientTab.WinLeft = FixedPaddingX; + + Btn_ServerTab.AutoWidth(C); + Btn_ServerTab.WinLeft = FixedPaddingX + Btn_ClientTab.WinWidth + 5; + Btn_Close.AutoWidth(C); Btn_Close.WinLeft = FixedArea.WinWidth-FixedPaddingX-Btn_Close.WinWidth; @@ -54,21 +152,33 @@ function BeforePaint(Canvas C, float X, float Y) { } function Load() { - IGPlus_SettingsContent(ClientArea).Load(); + if (IGPlus_SettingsContent(ClientArea) != none) + IGPlus_SettingsContent(ClientArea).Load(); + else if (IGPlus_ServerSettingsContent(ClientArea) != none) + IGPlus_ServerSettingsContent(ClientArea).Load(); } function Save() { - IGPlus_SettingsContent(ClientArea).Save(); + if (IGPlus_SettingsContent(ClientArea) != none) + IGPlus_SettingsContent(ClientArea).Save(); + else if (IGPlus_ServerSettingsContent(ClientArea) != none) + IGPlus_ServerSettingsContent(ClientArea).Save(); } defaultproperties { ClientClass=class'IGPlus_SettingsContent' FixedAreaClass=class'UWindowDialogClientWindow' + ClientTabClass=class'IGPlus_SettingsContent' + ServerTabClass=class'IGPlus_ServerSettingsContent' SaveButtonText="Save" SaveButtonToolTip="Saves the current settings to InstaGibPlus.ini" + ClientTabText="Client" + ClientTabToolTip="Open client settings" + ServerTabText="Server" + ServerTabToolTip="Open server settings" FixedPaddingX=20 FixedPaddingY=5 -} \ No newline at end of file +} diff --git a/Classes/UTPure.uc b/Classes/UTPure.uc index df0e607..72dff8c 100644 --- a/Classes/UTPure.uc +++ b/Classes/UTPure.uc @@ -989,6 +989,131 @@ function bool MutatorBroadcastLocalizedMessage( Actor Sender, Pawn Receiver, out } // MutatorBroadcastLocalizedMessage +function string IGPlus_NormalizeServerSettingKey(string Key) { + if (Key ~= "bAutoPause") return "bAutoPause"; + if (Key ~= "PauseTotalTime") return "PauseTotalTime"; + if (Key ~= "PauseTime") return "PauseTime"; + if (Key ~= "bForceDemo") return "bForceDemo"; + if (Key ~= "bRestrictTrading") return "bRestrictTrading"; + if (Key ~= "MaxTradeTimeMargin") return "MaxTradeTimeMargin"; + if (Key ~= "TradePingMargin") return "TradePingMargin"; + if (Key ~= "KillCamDelay") return "KillCamDelay"; + if (Key ~= "KillCamDuration") return "KillCamDuration"; + if (Key ~= "bJumpingPreservesMomentum") return "bJumpingPreservesMomentum"; + if (Key ~= "bOldLandingMomentum") return "bOldLandingMomentum"; + if (Key ~= "bEnableSingleButtonDodge") return "bEnableSingleButtonDodge"; + if (Key ~= "bUseFlipAnimation") return "bUseFlipAnimation"; + if (Key ~= "bEnableWallDodging") return "bEnableWallDodging"; + if (Key ~= "bDodgePreserveZMomentum") return "bDodgePreserveZMomentum"; + if (Key ~= "MaxMultiDodges") return "MaxMultiDodges"; + if (Key ~= "BrightskinMode") return "BrightskinMode"; + if (Key ~= "PlayerScale") return "PlayerScale"; + if (Key ~= "bAlwaysRenderFlagCarrier") return "bAlwaysRenderFlagCarrier"; + if (Key ~= "bAlwaysRenderDroppedFlags") return "bAlwaysRenderDroppedFlags"; + if (Key ~= "MaxPosError") return "MaxPosError"; + if (Key ~= "MaxHitError") return "MaxHitError"; + if (Key ~= "MaxJitterTime") return "MaxJitterTime"; + if (Key ~= "WarpFixDelay") return "WarpFixDelay"; + if (Key ~= "FireTimeout") return "FireTimeout"; + if (Key ~= "MinNetUpdateRate") return "MinNetUpdateRate"; + if (Key ~= "MaxNetUpdateRate") return "MaxNetUpdateRate"; + if (Key ~= "bEnableInputReplication") return "bEnableInputReplication"; + if (Key ~= "bEnableServerExtrapolation") return "bEnableServerExtrapolation"; + if (Key ~= "bEnableServerPacketReordering") return "bEnableServerPacketReordering"; + if (Key ~= "bEnableLoosePositionCheck") return "bEnableLoosePositionCheck"; + if (Key ~= "bPlayersAlwaysRelevant") return "bPlayersAlwaysRelevant"; + if (Key ~= "bEnablePingCompensatedSpawn") return "bEnablePingCompensatedSpawn"; + if (Key ~= "bEnableJitterBounding") return "bEnableJitterBounding"; + if (Key ~= "LooseCheckCorrectionFactor") return "LooseCheckCorrectionFactor"; + if (Key ~= "LooseCheckCorrectionFactorOnMover") return "LooseCheckCorrectionFactorOnMover"; + if (Key ~= "bEnableSnapshotInterpolation") return "bEnableSnapshotInterpolation"; + if (Key ~= "SnapshotInterpSendHz") return "SnapshotInterpSendHz"; + if (Key ~= "SnapshotInterpRewindMs") return "SnapshotInterpRewindMs"; + if (Key ~= "bEnableWarpFix") return "bEnableWarpFix"; + if (Key ~= "ShowTouchedPackage") return "ShowTouchedPackage"; + if (Key ~= "HitFeedbackMode") return "HitFeedbackMode"; + if (Key ~= "bEnableDamageDebugMode") return "bEnableDamageDebugMode"; + if (Key ~= "bEnableDamageDebugConsoleMessages") return "bEnableDamageDebugConsoleMessages"; + if (Key ~= "bEnableHitboxDebugMode") return "bEnableHitboxDebugMode"; + return ""; +} + +function IGPlus_ApplyServerSettingsToPlayers() { + local Pawn P; + local bbPlayer BP; + local bool bOldSnap; + + for (P = Level.PawnList; P != None; P = P.NextPawn) { + BP = bbPlayer(P); + if (BP == None) + continue; + + BP.TeleRadius = Settings.TeleRadius; + BP.BrightskinMode = Settings.BrightskinMode; + BP.KillCamDelay = FMax(0.0, Settings.KillCamDelay); + BP.KillCamDuration = Settings.KillCamDuration; + BP.bJumpingPreservesMomentum = Settings.bJumpingPreservesMomentum; + BP.bOldLandingMomentum = Settings.bOldLandingMomentum; + BP.bEnableSingleButtonDodge = Settings.bEnableSingleButtonDodge; + BP.bUseFlipAnimation = Settings.bUseFlipAnimation; + BP.bCanWallDodge = Settings.bEnableWallDodging; + BP.bDodgePreserveZMomentum = Settings.bDodgePreserveZMomentum; + BP.bAlwaysRelevant = Settings.bPlayersAlwaysRelevant || (BP.PlayerReplicationInfo != none && BP.PlayerReplicationInfo.HasFlag != none); + BP.IGPlus_EnableWarpFix = Settings.bEnableWarpFix; + BP.IGPlus_WarpFixDelay = Settings.WarpFixDelay; + BP.IGPlus_AlwaysRenderFlagCarrier = Settings.bAlwaysRenderFlagCarrier; + BP.IGPlus_AlwaysRenderDroppedFlags = Settings.bAlwaysRenderDroppedFlags; + BP.IGPlus_UseFastWeaponSwitch = Settings.bUseFastWeaponSwitch; + BP.IGPlus_EnableInputReplication = Settings.bEnableInputReplication; + bOldSnap = BP.IGPlus_EnableSnapshotInterpolation; + BP.IGPlus_EnableSnapshotInterpolation = Settings.bEnableSnapshotInterpolation; + if (bOldSnap != BP.IGPlus_EnableSnapshotInterpolation) { + BP.IGPlus_SnapInterp_ServerNextTime = 0; + BP.IGPlus_SnapInterp_ServerHasLast = false; + } + BP.bEnableDamageDebugMode = Settings.bEnableDamageDebugMode; + BP.bEnableDamageDebugConsoleMessages = Settings.bEnableDamageDebugConsoleMessages; + } +} + +function IGPlus_PrintServerSetting(PlayerPawn Sender, string Key) { + local string NormalKey; + + if (Settings == None) { + Sender.ClientMessage("Server settings unavailable"); + return; + } + + NormalKey = IGPlus_NormalizeServerSettingKey(Key); + if (NormalKey == "") { + Sender.ClientMessage("Unknown server setting:"@Key); + return; + } + + Sender.ClientMessage(NormalKey$"="$Settings.GetPropertyText(NormalKey)); +} + +function IGPlus_SetServerSetting(PlayerPawn Sender, string Key, string Value, optional bool bSendFeedback) { + local string NormalKey; + + if (Settings == None) { + Sender.ClientMessage("Server settings unavailable"); + return; + } + + NormalKey = IGPlus_NormalizeServerSettingKey(Key); + if (NormalKey == "") { + Sender.ClientMessage("Unknown server setting:"@Key); + return; + } + + Settings.SetPropertyText(NormalKey, Value); + Settings.SaveConfig(); + IGPlus_ApplyServerSettingsToPlayers(); + if (bSendFeedback) + Sender.ClientMessage(NormalKey$"="$Settings.GetPropertyText(NormalKey)); +} + // ================================================================================== // Mutate - Accepts commands from the users // ================================================================================== @@ -1000,6 +1125,7 @@ function Mutate(string MutateString, PlayerPawn Sender) local int zzi; local bool zzb; local string zzS; + local string zzS2; local PlayerReplicationInfo zzPRI; if (MutateString ~= "CheatInfo") @@ -1045,6 +1171,9 @@ function Mutate(string MutateString, PlayerPawn Sender) } if (CHSpectator(Sender) != None) Sender.ClientMessage("As spectator, you may need to add 'mutate pure' + command (mutate pureshowtickrate)"); + Sender.ClientMessage("Admin setting commands:"); + Sender.ClientMessage("- mutate IGPlusServerGet "); + Sender.ClientMessage("- mutate IGPlusServerSet "); } else if (MutateString ~= "PingCompSettings") { @@ -1070,6 +1199,83 @@ function Mutate(string MutateString, PlayerPawn Sender) else if (Sender.IsA('bbCHSpectator')) bbCHSpectator(Sender).IGPlusMenu(); } + else if (MutateString ~= "IGPlusServerGet") + { + Sender.ClientMessage("Usage: mutate IGPlusServerGet "); + } + else if (Left(MutateString, 16) ~= "IGPlusServerGet ") + { + if (Sender.bAdmin == false) + { + Sender.ClientMessage(BADminText); + } + else + { + zzS = class'StringUtils'.static.Trim(Mid(MutateString, 16)); + if (zzS == "") + Sender.ClientMessage("Usage: mutate IGPlusServerGet "); + else + IGPlus_PrintServerSetting(Sender, zzS); + } + } + else if (Left(MutateString, 22) ~= "IGPlusServerSetSilent ") + { + if (Sender.bAdmin == false) + { + Sender.ClientMessage(BADminText); + } + else + { + zzS = class'StringUtils'.static.Trim(Mid(MutateString, 22)); + zzi = InStr(zzS, " "); + if (zzi < 0) + { + Sender.ClientMessage("Usage: mutate IGPlusServerSetSilent "); + } + else + { + zzS2 = class'StringUtils'.static.Trim(Left(zzS, zzi)); + zzS = class'StringUtils'.static.Trim(Mid(zzS, zzi + 1)); + if (zzS2 == "" || zzS == "") + Sender.ClientMessage("Usage: mutate IGPlusServerSetSilent "); + else + IGPlus_SetServerSetting(Sender, zzS2, zzS, false); + } + } + } + else if (Left(MutateString, 16) ~= "IGPlusServerSet ") + { + if (Sender.bAdmin == false) + { + Sender.ClientMessage(BADminText); + } + else + { + zzS = class'StringUtils'.static.Trim(Mid(MutateString, 16)); + zzi = InStr(zzS, " "); + if (zzi < 0) + { + Sender.ClientMessage("Usage: mutate IGPlusServerSet "); + } + else + { + zzS2 = class'StringUtils'.static.Trim(Left(zzS, zzi)); + zzS = class'StringUtils'.static.Trim(Mid(zzS, zzi + 1)); + if (zzS2 == "" || zzS == "") + Sender.ClientMessage("Usage: mutate IGPlusServerSet "); + else + IGPlus_SetServerSetting(Sender, zzS2, zzS, true); + } + } + } + else if (MutateString ~= "IGPlusServerSetSilent") + { + Sender.ClientMessage("Usage: mutate IGPlusServerSetSilent "); + } + else if (MutateString ~= "IGPlusServerSet") + { + Sender.ClientMessage("Usage: mutate IGPlusServerSet "); + } else if (MutateString ~= "EnablePure") { if (Sender.bAdmin) @@ -1491,4 +1697,4 @@ defaultproperties { BADminText="Not allowed - Log in as admin!" bAlwaysTick=True -} \ No newline at end of file +} diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index a75b1fe..e2e8778 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -295,6 +295,10 @@ var float IGPlus_ZoomToggle_SensitivityFactorY; var ReplicationInfo IGPlus_AdditionalReplicationInfo; var bool IGPlus_TryOpenSettingsMenu; var IGPlus_SettingsDialog IGPlus_SettingsMenu; +var Object IGPlus_ServerSettingsHelper; +var ServerSettings IGPlus_ServerSettingsMenuData; +var bool IGPlus_ServerSettingsMenuLoaded; +var bool IGPlus_ServerSettingsMenuCanEdit; struct IGPlus_WarpFix { var vector OldLocation; @@ -523,6 +527,9 @@ replication IGPlus_ForcedSettingsInit, IGPlus_ForcedSettingRegister, IGPlus_ForcedSettingsApply, + IGPlus_ServerSettingsInit, + IGPlus_ServerSettingsSet, + IGPlus_ServerSettingsDone, IGPlus_ClientReStart, IGPlus_NotifyPlayerRestart, ClientAddMomentum; @@ -557,6 +564,7 @@ replication PrintWeaponState, IGPlus_ServerNetcodeHelp, IGPlus_ServerSetNetcodeSetting, + IGPlus_ServerRequestSettings, ServerSetDodgeSettings, xxExplodeOther, xxNN_AltFire, @@ -11247,6 +11255,104 @@ function IGPlus_ApplyNetcodeSetting(string Key, string Value) { ClientMessage(NormalKey $ "=" $ zzUTPure.Settings.GetPropertyText(NormalKey)); } +simulated function ServerSettings IGPlus_GetServerSettingsObject() { + if (IGPlus_ServerSettingsMenuData != none) + return IGPlus_ServerSettingsMenuData; + + IGPlus_ServerSettingsHelper = new(none, 'InstaGibPlus') class'Object'; + IGPlus_ServerSettingsMenuData = new(IGPlus_ServerSettingsHelper, 'IGPlus_ServerSettingsMenu') class'ServerSettings'; + return IGPlus_ServerSettingsMenuData; +} + +function IGPlus_ServerSettingsInit() { + IGPlus_ServerSettingsMenuLoaded = false; + IGPlus_ServerSettingsMenuCanEdit = false; + IGPlus_ServerSettingsMenuData = none; +} + +function IGPlus_ServerSettingsSet(string Key, string Value) { + local ServerSettings S; + + S = IGPlus_GetServerSettingsObject(); + if (S == none) + return; + + S.SetPropertyText(Key, Value); +} + +function IGPlus_ServerSettingsDone(bool bCanEdit) { + IGPlus_ServerSettingsMenuLoaded = true; + IGPlus_ServerSettingsMenuCanEdit = bCanEdit; +} + +function IGPlus_ServerSendSetting(string Key) { + if (zzUTPure == none || zzUTPure.Settings == none) + return; + + IGPlus_ServerSettingsSet(Key, zzUTPure.Settings.GetPropertyText(Key)); +} + +function IGPlus_ServerRequestSettings() { + if (bAdmin == false || zzUTPure == none || zzUTPure.Settings == none) { + IGPlus_ServerSettingsInit(); + IGPlus_ServerSettingsDone(false); + return; + } + + IGPlus_ServerSettingsInit(); + + IGPlus_ServerSendSetting("bAutoPause"); + IGPlus_ServerSendSetting("PauseTotalTime"); + IGPlus_ServerSendSetting("PauseTime"); + IGPlus_ServerSendSetting("bForceDemo"); + IGPlus_ServerSendSetting("bRestrictTrading"); + IGPlus_ServerSendSetting("MaxTradeTimeMargin"); + IGPlus_ServerSendSetting("TradePingMargin"); + IGPlus_ServerSendSetting("KillCamDelay"); + IGPlus_ServerSendSetting("KillCamDuration"); + + IGPlus_ServerSendSetting("bJumpingPreservesMomentum"); + IGPlus_ServerSendSetting("bOldLandingMomentum"); + IGPlus_ServerSendSetting("bEnableSingleButtonDodge"); + IGPlus_ServerSendSetting("bUseFlipAnimation"); + IGPlus_ServerSendSetting("bEnableWallDodging"); + IGPlus_ServerSendSetting("bDodgePreserveZMomentum"); + IGPlus_ServerSendSetting("MaxMultiDodges"); + IGPlus_ServerSendSetting("BrightskinMode"); + IGPlus_ServerSendSetting("PlayerScale"); + IGPlus_ServerSendSetting("bAlwaysRenderFlagCarrier"); + IGPlus_ServerSendSetting("bAlwaysRenderDroppedFlags"); + + IGPlus_ServerSendSetting("MaxPosError"); + IGPlus_ServerSendSetting("MaxHitError"); + IGPlus_ServerSendSetting("MaxJitterTime"); + IGPlus_ServerSendSetting("WarpFixDelay"); + IGPlus_ServerSendSetting("FireTimeout"); + IGPlus_ServerSendSetting("MinNetUpdateRate"); + IGPlus_ServerSendSetting("MaxNetUpdateRate"); + IGPlus_ServerSendSetting("bEnableInputReplication"); + IGPlus_ServerSendSetting("bEnableServerExtrapolation"); + IGPlus_ServerSendSetting("bEnableServerPacketReordering"); + IGPlus_ServerSendSetting("bEnableLoosePositionCheck"); + IGPlus_ServerSendSetting("bPlayersAlwaysRelevant"); + IGPlus_ServerSendSetting("bEnablePingCompensatedSpawn"); + IGPlus_ServerSendSetting("bEnableJitterBounding"); + IGPlus_ServerSendSetting("LooseCheckCorrectionFactor"); + IGPlus_ServerSendSetting("LooseCheckCorrectionFactorOnMover"); + IGPlus_ServerSendSetting("bEnableSnapshotInterpolation"); + IGPlus_ServerSendSetting("SnapshotInterpSendHz"); + IGPlus_ServerSendSetting("SnapshotInterpRewindMs"); + IGPlus_ServerSendSetting("bEnableWarpFix"); + + IGPlus_ServerSendSetting("ShowTouchedPackage"); + IGPlus_ServerSendSetting("HitFeedbackMode"); + IGPlus_ServerSendSetting("bEnableDamageDebugMode"); + IGPlus_ServerSendSetting("bEnableDamageDebugConsoleMessages"); + IGPlus_ServerSendSetting("bEnableHitboxDebugMode"); + + IGPlus_ServerSettingsDone(true); +} + exec function DrawServerLocation() { IGPlus_LocationOffsetFix_DrawServerLocation = !IGPlus_LocationOffsetFix_DrawServerLocation; } From 7c3ed6f58c14555c9677dfc19e712689186a976c Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:08:13 -0600 Subject: [PATCH 07/14] Consolidate snapshot debug path and telemetry plumbing --- Classes/IGPlus_WeaponImplementationBase.uc | 9 +- Classes/bbPlayer.uc | 560 +++++++++++++++------ 2 files changed, 417 insertions(+), 152 deletions(-) diff --git a/Classes/IGPlus_WeaponImplementationBase.uc b/Classes/IGPlus_WeaponImplementationBase.uc index 888e28b..3b7cd2d 100644 --- a/Classes/IGPlus_WeaponImplementationBase.uc +++ b/Classes/IGPlus_WeaponImplementationBase.uc @@ -27,10 +27,13 @@ function float IGPlus_GetSnapshotInterpRewindMs(Pawn Instigator) { if (bbP == None || bbP.IGPlus_EnableSnapshotInterpolation == false) return 0.0; - if (bbP.zzUTPure == None) - return 0.0; + if (bbP.IGPlus_ServerSnapInterpDelayValid) + return FMax(0.0, bbP.IGPlus_ServerSnapInterpDelayMsSmoothed); + + if (bbP.zzUTPure != None && bbP.zzUTPure.Settings != None) + return FMax(0.0, bbP.zzUTPure.Settings.SnapshotInterpRewindMs); - return FMax(0.0, bbP.zzUTPure.Settings.SnapshotInterpRewindMs); + return 0.0; } function PostBeginPlay() { diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index e2e8778..587040b 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -327,6 +327,7 @@ var float IGPlus_WarpFixDelay; var IGPlus_WarpFix IGPlus_WarpFixData; var IGPlus_WarpFixClient IGPlus_WarpFixClientData; var IGPlus_SnapData IGPlus_SnapReplicatedData; +var IGPlus_SnapData IGPlus_SnapReplicatedDataPrev; const IGPlus_LocationOffsetFix_DummyVel = vect(4.56,4.56,0.0); @@ -360,20 +361,26 @@ var float IGPlus_SnapInterp_InterpDelay; var float IGPlus_SnapInterp_LastArrivalTime; var float IGPlus_SnapInterp_MeanInterval; var float IGPlus_SnapInterp_JitterEstimate; -var bool IGPlus_SnapInterp_Debug; -var float IGPlus_SnapInterp_DebugNextTime; +var bool IGPlus_SnapInterp_NetDebug; +var float IGPlus_SnapInterp_NetDebugNextTime; var int IGPlus_SnapInterp_LastCounter; var float IGPlus_SnapInterp_ServerNextTime; var vector IGPlus_SnapInterp_ServerLastLocation; var float IGPlus_SnapInterp_ServerLastTime; var bool IGPlus_SnapInterp_ServerHasLast; var bool IGPlus_SnapInterp_EnabledLast; -var int IGPlus_SnapInterp_DebugHardSnapCount; -var int IGPlus_SnapInterp_DebugInterpCount; -var int IGPlus_SnapInterp_DebugClampCount; -var int IGPlus_SnapInterp_DebugFailCount; -var int IGPlus_SnapInterp_DebugExtrapolationCount; -var int IGPlus_SnapInterp_DebugSnapCount; +var int IGPlus_ClientSnapInterpDelayMs; +var int IGPlus_ClientSnapInterpDelaySampleCount; +var int IGPlus_ClientSnapInterpDelayMedianMs; +var float IGPlus_ServerSnapInterpDelayMsSmoothed; +var bool IGPlus_ServerSnapInterpDelayValid; +var float IGPlus_NextSnapInterpDelayReportTime; +var float IGPlus_NextSnapInterpDelayClientReportTime; +var int IGPlus_ServerSnapInterpDelayReportedMsEcho; +var int IGPlus_ServerSnapInterpDelayClampedMsEcho; +var int IGPlus_ServerSnapInterpDelaySmoothedMsEcho; +var bool IGPlus_ServerSnapInterpDelayValidEcho; +var float IGPlus_ServerSnapInterpDelayEchoTime; var bool IGPlus_AlwaysRenderFlagCarrier; var bool IGPlus_AlwaysRenderDroppedFlags; @@ -461,6 +468,11 @@ replication zzMinimumNetspeed, zzTrackFOV, zzWaitTime, + IGPlus_ServerSnapInterpDelayReportedMsEcho, + IGPlus_ServerSnapInterpDelayClampedMsEcho, + IGPlus_ServerSnapInterpDelaySmoothedMsEcho, + IGPlus_ServerSnapInterpDelayValidEcho, + IGPlus_ServerSnapInterpDelayEchoTime, bEnableDamageDebugMode, bEnableDamageDebugConsoleMessages; @@ -469,7 +481,8 @@ replication IGPlus_AdditionalReplicationInfo, IGPlus_WarpFixData, IGPlus_EnableSnapshotInterpolation, - IGPlus_SnapReplicatedData; + IGPlus_SnapReplicatedData, + IGPlus_SnapReplicatedDataPrev; unreliable if ( bDrawDebugData && RemoteRole == ROLE_AutonomousProxy ) clientForcedPosition, @@ -488,6 +501,7 @@ replication FakeCAPInterval, IGPlus_DamageEvent_ShowOnDeath, IGPlus_EnableDualButtonSwitch, + IGPlus_ClientSnapInterpDelayMs, PingAverage, zzbDemoRecording, zzNetspeed, @@ -6428,6 +6442,7 @@ event ServerTick(float DeltaTime) { AverageServerDeltaTime = (AverageServerDeltaTime*99 + DeltaTime) * 0.01; IGPlus_ProcessRemoteMovement(); xxRememberPosition(); + IGPlus_UpdateServerSnapInterpDelayEstimate(); if (IGPlus_EnableSnapshotInterpolation) { SnapshotSendHz = 30.0; @@ -6449,22 +6464,22 @@ event ServerTick(float DeltaTime) { bHardSnap = true; } - bOnMover = Mover(Base) != none; - - IGPlus_SnapReplicatedData.Location = Location; - IGPlus_SnapReplicatedData.Velocity = Velocity; - IGPlus_SnapReplicatedData.Rotation = Rotation; - IGPlus_SnapReplicatedData.Rotation.Roll = 0; - IGPlus_SnapReplicatedData.Counter += 1; - IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; - IGPlus_SnapReplicatedData.bBasedOnMover = bOnMover; - if (bOnMover) { - IGPlus_SnapReplicatedData.MoverLocation = Base.Location; - IGPlus_SnapReplicatedData.BaseMover = Base; - } else { - IGPlus_SnapReplicatedData.MoverLocation = vect(0,0,0); - IGPlus_SnapReplicatedData.BaseMover = None; - } + bOnMover = Mover(Base) != none; + IGPlus_SnapReplicatedDataPrev = IGPlus_SnapReplicatedData; + IGPlus_SnapReplicatedData.Location = Location; + IGPlus_SnapReplicatedData.Velocity = Velocity; + IGPlus_SnapReplicatedData.Rotation = Rotation; + IGPlus_SnapReplicatedData.Rotation.Roll = 0; + IGPlus_SnapReplicatedData.Counter += 1; + IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; + IGPlus_SnapReplicatedData.bBasedOnMover = bOnMover; + if (bOnMover) { + IGPlus_SnapReplicatedData.MoverLocation = Base.Location; + IGPlus_SnapReplicatedData.BaseMover = Base; + } else { + IGPlus_SnapReplicatedData.MoverLocation = vect(0,0,0); + IGPlus_SnapReplicatedData.BaseMover = None; + } IGPlus_SnapInterp_ServerLastLocation = Location; IGPlus_SnapInterp_ServerLastTime = Level.TimeSeconds; @@ -8281,6 +8296,169 @@ function IGPlus_ApplyWarpFix(PlayerReplicationInfo PRI) { P.SetLocation(P.IGPlus_WarpFixClientData.Last.OldLocation); } +simulated function IGPlus_UpdateClientSnapInterpDelayReport() { + local int i; + local int j; + local int SampleCount; + local int MedianDelayMs; + local float Sample; + local float DelaySamples[32]; + local PlayerReplicationInfo PRI; + local bbPlayer P; + + if (Role != ROLE_AutonomousProxy || Level.NetMode != NM_Client) + return; + + if (IGPlus_EnableSnapshotInterpolation == false) { + IGPlus_ClientSnapInterpDelayMs = -1; + IGPlus_ClientSnapInterpDelaySampleCount = 0; + IGPlus_ClientSnapInterpDelayMedianMs = -1; + return; + } + + if (Level.TimeSeconds < IGPlus_NextSnapInterpDelayClientReportTime) + return; + + IGPlus_NextSnapInterpDelayClientReportTime = Level.TimeSeconds + 0.25; + SampleCount = 0; + + if (GameReplicationInfo != None && PlayerReplicationInfo != None) { + for (i = 0; i < arraycount(GameReplicationInfo.PRIArray); ++i) { + PRI = GameReplicationInfo.PRIArray[i]; + if (PRI == none) + break; + + P = bbPlayer(PRI.Owner); + if (P == none || P == self) + continue; + if (P.Role != ROLE_SimulatedProxy) + continue; + if (P.IGPlus_EnableSnapshotInterpolation == false) + continue; + if (P.IGPlus_SnapInterp_Count < 2) + continue; + if (SampleCount >= arraycount(DelaySamples)) + break; + + DelaySamples[SampleCount] = FClamp(P.IGPlus_SnapInterp_InterpDelay * 1000.0, 0.0, 200.0); + SampleCount++; + } + } + + if (SampleCount <= 0) { + IGPlus_ClientSnapInterpDelayMs = -1; + IGPlus_ClientSnapInterpDelaySampleCount = 0; + IGPlus_ClientSnapInterpDelayMedianMs = -1; + return; + } + + for (i = 1; i < SampleCount; ++i) { + Sample = DelaySamples[i]; + j = i - 1; + while (j >= 0 && DelaySamples[j] > Sample) { + DelaySamples[j + 1] = DelaySamples[j]; + j--; + } + DelaySamples[j + 1] = Sample; + } + + if ((SampleCount & 1) == 1) + MedianDelayMs = int(DelaySamples[SampleCount / 2]); + else + MedianDelayMs = int(0.5 * (DelaySamples[(SampleCount / 2) - 1] + DelaySamples[SampleCount / 2])); + + IGPlus_ClientSnapInterpDelaySampleCount = SampleCount; + IGPlus_ClientSnapInterpDelayMedianMs = MedianDelayMs; + IGPlus_ClientSnapInterpDelayMs = Clamp(MedianDelayMs, 0, 200); +} + +function IGPlus_UpdateServerSnapInterpDelayEstimate() { + local float BaselineMs; + local float MinAllowedMs; + local float MaxAllowedMs; + local float ReportedMs; + local float SmoothedTargetMs; + local float SmoothedDeltaMs; + + if (Role != ROLE_Authority) + return; + + if (IGPlus_EnableSnapshotInterpolation == false || zzUTPure == None || zzUTPure.Settings == None) { + IGPlus_ServerSnapInterpDelayValid = false; + IGPlus_ServerSnapInterpDelayMsSmoothed = 0.0; + IGPlus_ServerSnapInterpDelayReportedMsEcho = -1; + IGPlus_ServerSnapInterpDelayClampedMsEcho = -1; + IGPlus_ServerSnapInterpDelaySmoothedMsEcho = 0; + IGPlus_ServerSnapInterpDelayValidEcho = false; + IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; + return; + } + + if (Level.TimeSeconds < IGPlus_NextSnapInterpDelayReportTime) + return; + + IGPlus_NextSnapInterpDelayReportTime = Level.TimeSeconds + 0.25; + IGPlus_ServerSnapInterpDelayReportedMsEcho = IGPlus_ClientSnapInterpDelayMs; + if (IGPlus_ClientSnapInterpDelayMs < 0) { + IGPlus_ServerSnapInterpDelayClampedMsEcho = -1; + IGPlus_ServerSnapInterpDelaySmoothedMsEcho = int(IGPlus_ServerSnapInterpDelayMsSmoothed + 0.5); + IGPlus_ServerSnapInterpDelayValidEcho = IGPlus_ServerSnapInterpDelayValid; + IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; + return; + } + + BaselineMs = FMax(0.0, zzUTPure.Settings.SnapshotInterpRewindMs); + MinAllowedMs = FMax(0.0, BaselineMs - 30.0); + MaxAllowedMs = FMin(200.0, BaselineMs + 60.0); + ReportedMs = FClamp(float(IGPlus_ClientSnapInterpDelayMs), MinAllowedMs, MaxAllowedMs); + IGPlus_ServerSnapInterpDelayClampedMsEcho = int(ReportedMs + 0.5); + + if (IGPlus_ServerSnapInterpDelayValid == false) { + IGPlus_ServerSnapInterpDelayMsSmoothed = ReportedMs; + IGPlus_ServerSnapInterpDelayValid = true; + IGPlus_ServerSnapInterpDelaySmoothedMsEcho = int(IGPlus_ServerSnapInterpDelayMsSmoothed + 0.5); + IGPlus_ServerSnapInterpDelayValidEcho = true; + IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; + return; + } + + SmoothedTargetMs = IGPlus_ServerSnapInterpDelayMsSmoothed + 0.2 * (ReportedMs - IGPlus_ServerSnapInterpDelayMsSmoothed); + SmoothedDeltaMs = FClamp(SmoothedTargetMs - IGPlus_ServerSnapInterpDelayMsSmoothed, -10.0, 10.0); + IGPlus_ServerSnapInterpDelayMsSmoothed = FClamp( + IGPlus_ServerSnapInterpDelayMsSmoothed + SmoothedDeltaMs, + MinAllowedMs, + MaxAllowedMs + ); + IGPlus_ServerSnapInterpDelaySmoothedMsEcho = int(IGPlus_ServerSnapInterpDelayMsSmoothed + 0.5); + IGPlus_ServerSnapInterpDelayValidEcho = IGPlus_ServerSnapInterpDelayValid; + IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; +} + +exec simulated function SnapInterpNetStatus(optional string TargetFilter) { + local int EchoAgeMs; + + EchoAgeMs = int((Level.TimeSeconds - IGPlus_ServerSnapInterpDelayEchoTime) * 1000.0); + if (EchoAgeMs < 0) + EchoAgeMs = 0; + + ClientMessage( + "SnapInterpNet client reportMs="$IGPlus_ClientSnapInterpDelayMs$ + " medianMs="$IGPlus_ClientSnapInterpDelayMedianMs$ + " samples="$IGPlus_ClientSnapInterpDelaySampleCount$ + " snapEnabled="$IGPlus_EnableSnapshotInterpolation + ); + + ClientMessage( + "SnapInterpNet server valid="$IGPlus_ServerSnapInterpDelayValidEcho$ + " reportedMs="$IGPlus_ServerSnapInterpDelayReportedMsEcho$ + " clampedMs="$IGPlus_ServerSnapInterpDelayClampedMsEcho$ + " smoothedMs="$IGPlus_ServerSnapInterpDelaySmoothedMsEcho$ + " ageMs="$EchoAgeMs + ); + + SnapInterpStatus(TargetFilter); +} + event PreRender( canvas zzCanvas ) { local int i; @@ -8296,6 +8474,14 @@ event PreRender( canvas zzCanvas ) // ensure players are visible during pauses IGPlus_LocationOffsetFix_AfterAll(0); + if (Role == ROLE_AutonomousProxy && Level.NetMode == NM_Client) + IGPlus_UpdateClientSnapInterpDelayReport(); + + if (IGPlus_SnapInterp_NetDebug && Level.TimeSeconds >= IGPlus_SnapInterp_NetDebugNextTime) { + IGPlus_SnapInterp_NetDebugNextTime = Level.TimeSeconds + 1.0; + SnapInterpNetStatus(); + } + if (GameReplicationInfo != None && PlayerReplicationInfo != None) { for (i = 0; i < arraycount(GameReplicationInfo.PRIArray); ++i) { zzPRI = GameReplicationInfo.PRIArray[i]; @@ -8768,6 +8954,52 @@ simulated function bool IGPlus_LocationOffsetFix_IsOnGround(out vector HitNormal && (HitNormal.Z >= 0.7); } +simulated function float IGPlus_SnapInterp_GetFallbackInterval() { + local float SnapshotSendHz; + local bbPlayer LP; + + SnapshotSendHz = 30.0; + if (zzUTPure != None && zzUTPure.Settings != None && zzUTPure.Settings.SnapshotInterpSendHz > 0.0) + SnapshotSendHz = zzUTPure.Settings.SnapshotInterpSendHz; + else { + LP = bbPlayer(GetLocalPlayer()); + if (LP != None && + LP.zzUTPure != None && + LP.zzUTPure.Settings != None && + LP.zzUTPure.Settings.SnapshotInterpSendHz > 0.0 + ) { + SnapshotSendHz = LP.zzUTPure.Settings.SnapshotInterpSendHz; + } + } + + return 1.0 / FMax(SnapshotSendHz, 1.0); +} + +simulated function float IGPlus_SnapInterp_GetExpectedInterval() { + return FMax(IGPlus_SnapInterp_MeanInterval, IGPlus_SnapInterp_GetFallbackInterval()); +} + +simulated function IGPlus_SnapInterp_UpdateTargetDelay(float ExpectedInterval) { + local float TargetDelay; + local float DelayDelta; + + ExpectedInterval = FMax(ExpectedInterval, 0.001); + TargetDelay = (2.5 * ExpectedInterval) + IGPlus_SnapInterp_JitterEstimate; + TargetDelay = FClamp(TargetDelay, 0.020, 0.200); + DelayDelta = FClamp(TargetDelay - IGPlus_SnapInterp_InterpDelay, -0.010, 0.020); + IGPlus_SnapInterp_InterpDelay = FClamp(IGPlus_SnapInterp_InterpDelay + DelayDelta, 0.020, 0.200); +} + +simulated function IGPlus_SnapInterp_BoostDelayForStress(optional float Scale) { + local float ExpectedInterval; + + if (Scale <= 0.0) + Scale = 0.5; + + ExpectedInterval = IGPlus_SnapInterp_GetExpectedInterval(); + IGPlus_SnapInterp_InterpDelay = FMin(0.200, IGPlus_SnapInterp_InterpDelay + Scale * ExpectedInterval); +} + simulated function IGPlus_SnapInterp_Push( vector Pos, vector Vel, @@ -8777,11 +9009,13 @@ simulated function IGPlus_SnapInterp_Push( optional bool bFromServerMoverState, optional bool bBasedOnMover, optional vector BaseMoverLocation, - optional Actor BaseMoverActor + optional Actor BaseMoverActor, + optional bool bUpdateDelayStats ) { local int BufSize; local int PrevIdx; local float Interval; + local float ExpectedInterval; local IGPlus_InterpSnapshot Snap; local IGPlus_InterpSnapshot PrevSnap; @@ -8829,31 +9063,33 @@ simulated function IGPlus_SnapInterp_Push( if (IGPlus_SnapInterp_Count > 0) { PrevIdx = (IGPlus_SnapInterp_DataIndex - 1 + BufSize) % BufSize; PrevSnap = IGPlus_SnapInterp_Data[PrevIdx]; - if (PrevSnap != None && VSize(Pos - PrevSnap.Loc) > 3000.0 * FMax(SnapTime - PrevSnap.Time, 0.016)) { - IGPlus_SnapInterp_Reset(); - IGPlus_SnapInterp_Data[0] = Snap; - IGPlus_SnapInterp_DataIndex = 1 % BufSize; - IGPlus_SnapInterp_Count = 1; - IGPlus_SnapInterp_LastArrivalTime = ArrivalTime; - return; + if (PrevSnap != None && VSize(Pos - PrevSnap.Loc) > 3000.0 * FMax(SnapTime - PrevSnap.Time, 0.016)) { + IGPlus_SnapInterp_Reset(); + IGPlus_SnapInterp_Data[0] = Snap; + IGPlus_SnapInterp_DataIndex = 1 % BufSize; + IGPlus_SnapInterp_Count = 1; + if (bUpdateDelayStats) + IGPlus_SnapInterp_LastArrivalTime = ArrivalTime; + return; + } } - } IGPlus_SnapInterp_DataIndex = (IGPlus_SnapInterp_DataIndex + 1) % BufSize; if (IGPlus_SnapInterp_Count < BufSize) IGPlus_SnapInterp_Count++; - // Update adaptive interpolation delay - if (IGPlus_SnapInterp_LastArrivalTime > 0) { + // Update adaptive interpolation delay. + if (bUpdateDelayStats && IGPlus_SnapInterp_LastArrivalTime > 0) { Interval = ArrivalTime - IGPlus_SnapInterp_LastArrivalTime; if (Interval > 0 && Interval < 1.0) { IGPlus_SnapInterp_MeanInterval += 0.1 * (Interval - IGPlus_SnapInterp_MeanInterval); IGPlus_SnapInterp_JitterEstimate += 0.1 * (Abs(Interval - IGPlus_SnapInterp_MeanInterval) - IGPlus_SnapInterp_JitterEstimate); - IGPlus_SnapInterp_InterpDelay = FMax(2.0 * IGPlus_SnapInterp_MeanInterval + 2.0 * IGPlus_SnapInterp_JitterEstimate, 0.020); - IGPlus_SnapInterp_InterpDelay = FMin(IGPlus_SnapInterp_InterpDelay, 0.200); + ExpectedInterval = IGPlus_SnapInterp_GetExpectedInterval(); + IGPlus_SnapInterp_UpdateTargetDelay(ExpectedInterval); } } - IGPlus_SnapInterp_LastArrivalTime = ArrivalTime; + if (bUpdateDelayStats) + IGPlus_SnapInterp_LastArrivalTime = ArrivalTime; } simulated function int IGPlus_SnapInterp_LerpAxis(int A, int B, float Alpha) { @@ -8990,46 +9226,26 @@ simulated function IGPlus_SnapInterp_Reset() { simulated function IGPlus_SnapInterp_After(float DeltaTime) { local bool bReplicatedLocation; + local bool bCanRecoverPrev; local int RepCounter; + local int PrevCounter; local byte InterpMode; local float RenderTime; local float ArrivalTime; local float SnapshotTime; + local float PrevSnapshotTime; + local float ExpectedInterval; local vector OutPos; local vector OutVel; local rotator OutRot; local vector FloorSnapLoc; - local bbPlayer LP; - local string TargetName; - local string DebugMode; local vector FloorHitLoc; local vector FloorHitNorm; local Actor FloorActor; local vector TraceExtent; - local int DebugSnapHz; - local int DebugExtrap; - local int DebugInterp; - local int DebugClamp; - local int DebugFail; - local int DebugHardSnap; - local int ModeTotal; - local int InterpPct; - local int ExtrapPct; - local int DebugYaw; - local int DebugPitch; - local int DebugAgeMs; - local int NewestIdx; - local IGPlus_InterpSnapshot NewestSnap; - - if (IGPlus_LocationOffsetFix_Moved == false) - return; - - if (IGPlus_LocationOffsetFix_CollisionDummy != none) { - IGPlus_LocationOffsetFix_CollisionDummy.bCollideWorld = false; - IGPlus_LocationOffsetFix_CollisionDummy.SetCollision(false, false, false); - } RepCounter = IGPlus_SnapReplicatedData.Counter; + PrevCounter = IGPlus_SnapReplicatedDataPrev.Counter; bReplicatedLocation = (RepCounter > IGPlus_SnapInterp_LastCounter) || (IGPlus_SnapInterp_LastCounter == 0 && RepCounter != 0); @@ -9037,14 +9253,34 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { ArrivalTime = Level.TimeSeconds; // Use arrival time for interpolation to avoid server clock quantization issues. SnapshotTime = ArrivalTime; + bCanRecoverPrev = true; if (IGPlus_SnapReplicatedData.bHardSnap) { - IGPlus_SnapInterp_DebugHardSnapCount += 1; IGPlus_SnapInterp_Reset(); + bCanRecoverPrev = false; + } + + if (bCanRecoverPrev && + IGPlus_SnapInterp_LastCounter > 0 && + PrevCounter > IGPlus_SnapInterp_LastCounter && + PrevCounter < RepCounter + ) { + ExpectedInterval = IGPlus_SnapInterp_GetExpectedInterval(); + PrevSnapshotTime = ArrivalTime - ExpectedInterval; + IGPlus_SnapInterp_Push( + IGPlus_SnapReplicatedDataPrev.Location, + IGPlus_SnapReplicatedDataPrev.Velocity, + IGPlus_SnapReplicatedDataPrev.Rotation, + PrevSnapshotTime, + ArrivalTime, + true, + IGPlus_SnapReplicatedDataPrev.bBasedOnMover, + IGPlus_SnapReplicatedDataPrev.MoverLocation, + IGPlus_SnapReplicatedDataPrev.BaseMover, + false); } IGPlus_SnapInterp_LastCounter = RepCounter; - IGPlus_SnapInterp_DebugSnapCount += 1; IGPlus_SnapInterp_Push( IGPlus_SnapReplicatedData.Location, IGPlus_SnapReplicatedData.Velocity, @@ -9054,7 +9290,16 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { true, IGPlus_SnapReplicatedData.bBasedOnMover, IGPlus_SnapReplicatedData.MoverLocation, - IGPlus_SnapReplicatedData.BaseMover); + IGPlus_SnapReplicatedData.BaseMover, + true); + } + + if (IGPlus_LocationOffsetFix_Moved == false) + return; + + if (IGPlus_LocationOffsetFix_CollisionDummy != none) { + IGPlus_LocationOffsetFix_CollisionDummy.bCollideWorld = false; + IGPlus_LocationOffsetFix_CollisionDummy.SetCollision(false, false, false); } if (IGPlus_SnapInterp_Count < 2) { @@ -9084,17 +9329,16 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { SetRotation(OutRot); DesiredRotation = OutRot; Velocity = OutVel; - if (InterpMode == 1) - IGPlus_SnapInterp_DebugInterpCount += 1; - else if (InterpMode == 2) - IGPlus_SnapInterp_DebugExtrapolationCount += 1; - else if (InterpMode == 3) - IGPlus_SnapInterp_DebugClampCount += 1; + if (InterpMode == 2) { + IGPlus_SnapInterp_BoostDelayForStress(); + } else if (InterpMode == 3) { + IGPlus_SnapInterp_BoostDelayForStress(); + } } else { bCollideWorld = false; SetLocation(IGPlus_LocationOffsetFix_OldLocation); bCollideWorld = true; - IGPlus_SnapInterp_DebugFailCount += 1; + IGPlus_SnapInterp_BoostDelayForStress(); } // Floor-snap: SetLocation with bCollideWorld=false bypasses UE1 floor tracing. @@ -9124,72 +9368,6 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { } } - LP = bbPlayer(GetLocalPlayer()); - if (Level.NetMode == NM_Client && - LP != none && - LP.IGPlus_SnapInterp_Debug && - (LP.ViewTarget == self || LP.ViewTarget == LP || LP.ViewTarget == none) && - Level.TimeSeconds >= LP.IGPlus_SnapInterp_DebugNextTime - ) { - LP.IGPlus_SnapInterp_DebugNextTime = Level.TimeSeconds + 1.0; - TargetName = "Unknown"; - if (PlayerReplicationInfo != none) - TargetName = PlayerReplicationInfo.PlayerName; - if (LP.ViewTarget == self) - DebugMode = "spectate"; - else - DebugMode = "play"; - - DebugSnapHz = IGPlus_SnapInterp_DebugSnapCount; - DebugExtrap = IGPlus_SnapInterp_DebugExtrapolationCount; - DebugInterp = IGPlus_SnapInterp_DebugInterpCount; - DebugClamp = IGPlus_SnapInterp_DebugClampCount; - DebugFail = IGPlus_SnapInterp_DebugFailCount; - DebugHardSnap = IGPlus_SnapInterp_DebugHardSnapCount; - ModeTotal = DebugInterp + DebugExtrap; - if (ModeTotal > 0) { - InterpPct = int(100.0 * float(DebugInterp) / float(ModeTotal)); - ExtrapPct = int(100.0 * float(DebugExtrap) / float(ModeTotal)); - } else { - InterpPct = 0; - ExtrapPct = 0; - } - DebugAgeMs = 0; - if (IGPlus_SnapInterp_Count > 0) { - NewestIdx = (IGPlus_SnapInterp_DataIndex - 1 + arraycount(IGPlus_SnapInterp_Data)) % arraycount(IGPlus_SnapInterp_Data); - NewestSnap = IGPlus_SnapInterp_Data[NewestIdx]; - if (NewestSnap != None && NewestSnap.Time > 0) - DebugAgeMs = int((RenderTime - NewestSnap.Time) * 1000.0); - } - - DebugYaw = Utils.RotU2S(Rotation.Yaw); - DebugPitch = Utils.RotU2S(Rotation.Pitch); - - LP.ClientMessage( - "SnapInterp target="$TargetName$ - " mode="$DebugMode$ - " count="$IGPlus_SnapInterp_Count$ - " delayMs="$int(IGPlus_SnapInterp_InterpDelay * 1000.0)$ - " snapHz="$DebugSnapHz$ - " ageMs="$DebugAgeMs$ - " hardSnap="$DebugHardSnap$ - " extrap="$DebugExtrap$ - " interpPct="$InterpPct$ - " extrapPct="$ExtrapPct$ - " yaw="$DebugYaw$ - " pitch="$DebugPitch$ - " clamp="$DebugClamp$ - " fail="$DebugFail - ); - - IGPlus_SnapInterp_DebugSnapCount = 0; - IGPlus_SnapInterp_DebugExtrapolationCount = 0; - IGPlus_SnapInterp_DebugInterpCount = 0; - IGPlus_SnapInterp_DebugClampCount = 0; - IGPlus_SnapInterp_DebugFailCount = 0; - IGPlus_SnapInterp_DebugHardSnapCount = 0; - } - IGPlus_LocationOffsetFix_Moved = false; if (IGPlus_LocationOffsetFix_FootstepQueued) { @@ -11090,16 +11268,81 @@ exec function PrintWeaponState() { if (Weapon != none) ClientMessage(Weapon.Name@Weapon.GetStateName()); } -exec function ToggleSnapInterpDebug() { - IGPlus_SnapInterp_Debug = !IGPlus_SnapInterp_Debug; - IGPlus_SnapInterp_DebugNextTime = 0; +exec function SnapInterpDebug() { + IGPlus_SnapInterp_NetDebug = !IGPlus_SnapInterp_NetDebug; + IGPlus_SnapInterp_NetDebugNextTime = 0; - if (IGPlus_SnapInterp_Debug) - ClientMessage("SnapInterp debug enabled (shows while playing and spectating)"); + if (IGPlus_SnapInterp_NetDebug) + ClientMessage("SnapInterp debug enabled (client report + server echo + proxy status)"); else ClientMessage("SnapInterp debug disabled"); } +exec simulated function SnapInterpStatus(optional string TargetFilter) { + local int i; + local PlayerReplicationInfo PRI; + local bbPlayer P; + local bbPlayer Target; + local string TargetName; + local string DebugMode; + local string FilterCaps; + + if (TargetFilter != "") + FilterCaps = Caps(TargetFilter); + + P = bbPlayer(ViewTarget); + if (P != none && + P.Role == ROLE_SimulatedProxy && + (FilterCaps == "" || (P.PlayerReplicationInfo != none && InStr(Caps(P.PlayerReplicationInfo.PlayerName), FilterCaps) >= 0)) + ) { + Target = P; + } + + if (Target == none && GameReplicationInfo != None && PlayerReplicationInfo != None) { + for (i = 0; i < arraycount(GameReplicationInfo.PRIArray); ++i) { + PRI = GameReplicationInfo.PRIArray[i]; + if (PRI == none) + break; + P = bbPlayer(PRI.Owner); + if (P == none) + continue; + if (P == self) + continue; + if (P.Role != ROLE_SimulatedProxy) + continue; + if (FilterCaps != "" && (P.PlayerReplicationInfo == none || InStr(Caps(P.PlayerReplicationInfo.PlayerName), FilterCaps) < 0)) + continue; + Target = P; + break; + } + } + + if (Target == none) { + ClientMessage("SnapInterp status: no simulated proxy target found"); + return; + } + + TargetName = "Unknown"; + if (Target.PlayerReplicationInfo != none) + TargetName = Target.PlayerReplicationInfo.PlayerName; + + if (ViewTarget == Target) + DebugMode = "spectate"; + else + DebugMode = "play"; + + ClientMessage( + "SnapInterp status target="$TargetName$ + " mode="$DebugMode$ + " snapEnabled="$Target.IGPlus_EnableSnapshotInterpolation$ + " count="$Target.IGPlus_SnapInterp_Count$ + " rep="$Target.IGPlus_SnapReplicatedData.Counter$ + " last="$Target.IGPlus_SnapInterp_LastCounter$ + " moved="$Target.IGPlus_LocationOffsetFix_Moved$ + " delayMs="$int(Target.IGPlus_SnapInterp_InterpDelay * 1000.0) + ); +} + exec function bEnableLoosePositionCheck(optional string Value) { IGPlus_SetNetcodeSetting("bEnableLoosePositionCheck", Value); } @@ -11171,6 +11414,9 @@ function IGPlus_PrintNetcodeHelp() { ClientMessage("SnapshotInterpRewindMs="$S.GetPropertyText("SnapshotInterpRewindMs")); ClientMessage("MaxJitterTime="$S.GetPropertyText("MaxJitterTime")); ClientMessage("bEnableJitterBounding="$S.GetPropertyText("bEnableJitterBounding")); + ClientMessage("SnapInterpDebug (toggle client report + server echo + proxy status)"); + ClientMessage("SnapInterpStatus [NameFilter] (one-shot proxy runtime status)"); + ClientMessage("SnapInterpNetStatus [NameFilter] (client report + server echo + proxy status)"); } exec function IGPlus_SetNetcodeSetting(string Key, optional string Value) { @@ -11223,12 +11469,24 @@ function IGPlus_ApplyNetcodeSettingsToPlayers() { bOldSnap = BP.IGPlus_EnableSnapshotInterpolation; BP.IGPlus_EnableSnapshotInterpolation = zzUTPure.Settings.bEnableSnapshotInterpolation; - if (bOldSnap != BP.IGPlus_EnableSnapshotInterpolation) { - BP.IGPlus_SnapInterp_ServerNextTime = 0; - BP.IGPlus_SnapInterp_ServerHasLast = false; + if (bOldSnap != BP.IGPlus_EnableSnapshotInterpolation) { + BP.IGPlus_SnapInterp_ServerNextTime = 0; + BP.IGPlus_SnapInterp_ServerHasLast = false; + BP.IGPlus_ServerSnapInterpDelayMsSmoothed = 0.0; + BP.IGPlus_ServerSnapInterpDelayValid = false; + BP.IGPlus_NextSnapInterpDelayReportTime = 0.0; + BP.IGPlus_NextSnapInterpDelayClientReportTime = 0.0; + BP.IGPlus_ClientSnapInterpDelayMs = -1; + BP.IGPlus_ClientSnapInterpDelaySampleCount = 0; + BP.IGPlus_ClientSnapInterpDelayMedianMs = -1; + BP.IGPlus_ServerSnapInterpDelayReportedMsEcho = -1; + BP.IGPlus_ServerSnapInterpDelayClampedMsEcho = -1; + BP.IGPlus_ServerSnapInterpDelaySmoothedMsEcho = 0; + BP.IGPlus_ServerSnapInterpDelayValidEcho = false; + BP.IGPlus_ServerSnapInterpDelayEchoTime = 0.0; + } } } -} function IGPlus_ApplyNetcodeSetting(string Key, string Value) { local string NormalKey; @@ -11414,4 +11672,8 @@ defaultproperties IGPlus_LocationOffsetFix_PredCompatMode=True IGPlus_EnableInputReplication=True + IGPlus_ClientSnapInterpDelayMs=-1 + IGPlus_ClientSnapInterpDelayMedianMs=-1 + IGPlus_ServerSnapInterpDelayReportedMsEcho=-1 + IGPlus_ServerSnapInterpDelayClampedMsEcho=-1 } From a4f7b510d867c2c1e64b9b3ad879dd3bed13e88f Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:25:01 -0600 Subject: [PATCH 08/14] Harden snapshot telemetry and add animation-aware stress fallback --- Classes/IGPlus_InterpSnapshot.uc | 2 + Classes/IGPlus_WeaponImplementationBase.uc | 11 +- Classes/bbPlayer.uc | 537 ++++++++++++++++++--- 3 files changed, 473 insertions(+), 77 deletions(-) diff --git a/Classes/IGPlus_InterpSnapshot.uc b/Classes/IGPlus_InterpSnapshot.uc index fd71b8c..78b542e 100644 --- a/Classes/IGPlus_InterpSnapshot.uc +++ b/Classes/IGPlus_InterpSnapshot.uc @@ -5,6 +5,8 @@ var vector Vel; var vector RelLoc; var vector MoverLoc; var rotator Rot; +var name AnimSeq; +var byte AnimFrameByte; var float Time; var bool bBasedOnMover; var Actor BaseMover; diff --git a/Classes/IGPlus_WeaponImplementationBase.uc b/Classes/IGPlus_WeaponImplementationBase.uc index 3b7cd2d..2c7f808 100644 --- a/Classes/IGPlus_WeaponImplementationBase.uc +++ b/Classes/IGPlus_WeaponImplementationBase.uc @@ -19,6 +19,7 @@ var float CachedTickRate; function float IGPlus_GetSnapshotInterpRewindMs(Pawn Instigator) { local bbPlayer bbP; + local float AcceptedAge; if (Instigator == None) return 0.0; @@ -27,8 +28,16 @@ function float IGPlus_GetSnapshotInterpRewindMs(Pawn Instigator) { if (bbP == None || bbP.IGPlus_EnableSnapshotInterpolation == false) return 0.0; - if (bbP.IGPlus_ServerSnapInterpDelayValid) + AcceptedAge = bbP.Level.TimeSeconds - bbP.IGPlus_ServerSnapInterpLastAcceptedTime; + if (AcceptedAge < 0.0) + AcceptedAge = 0.0; + + if (bbP.IGPlus_ServerSnapInterpDelayValid && + bbP.IGPlus_ServerSnapInterpTrusted && + AcceptedAge <= 1.0 + ) { return FMax(0.0, bbP.IGPlus_ServerSnapInterpDelayMsSmoothed); + } if (bbP.zzUTPure != None && bbP.zzUTPure.Settings != None) return FMax(0.0, bbP.zzUTPure.Settings.SnapshotInterpRewindMs); diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 587040b..7b555a0 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -314,6 +314,8 @@ struct IGPlus_SnapData { var vector Location; var vector Velocity; var rotator Rotation; + var name AnimSequence; + var byte AnimFrameByte; var vector MoverLocation; var int Counter; var bool bHardSnap; @@ -369,18 +371,40 @@ var vector IGPlus_SnapInterp_ServerLastLocation; var float IGPlus_SnapInterp_ServerLastTime; var bool IGPlus_SnapInterp_ServerHasLast; var bool IGPlus_SnapInterp_EnabledLast; +var int IGPlus_SnapInterp_StatSnapPackets; +var int IGPlus_SnapInterp_StatInterp; +var int IGPlus_SnapInterp_StatExtrap; +var int IGPlus_SnapInterp_StatClamp; +var int IGPlus_SnapInterp_StatFail; +var int IGPlus_SnapInterp_StatHardSnap; var int IGPlus_ClientSnapInterpDelayMs; var int IGPlus_ClientSnapInterpDelaySampleCount; var int IGPlus_ClientSnapInterpDelayMedianMs; +var int IGPlus_ClientSnapInterpDelaySpreadMs; var float IGPlus_ServerSnapInterpDelayMsSmoothed; var bool IGPlus_ServerSnapInterpDelayValid; +var bool IGPlus_ServerSnapInterpTrusted; +var float IGPlus_ServerSnapInterpLastAcceptedTime; var float IGPlus_NextSnapInterpDelayReportTime; var float IGPlus_NextSnapInterpDelayClientReportTime; var int IGPlus_ServerSnapInterpDelayReportedMsEcho; var int IGPlus_ServerSnapInterpDelayClampedMsEcho; var int IGPlus_ServerSnapInterpDelaySmoothedMsEcho; var bool IGPlus_ServerSnapInterpDelayValidEcho; +var bool IGPlus_ServerSnapInterpTrustedEcho; var float IGPlus_ServerSnapInterpDelayEchoTime; +var float IGPlus_ServerSnapInterpLastAcceptedTimeEcho; +var bool IGPlus_SnapInterp_CheckActive; +var float IGPlus_SnapInterp_CheckDuration; +var float IGPlus_SnapInterp_CheckEndTime; +var string IGPlus_SnapInterp_CheckProfile; +var bbPlayer IGPlus_SnapInterp_CheckTarget; +var int IGPlus_SnapInterp_CheckBaseSnapPackets; +var int IGPlus_SnapInterp_CheckBaseInterp; +var int IGPlus_SnapInterp_CheckBaseExtrap; +var int IGPlus_SnapInterp_CheckBaseClamp; +var int IGPlus_SnapInterp_CheckBaseFail; +var int IGPlus_SnapInterp_CheckBaseHardSnap; var bool IGPlus_AlwaysRenderFlagCarrier; var bool IGPlus_AlwaysRenderDroppedFlags; @@ -469,12 +493,14 @@ replication zzTrackFOV, zzWaitTime, IGPlus_ServerSnapInterpDelayReportedMsEcho, - IGPlus_ServerSnapInterpDelayClampedMsEcho, - IGPlus_ServerSnapInterpDelaySmoothedMsEcho, - IGPlus_ServerSnapInterpDelayValidEcho, - IGPlus_ServerSnapInterpDelayEchoTime, - bEnableDamageDebugMode, - bEnableDamageDebugConsoleMessages; + IGPlus_ServerSnapInterpDelayClampedMsEcho, + IGPlus_ServerSnapInterpDelaySmoothedMsEcho, + IGPlus_ServerSnapInterpDelayValidEcho, + IGPlus_ServerSnapInterpTrustedEcho, + IGPlus_ServerSnapInterpDelayEchoTime, + IGPlus_ServerSnapInterpLastAcceptedTimeEcho, + bEnableDamageDebugMode, + bEnableDamageDebugConsoleMessages; unreliable if ( Role == ROLE_Authority ) DuckFractionRepl, @@ -502,6 +528,8 @@ replication IGPlus_DamageEvent_ShowOnDeath, IGPlus_EnableDualButtonSwitch, IGPlus_ClientSnapInterpDelayMs, + IGPlus_ClientSnapInterpDelaySampleCount, + IGPlus_ClientSnapInterpDelaySpreadMs, PingAverage, zzbDemoRecording, zzNetspeed, @@ -6466,13 +6494,15 @@ event ServerTick(float DeltaTime) { bOnMover = Mover(Base) != none; IGPlus_SnapReplicatedDataPrev = IGPlus_SnapReplicatedData; - IGPlus_SnapReplicatedData.Location = Location; - IGPlus_SnapReplicatedData.Velocity = Velocity; - IGPlus_SnapReplicatedData.Rotation = Rotation; - IGPlus_SnapReplicatedData.Rotation.Roll = 0; - IGPlus_SnapReplicatedData.Counter += 1; - IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; - IGPlus_SnapReplicatedData.bBasedOnMover = bOnMover; + IGPlus_SnapReplicatedData.Location = Location; + IGPlus_SnapReplicatedData.Velocity = Velocity; + IGPlus_SnapReplicatedData.Rotation = Rotation; + IGPlus_SnapReplicatedData.Rotation.Roll = 0; + IGPlus_SnapReplicatedData.AnimSequence = AnimSequence; + IGPlus_SnapReplicatedData.AnimFrameByte = byte(FClamp(AnimFrame * 255.0, 0.0, 255.0)); + IGPlus_SnapReplicatedData.Counter += 1; + IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; + IGPlus_SnapReplicatedData.bBasedOnMover = bOnMover; if (bOnMover) { IGPlus_SnapReplicatedData.MoverLocation = Base.Location; IGPlus_SnapReplicatedData.BaseMover = Base; @@ -8313,6 +8343,7 @@ simulated function IGPlus_UpdateClientSnapInterpDelayReport() { IGPlus_ClientSnapInterpDelayMs = -1; IGPlus_ClientSnapInterpDelaySampleCount = 0; IGPlus_ClientSnapInterpDelayMedianMs = -1; + IGPlus_ClientSnapInterpDelaySpreadMs = -1; return; } @@ -8349,6 +8380,7 @@ simulated function IGPlus_UpdateClientSnapInterpDelayReport() { IGPlus_ClientSnapInterpDelayMs = -1; IGPlus_ClientSnapInterpDelaySampleCount = 0; IGPlus_ClientSnapInterpDelayMedianMs = -1; + IGPlus_ClientSnapInterpDelaySpreadMs = -1; return; } @@ -8369,6 +8401,7 @@ simulated function IGPlus_UpdateClientSnapInterpDelayReport() { IGPlus_ClientSnapInterpDelaySampleCount = SampleCount; IGPlus_ClientSnapInterpDelayMedianMs = MedianDelayMs; + IGPlus_ClientSnapInterpDelaySpreadMs = int(DelaySamples[SampleCount - 1] - DelaySamples[0]); IGPlus_ClientSnapInterpDelayMs = Clamp(MedianDelayMs, 0, 200); } @@ -8379,31 +8412,55 @@ function IGPlus_UpdateServerSnapInterpDelayEstimate() { local float ReportedMs; local float SmoothedTargetMs; local float SmoothedDeltaMs; + local int MinSampleCount; + local int MaxSpreadMs; + local float TrustTimeout; if (Role != ROLE_Authority) return; + MinSampleCount = 1; + MaxSpreadMs = 35; + TrustTimeout = 1.0; + if (IGPlus_EnableSnapshotInterpolation == false || zzUTPure == None || zzUTPure.Settings == None) { IGPlus_ServerSnapInterpDelayValid = false; + IGPlus_ServerSnapInterpTrusted = false; + IGPlus_ServerSnapInterpLastAcceptedTime = 0.0; IGPlus_ServerSnapInterpDelayMsSmoothed = 0.0; IGPlus_ServerSnapInterpDelayReportedMsEcho = -1; IGPlus_ServerSnapInterpDelayClampedMsEcho = -1; IGPlus_ServerSnapInterpDelaySmoothedMsEcho = 0; IGPlus_ServerSnapInterpDelayValidEcho = false; + IGPlus_ServerSnapInterpTrustedEcho = false; IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; + IGPlus_ServerSnapInterpLastAcceptedTimeEcho = 0.0; return; } + if (IGPlus_ServerSnapInterpTrusted && + (Level.TimeSeconds - IGPlus_ServerSnapInterpLastAcceptedTime > TrustTimeout) + ) { + IGPlus_ServerSnapInterpTrusted = false; + } + if (Level.TimeSeconds < IGPlus_NextSnapInterpDelayReportTime) return; IGPlus_NextSnapInterpDelayReportTime = Level.TimeSeconds + 0.25; IGPlus_ServerSnapInterpDelayReportedMsEcho = IGPlus_ClientSnapInterpDelayMs; - if (IGPlus_ClientSnapInterpDelayMs < 0) { + if (IGPlus_ClientSnapInterpDelayMs < 0 || + IGPlus_ClientSnapInterpDelaySampleCount < MinSampleCount || + IGPlus_ClientSnapInterpDelaySpreadMs < 0 || + IGPlus_ClientSnapInterpDelaySpreadMs > MaxSpreadMs + ) { + IGPlus_ServerSnapInterpTrusted = false; IGPlus_ServerSnapInterpDelayClampedMsEcho = -1; IGPlus_ServerSnapInterpDelaySmoothedMsEcho = int(IGPlus_ServerSnapInterpDelayMsSmoothed + 0.5); - IGPlus_ServerSnapInterpDelayValidEcho = IGPlus_ServerSnapInterpDelayValid; + IGPlus_ServerSnapInterpDelayValidEcho = false; + IGPlus_ServerSnapInterpTrustedEcho = false; IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; + IGPlus_ServerSnapInterpLastAcceptedTimeEcho = IGPlus_ServerSnapInterpLastAcceptedTime; return; } @@ -8416,9 +8473,13 @@ function IGPlus_UpdateServerSnapInterpDelayEstimate() { if (IGPlus_ServerSnapInterpDelayValid == false) { IGPlus_ServerSnapInterpDelayMsSmoothed = ReportedMs; IGPlus_ServerSnapInterpDelayValid = true; + IGPlus_ServerSnapInterpTrusted = true; + IGPlus_ServerSnapInterpLastAcceptedTime = Level.TimeSeconds; IGPlus_ServerSnapInterpDelaySmoothedMsEcho = int(IGPlus_ServerSnapInterpDelayMsSmoothed + 0.5); IGPlus_ServerSnapInterpDelayValidEcho = true; + IGPlus_ServerSnapInterpTrustedEcho = true; IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; + IGPlus_ServerSnapInterpLastAcceptedTimeEcho = IGPlus_ServerSnapInterpLastAcceptedTime; return; } @@ -8429,31 +8490,42 @@ function IGPlus_UpdateServerSnapInterpDelayEstimate() { MinAllowedMs, MaxAllowedMs ); + IGPlus_ServerSnapInterpTrusted = true; + IGPlus_ServerSnapInterpLastAcceptedTime = Level.TimeSeconds; IGPlus_ServerSnapInterpDelaySmoothedMsEcho = int(IGPlus_ServerSnapInterpDelayMsSmoothed + 0.5); - IGPlus_ServerSnapInterpDelayValidEcho = IGPlus_ServerSnapInterpDelayValid; + IGPlus_ServerSnapInterpDelayValidEcho = true; + IGPlus_ServerSnapInterpTrustedEcho = true; IGPlus_ServerSnapInterpDelayEchoTime = Level.TimeSeconds; + IGPlus_ServerSnapInterpLastAcceptedTimeEcho = IGPlus_ServerSnapInterpLastAcceptedTime; } exec simulated function SnapInterpNetStatus(optional string TargetFilter) { local int EchoAgeMs; + local int AcceptedAgeMs; EchoAgeMs = int((Level.TimeSeconds - IGPlus_ServerSnapInterpDelayEchoTime) * 1000.0); if (EchoAgeMs < 0) EchoAgeMs = 0; + AcceptedAgeMs = int((Level.TimeSeconds - IGPlus_ServerSnapInterpLastAcceptedTimeEcho) * 1000.0); + if (AcceptedAgeMs < 0) + AcceptedAgeMs = 0; ClientMessage( "SnapInterpNet client reportMs="$IGPlus_ClientSnapInterpDelayMs$ " medianMs="$IGPlus_ClientSnapInterpDelayMedianMs$ " samples="$IGPlus_ClientSnapInterpDelaySampleCount$ + " spreadMs="$IGPlus_ClientSnapInterpDelaySpreadMs$ " snapEnabled="$IGPlus_EnableSnapshotInterpolation ); ClientMessage( "SnapInterpNet server valid="$IGPlus_ServerSnapInterpDelayValidEcho$ + " trusted="$IGPlus_ServerSnapInterpTrustedEcho$ " reportedMs="$IGPlus_ServerSnapInterpDelayReportedMsEcho$ " clampedMs="$IGPlus_ServerSnapInterpDelayClampedMsEcho$ " smoothedMs="$IGPlus_ServerSnapInterpDelaySmoothedMsEcho$ - " ageMs="$EchoAgeMs + " ageMs="$EchoAgeMs$ + " acceptedAgeMs="$AcceptedAgeMs ); SnapInterpStatus(TargetFilter); @@ -8477,6 +8549,9 @@ event PreRender( canvas zzCanvas ) if (Role == ROLE_AutonomousProxy && Level.NetMode == NM_Client) IGPlus_UpdateClientSnapInterpDelayReport(); + if (IGPlus_SnapInterp_CheckActive && Level.TimeSeconds >= IGPlus_SnapInterp_CheckEndTime) + IGPlus_SnapInterp_FinishCheck(false); + if (IGPlus_SnapInterp_NetDebug && Level.TimeSeconds >= IGPlus_SnapInterp_NetDebugNextTime) { IGPlus_SnapInterp_NetDebugNextTime = Level.TimeSeconds + 1.0; SnapInterpNetStatus(); @@ -9004,6 +9079,8 @@ simulated function IGPlus_SnapInterp_Push( vector Pos, vector Vel, rotator SnapRot, + name SnapAnimSeq, + byte SnapAnimFrameByte, float SnapTime, float ArrivalTime, optional bool bFromServerMoverState, @@ -9029,6 +9106,8 @@ simulated function IGPlus_SnapInterp_Push( Snap.Vel = Vel; Snap.Rot = SnapRot; Snap.Rot.Roll = 0; + Snap.AnimSeq = SnapAnimSeq; + Snap.AnimFrameByte = SnapAnimFrameByte; Snap.Time = SnapTime; if (bFromServerMoverState) { Snap.bBasedOnMover = bBasedOnMover; @@ -9113,7 +9192,15 @@ simulated function rotator IGPlus_SnapInterp_LerpRot(rotator A, rotator B, float return R; } -simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vector OutPos, out vector OutVel, out rotator OutRot, out byte InterpMode) { +simulated function bool IGPlus_SnapInterp_Interpolate( + float RenderTime, + out vector OutPos, + out vector OutVel, + out rotator OutRot, + out name OutAnimSeq, + out byte OutAnimFrameByte, + out byte InterpMode +) { local int BufSize; local int Idx, Scans; local IGPlus_InterpSnapshot OlderSnap; @@ -9123,6 +9210,8 @@ simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vect local vector MoverOffset; InterpMode = 0; + OutAnimSeq = ''; + OutAnimFrameByte = 0; if (IGPlus_SnapInterp_Count < 1) return false; @@ -9160,11 +9249,18 @@ simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vect OutPos += MoverOffset * Alpha; } - OutVel = OlderSnap.Vel + (NewerSnap.Vel - OlderSnap.Vel) * Alpha; - OutRot = IGPlus_SnapInterp_LerpRot(OlderSnap.Rot, NewerSnap.Rot, Alpha); - InterpMode = 1; - return true; - } + OutVel = OlderSnap.Vel + (NewerSnap.Vel - OlderSnap.Vel) * Alpha; + OutRot = IGPlus_SnapInterp_LerpRot(OlderSnap.Rot, NewerSnap.Rot, Alpha); + if (Alpha < 0.5) { + OutAnimSeq = OlderSnap.AnimSeq; + OutAnimFrameByte = OlderSnap.AnimFrameByte; + } else { + OutAnimSeq = NewerSnap.AnimSeq; + OutAnimFrameByte = NewerSnap.AnimFrameByte; + } + InterpMode = 1; + return true; + } } ExtrapolationTime = FClamp(RenderTime - OlderSnap.Time, 0.0, 0.050); if (OlderSnap.bBasedOnMover && OlderSnap.BaseMover != None) { @@ -9176,12 +9272,14 @@ simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vect } else { OutPos = OlderSnap.Loc + OlderSnap.Vel * ExtrapolationTime; } - OutVel = OlderSnap.Vel; - OutRot = OlderSnap.Rot; - OutRot.Roll = 0; - InterpMode = 2; - return true; - } + OutVel = OlderSnap.Vel; + OutRot = OlderSnap.Rot; + OutRot.Roll = 0; + OutAnimSeq = OlderSnap.AnimSeq; + OutAnimFrameByte = OlderSnap.AnimFrameByte; + InterpMode = 2; + return true; + } NewerSnap = OlderSnap; } @@ -9198,16 +9296,45 @@ simulated function bool IGPlus_SnapInterp_Interpolate(float RenderTime, out vect } else { OutPos = NewerSnap.Loc; } - OutVel = NewerSnap.Vel; - OutRot = NewerSnap.Rot; - OutRot.Roll = 0; - InterpMode = 3; - return true; - } + OutVel = NewerSnap.Vel; + OutRot = NewerSnap.Rot; + OutRot.Roll = 0; + OutAnimSeq = NewerSnap.AnimSeq; + OutAnimFrameByte = NewerSnap.AnimFrameByte; + InterpMode = 3; + return true; + } return false; } +simulated function IGPlus_SnapInterp_ApplyAnimHint(name SnapAnimSeq, byte SnapAnimFrameByte, optional byte InterpMode) { + local float TweenTime; + local name SnapAnimGroup; + + // Avoid touching normal high-quality interpolation path. + if (InterpMode <= 1) + return; + + if (SnapAnimSeq == '') + return; + + if (AnimSequence != SnapAnimSeq) { + SnapAnimGroup = GetAnimGroup(SnapAnimSeq); + TweenTime = 0.06; + if (SnapAnimGroup == 'Jumping') + TweenTime = 0.08; + else if (SnapAnimGroup == 'Ducking') + TweenTime = 0.12; + else if (SnapAnimGroup == 'Landing') + TweenTime = 0.05; + + TweenAnim(SnapAnimSeq, TweenTime); + } + + AnimFrame = FClamp(float(SnapAnimFrameByte) / 255.0, 0.0, 1.0); +} + simulated function IGPlus_SnapInterp_Reset() { local int i; @@ -9227,6 +9354,7 @@ simulated function IGPlus_SnapInterp_Reset() { simulated function IGPlus_SnapInterp_After(float DeltaTime) { local bool bReplicatedLocation; local bool bCanRecoverPrev; + local bool bHasInterpolated; local int RepCounter; local int PrevCounter; local byte InterpMode; @@ -9235,6 +9363,8 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { local float SnapshotTime; local float PrevSnapshotTime; local float ExpectedInterval; + local name OutAnimSeq; + local byte OutAnimFrameByte; local vector OutPos; local vector OutVel; local rotator OutRot; @@ -9246,6 +9376,7 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { RepCounter = IGPlus_SnapReplicatedData.Counter; PrevCounter = IGPlus_SnapReplicatedDataPrev.Counter; + bHasInterpolated = false; bReplicatedLocation = (RepCounter > IGPlus_SnapInterp_LastCounter) || (IGPlus_SnapInterp_LastCounter == 0 && RepCounter != 0); @@ -9256,6 +9387,7 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { bCanRecoverPrev = true; if (IGPlus_SnapReplicatedData.bHardSnap) { + IGPlus_SnapInterp_StatHardSnap += 1; IGPlus_SnapInterp_Reset(); bCanRecoverPrev = false; } @@ -9267,12 +9399,14 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { ) { ExpectedInterval = IGPlus_SnapInterp_GetExpectedInterval(); PrevSnapshotTime = ArrivalTime - ExpectedInterval; - IGPlus_SnapInterp_Push( - IGPlus_SnapReplicatedDataPrev.Location, - IGPlus_SnapReplicatedDataPrev.Velocity, - IGPlus_SnapReplicatedDataPrev.Rotation, - PrevSnapshotTime, - ArrivalTime, + IGPlus_SnapInterp_Push( + IGPlus_SnapReplicatedDataPrev.Location, + IGPlus_SnapReplicatedDataPrev.Velocity, + IGPlus_SnapReplicatedDataPrev.Rotation, + IGPlus_SnapReplicatedDataPrev.AnimSequence, + IGPlus_SnapReplicatedDataPrev.AnimFrameByte, + PrevSnapshotTime, + ArrivalTime, true, IGPlus_SnapReplicatedDataPrev.bBasedOnMover, IGPlus_SnapReplicatedDataPrev.MoverLocation, @@ -9281,12 +9415,15 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { } IGPlus_SnapInterp_LastCounter = RepCounter; - IGPlus_SnapInterp_Push( - IGPlus_SnapReplicatedData.Location, - IGPlus_SnapReplicatedData.Velocity, - IGPlus_SnapReplicatedData.Rotation, - SnapshotTime, - ArrivalTime, + IGPlus_SnapInterp_StatSnapPackets += 1; + IGPlus_SnapInterp_Push( + IGPlus_SnapReplicatedData.Location, + IGPlus_SnapReplicatedData.Velocity, + IGPlus_SnapReplicatedData.Rotation, + IGPlus_SnapReplicatedData.AnimSequence, + IGPlus_SnapReplicatedData.AnimFrameByte, + SnapshotTime, + ArrivalTime, true, IGPlus_SnapReplicatedData.bBasedOnMover, IGPlus_SnapReplicatedData.MoverLocation, @@ -9294,6 +9431,33 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { true); } + if (IGPlus_SnapInterp_Count >= 2) { + RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_InterpDelay; + bHasInterpolated = IGPlus_SnapInterp_Interpolate( + RenderTime, + OutPos, + OutVel, + OutRot, + OutAnimSeq, + OutAnimFrameByte, + InterpMode + ); + if (bHasInterpolated) { + if (InterpMode == 1) + IGPlus_SnapInterp_StatInterp += 1; + else if (InterpMode == 2) { + IGPlus_SnapInterp_StatExtrap += 1; + IGPlus_SnapInterp_BoostDelayForStress(); + } else if (InterpMode == 3) { + IGPlus_SnapInterp_StatClamp += 1; + IGPlus_SnapInterp_BoostDelayForStress(); + } + } else { + IGPlus_SnapInterp_StatFail += 1; + IGPlus_SnapInterp_BoostDelayForStress(); + } + } + if (IGPlus_LocationOffsetFix_Moved == false) return; @@ -9311,6 +9475,11 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { OutRot.Roll = 0; SetRotation(OutRot); DesiredRotation = OutRot; + IGPlus_SnapInterp_ApplyAnimHint( + IGPlus_SnapReplicatedData.AnimSequence, + IGPlus_SnapReplicatedData.AnimFrameByte, + 2 + ); } else { bCollideWorld = false; SetLocation(IGPlus_LocationOffsetFix_OldLocation); @@ -9320,25 +9489,18 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { return; } - RenderTime = Level.TimeSeconds - IGPlus_SnapInterp_InterpDelay; - - if (IGPlus_SnapInterp_Interpolate(RenderTime, OutPos, OutVel, OutRot, InterpMode)) { + if (bHasInterpolated) { bCollideWorld = false; SetLocation(OutPos); bCollideWorld = true; SetRotation(OutRot); DesiredRotation = OutRot; Velocity = OutVel; - if (InterpMode == 2) { - IGPlus_SnapInterp_BoostDelayForStress(); - } else if (InterpMode == 3) { - IGPlus_SnapInterp_BoostDelayForStress(); - } + IGPlus_SnapInterp_ApplyAnimHint(OutAnimSeq, OutAnimFrameByte, InterpMode); } else { bCollideWorld = false; SetLocation(IGPlus_LocationOffsetFix_OldLocation); bCollideWorld = true; - IGPlus_SnapInterp_BoostDelayForStress(); } // Floor-snap: SetLocation with bCollideWorld=false bypasses UE1 floor tracing. @@ -11278,13 +11440,11 @@ exec function SnapInterpDebug() { ClientMessage("SnapInterp debug disabled"); } -exec simulated function SnapInterpStatus(optional string TargetFilter) { +simulated function bbPlayer IGPlus_SnapInterp_FindTarget(optional string TargetFilter) { local int i; local PlayerReplicationInfo PRI; local bbPlayer P; local bbPlayer Target; - local string TargetName; - local string DebugMode; local string FilterCaps; if (TargetFilter != "") @@ -11294,7 +11454,7 @@ exec simulated function SnapInterpStatus(optional string TargetFilter) { if (P != none && P.Role == ROLE_SimulatedProxy && (FilterCaps == "" || (P.PlayerReplicationInfo != none && InStr(Caps(P.PlayerReplicationInfo.PlayerName), FilterCaps) >= 0)) - ) { + ) { Target = P; } @@ -11317,6 +11477,223 @@ exec simulated function SnapInterpStatus(optional string TargetFilter) { } } + return Target; +} + +simulated function string IGPlus_SnapInterp_NormalizeCheckProfile(optional string Profile) { + if (Profile ~= "lossy" || Profile ~= "loss" || Profile ~= "jitter") + return "lossy"; + + return "clean"; +} + +simulated function IGPlus_SnapInterp_GetCheckThresholds( + string Profile, + out float MinInterpPct, + out float MaxExtrapPct, + out float MaxClampPct, + out float MaxFailPct, + out float MaxHardSnapPerMin +) { + if (Profile ~= "lossy") { + MinInterpPct = 80.0; + MaxExtrapPct = 15.0; + MaxClampPct = 10.0; + MaxFailPct = 2.0; + MaxHardSnapPerMin = 3.0; + } else { + MinInterpPct = 95.0; + MaxExtrapPct = 4.0; + MaxClampPct = 3.0; + MaxFailPct = 0.5; + MaxHardSnapPerMin = 1.0; + } +} + +simulated function IGPlus_SnapInterp_FinishCheck(bool bStopped) { + local bbPlayer Target; + local string TargetName; + local float DurationSec; + local int DeltaSnapPackets; + local int DeltaInterp; + local int DeltaExtrap; + local int DeltaClamp; + local int DeltaFail; + local int DeltaHardSnap; + local int TotalModes; + local float InterpPct; + local float ExtrapPct; + local float ClampPct; + local float FailPct; + local float HardSnapPerMin; + local float MinInterpPct; + local float MaxExtrapPct; + local float MaxClampPct; + local float MaxFailPct; + local float MaxHardSnapPerMin; + local bool bPass; + local string FailReasons; + + if (IGPlus_SnapInterp_CheckActive == false) + return; + + IGPlus_SnapInterp_CheckActive = false; + Target = IGPlus_SnapInterp_CheckTarget; + + if (bStopped) { + ClientMessage("SnapInterpCheck stopped"); + return; + } + + if (Target == none || Target.bDeleteMe) { + ClientMessage("SnapInterpCheck aborted: target unavailable"); + return; + } + + TargetName = "Unknown"; + if (Target.PlayerReplicationInfo != none) + TargetName = Target.PlayerReplicationInfo.PlayerName; + + DurationSec = FMax(IGPlus_SnapInterp_CheckDuration, 0.001); + DeltaSnapPackets = Max(0, Target.IGPlus_SnapInterp_StatSnapPackets - IGPlus_SnapInterp_CheckBaseSnapPackets); + DeltaInterp = Max(0, Target.IGPlus_SnapInterp_StatInterp - IGPlus_SnapInterp_CheckBaseInterp); + DeltaExtrap = Max(0, Target.IGPlus_SnapInterp_StatExtrap - IGPlus_SnapInterp_CheckBaseExtrap); + DeltaClamp = Max(0, Target.IGPlus_SnapInterp_StatClamp - IGPlus_SnapInterp_CheckBaseClamp); + DeltaFail = Max(0, Target.IGPlus_SnapInterp_StatFail - IGPlus_SnapInterp_CheckBaseFail); + DeltaHardSnap = Max(0, Target.IGPlus_SnapInterp_StatHardSnap - IGPlus_SnapInterp_CheckBaseHardSnap); + TotalModes = DeltaInterp + DeltaExtrap + DeltaClamp + DeltaFail; + + if (TotalModes > 0) { + InterpPct = 100.0 * float(DeltaInterp) / float(TotalModes); + ExtrapPct = 100.0 * float(DeltaExtrap) / float(TotalModes); + ClampPct = 100.0 * float(DeltaClamp) / float(TotalModes); + FailPct = 100.0 * float(DeltaFail) / float(TotalModes); + } else { + InterpPct = 0.0; + ExtrapPct = 0.0; + ClampPct = 0.0; + FailPct = 0.0; + } + + HardSnapPerMin = 60.0 * float(DeltaHardSnap) / DurationSec; + IGPlus_SnapInterp_GetCheckThresholds( + IGPlus_SnapInterp_CheckProfile, + MinInterpPct, + MaxExtrapPct, + MaxClampPct, + MaxFailPct, + MaxHardSnapPerMin + ); + + bPass = true; + if (DeltaSnapPackets <= 0 || TotalModes <= 0) { + bPass = false; + FailReasons = "no_samples"; + } + if (InterpPct < MinInterpPct) { + bPass = false; + if (FailReasons != "") + FailReasons = FailReasons$","; + FailReasons = FailReasons$"interp"; + } + if (ExtrapPct > MaxExtrapPct) { + bPass = false; + if (FailReasons != "") + FailReasons = FailReasons$","; + FailReasons = FailReasons$"extrap"; + } + if (ClampPct > MaxClampPct) { + bPass = false; + if (FailReasons != "") + FailReasons = FailReasons$","; + FailReasons = FailReasons$"clamp"; + } + if (FailPct > MaxFailPct) { + bPass = false; + if (FailReasons != "") + FailReasons = FailReasons$","; + FailReasons = FailReasons$"fail"; + } + if (HardSnapPerMin > MaxHardSnapPerMin) { + bPass = false; + if (FailReasons != "") + FailReasons = FailReasons$","; + FailReasons = FailReasons$"hardsnap"; + } + + ClientMessage( + "SnapInterpCheck result target="$TargetName$ + " profile="$IGPlus_SnapInterp_CheckProfile$ + " durS="$int(DurationSec + 0.5)$ + " packets="$DeltaSnapPackets$ + " interpPct="$int(InterpPct + 0.5)$ + " extrapPct="$int(ExtrapPct + 0.5)$ + " clampPct="$int(ClampPct + 0.5)$ + " failPct="$int(FailPct + 0.5)$ + " hardPerMin="$int(HardSnapPerMin + 0.5)$ + " pass="$bPass + ); + if (bPass == false) + ClientMessage("SnapInterpCheck thresholds failed: "$FailReasons); +} + +exec simulated function SnapInterpCheck(optional string Profile, optional float DurationSeconds, optional string TargetFilter) { + local bbPlayer Target; + local string TargetName; + + if (DurationSeconds <= 0.0) + DurationSeconds = 30.0; + DurationSeconds = FClamp(DurationSeconds, 5.0, 180.0); + Profile = IGPlus_SnapInterp_NormalizeCheckProfile(Profile); + + Target = IGPlus_SnapInterp_FindTarget(TargetFilter); + if (Target == none) { + ClientMessage("SnapInterpCheck: no simulated proxy target found"); + return; + } + + if (Target.PlayerReplicationInfo != none) + TargetName = Target.PlayerReplicationInfo.PlayerName; + else + TargetName = "Unknown"; + + if (IGPlus_SnapInterp_CheckActive) + IGPlus_SnapInterp_FinishCheck(true); + + IGPlus_SnapInterp_CheckActive = true; + IGPlus_SnapInterp_CheckDuration = DurationSeconds; + IGPlus_SnapInterp_CheckEndTime = Level.TimeSeconds + DurationSeconds; + IGPlus_SnapInterp_CheckProfile = Profile; + IGPlus_SnapInterp_CheckTarget = Target; + IGPlus_SnapInterp_CheckBaseSnapPackets = Target.IGPlus_SnapInterp_StatSnapPackets; + IGPlus_SnapInterp_CheckBaseInterp = Target.IGPlus_SnapInterp_StatInterp; + IGPlus_SnapInterp_CheckBaseExtrap = Target.IGPlus_SnapInterp_StatExtrap; + IGPlus_SnapInterp_CheckBaseClamp = Target.IGPlus_SnapInterp_StatClamp; + IGPlus_SnapInterp_CheckBaseFail = Target.IGPlus_SnapInterp_StatFail; + IGPlus_SnapInterp_CheckBaseHardSnap = Target.IGPlus_SnapInterp_StatHardSnap; + + ClientMessage( + "SnapInterpCheck start target="$TargetName$ + " profile="$Profile$ + " durS="$int(DurationSeconds + 0.5) + ); +} + +exec simulated function SnapInterpCheckStop() { + if (IGPlus_SnapInterp_CheckActive == false) { + ClientMessage("SnapInterpCheck is not running"); + return; + } + + IGPlus_SnapInterp_FinishCheck(true); +} + +exec simulated function SnapInterpStatus(optional string TargetFilter) { + local bbPlayer Target; + local string TargetName; + local string DebugMode; + + Target = IGPlus_SnapInterp_FindTarget(TargetFilter); if (Target == none) { ClientMessage("SnapInterp status: no simulated proxy target found"); return; @@ -11417,6 +11794,8 @@ function IGPlus_PrintNetcodeHelp() { ClientMessage("SnapInterpDebug (toggle client report + server echo + proxy status)"); ClientMessage("SnapInterpStatus [NameFilter] (one-shot proxy runtime status)"); ClientMessage("SnapInterpNetStatus [NameFilter] (client report + server echo + proxy status)"); + ClientMessage("SnapInterpCheck [clean|lossy] [DurationSec] [NameFilter]"); + ClientMessage("SnapInterpCheckStop"); } exec function IGPlus_SetNetcodeSetting(string Key, optional string Value) { @@ -11469,24 +11848,30 @@ function IGPlus_ApplyNetcodeSettingsToPlayers() { bOldSnap = BP.IGPlus_EnableSnapshotInterpolation; BP.IGPlus_EnableSnapshotInterpolation = zzUTPure.Settings.bEnableSnapshotInterpolation; - if (bOldSnap != BP.IGPlus_EnableSnapshotInterpolation) { - BP.IGPlus_SnapInterp_ServerNextTime = 0; - BP.IGPlus_SnapInterp_ServerHasLast = false; - BP.IGPlus_ServerSnapInterpDelayMsSmoothed = 0.0; - BP.IGPlus_ServerSnapInterpDelayValid = false; - BP.IGPlus_NextSnapInterpDelayReportTime = 0.0; - BP.IGPlus_NextSnapInterpDelayClientReportTime = 0.0; - BP.IGPlus_ClientSnapInterpDelayMs = -1; - BP.IGPlus_ClientSnapInterpDelaySampleCount = 0; - BP.IGPlus_ClientSnapInterpDelayMedianMs = -1; - BP.IGPlus_ServerSnapInterpDelayReportedMsEcho = -1; - BP.IGPlus_ServerSnapInterpDelayClampedMsEcho = -1; - BP.IGPlus_ServerSnapInterpDelaySmoothedMsEcho = 0; - BP.IGPlus_ServerSnapInterpDelayValidEcho = false; - BP.IGPlus_ServerSnapInterpDelayEchoTime = 0.0; - } + if (bOldSnap != BP.IGPlus_EnableSnapshotInterpolation) { + BP.IGPlus_SnapInterp_ServerNextTime = 0; + BP.IGPlus_SnapInterp_ServerHasLast = false; + BP.IGPlus_SnapInterp_CheckActive = false; + BP.IGPlus_ServerSnapInterpDelayMsSmoothed = 0.0; + BP.IGPlus_ServerSnapInterpDelayValid = false; + BP.IGPlus_ServerSnapInterpTrusted = false; + BP.IGPlus_ServerSnapInterpLastAcceptedTime = 0.0; + BP.IGPlus_NextSnapInterpDelayReportTime = 0.0; + BP.IGPlus_NextSnapInterpDelayClientReportTime = 0.0; + BP.IGPlus_ClientSnapInterpDelayMs = -1; + BP.IGPlus_ClientSnapInterpDelaySampleCount = 0; + BP.IGPlus_ClientSnapInterpDelayMedianMs = -1; + BP.IGPlus_ClientSnapInterpDelaySpreadMs = -1; + BP.IGPlus_ServerSnapInterpDelayReportedMsEcho = -1; + BP.IGPlus_ServerSnapInterpDelayClampedMsEcho = -1; + BP.IGPlus_ServerSnapInterpDelaySmoothedMsEcho = 0; + BP.IGPlus_ServerSnapInterpDelayValidEcho = false; + BP.IGPlus_ServerSnapInterpTrustedEcho = false; + BP.IGPlus_ServerSnapInterpDelayEchoTime = 0.0; + BP.IGPlus_ServerSnapInterpLastAcceptedTimeEcho = 0.0; } } +} function IGPlus_ApplyNetcodeSetting(string Key, string Value) { local string NormalKey; From 90b831811fd122b296d60bafb960ba38c7ce04a2 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:18:53 -0600 Subject: [PATCH 09/14] Refine snapshot animation hint freshness and debug --- Classes/IGPlus_InterpSnapshot.uc | 3 + Classes/bbPlayer.uc | 332 +++++++++++++++++++++++++++++-- 2 files changed, 321 insertions(+), 14 deletions(-) diff --git a/Classes/IGPlus_InterpSnapshot.uc b/Classes/IGPlus_InterpSnapshot.uc index 78b542e..306223c 100644 --- a/Classes/IGPlus_InterpSnapshot.uc +++ b/Classes/IGPlus_InterpSnapshot.uc @@ -7,6 +7,9 @@ var vector MoverLoc; var rotator Rot; var name AnimSeq; var byte AnimFrameByte; +var byte AnimIntentBits; +var byte AnimDodgeDir; +var byte AnimPhysics; var float Time; var bool bBasedOnMover; var Actor BaseMover; diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 7b555a0..6b3de20 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -316,6 +316,9 @@ struct IGPlus_SnapData { var rotator Rotation; var name AnimSequence; var byte AnimFrameByte; + var byte AnimIntentBits; + var byte AnimDodgeDir; + var byte AnimPhysics; var vector MoverLocation; var int Counter; var bool bHardSnap; @@ -366,6 +369,9 @@ var float IGPlus_SnapInterp_JitterEstimate; var bool IGPlus_SnapInterp_NetDebug; var float IGPlus_SnapInterp_NetDebugNextTime; var int IGPlus_SnapInterp_LastCounter; +var byte IGPlus_SnapInterp_LastInterpMode; +var int IGPlus_SnapInterp_LastHintAgeMs; +var string IGPlus_SnapInterp_LastHintSource; var float IGPlus_SnapInterp_ServerNextTime; var vector IGPlus_SnapInterp_ServerLastLocation; var float IGPlus_SnapInterp_ServerLastTime; @@ -6463,6 +6469,9 @@ event ServerTick(float DeltaTime) { local bool bSendSnapshot; local bool bHardSnap; local bool bOnMover; + local byte SnapshotIntentBits; + local byte SnapshotDodgeDir; + local byte SnapshotPhysics; local float SnapshotDeltaTime; local float SnapshotSendHz; local float SnapshotInterval; @@ -6500,6 +6509,50 @@ event ServerTick(float DeltaTime) { IGPlus_SnapReplicatedData.Rotation.Roll = 0; IGPlus_SnapReplicatedData.AnimSequence = AnimSequence; IGPlus_SnapReplicatedData.AnimFrameByte = byte(FClamp(AnimFrame * 255.0, 0.0, 255.0)); + SnapshotIntentBits = 0; + if (bIsCrouching || DuckFraction > 0.25) + SnapshotIntentBits = SnapshotIntentBits | 1; + if (bDodging || DodgeDir == DODGE_Active) + SnapshotIntentBits = SnapshotIntentBits | 2; + + SnapshotDodgeDir = 0; + if (DodgeDir == DODGE_Left) + SnapshotDodgeDir = 1; + else if (DodgeDir == DODGE_Right) + SnapshotDodgeDir = 2; + else if (DodgeDir == DODGE_Forward) + SnapshotDodgeDir = 3; + else if (DodgeDir == DODGE_Back) + SnapshotDodgeDir = 4; + else if (DodgeDir == DODGE_Active) { + if (AnimSequence == 'DodgeL') + SnapshotDodgeDir = 1; + else if (AnimSequence == 'DodgeR') + SnapshotDodgeDir = 2; + else if (AnimSequence == 'DodgeB') + SnapshotDodgeDir = 4; + else if (AnimSequence == 'DodgeF' || AnimSequence == 'Flip') + SnapshotDodgeDir = 3; + } + + switch (Physics) { + case PHYS_None: SnapshotPhysics = 0; break; + case PHYS_Walking: SnapshotPhysics = 1; break; + case PHYS_Falling: SnapshotPhysics = 2; break; + case PHYS_Swimming: SnapshotPhysics = 3; break; + case PHYS_Flying: SnapshotPhysics = 4; break; + case PHYS_Rotating: SnapshotPhysics = 5; break; + case PHYS_Projectile: SnapshotPhysics = 6; break; + case PHYS_Rolling: SnapshotPhysics = 7; break; + case PHYS_Interpolating: SnapshotPhysics = 8; break; + case PHYS_MovingBrush: SnapshotPhysics = 9; break; + case PHYS_Spider: SnapshotPhysics = 10; break; + case PHYS_Trailer: SnapshotPhysics = 11; break; + default: SnapshotPhysics = 0; break; + } + IGPlus_SnapReplicatedData.AnimIntentBits = SnapshotIntentBits; + IGPlus_SnapReplicatedData.AnimDodgeDir = SnapshotDodgeDir; + IGPlus_SnapReplicatedData.AnimPhysics = SnapshotPhysics; IGPlus_SnapReplicatedData.Counter += 1; IGPlus_SnapReplicatedData.bHardSnap = bHardSnap; IGPlus_SnapReplicatedData.bBasedOnMover = bOnMover; @@ -9081,6 +9134,9 @@ simulated function IGPlus_SnapInterp_Push( rotator SnapRot, name SnapAnimSeq, byte SnapAnimFrameByte, + byte SnapAnimIntentBits, + byte SnapAnimDodgeDir, + byte SnapAnimPhysics, float SnapTime, float ArrivalTime, optional bool bFromServerMoverState, @@ -9108,6 +9164,9 @@ simulated function IGPlus_SnapInterp_Push( Snap.Rot.Roll = 0; Snap.AnimSeq = SnapAnimSeq; Snap.AnimFrameByte = SnapAnimFrameByte; + Snap.AnimIntentBits = SnapAnimIntentBits; + Snap.AnimDodgeDir = SnapAnimDodgeDir; + Snap.AnimPhysics = SnapAnimPhysics; Snap.Time = SnapTime; if (bFromServerMoverState) { Snap.bBasedOnMover = bBasedOnMover; @@ -9199,6 +9258,10 @@ simulated function bool IGPlus_SnapInterp_Interpolate( out rotator OutRot, out name OutAnimSeq, out byte OutAnimFrameByte, + out byte OutAnimIntentBits, + out byte OutAnimDodgeDir, + out byte OutAnimPhysics, + out float OutHintTime, out byte InterpMode ) { local int BufSize; @@ -9212,6 +9275,10 @@ simulated function bool IGPlus_SnapInterp_Interpolate( InterpMode = 0; OutAnimSeq = ''; OutAnimFrameByte = 0; + OutAnimIntentBits = 0; + OutAnimDodgeDir = 0; + OutAnimPhysics = 0; + OutHintTime = 0.0; if (IGPlus_SnapInterp_Count < 1) return false; @@ -9254,9 +9321,17 @@ simulated function bool IGPlus_SnapInterp_Interpolate( if (Alpha < 0.5) { OutAnimSeq = OlderSnap.AnimSeq; OutAnimFrameByte = OlderSnap.AnimFrameByte; + OutAnimIntentBits = OlderSnap.AnimIntentBits; + OutAnimDodgeDir = OlderSnap.AnimDodgeDir; + OutAnimPhysics = OlderSnap.AnimPhysics; + OutHintTime = OlderSnap.Time; } else { OutAnimSeq = NewerSnap.AnimSeq; OutAnimFrameByte = NewerSnap.AnimFrameByte; + OutAnimIntentBits = NewerSnap.AnimIntentBits; + OutAnimDodgeDir = NewerSnap.AnimDodgeDir; + OutAnimPhysics = NewerSnap.AnimPhysics; + OutHintTime = NewerSnap.Time; } InterpMode = 1; return true; @@ -9277,6 +9352,10 @@ simulated function bool IGPlus_SnapInterp_Interpolate( OutRot.Roll = 0; OutAnimSeq = OlderSnap.AnimSeq; OutAnimFrameByte = OlderSnap.AnimFrameByte; + OutAnimIntentBits = OlderSnap.AnimIntentBits; + OutAnimDodgeDir = OlderSnap.AnimDodgeDir; + OutAnimPhysics = OlderSnap.AnimPhysics; + OutHintTime = OlderSnap.Time; InterpMode = 2; return true; } @@ -9301,6 +9380,10 @@ simulated function bool IGPlus_SnapInterp_Interpolate( OutRot.Roll = 0; OutAnimSeq = NewerSnap.AnimSeq; OutAnimFrameByte = NewerSnap.AnimFrameByte; + OutAnimIntentBits = NewerSnap.AnimIntentBits; + OutAnimDodgeDir = NewerSnap.AnimDodgeDir; + OutAnimPhysics = NewerSnap.AnimPhysics; + OutHintTime = NewerSnap.Time; InterpMode = 3; return true; } @@ -9308,31 +9391,185 @@ simulated function bool IGPlus_SnapInterp_Interpolate( return false; } -simulated function IGPlus_SnapInterp_ApplyAnimHint(name SnapAnimSeq, byte SnapAnimFrameByte, optional byte InterpMode) { +simulated function bool IGPlus_SnapInterp_ApplyIntentHint( + byte SnapAnimIntentBits, + byte SnapAnimDodgeDir, + byte SnapAnimPhysics, + optional byte InterpMode, + optional float SnapshotAgeSec +) { + local name TargetSeq; local float TweenTime; - local name SnapAnimGroup; + local float MaxIntentAgeSec; - // Avoid touching normal high-quality interpolation path. + // Intent hints are only used in stressed modes. if (InterpMode <= 1) - return; + return false; + + // Ignore stale intent under heavy loss/clamp to avoid wrong transient pose hints. + MaxIntentAgeSec = 0.150; + if (SnapshotAgeSec > MaxIntentAgeSec) + return false; + + // Bit 1: dodging + if ((SnapAnimIntentBits & 2) != 0) { + switch (GetDodgeDir(SnapAnimDodgeDir)) { + case DODGE_Left: + TargetSeq = 'DodgeL'; + break; + case DODGE_Right: + TargetSeq = 'DodgeR'; + break; + case DODGE_Back: + TargetSeq = 'DodgeB'; + break; + case DODGE_Forward: + case DODGE_Active: + if (bUseFlipAnimation) + TargetSeq = 'Flip'; + else + TargetSeq = 'DodgeF'; + break; + default: + TargetSeq = 'DodgeF'; + break; + } + + if (AnimSequence != TargetSeq) + TweenAnim(TargetSeq, 0.08); + return true; + } + + // Bit 0: ducking/crouch. + if ((SnapAnimIntentBits & 1) != 0) { + if (GetAnimGroup(AnimSequence) != 'Ducking') { + TweenTime = 0.12; + if (GetPhysics(int(SnapAnimPhysics)) == PHYS_Falling) + TweenTime = 0.08; + TweenAnim('DuckWlkS', TweenTime); + } + return true; + } + + return false; +} + +simulated function bool IGPlus_SnapInterp_IsTransientAnim(name SnapAnimSeq, name SnapAnimGroup) { + if (SnapAnimGroup == 'Jumping' || SnapAnimGroup == 'Landing') + return true; + + if (SnapAnimSeq == 'DodgeL' || + SnapAnimSeq == 'DodgeR' || + SnapAnimSeq == 'DodgeF' || + SnapAnimSeq == 'DodgeB' || + SnapAnimSeq == 'Flip') + return true; + + return false; +} + +simulated function float IGPlus_SnapInterp_GetAnimDurationSec(name SnapAnimSeq, name SnapAnimGroup) { + // Based on UT99 sequence data: + // - Jump/Land/Dodge are mostly 1-frame transients + // - Flip is 20 (male/boss) to 31 (female) frames. + if (SnapAnimSeq == 'Flip') + return 0.50; + if (SnapAnimSeq == 'DodgeL' || + SnapAnimSeq == 'DodgeR' || + SnapAnimSeq == 'DodgeF' || + SnapAnimSeq == 'DodgeB') + return 0.10; + if (SnapAnimSeq == 'JumpLgFr' || SnapAnimSeq == 'JumpSmFr') + return 0.08; + if (SnapAnimSeq == 'LandLgFr' || SnapAnimSeq == 'LandSmFr') + return 0.06; + + if (SnapAnimGroup == 'Landing') + return 0.08; + if (SnapAnimGroup == 'Jumping') + return 0.20; + if (SnapAnimGroup == 'Ducking') + return 1.00; + + return 0.25; +} + +simulated function bool IGPlus_SnapInterp_IsAnimHintFresh( + name SnapAnimSeq, + name SnapAnimGroup, + byte SnapAnimFrameByte, + float SnapshotAgeSec, + optional float FreshnessMarginSec +) { + local float SequenceDuration; + local float FrameAlpha; + local float RemainingDuration; + local float FreshnessLimit; + + SequenceDuration = IGPlus_SnapInterp_GetAnimDurationSec(SnapAnimSeq, SnapAnimGroup); + FrameAlpha = FClamp(float(SnapAnimFrameByte) / 255.0, 0.0, 1.0); + RemainingDuration = SequenceDuration * FClamp(1.0 - FrameAlpha, 0.0, 1.0); + FreshnessLimit = FMin(0.300, FMax(0.040, RemainingDuration + FreshnessMarginSec)); + + return SnapshotAgeSec <= FreshnessLimit; +} + +simulated function bool IGPlus_SnapInterp_ApplyAnimHint( + name SnapAnimSeq, + byte SnapAnimFrameByte, + optional byte InterpMode, + optional float SnapshotAgeSec +) { + local bool bTransient; + local float TweenTime; + local name SnapAnimGroup; + local float FreshnessMarginSec; + + // InterpMode 0 means no usable interpolation output. + if (InterpMode <= 0) + return false; if (SnapAnimSeq == '') - return; + return false; + + SnapAnimGroup = GetAnimGroup(SnapAnimSeq); + bTransient = IGPlus_SnapInterp_IsTransientAnim(SnapAnimSeq, SnapAnimGroup); + + // In normal interpolation mode, only allow transient sequence hints. + if (InterpMode == 1 && bTransient == false) + return false; + + // Expire hints based on sequence duration + frame position. + FreshnessMarginSec = 0.060; + if (InterpMode > 1) + FreshnessMarginSec = 0.090; + if (IGPlus_SnapInterp_IsAnimHintFresh( + SnapAnimSeq, + SnapAnimGroup, + SnapAnimFrameByte, + SnapshotAgeSec, + FreshnessMarginSec + ) == false) + return false; + + // Jumping sequences are highly transient; only use while proxy is actually airborne. + if (SnapAnimGroup == 'Jumping') { + if (Physics != PHYS_Falling && Velocity.Z <= 0.0) + return false; + } if (AnimSequence != SnapAnimSeq) { - SnapAnimGroup = GetAnimGroup(SnapAnimSeq); TweenTime = 0.06; if (SnapAnimGroup == 'Jumping') TweenTime = 0.08; else if (SnapAnimGroup == 'Ducking') TweenTime = 0.12; - else if (SnapAnimGroup == 'Landing') - TweenTime = 0.05; TweenAnim(SnapAnimSeq, TweenTime); } AnimFrame = FClamp(float(SnapAnimFrameByte) / 255.0, 0.0, 1.0); + return true; } simulated function IGPlus_SnapInterp_Reset() { @@ -9355,6 +9592,8 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { local bool bReplicatedLocation; local bool bCanRecoverPrev; local bool bHasInterpolated; + local bool bAppliedIntentHint; + local bool bAppliedAnimHint; local int RepCounter; local int PrevCounter; local byte InterpMode; @@ -9363,8 +9602,13 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { local float SnapshotTime; local float PrevSnapshotTime; local float ExpectedInterval; + local float OutHintTime; + local float HintAgeSec; local name OutAnimSeq; local byte OutAnimFrameByte; + local byte OutAnimIntentBits; + local byte OutAnimDodgeDir; + local byte OutAnimPhysics; local vector OutPos; local vector OutVel; local rotator OutRot; @@ -9377,6 +9621,14 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { RepCounter = IGPlus_SnapReplicatedData.Counter; PrevCounter = IGPlus_SnapReplicatedDataPrev.Counter; bHasInterpolated = false; + bAppliedIntentHint = false; + bAppliedAnimHint = false; + InterpMode = 0; + OutHintTime = 0.0; + HintAgeSec = 0.0; + IGPlus_SnapInterp_LastInterpMode = 0; + IGPlus_SnapInterp_LastHintAgeMs = 0; + IGPlus_SnapInterp_LastHintSource = "none"; bReplicatedLocation = (RepCounter > IGPlus_SnapInterp_LastCounter) || (IGPlus_SnapInterp_LastCounter == 0 && RepCounter != 0); @@ -9405,6 +9657,9 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { IGPlus_SnapReplicatedDataPrev.Rotation, IGPlus_SnapReplicatedDataPrev.AnimSequence, IGPlus_SnapReplicatedDataPrev.AnimFrameByte, + IGPlus_SnapReplicatedDataPrev.AnimIntentBits, + IGPlus_SnapReplicatedDataPrev.AnimDodgeDir, + IGPlus_SnapReplicatedDataPrev.AnimPhysics, PrevSnapshotTime, ArrivalTime, true, @@ -9422,6 +9677,9 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { IGPlus_SnapReplicatedData.Rotation, IGPlus_SnapReplicatedData.AnimSequence, IGPlus_SnapReplicatedData.AnimFrameByte, + IGPlus_SnapReplicatedData.AnimIntentBits, + IGPlus_SnapReplicatedData.AnimDodgeDir, + IGPlus_SnapReplicatedData.AnimPhysics, SnapshotTime, ArrivalTime, true, @@ -9440,9 +9698,16 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { OutRot, OutAnimSeq, OutAnimFrameByte, + OutAnimIntentBits, + OutAnimDodgeDir, + OutAnimPhysics, + OutHintTime, InterpMode ); if (bHasInterpolated) { + IGPlus_SnapInterp_LastInterpMode = InterpMode; + HintAgeSec = FMax(0.0, Level.TimeSeconds - OutHintTime); + IGPlus_SnapInterp_LastHintAgeMs = int(HintAgeSec * 1000.0 + 0.5); if (InterpMode == 1) IGPlus_SnapInterp_StatInterp += 1; else if (InterpMode == 2) { @@ -9475,11 +9740,29 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { OutRot.Roll = 0; SetRotation(OutRot); DesiredRotation = OutRot; - IGPlus_SnapInterp_ApplyAnimHint( - IGPlus_SnapReplicatedData.AnimSequence, - IGPlus_SnapReplicatedData.AnimFrameByte, - 2 + InterpMode = 2; + HintAgeSec = 0.0; + IGPlus_SnapInterp_LastInterpMode = InterpMode; + IGPlus_SnapInterp_LastHintAgeMs = 0; + bAppliedIntentHint = IGPlus_SnapInterp_ApplyIntentHint( + IGPlus_SnapReplicatedData.AnimIntentBits, + IGPlus_SnapReplicatedData.AnimDodgeDir, + IGPlus_SnapReplicatedData.AnimPhysics, + InterpMode, + HintAgeSec ); + if (bAppliedIntentHint == false) { + bAppliedAnimHint = IGPlus_SnapInterp_ApplyAnimHint( + IGPlus_SnapReplicatedData.AnimSequence, + IGPlus_SnapReplicatedData.AnimFrameByte, + InterpMode, + HintAgeSec + ); + } + if (bAppliedIntentHint) + IGPlus_SnapInterp_LastHintSource = "intent"; + else if (bAppliedAnimHint) + IGPlus_SnapInterp_LastHintSource = "seq"; } else { bCollideWorld = false; SetLocation(IGPlus_LocationOffsetFix_OldLocation); @@ -9496,7 +9779,25 @@ simulated function IGPlus_SnapInterp_After(float DeltaTime) { SetRotation(OutRot); DesiredRotation = OutRot; Velocity = OutVel; - IGPlus_SnapInterp_ApplyAnimHint(OutAnimSeq, OutAnimFrameByte, InterpMode); + bAppliedIntentHint = IGPlus_SnapInterp_ApplyIntentHint( + OutAnimIntentBits, + OutAnimDodgeDir, + OutAnimPhysics, + InterpMode, + HintAgeSec + ); + if (bAppliedIntentHint == false) { + bAppliedAnimHint = IGPlus_SnapInterp_ApplyAnimHint( + OutAnimSeq, + OutAnimFrameByte, + InterpMode, + HintAgeSec + ); + } + if (bAppliedIntentHint) + IGPlus_SnapInterp_LastHintSource = "intent"; + else if (bAppliedAnimHint) + IGPlus_SnapInterp_LastHintSource = "seq"; } else { bCollideWorld = false; SetLocation(IGPlus_LocationOffsetFix_OldLocation); @@ -11716,7 +12017,10 @@ exec simulated function SnapInterpStatus(optional string TargetFilter) { " rep="$Target.IGPlus_SnapReplicatedData.Counter$ " last="$Target.IGPlus_SnapInterp_LastCounter$ " moved="$Target.IGPlus_LocationOffsetFix_Moved$ - " delayMs="$int(Target.IGPlus_SnapInterp_InterpDelay * 1000.0) + " delayMs="$int(Target.IGPlus_SnapInterp_InterpDelay * 1000.0)$ + " interpMode="$Target.IGPlus_SnapInterp_LastInterpMode$ + " hint="$Target.IGPlus_SnapInterp_LastHintSource$ + " hintAgeMs="$Target.IGPlus_SnapInterp_LastHintAgeMs ); } From 3317e2b71b879423b90eaa43d9dd41df5f8e3bb8 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:22:28 -0600 Subject: [PATCH 10/14] Fix snapshot forward-dodge flip intent selection --- Classes/bbPlayer.uc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 6b3de20..2852235 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -6534,6 +6534,10 @@ event ServerTick(float DeltaTime) { else if (AnimSequence == 'DodgeF' || AnimSequence == 'Flip') SnapshotDodgeDir = 3; } + // Bit 2 tags forward dodge visual intent: Flip vs DodgeF. + // This must come from the source player, not observer-local config. + if (SnapshotDodgeDir == 3 && (AnimSequence == 'Flip' || bUseFlipAnimation)) + SnapshotIntentBits = SnapshotIntentBits | 4; switch (Physics) { case PHYS_None: SnapshotPhysics = 0; break; @@ -9425,7 +9429,7 @@ simulated function bool IGPlus_SnapInterp_ApplyIntentHint( break; case DODGE_Forward: case DODGE_Active: - if (bUseFlipAnimation) + if ((SnapAnimIntentBits & 4) != 0) TargetSeq = 'Flip'; else TargetSeq = 'DodgeF'; From 2dc16fe9402d5ec03ec218b26e71dffe4ffc1a86 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:28:07 -0600 Subject: [PATCH 11/14] Make snapshot forward-flip intent strictly source-accurate --- Classes/bbPlayer.uc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 2852235..1148cb6 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -6535,8 +6535,9 @@ event ServerTick(float DeltaTime) { SnapshotDodgeDir = 3; } // Bit 2 tags forward dodge visual intent: Flip vs DodgeF. - // This must come from the source player, not observer-local config. - if (SnapshotDodgeDir == 3 && (AnimSequence == 'Flip' || bUseFlipAnimation)) + // Keep this strictly source-accurate: set only when the sampled + // sequence is actually Flip at snapshot time. + if (SnapshotDodgeDir == 3 && AnimSequence == 'Flip') SnapshotIntentBits = SnapshotIntentBits | 4; switch (Physics) { From df1f6310d0a72176e5f37733d224c2d5df3097a4 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:45:56 -0600 Subject: [PATCH 12/14] Improve behavior around movers --- .gitignore | 5 +- Classes/IGPlus_WeaponImplementation.uc | 18 ++ Classes/IGPlus_WeaponImplementationBase.uc | 347 ++++++++++++++++++++- Classes/ST_ShockProj.uc | 1 + Classes/ST_ShockRifle.uc | 124 ++++++-- Classes/ST_SniperRifle.uc | 8 +- Classes/ST_UT_FlakCannon.uc | 2 - Classes/UTPlusDummy.uc | 32 +- Classes/UTPure.uc | 5 - Classes/bbPlayer.uc | 191 +++++++++++- 10 files changed, 664 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 3885073..3ae6c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,11 @@ /Build/**/* /System/ -/IG+ Extras/ -/OtherMods/ +/Extras/ build_all.sh build.sh .env .cursorignore -.DS_Store \ No newline at end of file +.DS_Store diff --git a/Classes/IGPlus_WeaponImplementation.uc b/Classes/IGPlus_WeaponImplementation.uc index 26bd503..28db156 100644 --- a/Classes/IGPlus_WeaponImplementation.uc +++ b/Classes/IGPlus_WeaponImplementation.uc @@ -70,6 +70,24 @@ simulated function bool CheckBodyShotCompensated(UTPlusDummy D, vector HitLocati function float GetAverageTickRate(); +function float IGPlus_GetOneWayLatencyMs(Pawn Instigator); + +function float IGPlus_GetHitscanRewindMs(Pawn Instigator); + +function vector IGPlus_AdjustLocationToCurrentMoverFrame( + Pawn Instigator, + vector FireLoc, + float SampleLatencyMs, + optional Mover BaseMover +); + +function vector IGPlus_AdjustLocationToHistoricalMoverFrame( + Pawn Instigator, + vector FireLoc, + float SampleLatencyMs, + optional Mover BaseMover +); + function SimulateProjectileWithHistory(ST_TranslocatorTarget TTarget, int Ping); function SimulateProjectile(Projectile P, int Ping); diff --git a/Classes/IGPlus_WeaponImplementationBase.uc b/Classes/IGPlus_WeaponImplementationBase.uc index 2c7f808..ca21031 100644 --- a/Classes/IGPlus_WeaponImplementationBase.uc +++ b/Classes/IGPlus_WeaponImplementationBase.uc @@ -45,6 +45,151 @@ function float IGPlus_GetSnapshotInterpRewindMs(Pawn Instigator) { return 0.0; } +function float IGPlus_GetLatencyMs(Pawn Instigator, optional bool bOneWay) { + local bbPlayer bbP; + local float PingMs; + + if (Instigator == None) + return 0.0; + + bbP = bbPlayer(Instigator); + if (bbP != None) { + if (bbP.PingAverage > 0) + PingMs = float(bbP.PingAverage); + if (PingMs <= 0.0 && bbP.PlayerReplicationInfo != None && bbP.PlayerReplicationInfo.Ping > 0) + PingMs = float(bbP.PlayerReplicationInfo.Ping); + } + + if (PingMs <= 0.0 && Instigator.PlayerReplicationInfo != None && Instigator.PlayerReplicationInfo.Ping > 0) + PingMs = float(Instigator.PlayerReplicationInfo.Ping); + + if (bOneWay) + return FMax(0.0, 0.5 * PingMs); + return FMax(0.0, PingMs); +} + +function float IGPlus_GetOneWayLatencyMs(Pawn Instigator) { + return IGPlus_GetLatencyMs(Instigator, true); +} + +function float IGPlus_GetHitscanRewindMs(Pawn Instigator) { + local bbPlayer bbP; + local float EffectivePing; + local float InterpMs; + local float BasePingMs; + + if (Instigator == None) + return 0.0; + + bbP = bbPlayer(Instigator); + if (bbP == None) { + return IGPlus_GetLatencyMs(Instigator, false); + } + + InterpMs = IGPlus_GetSnapshotInterpRewindMs(Instigator); + BasePingMs = IGPlus_GetLatencyMs(Instigator, false); + if (InterpMs > 0.0) { + if (bbP.PingAverage > 0) + EffectivePing = (0.5 * float(bbP.PingAverage)) + InterpMs; + else + EffectivePing = (0.5 * BasePingMs) + InterpMs; + } else + EffectivePing = BasePingMs; + + if (WSettingsRepl != None && EffectivePing > WSettingsRepl.PingCompensationMax) + EffectivePing = WSettingsRepl.PingCompensationMax; + if (EffectivePing < 0.0) + EffectivePing = 0.0; + + return EffectivePing; +} + +function vector IGPlus_AdjustLocationToCurrentMoverFrame( + Pawn Instigator, + vector FireLoc, + float SampleLatencyMs, + optional Mover BaseMover +) { + local bbPlayer bbP; + local UTPure PureRef; + local ST_MoverDummy MD; + local Mover M; + local float SampleSeconds; + local float SampleTimeStamp; + local vector HistoricalLoc; + + if (Instigator == None || SampleLatencyMs <= 0.0) + return FireLoc; + + M = BaseMover; + if (M == None) + M = Mover(Instigator.Base); + if (M == None) + return FireLoc; + + bbP = bbPlayer(Instigator); + if (bbP == None) + return FireLoc; + + PureRef = bbP.zzUTPure; + if (PureRef == None) + return FireLoc; + + SampleSeconds = 0.001 * SampleLatencyMs * Level.TimeDilation; + + MD = PureRef.FindMoverDummy(M); + if (MD == None) + return FireLoc + M.Velocity * SampleSeconds; + + SampleTimeStamp = Level.TimeSeconds - SampleSeconds; + HistoricalLoc = MD.GetHistoricalLocation(SampleTimeStamp); + + return FireLoc + (M.Location - HistoricalLoc); +} + +function vector IGPlus_AdjustLocationToHistoricalMoverFrame( + Pawn Instigator, + vector FireLoc, + float SampleLatencyMs, + optional Mover BaseMover +) { + local bbPlayer bbP; + local UTPure PureRef; + local ST_MoverDummy MD; + local Mover M; + local float SampleSeconds; + local float SampleTimeStamp; + local vector HistoricalLoc; + + if (Instigator == None || SampleLatencyMs <= 0.0) + return FireLoc; + + M = BaseMover; + if (M == None) + M = Mover(Instigator.Base); + if (M == None) + return FireLoc; + + bbP = bbPlayer(Instigator); + if (bbP == None) + return FireLoc; + + PureRef = bbP.zzUTPure; + if (PureRef == None) + return FireLoc; + + SampleSeconds = 0.001 * SampleLatencyMs * Level.TimeDilation; + + MD = PureRef.FindMoverDummy(M); + if (MD == None) + return FireLoc - M.Velocity * SampleSeconds; + + SampleTimeStamp = Level.TimeSeconds - SampleSeconds; + HistoricalLoc = MD.GetHistoricalLocation(SampleTimeStamp); + + return FireLoc + (HistoricalLoc - M.Location); +} + function PostBeginPlay() { super.PostBeginPlay(); @@ -675,6 +820,148 @@ function bool LineIntersectsCylinder(vector Start, vector End, vector CylinderCe return (DistXYSq <= Square(Radius) && DistZ <= HalfHeight); } +function bool PointInsideCylinder(vector P, vector CylinderCenter, float Radius, float HalfHeight) { + local float DistXYSq; + + DistXYSq = Square(P.X - CylinderCenter.X) + Square(P.Y - CylinderCenter.Y); + return (DistXYSq <= Square(Radius) && Abs(P.Z - CylinderCenter.Z) <= HalfHeight); +} + +// Prevent compensated hits through a target's lift when firing from below. +// Uses mover history at the same rewind time as pawn compensation. +function bool IsCompensatedHitBlockedByTargetMover( + UTPlusDummy TargetDummy, + Pawn Shooter, + vector StartTrace, + vector HitLocation, + int RewindPing +) { + local Mover BaseMover; + local vector HistoricalMoverLoc; + + if (TargetDummy == None || TargetDummy.Actual == None || Shooter == None) + return false; + + BaseMover = Mover(TargetDummy.Actual.Base); + if (BaseMover == None) + return false; + + // Same-base shots should not be blocked by this protection. + if (Shooter.Base == BaseMover) + return false; + + HistoricalMoverLoc = BaseMover.Location; + // Fallback stays at current mover location if history is unavailable. + GetHistoricalMoverLocationForRewind(Shooter, BaseMover, RewindPing, HistoricalMoverLoc); + + // This guard is only for the "from below through mover" case. + if (StartTrace.Z >= HistoricalMoverLoc.Z || HitLocation.Z <= HistoricalMoverLoc.Z) + return false; + + return LineIntersectsCylinder( + StartTrace, + HitLocation, + HistoricalMoverLoc, + BaseMover.CollisionRadius, + BaseMover.CollisionHeight + ); +} + +function bool ShouldIgnoreCompensatedSelfBaseHit( + Pawn Shooter, + vector StartTrace, + vector HitLocation, + int RewindPing +) { + local Mover BaseMover; + local vector HistoricalMoverLoc; + local bool bHasHistorical; + local vector ShotDir; + + if (Shooter == None) + return false; + + BaseMover = Mover(Shooter.Base); + if (BaseMover == None) + return false; + + HistoricalMoverLoc = BaseMover.Location; + bHasHistorical = GetHistoricalMoverLocationForRewind(Shooter, BaseMover, RewindPing, HistoricalMoverLoc); + if (!bHasHistorical) + return false; + + // Our mover proxy is cylindrical while real movers are brush geometry. When + // the shooter rides an upward mover, compensated muzzle origins can land + // inside this proxy even when the actual shot should clear the lift. + ShotDir = Normal(HitLocation - StartTrace); + if (ShotDir.Z >= 0.0 && + PointInsideCylinder( + StartTrace, + HistoricalMoverLoc, + BaseMover.CollisionRadius, + BaseMover.CollisionHeight + ) + ) { + return true; + } + + // Ignore a self-base hit only if the shot segment does not intersect the + // mover at the same historical rewind frame. + return !LineIntersectsCylinder( + StartTrace, + HitLocation, + HistoricalMoverLoc, + BaseMover.CollisionRadius, + BaseMover.CollisionHeight + ); +} + +function bool GetHistoricalMoverLocationForRewind( + Pawn Shooter, + Mover M, + int RewindPing, + out vector HistoricalMoverLoc +) { + local bbPlayer bbShooter; + local UTPure PureRef; + local ST_MoverDummy MD; + local float TargetTimeStamp; + + if (M == None || Shooter == None || RewindPing <= 0) + return false; + + bbShooter = bbPlayer(Shooter); + if (bbShooter == None) + return false; + + PureRef = bbShooter.zzUTPure; + if (PureRef == None) + return false; + + MD = PureRef.FindMoverDummy(M); + if (MD == None) + return false; + + TargetTimeStamp = Level.TimeSeconds - 0.001 * float(RewindPing) * Level.TimeDilation; + HistoricalMoverLoc = MD.GetHistoricalLocation(TargetTimeStamp); + return true; +} + +function int ResolveSelfBaseSamplePing(Pawn Shooter, int RewindPing) { + local int OneWayPing; + + if (Shooter == None) + return RewindPing; + + // For own-base occlusion we want the lift frame the shooter saw when firing, + // so use one-way latency instead of target rewind timeline. + OneWayPing = int(IGPlus_GetOneWayLatencyMs(Shooter)); + if (OneWayPing > 0) + return OneWayPing; + + return RewindPing; +} + // Check for beam weapon (PBolt/Pulse Gun) collisions during simulation // Returns true if the translocator was hit function bool CheckSimulationBeamCollisions(ST_TranslocatorTarget TTarget, float DeltaTime) { @@ -1030,6 +1317,7 @@ simulated function Actor TraceShotInternal( vector EndTrace, vector StartTrace, Pawn PawnOwner, + int RewindPing, bool bWeaponShock, bool bSProjBlocks, bool bCompensated @@ -1053,6 +1341,8 @@ simulated function Actor TraceShotInternal( if (CheckBodyShotCompensated(D, HitLocation, Dir) == false && CheckHeadShotCompensated(D, HitLocation, Dir) == false) continue; + if (IsCompensatedHitBlockedByTargetMover(D, PawnOwner, StartTrace, HitLocation, RewindPing)) + continue; Other = D.Actual; break; } @@ -1089,10 +1379,16 @@ simulated function Actor TraceShotInternal( } } - if ((A == Level) || (Mover(A) != None) || A.bProjTarget || (A.bBlockPlayers && A.bBlockActors)) { - if (bSProjBlocks || A.IsA('ShockProj') == false || bWeaponShock) { - Other = A; - break; + if ((A == Level) || (Mover(A) != None) || A.bProjTarget || (A.bBlockPlayers && A.bBlockActors)) { + // If compensated muzzle origin clips into the instigator's own mover, skip + // only the immediate self-base hit so the ray can continue normally. + if (bCompensated && A == PawnOwner.Base && Mover(A) != None) { + if (ShouldIgnoreCompensatedSelfBaseHit(PawnOwner, StartTrace, HitLocation, ResolveSelfBaseSamplePing(PawnOwner, RewindPing))) + continue; + } + if (bSProjBlocks || A.IsA('ShockProj') == false || bWeaponShock) { + Other = A; + break; } } } @@ -1117,12 +1413,7 @@ simulated function Actor TraceShot(out vector HitLocation, out vector HitNormal, if (bbP != None) { PureRef = bbP.zzUTPure; - EffectivePing = float(bbP.PingAverage) + IGPlus_GetSnapshotInterpRewindMs(PawnOwner); - - if (EffectivePing > WSettingsRepl.PingCompensationMax) - EffectivePing = WSettingsRepl.PingCompensationMax; - if (EffectivePing < 0.0) - EffectivePing = 0.0; + EffectivePing = IGPlus_GetHitscanRewindMs(PawnOwner); Ping = int(EffectivePing); } @@ -1133,12 +1424,32 @@ simulated function Actor TraceShot(out vector HitLocation, out vector HitNormal, if (WSettingsRepl.bEnablePingCompensation && bbP != none) { PureRef.CompensateFor(Ping, PawnOwner); - Other = TraceShotInternal(HitLocation, HitNormal, EndTrace, StartTrace, PawnOwner, bWeaponShock, bSProjBlocks, true); + Other = TraceShotInternal( + HitLocation, + HitNormal, + EndTrace, + StartTrace, + PawnOwner, + Ping, + bWeaponShock, + bSProjBlocks, + true + ); PureRef.EndCompensation(); return Other; } - return TraceShotInternal(HitLocation, HitNormal, EndTrace, StartTrace, PawnOwner, bWeaponShock, bSProjBlocks, false); + return TraceShotInternal( + HitLocation, + HitNormal, + EndTrace, + StartTrace, + PawnOwner, + 0, + bWeaponShock, + bSProjBlocks, + false + ); } simulated function Actor TraceShotClient(out vector HitLocation, out vector HitNormal, vector EndTrace, vector StartTrace, Pawn PawnOwner) @@ -1149,7 +1460,17 @@ simulated function Actor TraceShotClient(out vector HitLocation, out vector HitN bSProjBlocks = WSettingsRepl.ShockProjectileBlockBullets; bWeaponShock = (PawnOwner.Weapon != none && PawnOwner.Weapon.IsA('ShockRifle')); - return TraceShotInternal(HitLocation, HitNormal, EndTrace, StartTrace, PawnOwner, bWeaponShock, bSProjBlocks, false); + return TraceShotInternal( + HitLocation, + HitNormal, + EndTrace, + StartTrace, + PawnOwner, + 0, + bWeaponShock, + bSProjBlocks, + false + ); } defaultproperties { diff --git a/Classes/ST_ShockProj.uc b/Classes/ST_ShockProj.uc index 5f35648..38920b7 100644 --- a/Classes/ST_ShockProj.uc +++ b/Classes/ST_ShockProj.uc @@ -52,6 +52,7 @@ simulated function PostBeginPlay() { Health = WImp.WeaponSettings.ShockProjectileHealth; } } + Super.PostBeginPlay(); } diff --git a/Classes/ST_ShockRifle.uc b/Classes/ST_ShockRifle.uc index 04355fe..6de5f93 100644 --- a/Classes/ST_ShockRifle.uc +++ b/Classes/ST_ShockRifle.uc @@ -11,10 +11,12 @@ var float yMod; var vector CDO; var ST_ShockProj LocalDummy; +var vector PendingSmokeLocation; // Explicit client aim data (sent via ServerExplicitFire/AltFire) var vector ExplicitClientLoc; var rotator ExplicitClientRot; +var Mover ExplicitClientBaseMover; var bool bUseExplicitData; var bool bClientShownVisuals; @@ -90,9 +92,16 @@ function bool IsPositionReasonable(vector ClientLoc) } // Called by ClientFire. Sends exact Client data to server. -function ServerExplicitFire(vector ClientLoc, rotator ClientRot, bool bClientVisuals, optional bool bIsSwitching) +function ServerExplicitFire( + vector ClientLoc, + rotator ClientRot, + bool bClientVisuals, + optional bool bIsSwitching +) { local PlayerPawn P; + local vector RawClientLoc; + local vector CheckLoc; P = PlayerPawn(Owner); if (P == None) @@ -100,6 +109,17 @@ function ServerExplicitFire(vector ClientLoc, rotator ClientRot, bool bClientVis if (!IsPingCompEnabled()) return; + + RawClientLoc = ClientLoc; + CheckLoc = RawClientLoc; + if (WImp != None) + CheckLoc = WImp.IGPlus_AdjustLocationToCurrentMoverFrame( + Pawn(Owner), + CheckLoc, + WImp.IGPlus_GetOneWayLatencyMs(Pawn(Owner)) + ); + else if (bbPlayer(Owner) != None) + CheckLoc.Z += bbPlayer(Owner).GetMoverFireZOffset(); // Handle Switching Fire (High Priority) if ( (AmmoType != None) && (AmmoType.AmmoAmount > 0) && (bIsSwitching || (P.PendingWeapon != None && P.PendingWeapon != self) || P.Weapon != self) ) @@ -112,12 +132,13 @@ function ServerExplicitFire(vector ClientLoc, rotator ClientRot, bool bClientVis LastServerFireTime = Level.TimeSeconds; // Position validation - if (bbPlayer(Owner) != None) - ClientLoc.Z += bbPlayer(Owner).GetMoverFireZOffset(); - if (IsPositionReasonable(ClientLoc)) - ExplicitClientLoc = ClientLoc; - else + if (IsPositionReasonable(CheckLoc)) { + ExplicitClientLoc = RawClientLoc; + ExplicitClientBaseMover = Mover(Owner.Base); + } else { ExplicitClientLoc = Owner.Location; + ExplicitClientBaseMover = Mover(Owner.Base); + } ExplicitClientRot = ClientRot; bUseExplicitData = true; @@ -149,13 +170,14 @@ function ServerExplicitFire(vector ClientLoc, rotator ClientRot, bool bClientVis if (Level.TimeSeconds - LastServerFireTime < FIRE_RATE_LIMIT) return; - // Position validation - use server position if client position is unreasonable - if (bbPlayer(Owner) != None) - ClientLoc.Z += bbPlayer(Owner).GetMoverFireZOffset(); - if (IsPositionReasonable(ClientLoc)) - ExplicitClientLoc = ClientLoc; - else - ExplicitClientLoc = Owner.Location; + // Position validation - use server position if client position is unreasonable + if (IsPositionReasonable(CheckLoc)) { + ExplicitClientLoc = RawClientLoc; + ExplicitClientBaseMover = Mover(Owner.Base); + } else { + ExplicitClientLoc = Owner.Location; + ExplicitClientBaseMover = Mover(Owner.Base); + } ExplicitClientRot = ClientRot; bUseExplicitData = true; @@ -185,9 +207,16 @@ function ServerExplicitFire(vector ClientLoc, rotator ClientRot, bool bClientVis bClientShownVisuals = false; } -function ServerExplicitAltFire(vector ClientLoc, rotator ClientRot, bool bClientVisuals, optional bool bIsSwitching) +function ServerExplicitAltFire( + vector ClientLoc, + rotator ClientRot, + bool bClientVisuals, + optional bool bIsSwitching +) { local PlayerPawn P; + local vector RawClientLoc; + local vector CheckLoc; P = PlayerPawn(Owner); if (P == None) @@ -196,17 +225,29 @@ function ServerExplicitAltFire(vector ClientLoc, rotator ClientRot, bool bClient if (!IsPingCompEnabled()) return; + RawClientLoc = ClientLoc; + CheckLoc = RawClientLoc; + if (WImp != None) + CheckLoc = WImp.IGPlus_AdjustLocationToCurrentMoverFrame( + Pawn(Owner), + CheckLoc, + WImp.IGPlus_GetOneWayLatencyMs(Pawn(Owner)) + ); + else if (bbPlayer(Owner) != None) + CheckLoc.Z += bbPlayer(Owner).GetMoverFireZOffset(); + // Handle Switching Fire (High Priority) if ( (AmmoType != None) && (AmmoType.AmmoAmount > 0) && (bIsSwitching || (P.PendingWeapon != None && P.PendingWeapon != self) || P.Weapon != self) ) { AmmoType.UseAmmo(1); - if (bbPlayer(Owner) != None) - ClientLoc.Z += bbPlayer(Owner).GetMoverFireZOffset(); - if (IsPositionReasonable(ClientLoc)) - ExplicitClientLoc = ClientLoc; - else + if (IsPositionReasonable(CheckLoc)) { + ExplicitClientLoc = RawClientLoc; + ExplicitClientBaseMover = Mover(Owner.Base); + } else { ExplicitClientLoc = Owner.Location; + ExplicitClientBaseMover = Mover(Owner.Base); + } ExplicitClientRot = ClientRot; bUseExplicitData = true; @@ -233,13 +274,14 @@ function ServerExplicitAltFire(vector ClientLoc, rotator ClientRot, bool bClient if (bChangeWeapon || IsInState('DownWeapon')) return; - // Position validation - use server position if client position is unreasonable - if (bbPlayer(Owner) != None) - ClientLoc.Z += bbPlayer(Owner).GetMoverFireZOffset(); - if (IsPositionReasonable(ClientLoc)) - ExplicitClientLoc = ClientLoc; - else - ExplicitClientLoc = Owner.Location; + // Position validation - use server position if client position is unreasonable + if (IsPositionReasonable(CheckLoc)) { + ExplicitClientLoc = RawClientLoc; + ExplicitClientBaseMover = Mover(Owner.Base); + } else { + ExplicitClientLoc = Owner.Location; + ExplicitClientBaseMover = Mover(Owner.Base); + } ExplicitClientRot = ClientRot; bUseExplicitData = true; @@ -271,12 +313,26 @@ function ServerExplicitAltFire(vector ClientLoc, rotator ClientRot, bool bClient function Projectile ExplicitProjectileFire(class ProjClass, float ProjSpeed, bool bWarn) { local Vector Start, X,Y,Z; + local Pawn PawnOwner; + local vector AdjustedClientLoc; - Owner.MakeNoise(Pawn(Owner).SoundDampening); + PawnOwner = Pawn(Owner); + Owner.MakeNoise(PawnOwner.SoundDampening); // Use Explicit Client Rotation GetAxes(ExplicitClientRot,X,Y,Z); - Start = ExplicitClientLoc + CalcDrawOffset() + FireOffset.X * X + FireOffset.Y * Y + FireOffset.Z * Z; + AdjustedClientLoc = ExplicitClientLoc; + if (WImp != None && IsPingCompEnabled()) + AdjustedClientLoc = WImp.IGPlus_AdjustLocationToCurrentMoverFrame( + PawnOwner, + AdjustedClientLoc, + WImp.IGPlus_GetOneWayLatencyMs(PawnOwner), + ExplicitClientBaseMover + ); + + // For projectile spawn, use current mover frame to avoid immediate collision + // with the shooter's own lift on server. + Start = AdjustedClientLoc + CalcDrawOffset() + FireOffset.X * X + FireOffset.Y * Y + FireOffset.Z * Z; AdjustedAim = ExplicitClientRot; @@ -538,6 +594,7 @@ function TraceFire(float Accuracy) { local Pawn PawnOwner; local rotator AimRot; local vector AimLoc; + local vector SmokeLocation; PawnOwner = Pawn(Owner); @@ -548,6 +605,9 @@ function TraceFire(float Accuracy) { { AimRot = ExplicitClientRot; AimLoc = ExplicitClientLoc; + // ExplicitClientLoc is captured at client fire time. For hitscan beam traces, + // keep that origin as-is; shifting it to current mover frame skews vertical + // aim while riding movers. } else { @@ -557,6 +617,7 @@ function TraceFire(float Accuracy) { GetAxes(AimRot,X,Y,Z); StartTrace = AimLoc + CalcDrawOffset() + FireOffset.Y * Y + FireOffset.Z * Z; + SmokeLocation = AimLoc + CalcDrawOffset() + (FireOffset.X + 20) * X + FireOffset.Y * Y + FireOffset.Z * Z; EndTrace = StartTrace + (Accuracy * (FRand() - 0.5 )* Y * 1000) + (Accuracy * (FRand() - 0.5 ) * Z * 1000); @@ -584,6 +645,7 @@ function TraceFire(float Accuracy) { else Other = PawnOwner.TraceShot(HitLocation,HitNormal,EndTrace,StartTrace); + PendingSmokeLocation = SmokeLocation; ProcessTraceHit(Other, HitLocation, HitNormal, vector(AdjustedAim), Y, Z); } @@ -610,8 +672,12 @@ function ProcessTraceHit(Actor Other, Vector HitLocation, Vector HitNormal, Vect if (PlayerOwner != None) PlayerOwner.ClientInstantFlash(-0.4, vect(450, 190, 650)); + if (PendingSmokeLocation == vect(0,0,0)) + PendingSmokeLocation = Owner.Location + CalcDrawOffset() + (FireOffset.X + 20) * X + FireOffset.Y * Y + FireOffset.Z * Z; + // Server-side beam spawning - SpawnEffect(HitLocation, Owner.Location + CalcDrawOffset() + (FireOffset.X + 20) * X + FireOffset.Y * Y + FireOffset.Z * Z); + SpawnEffect(HitLocation, PendingSmokeLocation); + PendingSmokeLocation = vect(0,0,0); if (IsPingCompEnabled() && bbP != None && bbP.ClientWeaponSettingsData.bShockProjectileUseClientSideAnimations == false) { Dummy = ST_ProjectileDummy(Other); diff --git a/Classes/ST_SniperRifle.uc b/Classes/ST_SniperRifle.uc index 986b579..54de5d0 100644 --- a/Classes/ST_SniperRifle.uc +++ b/Classes/ST_SniperRifle.uc @@ -154,14 +154,18 @@ function TraceFire(float Accuracy) { local vector HitLocation, HitNormal, StartTrace, EndTrace, X,Y,Z; local actor Other; local Pawn PawnOwner; + local float RewindMs; PawnOwner = Pawn(Owner); Owner.MakeNoise(PawnOwner.SoundDampening); GetAxes(PawnOwner.ViewRotation,X,Y,Z); StartTrace = Owner.Location + PawnOwner.Eyeheight * vect(0,0,1); - if (bbPlayer(Owner) != None && WImp.WeaponSettings.bEnablePingCompensation) - StartTrace.Z -= bbPlayer(Owner).GetMoverFireZOffset(true); + if (WImp != None && WImp.WeaponSettings.bEnablePingCompensation) + { + RewindMs = WImp.IGPlus_GetHitscanRewindMs(PawnOwner); + StartTrace = WImp.IGPlus_AdjustLocationToHistoricalMoverFrame(PawnOwner, StartTrace, RewindMs); + } AdjustedAim = PawnOwner.AdjustAim(1000000, StartTrace, 2*AimError, False, False); X = vector(AdjustedAim); EndTrace = StartTrace + 100000 * X; diff --git a/Classes/ST_UT_FlakCannon.uc b/Classes/ST_UT_FlakCannon.uc index 4da045d..d79b224 100644 --- a/Classes/ST_UT_FlakCannon.uc +++ b/Classes/ST_UT_FlakCannon.uc @@ -633,8 +633,6 @@ simulated function SpawnClientChunk(class ChunkClass, vector Pos, ro C.ChunkIndex = Index; C.bCollideWorld = true; C.SetCollision(false, false, false); - if (Pawn(Owner) != None && Pawn(Owner).PlayerReplicationInfo != None) - C.LifeSpan = Pawn(Owner).PlayerReplicationInfo.Ping * 0.00125 * Level.TimeDilation; } simulated function SpawnClientSideChunks() { diff --git a/Classes/UTPlusDummy.uc b/Classes/UTPlusDummy.uc index b08a593..5648ac3 100644 --- a/Classes/UTPlusDummy.uc +++ b/Classes/UTPlusDummy.uc @@ -218,6 +218,11 @@ function TakeDamage( function CompSwap(UTPlusSnapshot SnapA, UTPlusSnapshot SnapB, float Alpha, float TargetTimeStamp) { local vector TargetLoc; local vector TargetVel; + local bbPlayer BP; + local ST_MoverDummy MD; + local Mover BaseMover; + local vector HistoricalMoverLoc; + local float SnapshotMoverZ; if (Actual == None || Actual.bDeleteMe || SnapA == None || bCompActive) return; @@ -237,8 +242,29 @@ function CompSwap(UTPlusSnapshot SnapA, UTPlusSnapshot SnapB, float Alpha, float TargetVel = SnapA.Vel; } - if (SnapA.bBasedOnMover && Actual.Base == SnapA.BaseMover) - TargetLoc.Z += Actual.Base.Location.Z - SnapA.MoverZ; + if (SnapA.bBasedOnMover && Actual.Base == SnapA.BaseMover) { + BaseMover = Mover(SnapA.BaseMover); + if (BaseMover != None) { + BP = bbPlayer(Actual); + if (BP != None && BP.zzUTPure != None) + MD = BP.zzUTPure.FindMoverDummy(BaseMover); + + // If sub-tick interpolation is active and both snapshots are from the + // same mover base, interpolate the mover snapshot Z consistently. + SnapshotMoverZ = SnapA.MoverZ; + if (SnapB != None && SnapB.bBasedOnMover && SnapB.BaseMover == SnapA.BaseMover) + SnapshotMoverZ = SnapA.MoverZ + (SnapB.MoverZ - SnapA.MoverZ) * FClamp(Alpha, 0.0, 1.0); + + // Use historical mover position at the same rewind timestamp. + // Fallback to live mover only when no history actor is available. + if (MD != None) + HistoricalMoverLoc = MD.GetHistoricalLocation(TargetTimeStamp); + else + HistoricalMoverLoc = BaseMover.Location; + + TargetLoc.Z += HistoricalMoverLoc.Z - SnapshotMoverZ; + } + } ActualWasColliding = Actual.bCollideActors; ActualWasBlockingActors = Actual.bBlockActors; @@ -321,4 +347,4 @@ simulated function bool AdjustHitLocation(out vector HitLocation, vector TraceDi defaultproperties { bHidden=True RemoteRole=ROLE_None; -} \ No newline at end of file +} diff --git a/Classes/UTPure.uc b/Classes/UTPure.uc index 72dff8c..078d13c 100644 --- a/Classes/UTPure.uc +++ b/Classes/UTPure.uc @@ -530,15 +530,10 @@ function RegisterProjectile(Projectile P) { function CompensateFor(int Ping, optional Pawn Instigator) { local UTPlusDummy D; local ST_ProjectileDummy PD; - local ST_MoverDummy MD; local Pawn DActual; bCompensationIsActive = true; - for (MD = MoverDummies; MD != none; MD = MD.Next) { - MD.CompStart(Ping, Instigator); - } - for (D = CompDummies; D != none; D = D.Next) { DActual = D.Actual; diff --git a/Classes/bbPlayer.uc b/Classes/bbPlayer.uc index 1148cb6..c7c249a 100644 --- a/Classes/bbPlayer.uc +++ b/Classes/bbPlayer.uc @@ -3839,6 +3839,169 @@ simulated function actor NN_TraceShot(out vector HitLocation, out vector HitNorm return Other; } +function bool IGPlus_LineIntersectsCylinder( + vector Start, + vector End, + vector CylinderCenter, + float Radius, + float HalfHeight +) { + local vector ClosestPoint, LineDelta, ToCenter; + local float LineLengthSq, T, DistXYSq, DistZ; + + LineDelta = End - Start; + LineLengthSq = LineDelta dot LineDelta; + if (LineLengthSq < 0.000001) + return false; + + ToCenter = CylinderCenter - Start; + T = (ToCenter dot LineDelta) / LineLengthSq; + T = FClamp(T, 0.0, 1.0); + + ClosestPoint = Start + LineDelta * T; + + DistXYSq = + (ClosestPoint.X - CylinderCenter.X) * (ClosestPoint.X - CylinderCenter.X) + + (ClosestPoint.Y - CylinderCenter.Y) * (ClosestPoint.Y - CylinderCenter.Y); + DistZ = Abs(ClosestPoint.Z - CylinderCenter.Z); + + return (DistXYSq <= Radius * Radius && DistZ <= HalfHeight); +} + +function bool IGPlus_PointInsideCylinder( + vector P, + vector CylinderCenter, + float Radius, + float HalfHeight +) { + local float DistXYSq; + + DistXYSq = + (P.X - CylinderCenter.X) * (P.X - CylinderCenter.X) + + (P.Y - CylinderCenter.Y) * (P.Y - CylinderCenter.Y); + return (DistXYSq <= Radius * Radius && Abs(P.Z - CylinderCenter.Z) <= HalfHeight); +} + +function bool IGPlus_IsTraceBlockedByTargetMover( + UTPlusDummy TargetDummy, + vector StartTrace, + vector HitLocation +) { + local Mover BaseMover; + local vector HistoricalMoverLoc; + + if (TargetDummy == None || TargetDummy.Actual == None) + return false; + + BaseMover = Mover(TargetDummy.Actual.Base); + if (BaseMover == None) + return false; + + // Same-base shots should not be blocked by this protection. + if (Base == BaseMover) + return false; + + HistoricalMoverLoc = BaseMover.Location; + IGPlus_GetHistoricalMoverLocation(BaseMover, HistoricalMoverLoc); + + // Only guard "from below through mover" shots. + if (StartTrace.Z >= HistoricalMoverLoc.Z || HitLocation.Z <= HistoricalMoverLoc.Z) + return false; + + return IGPlus_LineIntersectsCylinder( + StartTrace, + HitLocation, + HistoricalMoverLoc, + BaseMover.CollisionRadius, + BaseMover.CollisionHeight + ); +} + +function float IGPlus_GetLatencyMs(optional bool bOneWay) { + local float PingMs; + + if (PingAverage > 0) + PingMs = float(PingAverage); + else if (PlayerReplicationInfo != None && PlayerReplicationInfo.Ping > 0) + PingMs = float(PlayerReplicationInfo.Ping); + + if (bOneWay) + return FMax(0.0, 0.5 * PingMs); + return FMax(0.0, PingMs); +} + +function float IGPlus_GetOneWayLatencyMs() { + return IGPlus_GetLatencyMs(true); +} + +function bool IGPlus_GetHistoricalMoverLocationByLatency( + Mover M, + float SampleLatencyMs, + out vector HistoricalMoverLoc +) { + local ST_MoverDummy MD; + local float TargetTimeStamp; + + if (M == None || zzUTPure == None || SampleLatencyMs <= 0.0) + return false; + + MD = zzUTPure.FindMoverDummy(M); + if (MD == None) + return false; + + TargetTimeStamp = Level.TimeSeconds - 0.001 * SampleLatencyMs * Level.TimeDilation; + HistoricalMoverLoc = MD.GetHistoricalLocation(TargetTimeStamp); + return true; +} + +function bool IGPlus_ShouldIgnoreOwnBaseHit( + vector StartTrace, + vector HitLocation +) { + local Mover BaseMover; + local vector HistoricalMoverLoc; + local bool bHasHistorical; + local vector ShotDir; + local float SampleLatencyMs; + + BaseMover = Mover(Base); + if (BaseMover == None) + return false; + + // Self-base occlusion should match shooter view time (one-way latency). + SampleLatencyMs = IGPlus_GetOneWayLatencyMs(); + bHasHistorical = IGPlus_GetHistoricalMoverLocationByLatency(BaseMover, SampleLatencyMs, HistoricalMoverLoc); + if (!bHasHistorical) + return false; + + ShotDir = Normal(HitLocation - StartTrace); + if (ShotDir.Z >= 0.0 && + IGPlus_PointInsideCylinder( + StartTrace, + HistoricalMoverLoc, + BaseMover.CollisionRadius, + BaseMover.CollisionHeight + ) + ) { + return true; + } + + // Ignore a self-base hit only if the shot segment does not intersect the + // mover at the same historical rewind frame. + return !IGPlus_LineIntersectsCylinder( + StartTrace, + HitLocation, + HistoricalMoverLoc, + BaseMover.CollisionRadius, + BaseMover.CollisionHeight + ); +} + +function bool IGPlus_GetHistoricalMoverLocation(Mover M, out vector HistoricalMoverLoc) { + // Target mover blocking uses the full compensation timeline. + return IGPlus_GetHistoricalMoverLocationByLatency(M, IGPlus_GetLatencyMs(false), HistoricalMoverLoc); +} + function Actor TraceShot(out vector HitLocation, out vector HitNormal, vector EndTrace, vector StartTrace) { local Actor A, Other; @@ -3865,15 +4028,17 @@ function Actor TraceShot(out vector HitLocation, out vector HitNormal, vector En continue; } - if (A.IsA('UTPlusDummy')) { - D = UTPlusDummy(A); - if (D != none && D.Actual != none && D.Actual != self) { - if (D.AdjustHitLocation(HitLocation, EndTrace - StartTrace)) { - Other = D.Actual; - break; + if (A.IsA('UTPlusDummy')) { + D = UTPlusDummy(A); + if (D != none && D.Actual != none && D.Actual != self) { + if (D.AdjustHitLocation(HitLocation, EndTrace - StartTrace)) { + if (IGPlus_IsTraceBlockedByTargetMover(D, StartTrace, HitLocation)) + continue; + Other = D.Actual; + break; + } } } - } else if (A.IsA('ST_ProjectileDummy')) { PD = ST_ProjectileDummy(A); if (PD.Actual != None) { @@ -3888,12 +4053,14 @@ function Actor TraceShot(out vector HitLocation, out vector HitNormal, vector En break; } } - } else if ((A == Level) || (Mover(A) != None) || A.bProjTarget || (A.bBlockPlayers && A.bBlockActors)) { - if (bSProjBlocks || !A.IsA('ShockProj') || bWeaponShock) { - Other = A; - break; + } else if ((A == Level) || (Mover(A) != None) || A.bProjTarget || (A.bBlockPlayers && A.bBlockActors)) { + if (A == Base && Mover(A) != None && IGPlus_ShouldIgnoreOwnBaseHit(StartTrace, HitLocation)) + continue; + if (bSProjBlocks || !A.IsA('ShockProj') || bWeaponShock) { + Other = A; + break; + } } - } if (Other != none) break; } From a00b16e57fe9bad14729ed719616f04ddd7938a7 Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:37:41 -0600 Subject: [PATCH 13/14] Improve !igplusmenu server setting GUI, fix shockproj mover edge case --- Classes/IGPlus_ServerSettingsContent.uc | 55 +++++++++--- Classes/IGPlus_SettingsScroll.uc | 59 ++++++------- Classes/ST_ShockRifle.uc | 16 ++-- Classes/bbCHSpectator.uc | 111 +++++++++++++++++++++++- 4 files changed, 189 insertions(+), 52 deletions(-) diff --git a/Classes/IGPlus_ServerSettingsContent.uc b/Classes/IGPlus_ServerSettingsContent.uc index 6fbe9c8..f38c6ba 100644 --- a/Classes/IGPlus_ServerSettingsContent.uc +++ b/Classes/IGPlus_ServerSettingsContent.uc @@ -228,44 +228,68 @@ function bbPlayer ResolveOwnerBBPlayer() { return bbPlayer(ResolveOwnerPawn()); } +function bbCHSpectator ResolveOwnerBBSpectator() { + return bbCHSpectator(ResolveOwnerPawn()); +} + function ServerSettings FindServerSettingsObject() { local bbPlayer P; + local bbCHSpectator S; P = ResolveOwnerBBPlayer(); - if (P == none) - return none; + if (P != none) + return P.IGPlus_GetServerSettingsObject(); + + S = ResolveOwnerBBSpectator(); + if (S != none) + return S.IGPlus_GetServerSettingsObject(); - return P.IGPlus_GetServerSettingsObject(); + return none; } function ResetLocalServerSettingsCache() { local bbPlayer P; + local bbCHSpectator S; P = ResolveOwnerBBPlayer(); - if (P == none) + if (P != none) { + P.IGPlus_ServerSettingsInit(); return; + } - P.IGPlus_ServerSettingsInit(); + S = ResolveOwnerBBSpectator(); + if (S != none) + S.IGPlus_ServerSettingsInit(); } function bool AreServerSettingsLoaded() { local bbPlayer P; + local bbCHSpectator S; P = ResolveOwnerBBPlayer(); - if (P == none) - return false; + if (P != none) + return P.IGPlus_ServerSettingsMenuLoaded; + + S = ResolveOwnerBBSpectator(); + if (S != none) + return S.IGPlus_ServerSettingsMenuLoaded; - return P.IGPlus_ServerSettingsMenuLoaded; + return false; } function bool HasServerAdminAccess() { local bbPlayer P; + local bbCHSpectator S; P = ResolveOwnerBBPlayer(); if (P != none) return P.IGPlus_ServerSettingsMenuCanEdit; - return IsAdmin(); + S = ResolveOwnerBBSpectator(); + if (S != none) + return S.IGPlus_ServerSettingsMenuCanEdit; + + return false; } function float GetNowTimeSeconds() { @@ -296,6 +320,7 @@ function bool IsAdmin() { function RequestServerSettings(optional bool bForce) { local bbPlayer P; + local bbCHSpectator S; local float NowTime; NowTime = GetNowTimeSeconds(); @@ -303,11 +328,17 @@ function RequestServerSettings(optional bool bForce) { return; P = ResolveOwnerBBPlayer(); - if (P == none) + if (P != none) { + P.IGPlus_ServerRequestSettings(); + NextRefreshRequestTime = NowTime + 1.0; return; + } - P.IGPlus_ServerRequestSettings(); - NextRefreshRequestTime = NowTime + 1.0; + S = ResolveOwnerBBSpectator(); + if (S != none) { + S.IGPlus_ServerRequestSettings(); + NextRefreshRequestTime = NowTime + 1.0; + } } function SetStatusText(string Text) { diff --git a/Classes/IGPlus_SettingsScroll.uc b/Classes/IGPlus_SettingsScroll.uc index cac8a49..91327a5 100644 --- a/Classes/IGPlus_SettingsScroll.uc +++ b/Classes/IGPlus_SettingsScroll.uc @@ -7,8 +7,7 @@ enum ESettingsTab { var UWindowSmallButton Btn_Close; var UWindowSmallButton Btn_Save; -var UWindowSmallButton Btn_ClientTab; -var UWindowSmallButton Btn_ServerTab; +var UWindowSmallButton Btn_TabToggle; var localized string SaveButtonText; var localized string SaveButtonToolTip; var localized string ClientTabText; @@ -25,6 +24,19 @@ var UWindowDialogClientWindow ServerTabArea; var float FixedPaddingX; var float FixedPaddingY; +function UpdateTabToggleButton() { + if (Btn_TabToggle == none) + return; + + if (ActiveTab == ST_Client) { + Btn_TabToggle.SetText(ServerTabText); + Btn_TabToggle.ToolTipString = ServerTabToolTip; + } else { + Btn_TabToggle.SetText(ClientTabText); + Btn_TabToggle.ToolTipString = ClientTabToolTip; + } +} + function EnsureServerTabArea() { if (ServerTabArea != none) return; @@ -59,10 +71,7 @@ function SetActiveTab(ESettingsTab NewTab) { ActiveTab = ST_Client; } - if (Btn_ClientTab != none) - Btn_ClientTab.bDisabled = (ActiveTab == ST_Client); - if (Btn_ServerTab != none) - Btn_ServerTab.bDisabled = (ActiveTab == ST_Server); + UpdateTabToggleButton(); Load(); } @@ -70,27 +79,14 @@ function SetActiveTab(ESettingsTab NewTab) { function Created() { super.Created(); - Btn_ClientTab = UWindowSmallButton(FixedArea.CreateControl( + Btn_TabToggle = UWindowSmallButton(FixedArea.CreateControl( class'UWindowSmallButton', FixedPaddingX, FixedPaddingY, 32, 16 )); - Btn_ClientTab.SetText(ClientTabText); - Btn_ClientTab.ToolTipString = ClientTabToolTip; - Btn_ClientTab.Register(self); - - Btn_ServerTab = UWindowSmallButton(FixedArea.CreateControl( - class'UWindowSmallButton', - FixedPaddingX + 40, - FixedPaddingY, - 32, - 16 - )); - Btn_ServerTab.SetText(ServerTabText); - Btn_ServerTab.ToolTipString = ServerTabToolTip; - Btn_ServerTab.Register(self); + Btn_TabToggle.Register(self); Btn_Save = UWindowSmallButton(FixedArea.CreateControl( class'UWindowSmallButton', @@ -116,7 +112,8 @@ function Created() { ClientTabArea = ClientArea; EnsureServerTabArea(); - ActiveTab = ST_Client; + // Force first SetActiveTab call to run initialization path. + ActiveTab = ST_Server; SetActiveTab(ST_Client); } @@ -124,11 +121,12 @@ function Notify(UWindowDialogControl C, byte E) { Super.Notify(C, E); - if (E == DE_Click && C == Btn_ClientTab) - SetActiveTab(ST_Client); - else if (E == DE_Click && C == Btn_ServerTab) - SetActiveTab(ST_Server); - else if (E == DE_Click && C == Btn_Save) { + if (E == DE_Click && C == Btn_TabToggle) { + if (ActiveTab == ST_Client) + SetActiveTab(ST_Server); + else + SetActiveTab(ST_Client); + } else if (E == DE_Click && C == Btn_Save) { Save(); if (IGPlus_SettingsContent(ClientArea) != none) Load(); @@ -138,11 +136,8 @@ function Notify(UWindowDialogControl C, byte E) function BeforePaint(Canvas C, float X, float Y) { super.BeforePaint(C, X, Y); - Btn_ClientTab.AutoWidth(C); - Btn_ClientTab.WinLeft = FixedPaddingX; - - Btn_ServerTab.AutoWidth(C); - Btn_ServerTab.WinLeft = FixedPaddingX + Btn_ClientTab.WinWidth + 5; + Btn_TabToggle.AutoWidth(C); + Btn_TabToggle.WinLeft = FixedPaddingX; Btn_Close.AutoWidth(C); Btn_Close.WinLeft = FixedArea.WinWidth-FixedPaddingX-Btn_Close.WinWidth; diff --git a/Classes/ST_ShockRifle.uc b/Classes/ST_ShockRifle.uc index 6de5f93..4fad87c 100644 --- a/Classes/ST_ShockRifle.uc +++ b/Classes/ST_ShockRifle.uc @@ -137,7 +137,8 @@ function ServerExplicitFire( ExplicitClientBaseMover = Mover(Owner.Base); } else { ExplicitClientLoc = Owner.Location; - ExplicitClientBaseMover = Mover(Owner.Base); + // Fallback already uses server-current position; skip mover re-adjust later. + ExplicitClientBaseMover = none; } ExplicitClientRot = ClientRot; @@ -176,7 +177,7 @@ function ServerExplicitFire( ExplicitClientBaseMover = Mover(Owner.Base); } else { ExplicitClientLoc = Owner.Location; - ExplicitClientBaseMover = Mover(Owner.Base); + ExplicitClientBaseMover = none; } ExplicitClientRot = ClientRot; @@ -246,7 +247,7 @@ function ServerExplicitAltFire( ExplicitClientBaseMover = Mover(Owner.Base); } else { ExplicitClientLoc = Owner.Location; - ExplicitClientBaseMover = Mover(Owner.Base); + ExplicitClientBaseMover = none; } ExplicitClientRot = ClientRot; @@ -280,7 +281,7 @@ function ServerExplicitAltFire( ExplicitClientBaseMover = Mover(Owner.Base); } else { ExplicitClientLoc = Owner.Location; - ExplicitClientBaseMover = Mover(Owner.Base); + ExplicitClientBaseMover = none; } ExplicitClientRot = ClientRot; @@ -319,10 +320,11 @@ function Projectile ExplicitProjectileFire(class ProjClass, float Pr PawnOwner = Pawn(Owner); Owner.MakeNoise(PawnOwner.SoundDampening); - // Use Explicit Client Rotation - GetAxes(ExplicitClientRot,X,Y,Z); + // Use Explicit Client Rotation + GetAxes(ExplicitClientRot,X,Y,Z); AdjustedClientLoc = ExplicitClientLoc; - if (WImp != None && IsPingCompEnabled()) + // Apply mover-frame conversion only for explicit data captured while riding a mover. + if (WImp != None && IsPingCompEnabled() && ExplicitClientBaseMover != none) AdjustedClientLoc = WImp.IGPlus_AdjustLocationToCurrentMoverFrame( PawnOwner, AdjustedClientLoc, diff --git a/Classes/bbCHSpectator.uc b/Classes/bbCHSpectator.uc index ee1b956..e1371dc 100644 --- a/Classes/bbCHSpectator.uc +++ b/Classes/bbCHSpectator.uc @@ -42,6 +42,10 @@ var Pawn FlagCarrierToFollow; var bool IGPlus_TryOpenSettingsMenu; var IGPlus_SettingsDialog IGPlus_SettingsMenu; +var Object IGPlus_ServerSettingsHelper; +var ServerSettings IGPlus_ServerSettingsMenuData; +var bool IGPlus_ServerSettingsMenuLoaded; +var bool IGPlus_ServerSettingsMenuCanEdit; replication { @@ -53,7 +57,14 @@ replication // Server->Client reliable if ( Role == ROLE_Authority ) - IGPlusMenu, xxSetTimes; //, xxClientActivateMover; + IGPlusMenu, + xxSetTimes, + IGPlus_ServerSettingsInit, + IGPlus_ServerSettingsSet, + IGPlus_ServerSettingsDone; //, xxClientActivateMover; + + reliable if (Role < ROLE_Authority) + IGPlus_ServerRequestSettings; reliable if ( (!bDemoRecording || (bClientDemoRecording && bClientDemoNetFunc) || (Level.NetMode==NM_Standalone)) && Role == ROLE_Authority ) ReceiveWeaponEffect; @@ -970,6 +981,104 @@ function IGPlus_OpenSettingsMenu() { IGPlus_SettingsMenu.Load(); } +simulated function ServerSettings IGPlus_GetServerSettingsObject() { + if (IGPlus_ServerSettingsMenuData != none) + return IGPlus_ServerSettingsMenuData; + + IGPlus_ServerSettingsHelper = new(none, 'InstaGibPlus') class'Object'; + IGPlus_ServerSettingsMenuData = new(IGPlus_ServerSettingsHelper, 'IGPlus_ServerSettingsMenu') class'ServerSettings'; + return IGPlus_ServerSettingsMenuData; +} + +function IGPlus_ServerSettingsInit() { + IGPlus_ServerSettingsMenuLoaded = false; + IGPlus_ServerSettingsMenuCanEdit = false; + IGPlus_ServerSettingsMenuData = none; +} + +function IGPlus_ServerSettingsSet(string Key, string Value) { + local ServerSettings S; + + S = IGPlus_GetServerSettingsObject(); + if (S == none) + return; + + S.SetPropertyText(Key, Value); +} + +function IGPlus_ServerSettingsDone(bool bCanEdit) { + IGPlus_ServerSettingsMenuLoaded = true; + IGPlus_ServerSettingsMenuCanEdit = bCanEdit; +} + +function IGPlus_ServerSendSetting(string Key) { + if (zzUTPure == none || zzUTPure.Settings == none) + return; + + IGPlus_ServerSettingsSet(Key, zzUTPure.Settings.GetPropertyText(Key)); +} + +function IGPlus_ServerRequestSettings() { + if (bAdmin == false || zzUTPure == none || zzUTPure.Settings == none) { + IGPlus_ServerSettingsInit(); + IGPlus_ServerSettingsDone(false); + return; + } + + IGPlus_ServerSettingsInit(); + + IGPlus_ServerSendSetting("bAutoPause"); + IGPlus_ServerSendSetting("PauseTotalTime"); + IGPlus_ServerSendSetting("PauseTime"); + IGPlus_ServerSendSetting("bForceDemo"); + IGPlus_ServerSendSetting("bRestrictTrading"); + IGPlus_ServerSendSetting("MaxTradeTimeMargin"); + IGPlus_ServerSendSetting("TradePingMargin"); + IGPlus_ServerSendSetting("KillCamDelay"); + IGPlus_ServerSendSetting("KillCamDuration"); + + IGPlus_ServerSendSetting("bJumpingPreservesMomentum"); + IGPlus_ServerSendSetting("bOldLandingMomentum"); + IGPlus_ServerSendSetting("bEnableSingleButtonDodge"); + IGPlus_ServerSendSetting("bUseFlipAnimation"); + IGPlus_ServerSendSetting("bEnableWallDodging"); + IGPlus_ServerSendSetting("bDodgePreserveZMomentum"); + IGPlus_ServerSendSetting("MaxMultiDodges"); + IGPlus_ServerSendSetting("BrightskinMode"); + IGPlus_ServerSendSetting("PlayerScale"); + IGPlus_ServerSendSetting("bAlwaysRenderFlagCarrier"); + IGPlus_ServerSendSetting("bAlwaysRenderDroppedFlags"); + + IGPlus_ServerSendSetting("MaxPosError"); + IGPlus_ServerSendSetting("MaxHitError"); + IGPlus_ServerSendSetting("MaxJitterTime"); + IGPlus_ServerSendSetting("WarpFixDelay"); + IGPlus_ServerSendSetting("FireTimeout"); + IGPlus_ServerSendSetting("MinNetUpdateRate"); + IGPlus_ServerSendSetting("MaxNetUpdateRate"); + IGPlus_ServerSendSetting("bEnableInputReplication"); + IGPlus_ServerSendSetting("bEnableServerExtrapolation"); + IGPlus_ServerSendSetting("bEnableServerPacketReordering"); + IGPlus_ServerSendSetting("bEnableLoosePositionCheck"); + IGPlus_ServerSendSetting("bPlayersAlwaysRelevant"); + IGPlus_ServerSendSetting("bEnablePingCompensatedSpawn"); + IGPlus_ServerSendSetting("bEnableJitterBounding"); + IGPlus_ServerSendSetting("LooseCheckCorrectionFactor"); + IGPlus_ServerSendSetting("LooseCheckCorrectionFactorOnMover"); + IGPlus_ServerSendSetting("bEnableSnapshotInterpolation"); + IGPlus_ServerSendSetting("SnapshotInterpSendHz"); + IGPlus_ServerSendSetting("SnapshotInterpRewindMs"); + IGPlus_ServerSendSetting("bEnableWarpFix"); + + IGPlus_ServerSendSetting("ShowTouchedPackage"); + IGPlus_ServerSendSetting("HitFeedbackMode"); + IGPlus_ServerSendSetting("bEnableDamageDebugMode"); + IGPlus_ServerSendSetting("bEnableDamageDebugConsoleMessages"); + IGPlus_ServerSendSetting("bEnableHitboxDebugMode"); + + IGPlus_ServerSettingsDone(true); +} + // Notification of other player respawning, play effects locally function IGPlus_NotifyPlayerRestart(vector Loc, rotator Dir, bbPlayer Other) { local UTTeleportEffect PTE; From 8eb14b6ff6b61988012e6b104a86df73b69396ed Mon Sep 17 00:00:00 2001 From: rX <126023192+rxut@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:34:03 -0600 Subject: [PATCH 14/14] Updated README --- README.md | 616 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 484 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 636fd2e..e71a907 100644 --- a/README.md +++ b/README.md @@ -27,89 +27,102 @@ An InstaGib focussed fork of TimTim's NewNet mutator for Unreal Tournament. These settings can be found in **InstaGibPlus.ini** under section **\[ClientSettings\]**. 1. [bForceModels](#bforcemodels) -2. [DesiredSkin](#desiredskin) -3. [DesiredSkinFemale](#desiredskinfemale) -4. [DesiredTeamSkin](#desiredteamskin) -5. [DesiredTeamSkinFemale](#desiredteamskinfemale) -6. [SkinEnemyIndexMap](#skinenemyindexmap) -7. [SkinTeamIndexMap](#skinteamindexmap) -8. [bSkinEnemyUseIndexMap](#bskinenemyuseindexmap) -9. [bSkinTeamUseIndexMap](#bskinteamuseindexmap) -10. [bUnlitSkins](#bunlitskins) -11. [HitSound](#hitsound) -12. [TeamHitSound](#teamhitsound) -13. [bDisableForceHitSounds](#bdisableforcehitsounds) -14. [bEnableHitSounds](#benablehitsounds) -15. [bEnableTeamHitSounds](#benableteamhitsounds) -16. [bHitSoundPitchShift](#bhitsoundpitchshift) -17. [bHitSoundTeamPitchShift](#bhitsoundteampitchshift) -18. [HitSoundSource](#hitsoundsource) -19. [SelectedHitSound](#selectedhitsound) -20. [SelectedTeamHitSound](#selectedteamhitsound) -21. [HitSoundVolume](#hitsoundvolume) -22. [HitSoundTeamVolume](#hitsoundteamvolume) -23. [sHitSound](#shitsound) -24. [bDoEndShot](#bdoendshot) -25. [bAutoDemo](#bautodemo) -26. [DemoMask](#demomask) -27. [DemoPath](#demopath) -28. [DemoChar](#demochar) -29. [bTeamInfo](#bteaminfo) -30. [bShootDead](#bshootdead) -31. [cShockBeam](#cshockbeam) -32. [bHideOwnBeam](#bhideownbeam) -33. [BeamScale](#beamscale) -34. [BeamFadeCurve](#beamfadecurve) -35. [BeamDuration](#beamduration) -36. [BeamOriginMode](#beamoriginmode) -37. [BeamDestinationMode](#beamdestinationmode) -38. [SSRRingType](#ssrringtype) -39. [bNoOwnFootsteps](#bnoownfootsteps) -40. [DesiredNetUpdateRate](#desirednetupdaterate) -41. [DesiredNetspeed](#desirednetspeed) -42. [FakeCAPInterval](#fakecapinterval) -43. [bNoSmoothing](#bnosmoothing) -44. [bLogClientMessages](#blogclientmessages) -45. [bDebugMovement](#bdebugmovement) -46. [bEnableKillCam](#benablekillcam) -47. [MinDodgeClickTime](#mindodgeclicktime) -48. [bUseOldMouseInput](#buseoldmouseinput) -49. [SmoothVRController](#smoothvrcontroller) -50. [bShowFPS](#bshowfps) -51. [FPSLocationX](#fpslocationx) -52. [FPSLocationY](#fpslocationy) -53. [FPSDetail](#fpsdetail) -54. [FPSCounterSmoothingStrength](#fpscountersmoothingstrength) -55. [KillCamMinDelay](#killcammindelay) -56. [bAllowWeaponShake](#ballowweaponshake) -57. [bAutoReady](#bautoready) -58. [bShowDeathReport](#bshowdeathreport) -59. [bSmoothFOVChanges](#bsmoothfovchanges) -60. [bEnableKillFeed](#benablekillfeed) -61. [KillFeedX](#killfeedx) -62. [KillFeedY](#killfeedy) -63. [KillFeedSpeed](#killfeedspeed) -64. [KillFeedScale](#killfeedscale) -65. [FraggerScopeChoice](#fraggerscopechoice) -66. [bEnableNetStats](#benablenetstats) -67. [bNetStatsUnconfirmedTime](#bnetstatsunconfirmedtime) -68. [bNetStatsLocationError](#bnetstatslocationerror) -69. [bNetStatsFrameTime](#bnetstatsframetime) -69. [NetStatsLocationX](#netstatslocationx) -70. [NetStatsLocationY](#netstatslocationy) -71. [NetStatsWidth](#netstatswidth) -72. [bEnableHitMarker](#benablehitmarker) -73. [bEnableTeamHitMarker](#benableteamhitmarker) -74. [HitMarkerColorMode](#hitmarkercolormode) -75. [HitMarkerColor](#hitmarkercolor) -76. [HitMarkerTeamColor](#hitmarkerteamcolor) -77. [HitMarkerSize](#hitmarkersize) -78. [HitMarkerOffset](#hitmarkeroffset) -79. [HitMarkerDuration](#hitmarkerduration) -70. [HitMarkerDecayExponent](#hitmarkerdecayexponent) -81. [HitMarkerSource](#hitmarkersource) -82. [bUseCrosshairFactory](#busecrosshairfactory) -83. [CrosshairLayers](#crosshairlayers) +1. [DesiredSkin](#desiredskin) +1. [DesiredSkinFemale](#desiredskinfemale) +1. [DesiredTeamSkin](#desiredteamskin) +1. [DesiredTeamSkinFemale](#desiredteamskinfemale) +1. [SkinEnemyIndexMap](#skinenemyindexmap) +1. [SkinTeamIndexMap](#skinteamindexmap) +1. [bSkinEnemyUseIndexMap](#bskinenemyuseindexmap) +1. [bSkinTeamUseIndexMap](#bskinteamuseindexmap) +1. [bUnlitSkins](#bunlitskins) +1. [HitSound](#hitsound) +1. [TeamHitSound](#teamhitsound) +1. [bDisableForceHitSounds](#bdisableforcehitsounds) +1. [bEnableHitSounds](#benablehitsounds) +1. [bEnableTeamHitSounds](#benableteamhitsounds) +1. [bHitSoundPitchShift](#bhitsoundpitchshift) +1. [bHitSoundTeamPitchShift](#bhitsoundteampitchshift) +1. [HitSoundSource](#hitsoundsource) +1. [SelectedHitSound](#selectedhitsound) +1. [SelectedTeamHitSound](#selectedteamhitsound) +1. [HitSoundVolume](#hitsoundvolume) +1. [HitSoundTeamVolume](#hitsoundteamvolume) +1. [sHitSound](#shitsound) +1. [bDoEndShot](#bdoendshot) +1. [bAutoDemo](#bautodemo) +1. [DemoMask](#demomask) +1. [DemoPath](#demopath) +1. [DemoChar](#demochar) +1. [bTeamInfo](#bteaminfo) +1. [bShootDead](#bshootdead) +1. [cShockBeam](#cshockbeam) +1. [bHideOwnBeam](#bhideownbeam) +1. [bBeamEnableLight](#bbeamenablelight) +1. [BeamScale](#beamscale) +1. [BeamFadeCurve](#beamfadecurve) +1. [BeamDuration](#beamduration) +1. [BeamOriginMode](#beamoriginmode) +1. [BeamDestinationMode](#beamdestinationmode) +1. [SSRRingType](#ssrringtype) +1. [bNoOwnFootsteps](#bnoownfootsteps) +1. [DesiredNetUpdateRate](#desirednetupdaterate) +1. [DesiredNetspeed](#desirednetspeed) +1. [FakeCAPInterval](#fakecapinterval) +1. [bNoSmoothing](#bnosmoothing) +1. [bLogClientMessages](#blogclientmessages) +1. [bDemoShowClientMessages](#bdemoshowclientmessages) +1. [bDebugMovement](#bdebugmovement) +1. [bEnableKillCam](#benablekillcam) +1. [MinDodgeClickTime](#mindodgeclicktime) +1. [bUseOldMouseInput](#buseoldmouseinput) +1. [SmoothVRController](#smoothvrcontroller) +1. [bShowFPS](#bshowfps) +1. [FPSLocationX](#fpslocationx) +1. [FPSLocationY](#fpslocationy) +1. [FPSDetail](#fpsdetail) +1. [FPSCounterSmoothingStrength](#fpscountersmoothingstrength) +1. [KillCamMinDelay](#killcammindelay) +1. [bReduceEyeHeightInAir](#breduceeyeheightinair) +1. [bAllowWeaponShake](#ballowweaponshake) +1. [bAutoReady](#bautoready) +1. [bShowDeathReport](#bshowdeathreport) +1. [bSmoothFOVChanges](#bsmoothfovchanges) +1. [bEnableLocationOffsetFix](#benablelocationoffsetfix) +1. [bEnableKillFeed](#benablekillfeed) +1. [KillFeedX](#killfeedx) +1. [KillFeedY](#killfeedy) +1. [KillFeedSpeed](#killfeedspeed) +1. [KillFeedScale](#killfeedscale) +1. [FraggerScopeChoice](#fraggerscopechoice) +1. [bEnableNetStats](#benablenetstats) +1. [bNetStatsUnconfirmedTime](#bnetstatsunconfirmedtime) +1. [bNetStatsLocationError](#bnetstatslocationerror) +1. [bNetStatsFrameTime](#bnetstatsframetime) +1. [NetStatsLocationX](#netstatslocationx) +1. [NetStatsLocationY](#netstatslocationy) +1. [NetStatsWidth](#netstatswidth) +1. [bEnableHitMarker](#benablehitmarker) +1. [bEnableTeamHitMarker](#benableteamhitmarker) +1. [HitMarkerColorMode](#hitmarkercolormode) +1. [HitMarkerColor](#hitmarkercolor) +1. [HitMarkerTeamColor](#hitmarkerteamcolor) +1. [HitMarkerSize](#hitmarkersize) +1. [HitMarkerOffset](#hitmarkeroffset) +1. [HitMarkerDuration](#hitmarkerduration) +1. [HitMarkerDecayExponent](#hitmarkerdecayexponent) +1. [HitMarkerSource](#hitmarkersource) +1. [bUseCrosshairFactory](#busecrosshairfactory) +1. [CrosshairLayers](#crosshairlayers) +1. [bBioUseClientSideAnimations](#bbiouseclientsideanimations) +1. [bShockBeamUseClientSideAnimations](#bshockbeamuseclientsideanimations) +1. [bShockProjectileUseClientSideAnimations](#bshockprojectileuseclientsideanimations) +1. [bPulseUseClientSideAnimations](#bpulseuseclientsideanimations) +1. [bRipperUseClientSideAnimations](#bripperuseclientsideanimations) +1. [bFlakUseClientSideAnimations](#bflakuseclientsideanimations) +1. [bRocketUseClientSideAnimations](#brocketuseclientsideanimations) +1. [bSniperUseClientSideAnimations](#bsniperuseclientsideanimations) +1. [bTranslocatorUseClientSideAnimations](#btranslocatoruseclientsideanimations) ## bForceModels **Type: bool** @@ -221,6 +234,15 @@ Whether the HitSound from hitting enemies should be pitch shifted depending on d Whether the HitSound from hitting teammates should be pitch shifted depending on damage dealt or not. +## HitSoundSource +**Type: EHitSoundSource** +**Default: HSSRC_Server** + +Selects where hit sound events come from. + +* `HSSRC_Server` ➜ Use server-side hit confirmations (accurate but delayed by ping). +* `HSSRC_Client` ➜ Use client-side hit detection (instant but can be inaccurate). + ## SelectedHitSound **Type: int** **Default: 0** @@ -324,6 +346,13 @@ The style of beam to use for the SuperShockRifle. If `True`, hides your own SuperShockRifle beams, no matter the value of [cShockBeam](#cshockbeam). +## bBeamEnableLight +**Type: bool** +**Default: True** + +If `True`, shock beams emit dynamic light. +If `False`, dynamic light from beams is disabled. + ## BeamScale **Type: float** **Default: 0.45** @@ -402,7 +431,7 @@ Higher values result in less frequent acknowledgements which can result in degra ## bNoSmoothing **Type: bool** -**Default: False** +**Default: True** The default mouse input smoothing algorithm always smears input over at least two frames, half the input being applied on one frame, the other half on the next frame. If set to `True`, the game will always apply all input on the current frame. If set to `False`, the default algorithm will be used. @@ -413,6 +442,14 @@ This is a backport from UT99 client version 469, where the equivalent setting is **Default: True** Causes all ClientMessages to be logged, if set to `True` + +## bDemoShowClientMessages +**Type: bool** +**Default: False** + +If `True`, ClientMessages are shown while playing demos. +If `False`, ClientMessages are hidden while playing demos. + ## bDebugMovement **Type: bool** **Default: False** @@ -492,6 +529,13 @@ How many samples to average FPS over. Minimum time between death and when KillCam starts rotating towards killer. +## bReduceEyeHeightInAir +**Type: bool** +**Default: False** + +If `True`, reduces eye height while in air. +If `False`, uses normal eye height while in air. + ## bAllowWeaponShake **Type: bool** **Default: True** @@ -517,6 +561,13 @@ If `True`, show a report of damage taken that lead to death. The report starts f If `True`, smooth changes to your FOV, which can happen when spawning, teleporting or zooming. If `False`, your FOV immediately changes to the desired FOV without a smooth transition. +## bEnableLocationOffsetFix +**Type: bool** +**Default: True** + +If `True`, enables client-side location offset correction to reduce visual jitter/offset artifacts. +If `False`, disables that correction. + ## bEnableKillFeed **Type: bool** **Default: True** @@ -709,6 +760,60 @@ A crosshair is made up of individual images that are drawn in a specific order p * `bSmooth` controls whether sharp edges should be smoothed out when `ScaleX` or `ScaleY` are greater than 1 * `bUse` controls whether this layer should be drawn, `True` to draw, `False` to ignore +## bBioUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Bio Rifle ping-compensated logic. + +## bShockBeamUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Shock Beam ping-compensated logic. + +## bShockProjectileUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Shock Projectile ping-compensated logic. + +## bPulseUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Pulse Gun ping-compensated logic. + +## bRipperUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Ripper ping-compensated logic. + +## bFlakUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Flak ping-compensated logic. + +## bRocketUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Rocket Launcher ping-compensated logic. + +## bSniperUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Sniper ping-compensated logic. + +## bTranslocatorUseClientSideAnimations +**Type: bool** +**Default: False** + +Enable client-side animation timing for Translocator ping-compensated logic. + # Client Commands The following commands are additions by IG+ to the standard set of commands. @@ -722,36 +827,65 @@ Types for parameters: Parameters marked `optional` do not have to be supplied. 1. [EnableDebugData](#enabledebugdata) -2. [EnableHitSound](#enablehitsound) -3. [EnableTeamHitSound](#enableteamhitsound) -4. [SetHitSound](#sethitsound) -5. [SetTeamHitSound](#setteamhitsound) -6. [ForceModels](#forcemodels) -7. [ListSkins](#listskins) -8. [SetForcedSkins](#setforcedskins) -9. [SetForcedTeamSkins](#setforcedteamskins) -10. [SetShockBeam](#setshockbeam) -11. [SetBeamScale](#setbeamscale) -12. [MyIgSettings](#myigsettings) -13. [SetNetUpdateRate](#setnetupdaterate) -14. [SetMouseSmoothing](#setmousesmoothing) -15. [SetKillCamEnabled](#setkillcamenabled) -16. [DropFlag](#dropflag) -17. [PureLogo](#purelogo) -18. [TeamInfo](#teaminfo) -19. [SetMinDodgeClickTime](#setmindodgeclicktime) -20. [mdct](#mdct) -21. [EndShot](#endshot) -22. [Hold](#hold) -23. [Go](#go) -24. [AutoDemo](#autodemo) -25. [ShootDead](#shootdead) -26. [SetDemoMask](#setdemomask) -27. [DemoStart](#demostart) -28. [ShowFPS](#showfps) -29. [ShowOwnBeam](#showownbeam) -30. [Ready](#ready) -31. [ZoomToggle](#zoomtoggle) +1. [EnableHitSound](#enablehitsound) +1. [EnableTeamHitSound](#enableteamhitsound) +1. [SetHitSound](#sethitsound) +1. [SetTeamHitSound](#setteamhitsound) +1. [ForceModels](#forcemodels) +1. [ListSkins](#listskins) +1. [SetForcedSkins](#setforcedskins) +1. [SetForcedTeamSkins](#setforcedteamskins) +1. [SetShockBeam](#setshockbeam) +1. [SetBeamScale](#setbeamscale) +1. [MyIgSettings](#myigsettings) +1. [SetNetUpdateRate](#setnetupdaterate) +1. [SetMouseSmoothing](#setmousesmoothing) +1. [SetKillCamEnabled](#setkillcamenabled) +1. [DropFlag](#dropflag) +1. [PureLogo](#purelogo) +1. [TeamInfo](#teaminfo) +1. [SetMinDodgeClickTime](#setmindodgeclicktime) +1. [mdct](#mdct) +1. [EndShot](#endshot) +1. [Hold](#hold) +1. [Go](#go) +1. [AutoDemo](#autodemo) +1. [ShootDead](#shootdead) +1. [SetDemoMask](#setdemomask) +1. [DemoStart](#demostart) +1. [ShowFPS](#showfps) +1. [ShowOwnBeam](#showownbeam) +1. [Ready](#ready) +1. [ZoomToggle](#zoomtoggle) +1. [IGPlusMenu](#igplusmenu) +1. [EnableClientSideAnimations](#enableclientsideanimations) +1. [DisableClientSideAnimations](#disableclientsideanimations) +1. [BioClientSideAnimations](#bioclientsideanimations) +1. [ShockBeamClientSideAnimations](#shockbeamclientsideanimations) +1. [ShockProjectileClientSideAnimations](#shockprojectileclientsideanimations) +1. [PulseClientSideAnimations](#pulseclientsideanimations) +1. [RipperClientSideAnimations](#ripperclientsideanimations) +1. [FlakClientSideAnimations](#flakclientsideanimations) +1. [RocketClientSideAnimations](#rocketclientsideanimations) +1. [SniperClientSideAnimations](#sniperclientsideanimations) +1. [TranslocatorClientSideAnimations](#translocatorclientsideanimations) +1. [NetcodeHelp](#netcodehelp) +1. [bEnableLoosePositionCheck](#benableloosepositioncheck-command) +1. [LooseCheckCorrectionFactor](#loosecheckcorrectionfactor-command) +1. [LooseCheckCorrectionFactorOnMover](#loosecheckcorrectionfactoronmover-command) +1. [bEnableInputReplication](#benableinputreplication-command) +1. [bEnableSnapshotInterpolation](#benablesnapshotinterpolation-command) +1. [SnapshotInterpSendHz](#snapshotinterpsendhz-command) +1. [SnapshotInterpRewindMs](#snapshotinterprewindms-command) +1. [MaxJitterTime](#maxjittertime-command) +1. [bEnableJitterBounding](#benablejitterbounding-command) +1. [SnapInterpDebug](#snapinterpdebug) +1. [SnapInterpStatus](#snapinterpstatus) +1. [SnapInterpNetStatus](#snapinterpnetstatus) +1. [SnapInterpCheck](#snapinterpcheck) +1. [SnapInterpCheckStop](#snapinterpcheckstop) +1. [TraceInput](#traceinput) +1. [ShowTickrate / ShowNetSpeeds](#showtickrate--shownetspeeds) ## EnableDebugData **Parameters: (bool b)** @@ -880,6 +1014,138 @@ If `SensitivityY` is not provided by the user, it is assumed to be the same as ` - Same sensitivity as un-zoomed: `ZoomToggle 1.0` - Half the sensitivity as un-zoomed: `ZoomToggle 0.5` +## IGPlusMenu +Opens the IG+ settings window. + +## EnableClientSideAnimations +Enables all per-weapon client-side animation toggles (unless forced by server). + +## DisableClientSideAnimations +Disables all per-weapon client-side animation toggles (unless forced by server). + +## BioClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Bio weapon behavior. + +## ShockBeamClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Shock Beam behavior. + +## ShockProjectileClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Shock Projectile behavior. + +## PulseClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Pulse behavior. + +## RipperClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Ripper behavior. + +## FlakClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Flak behavior. + +## RocketClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Rocket behavior. + +## SniperClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Sniper behavior. + +## TranslocatorClientSideAnimations +**Parameters: (bool b)** + +Enable/disable client-side animations for Translocator behavior. + +## NetcodeHelp +Prints netcode runtime settings and available netcode debug commands. + +## bEnableLoosePositionCheck (command) +**Parameters: (optional string Value)** + +Read/set server setting [bEnableLoosePositionCheck](#benableloosepositioncheck) (admin only). + +## LooseCheckCorrectionFactor (command) +**Parameters: (optional string Value)** + +Read/set server setting [LooseCheckCorrectionFactor](#loosecheckcorrectionfactor) (admin only). + +## LooseCheckCorrectionFactorOnMover (command) +**Parameters: (optional string Value)** + +Read/set server setting [LooseCheckCorrectionFactorOnMover](#loosecheckcorrectionfactoronmover) (admin only). + +## bEnableInputReplication (command) +**Parameters: (optional string Value)** + +Read/set server setting [bEnableInputReplication](#benableinputreplication) (admin only). + +## bEnableSnapshotInterpolation (command) +**Parameters: (optional string Value)** + +Read/set server setting [bEnableSnapshotInterpolation](#benablesnapshotinterpolation) (admin only). + +## SnapshotInterpSendHz (command) +**Parameters: (optional string Value)** + +Read/set server setting [SnapshotInterpSendHz](#snapshotinterpsendhz) (admin only). + +## SnapshotInterpRewindMs (command) +**Parameters: (optional string Value)** + +Read/set server setting [SnapshotInterpRewindMs](#snapshotinterprewindms) (admin only). + +## MaxJitterTime (command) +**Parameters: (optional string Value)** + +Read/set server setting [MaxJitterTime](#maxjittertime) (admin only). + +## bEnableJitterBounding (command) +**Parameters: (optional string Value)** + +Read/set server setting [bEnableJitterBounding](#benablejitterbounding) (admin only). + +## SnapInterpDebug +Toggles snapshot interpolation debug reporting. + +## SnapInterpStatus +**Parameters: (optional string NameFilter)** + +Prints one-shot proxy runtime status. + +## SnapInterpNetStatus +**Parameters: (optional string NameFilter)** + +Prints client report + server echo + proxy status. + +## SnapInterpCheck +**Parameters: (optional string Profile, optional float DurationSec, optional string NameFilter)** + +Runs an interpolation quality check against a simulated proxy target. + +## SnapInterpCheckStop +Stops an active SnapInterpCheck run. + +## TraceInput +Toggles logging of client input trace data. + +## ShowTickrate / ShowNetSpeeds +These are mutate commands exposed by UTPure command handling: + +* `mutate PureShowTickrate` +* `mutate PureShowNetspeeds` + # Server Installation Extract the zipped files to your installation's folder. @@ -932,6 +1198,12 @@ When connected to the server type **'mutate playerhelp'** in the console to view - BanID x (Will Ban & Kick player with ID x) - EnablePure/DisablePure - ShowDemos (Will show who is recording demos) +- mutate IGPlusServerGet \ +- mutate IGPlusServerSet \ \ +- mutate IGPlusServerSetSilent \ \ +- mutate PureShowTickrate +- mutate PureShowNetspeeds +- mutate PingCompSettings As spectator, you may need to add 'mutate pure' + command (mutate pureshowtickrate) @@ -941,7 +1213,6 @@ Server settings can be found inside InstaGibPlus.ini. 1. [HeadshotDamage](#headshotdamage) 1. [SniperSpeed](#sniperspeed) 1. [SniperDamagePri](#sniperdamagepri) -1. [SetPendingWeapon](#setpendingweapon) 1. [NNAnnouncer](#nnannouncer) 1. [bUTPureEnabled](#butpureenabled) 1. [Advertise](#advertise) @@ -958,13 +1229,16 @@ Server settings can be found inside InstaGibPlus.ini. 1. [bAdvancedTeamSay](#badvancedteamsay) 1. [ForceSettingsLevel](#forcesettingslevel) 1. [bWarmup](#bwarmup) +1. [WarmupTimeLimit](#warmuptimelimit) 1. [bCoaches](#bcoaches) 1. [bAutoPause](#bautopause) 1. [PauseTotalTime](#pausetotaltime) 1. [PauseTime](#pausetime) +1. [Timeouts](#timeouts) 1. [ForceModels](#forcemodels) 1. [ImprovedHUD](#improvedhud) 1. [bDelayedPickupSpawn](#bdelayedpickupspawn) +1. [bUseFastWeaponSwitch](#busefastweaponswitch) 1. [bTellSpectators](#btellspectators) 1. [PlayerPacks](#playerpacks) 1. [DefaultHitSound](#defaulthitsound) @@ -989,7 +1263,6 @@ Server settings can be found inside InstaGibPlus.ini. 1. [PlayerScale](#playerscale) 1. [bAlwaysRenderFlagCarrier](#balwaysrenderflagcarrier) 1. [bAlwaysRenderDroppedFlags](#balwaysrenderdroppedflags) -1. [MinPosError](#minposerror) 1. [MaxPosError](#maxposerror) 1. [MaxHitError](#maxhiterror) 1. [MaxJitterTime](#maxjittertime) @@ -1002,11 +1275,19 @@ Server settings can be found inside InstaGibPlus.ini. 1. [bPlayersAlwaysRelevant](#bplayersalwaysrelevant) 1. [bEnablePingCompensatedSpawn](#benablepingcompensatedspawn) 1. [bEnableJitterBounding](#benablejitterbounding) +1. [LooseCheckCorrectionFactor](#loosecheckcorrectionfactor) +1. [LooseCheckCorrectionFactorOnMover](#loosecheckcorrectionfactoronmover) +1. [bEnableSnapshotInterpolation](#benablesnapshotinterpolation) +1. [SnapshotInterpSendHz](#snapshotinterpsendhz) +1. [SnapshotInterpRewindMs](#snapshotinterprewindms) 1. [bEnableWarpFix](#benablewarpfix) 1. [WarpFixDelay](#warpfixdelay) 1. [FireTimeout](#firetimeout) 1. [bEnableCarcassCollision](#benablecarcasscollision) 1. [ShowTouchedPackage](#showtouchedpackage) +1. [bEnableDamageDebugMode](#benabledamagedebugmode) +1. [bEnableDamageDebugConsoleMessages](#benabledamagedebugconsolemessages) +1. [bEnableHitboxDebugMode](#benablehitboxdebugmode) 1. [ExcludeMapsForKickers](#excludemapsforkickers) 1. [HitFeedbackMode](#hitfeedbackmode) 1. [ForcedSettings](#forcedsettings) @@ -1032,14 +1313,10 @@ Controls sniper rifle reload time, higher values lead to less time between shots Controls damage of body hits by sniper rifle. -## SetPendingWeapon - -**removed** - ## NNAnnouncer **Type: bool** -**Default: False** +**Default: True** Whether to automatically add an announcer for multi-kills, or not. @@ -1176,6 +1453,14 @@ When to check that default settings for all objects are correct client-side. Whether to allow warmup in tournament games or not. +## WarmupTimeLimit + +**Type: int** +**Default: 0** +**Unit: s** + +Maximum warmup length in seconds. `0` means no explicit warmup time limit. + ### bCoaches **Type: bool** @@ -1204,6 +1489,13 @@ Maximum time a team may pause the game for. Length of pause when a pause is triggered. +## Timeouts + +**Type: int** +**Default: 0** + +Maximum number of timeouts a team may take (used with coaches/tournament flow). + ### ForceModels **Type: int** @@ -1233,6 +1525,14 @@ Enable various HUD improvements. Depends on PureClickBoard mutator (set [bUseCli Enable or disable delayed first pickup spawn. +## bUseFastWeaponSwitch + +**Type: bool** +**Default: False** + +If `True`, use the fast weapon switch path. +If `False`, use normal weapon switching behavior. + ## bTellSpectators **Type: bool** @@ -1290,18 +1590,10 @@ Horizontal speed with which to throw weapons. Forces clients to do demos. -## MinPosError - -**Type: float** -**Default: 100** -**Unit: uu²** - -Unused. Intended to be minimum squared distance error for updating clients. - ## MaxPosError **Type: float** -**Default: 3000** +**Default: 1000** **Unit: uu²** Unused. Intended to be maximum squared distance error for updating clients. @@ -1321,6 +1613,27 @@ Distance to any position over the last 500ms for hits to be counted. Send package-names of touched actors to clients when those clients touch the actors. +## bEnableDamageDebugMode + +**Type: bool** +**Default: False** + +If `True`, enables server-side damage debug instrumentation. + +## bEnableDamageDebugConsoleMessages + +**Type: bool** +**Default: False** + +If `True`, emits damage debug information to console messages. + +## bEnableHitboxDebugMode + +**Type: bool** +**Default: False** + +If `True`, enables hitbox debug instrumentation. + ## ExcludeMapsForKickers **Type: string\[128\]** @@ -1448,6 +1761,45 @@ If enabled, updates by clients over more than [MaxJitterTime](#maxjittertime) wi Disable to restore default netcode behavior. +## LooseCheckCorrectionFactor + +**Type: float** +**Default: 1.0** + +Multiplier for loose position check correction strength. + +## LooseCheckCorrectionFactorOnMover + +**Type: float** +**Default: 1.0** + +Multiplier for loose position check correction strength while standing on movers. + +## bEnableSnapshotInterpolation + +**Type: bool** +**Default: False** + +If enabled, clients use snapshot interpolation for simulated proxies. + +This feature is highly experimental and should be enabled only for controlled testing. + +## SnapshotInterpSendHz + +**Type: float** +**Default: 30.0** +**Unit: Hz** + +Snapshot send rate used by snapshot interpolation. + +## SnapshotInterpRewindMs + +**Type: float** +**Default: 66.0** +**Unit: ms** + +Interpolation rewind delay used by snapshot interpolation. + ## bEnableWarpFix **Type: bool** @@ -1496,7 +1848,7 @@ If True, players retain the momentum of their in-air movement upon landing. If F ## bEnableSingleButtonDodge **Type: bool** -**Default: False** +**Default: True** Enables an input button clients can bind to a key, which makes the client dodge in the direction it is currently walking. @@ -1614,4 +1966,4 @@ Shots more than this long in the past are rejected. **spect** - Starting this project. **Deaod** - Maintenance. **UT99 Community** - For their endless patience, support and help testing and reporting bugs. -**Epic** - For not open sourcing a 20 year old game running on their 20 year old engine. \ No newline at end of file +**Epic** - For not open sourcing a 20 year old game running on their 20 year old engine.