diff --git a/rngfix/gamedata/rngfix.games.txt b/rngfix/gamedata/rngfix.games.txt new file mode 100644 index 0000000..34a962f --- /dev/null +++ b/rngfix/gamedata/rngfix.games.txt @@ -0,0 +1,83 @@ +"Games" +{ + "#default" + { + "Keys" + { + "IGameMovement" "GameMovement001" + + "IServerGameEnts" "ServerGameEnts001" + } + + "Signatures" + { + "CreateInterface" + { + "library" "server" + "windows" "@CreateInterface" + "linux" "@CreateInterface" + } + } + + "Offsets" + { + "ProcessMovement" + { + "windows" "1" + "linux" "2" + } + } + } + + "csgo" + { + "Offsets" + { + // applies to trigger_vphysics_motion and trigger_wind + "CBaseVPhysicsTrigger::PassesTriggerFilters" + { + "windows" "199" + "linux" "200" + } + + // applies to all other triggers + "CBaseTrigger::PassesTriggerFilters" + { + "windows" "209" + "linux" "210" + } + + "IServerGameEnts::MarkEntitiesAsTouching" + { + "windows" "1" + "linux" "2" + } + } + } + + "cstrike" + { + "Offsets" + { + // applies to trigger_vphysics_motion and trigger_wind + "CBaseVPhysicsTrigger::PassesTriggerFilters" + { + "windows" "188" + "linux" "189" + } + + // applies to all other triggers + "CBaseTrigger::PassesTriggerFilters" + { + "windows" "197" + "linux" "198" + } + + "IServerGameEnts::MarkEntitiesAsTouching" + { + "windows" "2" + "linux" "3" + } + } + } +} diff --git a/rngfix/scripting/rngfix.sp b/rngfix/scripting/rngfix.sp new file mode 100644 index 0000000..a0b9c6f --- /dev/null +++ b/rngfix/scripting/rngfix.sp @@ -0,0 +1,1291 @@ +#include +#include + +#include + +#pragma semicolon 1 +#pragma newdecls required + +#define PLUGIN_VERSION "1.1.2" + +public Plugin myinfo = +{ + name = "RNGFix", + author = "rio", + description = "Fixes physics bugs in movement game modes", + version = PLUGIN_VERSION, + url = "https://github.com/jason-e/rngfix" +} + +// Engine constants, NOT settings (do not change) +#define LAND_HEIGHT 2.0 // Maximum height above ground at which you can "land" +#define NON_JUMP_VELOCITY 140.0 // Maximum Z velocity you are allowed to have and still land +#define MIN_STANDABLE_ZNRM 0.7 // Minimum surface normal Z component of a walkable surface +#define AIR_SPEED_CAP 30.0 // Constant used to limit air acceleration +#define DUCK_MIN_DUCKSPEED 1.5 // Minimum duckspeed to start ducking +#define DEFAULT_JUMP_IMPULSE 301.99337741 // sqrt(2 * 57.0 units * 800.0 u/s^2) + +float g_vecMins[3]; +float g_vecMaxsUnducked[3]; +float g_vecMaxsDucked[3]; +float g_flDuckDelta; + +int g_iTick[MAXPLAYERS+1]; +float g_flFrameTime[MAXPLAYERS+1]; + +bool g_bTouchingTrigger[MAXPLAYERS+1][2048]; + +int g_iButtons[MAXPLAYERS+1]; +float g_vVel[MAXPLAYERS+1][3]; +float g_vAngles[MAXPLAYERS+1][3]; +int g_iOldButtons[MAXPLAYERS+1]; + +int g_iLastTickPredicted[MAXPLAYERS+1]; + +float g_vPreCollisionVelocity[MAXPLAYERS+1][3]; +float g_vLastBaseVelocity[MAXPLAYERS+1][3]; +int g_iLastGroundEnt[MAXPLAYERS+1]; +int g_iLastLandTick[MAXPLAYERS+1]; +int g_iLastCollisionTick[MAXPLAYERS+1]; +int g_iLastMapTeleportTick[MAXPLAYERS+1]; +bool g_bMapTeleportedSequentialTicks[MAXPLAYERS+1]; +float g_vCollisionPoint[MAXPLAYERS+1][3]; +float g_vCollisionNormal[MAXPLAYERS+1][3]; + +enum +{ + UPHILL_LOSS = -1, // Force a jump, AND negatively affect speed as if a collision occurred (fix RNG not in player's favor) + UPHILL_DEFAULT = 0, // Do nothing (retain RNG) + UPHILL_NEUTRAL = 1 // Force a jump (respecting NON_JUMP_VELOCITY) (fix RNG in player's favor) +} + +// Plugin settings +ConVar g_cvDownhill; +ConVar g_cvUphill; +ConVar g_cvEdge; +ConVar g_cvTriggerjump; +ConVar g_cvTelehop; +ConVar g_cvStairs; +ConVar g_cvUseOldSlopefixLogic; +ConVar g_cvDebug; + +// Core physics ConVars +ConVar g_cvMaxVelocity; +ConVar g_cvGravity; +ConVar g_cvAirAccelerate; + +// In CSS and CSGO but apparently not used in CSS +ConVar g_cvTimeBetweenDucks; + +// CSGO-only +ConVar g_cvJumpImpulse; +ConVar g_cvAutoBunnyHopping; + +Handle g_hPassesTriggerFilters; +Handle g_hProcessMovementHookPre; +Address g_IServerGameEnts; +Handle g_hMarkEntitiesAsTouching; + +bool g_bIsSurfMap; + +bool g_bLateLoad; + +int g_iLaserIndex; +int g_color1[] = {0, 100, 255, 255}; +int g_color2[] = {0, 255, 0, 255}; + +void DebugMsg(int client, const char[] fmt, any ...) +{ + if (!g_cvDebug.BoolValue) return; + + char output[1024]; + VFormat(output, sizeof(output), fmt, 3); + PrintToConsole(client, "[%i] %s", g_iTick[client], output); +} + +void DebugLaser(int client, const float p1[3], const float p2[3], float life, float width, const int color[4]) +{ + if (g_cvDebug.IntValue < 2) return; + + TE_SetupBeamPoints(p2, p1, g_iLaserIndex, 0, 0, 0, life, width, width, 10, 0.0, color, 0); + TE_SendToClient(client); +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + g_bLateLoad = late; + return APLRes_Success; +} + +public void OnPluginStart() +{ + EngineVersion engine = GetEngineVersion(); + + if (engine != Engine_CSGO && engine != Engine_CSS) + { + SetFailState("Game is not supported"); + } + + g_vecMins = view_as({-16.0, -16.0, 0.0}); + g_vecMaxsUnducked = view_as({16.0, 16.0, 0.0}); + g_vecMaxsDucked = view_as({16.0, 16.0, 0.0}); + + switch (engine) + { + case Engine_CSGO: + { + g_vecMaxsUnducked[2] = 72.0; + g_vecMaxsDucked[2] = 64.0; + } + case Engine_CSS: + { + g_vecMaxsUnducked[2] = 62.0; + g_vecMaxsDucked[2] = 45.0; + } + } + + g_flDuckDelta = (g_vecMaxsUnducked[2]-g_vecMaxsDucked[2]) / 2; + + g_cvDownhill = CreateConVar("rngfix_downhill", "1", "Enable downhill incline fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvUphill = CreateConVar("rngfix_uphill", "1", "Enable uphill incline fix. Set to -1 to normalize effects not in the player's favor (not recommended).", FCVAR_NOTIFY, true, -1.0, true, 1.0); + g_cvEdge = CreateConVar("rngfix_edge", "1", "Enable edgebug fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvTriggerjump = CreateConVar("rngfix_triggerjump", "1", "Enable trigger jump fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvTelehop = CreateConVar("rngfix_telehop", "1", "Enable telehop fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvStairs = CreateConVar("rngfix_stairs", "1", "Enable stair slide fix (surf only). You must have Movement Unlocker for sliding to work on CSGO.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + + g_cvUseOldSlopefixLogic = CreateConVar("rngfix_useoldslopefixlogic", "0", "Old Slopefix had some logic errors that could cause double boosts. Enable this on a per-map basis to retain old behavior. (NOT RECOMMENDED)", FCVAR_NOTIFY, true, 0.0, true, 1.0); + + g_cvDebug = CreateConVar("rngfix_debug", "0", "1 = Enable debug messages. 2 = Enable debug messages and lasers.", _, true, 0.0, true, 2.0); + + AutoExecConfig(); + + g_cvMaxVelocity = FindConVar("sv_maxvelocity"); + g_cvGravity = FindConVar("sv_gravity"); + g_cvAirAccelerate = FindConVar("sv_airaccelerate"); + + if (g_cvMaxVelocity == null || g_cvGravity == null || g_cvAirAccelerate == null) + { + SetFailState("Could not find all ConVars"); + } + + // Not required + g_cvTimeBetweenDucks = FindConVar("sv_timebetweenducks"); + g_cvJumpImpulse = FindConVar("sv_jump_impulse"); + g_cvAutoBunnyHopping = FindConVar("sv_autobunnyhopping"); + + Handle gamedataConf = LoadGameConfigFile("rngfix.games"); + if (gamedataConf == null) SetFailState("Failed to load rngfix gamedata"); + + // PassesTriggerFilters + StartPrepSDKCall(SDKCall_Entity); + if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "CBaseTrigger::PassesTriggerFilters")) + { + SetFailState("Failed to get CBaseTrigger::PassesTriggerFilters offset"); + } + PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer); + g_hPassesTriggerFilters = EndPrepSDKCall(); + + if (g_hPassesTriggerFilters == null) SetFailState("Unable to prepare SDKCall for CBaseTrigger::PassesTriggerFilters"); + + // CreateInterface + // Thanks SlidyBat and ici + StartPrepSDKCall(SDKCall_Static); + if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Signature, "CreateInterface")) + { + SetFailState("Failed to get CreateInterface"); + } + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + Handle CreateInterface = EndPrepSDKCall(); + + if (CreateInterface == null) SetFailState("Unable to prepare SDKCall for CreateInterface"); + + char interfaceName[64]; + + // ProcessMovement + if (!GameConfGetKeyValue(gamedataConf, "IGameMovement", interfaceName, sizeof(interfaceName))) + { + SetFailState("Failed to get IGameMovement interface name"); + } + Address IGameMovement = SDKCall(CreateInterface, interfaceName, 0); + if (!IGameMovement) + { + SetFailState("Failed to get IGameMovement pointer"); + } + + int offset = GameConfGetOffset(gamedataConf, "ProcessMovement"); + if (offset == -1) SetFailState("Failed to get ProcessMovement offset"); + + g_hProcessMovementHookPre = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPre); + DHookAddParam(g_hProcessMovementHookPre, HookParamType_CBaseEntity); + DHookAddParam(g_hProcessMovementHookPre, HookParamType_ObjectPtr); + DHookRaw(g_hProcessMovementHookPre, false, IGameMovement); + + // MarkEntitiesAsTouching + if (!GameConfGetKeyValue(gamedataConf, "IServerGameEnts", interfaceName, sizeof(interfaceName))) + { + SetFailState("Failed to get IServerGameEnts interface name"); + } + g_IServerGameEnts = SDKCall(CreateInterface, interfaceName, 0); + if (!g_IServerGameEnts) + { + SetFailState("Failed to get IServerGameEnts pointer"); + } + + StartPrepSDKCall(SDKCall_Raw); + if (!PrepSDKCall_SetFromConf(gamedataConf, SDKConf_Virtual, "IServerGameEnts::MarkEntitiesAsTouching")) + { + SetFailState("Failed to get IServerGameEnts::MarkEntitiesAsTouching offset"); + } + PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_Edict, SDKPass_Pointer); + g_hMarkEntitiesAsTouching = EndPrepSDKCall(); + + if (g_hMarkEntitiesAsTouching == null) SetFailState("Unable to prepare SDKCall for IServerGameEnts::MarkEntitiesAsTouching"); + + delete CreateInterface; + delete gamedataConf; + + if (g_bLateLoad) + { + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) OnClientPutInServer(client); + } + + char classname[64]; + for (int entity = MaxClients+1; entity < sizeof(g_bTouchingTrigger[]); entity++) + { + if (!IsValidEntity(entity)) continue; + GetEntPropString(entity, Prop_Data, "m_iClassname", classname, sizeof(classname)); + HookTrigger(entity, classname); + } + } +} + +public void OnMapStart() +{ + g_iLaserIndex = PrecacheModel("materials/sprites/laserbeam.vmt", true); + + char map[PLATFORM_MAX_PATH]; + GetCurrentMap(map, sizeof(map)); + g_bIsSurfMap = StrContains(map, "surf_", false) == 0; +} + +public void OnEntityCreated(int entity, const char[] classname) +{ + if (entity >= sizeof(g_bTouchingTrigger[])) return; + HookTrigger(entity, classname); +} + +void HookTrigger(int entity, const char[] classname) +{ + if (StrContains(classname, "trigger_") != -1) + { + SDKHook(entity, SDKHook_StartTouchPost, Hook_TriggerStartTouch); + SDKHook(entity, SDKHook_EndTouchPost, Hook_TriggerEndTouch); + } + + if (StrContains(classname, "trigger_teleport") != -1) + { + SDKHook(entity, SDKHook_TouchPost, Hook_TriggerTeleportTouchPost); + } +} + +public void OnClientConnected(int client) +{ + g_iTick[client] = 0; + for (int i = 0; i < sizeof(g_bTouchingTrigger[]); i++) g_bTouchingTrigger[client][i] = false; +} + +public void OnClientPutInServer(int client) +{ + SDKHook(client, SDKHook_GroundEntChangedPost, Hook_PlayerGroundEntChanged); + SDKHook(client, SDKHook_PostThink, Hook_PlayerPostThink); +} + +public Action Hook_TriggerStartTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + g_bTouchingTrigger[other][entity] = true; + DebugMsg(other, "StartTouch %i", entity); + } + + return Plugin_Continue; +} + +// TODO Would be nice to have IServerTools::FindEntityByName / CGlobalEntityList::FindEntityByName +bool NameExists(const char[] targetname) +{ + // Assume special types exist + if (targetname[0] == '!') return true; + + char targetname2[128]; + + int max = GetMaxEntities(); + for (int entity = 1; entity < max; entity++) + { + if (!IsValidEntity(entity)) continue; + if (GetEntPropString(entity, Prop_Data, "m_iName", targetname2, sizeof(targetname2)) == 0) continue; + + if (StrEqual(targetname, targetname2)) return true; + } + + return false; +} + +public void Hook_TriggerTeleportTouchPost(int entity, int other) +{ + if (!(1 <= other <= MaxClients)) return; + + if (!SDKCall(g_hPassesTriggerFilters, entity, other)) return; + + char targetstring[128]; + if (GetEntPropString(entity, Prop_Data, "m_target", targetstring, sizeof(targetstring)) == 0) return; + + if (!NameExists(targetstring)) return; + + if (g_iLastMapTeleportTick[other] == g_iTick[other]-1) + { + g_bMapTeleportedSequentialTicks[other] = true; + } + + g_iLastMapTeleportTick[other] = g_iTick[other]; + + DebugMsg(other, "Triggered teleport %i", entity); +} + +public Action Hook_TriggerEndTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + g_bTouchingTrigger[other][entity] = false; + DebugMsg(other, "EndTouch %i", entity); + } + return Plugin_Continue; +} + +public bool PlayerFilter(int entity, int mask) +{ + return !(1 <= entity <= MaxClients); +} + +float GetJumpImpulse() +{ + if (g_cvJumpImpulse != null) + { + return g_cvJumpImpulse.FloatValue; + } + else + { + return DEFAULT_JUMP_IMPULSE; + } +} + +bool IsDuckCoolingDown(int client) +{ + // TODO Is this stuff in MoveData? + + // Ducking is prevented if the last switch to a ducked state from an unducked state is sooner than sv_timebetweenducks ago. + // Note: This cooldown is based on client's curtime (GetGameTime() in this context) and thus is unaffected by m_flLaggedMovementValue. + if (g_cvTimeBetweenDucks != null && HasEntProp(client, Prop_Data, "m_flLastDuckTime")) + { + if (GetGameTime() - GetEntPropFloat(client, Prop_Data, "m_flLastDuckTime") < g_cvTimeBetweenDucks.FloatValue) return true; + } + + // m_flDuckSpeed is decreased by 2.0 to a minimum of 0.0 every time the duck key is pressed OR released. + // It recovers at a rate of 3.0 * m_flLaggedMovementValue per second and caps at 8.0. + // Switching to a ducked state from an unducked state is prevented if it is less than 1.5. + if (HasEntProp(client, Prop_Data, "m_flDuckSpeed")) + { + if (GetEntPropFloat(client, Prop_Data, "m_flDuckSpeed") < DUCK_MIN_DUCKSPEED) return true; + } + + return false; +} + +void Duck(int client, float origin[3], float mins[3], float maxs[3]) +{ + bool ducking = GetEntityFlags(client) & FL_DUCKING != 0; + + bool nextDucking = ducking; + + if (g_iButtons[client] & IN_DUCK != 0 && !ducking) + { + if (!IsDuckCoolingDown(client)) + { + origin[2] += g_flDuckDelta; + nextDucking = true; + } + } + else if (g_iButtons[client] & IN_DUCK == 0 && ducking) + { + origin[2] -= g_flDuckDelta; + + TR_TraceHullFilter(origin, origin, g_vecMins, g_vecMaxsUnducked, MASK_PLAYERSOLID, PlayerFilter); + + // Cannot unduck in air, not enough room + if (TR_DidHit()) origin[2] += g_flDuckDelta; + else nextDucking = false; + } + + mins = g_vecMins; + maxs = nextDucking ? g_vecMaxsDucked : g_vecMaxsUnducked; +} + +bool CanJump(int client) +{ + if (g_iButtons[client] & IN_JUMP == 0) return false; + if (g_iOldButtons[client] & IN_JUMP != 0 && !(g_cvAutoBunnyHopping != null && g_cvAutoBunnyHopping.BoolValue)) return false; + + return true; +} + +void CheckJumpButton(int client, float velocity[3]) +{ + // Skip dead and water checks since we already did them. + + // We need to check for ground somewhere so stick it here. + if (GetEntityFlags(client) & FL_ONGROUND == 0) return; + + if (!CanJump(client)) return; + + // TODO Incorporate surfacedata jump factor + + // This conditional is why jumping while crouched jumps higher! Bad! + if (GetEntProp(client, Prop_Data, "m_bDucking") != 0 || GetEntityFlags(client) & FL_DUCKING != 0) + { + velocity[2] = GetJumpImpulse(); + } + else + { + velocity[2] += GetJumpImpulse(); + } + + // Jumping does an extra half tick of gravity! Bad! + FinishGravity(client, velocity); +} + +void AirAccelerate(int client, float velocity[3], Handle hParams) +{ + // This also includes the initial parts of AirMove() + + float fore[3], side[3]; + float wishvel[3], wishdir[3]; + + GetAngleVectors(g_vAngles[client], fore, side, NULL_VECTOR); + + fore[2] = 0.0; + side[2] = 0.0; + NormalizeVector(fore, fore); + NormalizeVector(side, side); + + for (int i = 0; i < 2; i++) wishvel[i] = fore[i] * g_vVel[client][0] + side[i] * g_vVel[client][1]; + + float wishspeed = NormalizeVector(wishvel, wishdir); + float m_flMaxSpeed = DHookGetParamObjectPtrVar(hParams, 2, 56, ObjectValueType_Float); + if (wishspeed > m_flMaxSpeed && m_flMaxSpeed != 0.0) wishspeed = m_flMaxSpeed; + + if (wishspeed) + { + float wishspd = wishspeed; + if (wishspd > AIR_SPEED_CAP) wishspd = AIR_SPEED_CAP; + + float currentspeed = GetVectorDotProduct(velocity, wishdir); + float addspeed = wishspd - currentspeed; + + if (addspeed > 0) + { + float accelspeed = g_cvAirAccelerate.FloatValue * wishspeed * g_flFrameTime[client]; + if (accelspeed > addspeed) accelspeed = addspeed; + + for (int i = 0; i < 2; i++) velocity[i] += accelspeed * wishdir[i]; + } + } +} + +void CheckVelocity(float velocity[3]) +{ + for (int i = 0; i < 3; i++) + { + if (velocity[i] > g_cvMaxVelocity.FloatValue) velocity[i] = g_cvMaxVelocity.FloatValue; + else if (velocity[i] < -g_cvMaxVelocity.FloatValue) velocity[i] = -g_cvMaxVelocity.FloatValue; + } +} + +void StartGravity(int client, float velocity[3]) +{ + float localGravity = GetEntPropFloat(client, Prop_Data, "m_flGravity"); + if (localGravity == 0.0) localGravity = 1.0; + + velocity[2] -= localGravity * g_cvGravity.FloatValue * 0.5 * g_flFrameTime[client]; + + float baseVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + velocity[2] += baseVelocity[2] * g_flFrameTime[client]; + + // baseVelocity[2] would get cleared here but we shouldn't do that since this is just a prediction. + + CheckVelocity(velocity); +} + +void FinishGravity(int client, float velocity[3]) +{ + float localGravity = GetEntPropFloat(client, Prop_Data, "m_flGravity"); + if (localGravity == 0.0) localGravity = 1.0; + + velocity[2] -= localGravity * g_cvGravity.FloatValue * 0.5 * g_flFrameTime[client]; + + CheckVelocity(velocity); +} + +bool CheckWater(int client) +{ + // The cached water level is updated multiple times per tick, including after movement happens, + // so we can just check the cached value here. + return GetEntProp(client, Prop_Data, "m_nWaterLevel") > 1; +} + +void PreventCollision(int client, Handle hParams, const float origin[3], const float collisionPoint[3], const float velocity_tick[3]) +{ + DebugLaser(client, origin, collisionPoint, 15.0, 0.5, g_color1); + + // Rewind part of a tick so at the end of this tick we will end up close to the ground without colliding with it. + // This effectively simulates a mid-tick jump (we lose part of a tick but its a miniscule trade-off). + // This is also only an approximation of a partial tick rewind but it's good enough. + float newOrigin[3]; + SubtractVectors(collisionPoint, velocity_tick, newOrigin); + + // Add a little space between us and the ground so we don't accidentally hit it anyway, maybe due to floating point error or something. + // I don't know if this is necessary but I would rather be safe. + newOrigin[2] += 0.1; + + // Since the MoveData for this tick has already been filled and is about to be used, we need + // to modify it directly instead of changing the player entity's actual position (such as with TeleportEntity). + DHookSetParamObjectPtrVarVector(hParams, 2, GetEngineVersion() == Engine_CSGO ? 172 : 152, ObjectValueType_Vector, newOrigin); + + DebugLaser(client, origin, newOrigin, 15.0, 0.5, g_color2); + + float adjustment[3]; + SubtractVectors(newOrigin, origin, adjustment); + DebugMsg(client, "Moved: %.2f %.2f %.2f", adjustment[0], adjustment[1], adjustment[2]); + + // No longer colliding this tick, clear our prediction flag + g_iLastCollisionTick[client] = 0; +} + +void ClipVelocity(const float velocity[3], const float nrm[3], float out[3]) +{ + float backoff = GetVectorDotProduct(velocity, nrm); + + for (int i = 0; i < 3; i++) + { + out[i] = velocity[i] - nrm[i]*backoff; + } + + // The adjust step only matters with overbounce which doesnt apply to walkable surfaces. +} + +void SetVelocity(int client, float velocity[3], bool dontUseTeleportEntity = false) +{ + // Pull out basevelocity from desired true velocity + // Use the pre-tick basevelocity because that is what influenced this tick's movement and the desired new velocity. + SubtractVectors(velocity, g_vLastBaseVelocity[client], velocity); + + if (dontUseTeleportEntity && GetEntPropEnt(client, Prop_Data, "m_hMoveParent") == -1) + { + SetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", velocity); + SetEntPropVector(client, Prop_Data, "m_vecVelocity", velocity); + } + else + { + float baseVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + + TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, velocity); + + // TeleportEntity with non-null velocity wipes out basevelocity, so restore it after. + // Since we didn't change position, nothing should change regarding influences on basevelocity. + SetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + } +} + +public MRESReturn DHook_ProcessMovementPre(Handle hParams) +{ + int client = DHookGetParam(hParams, 1); + + g_iTick[client]++; + g_flFrameTime[client] = GetTickInterval() * GetEntPropFloat(client, Prop_Data, "m_flLaggedMovementValue"); + g_bMapTeleportedSequentialTicks[client] = false; + + // If we are actually not doing ANY of the fixes that rely on pre-tick collision prediction, skip all this. + if (!g_cvUphill.BoolValue && !g_cvEdge.BoolValue && !g_cvStairs.BoolValue && !g_cvTelehop.BoolValue && !g_cvDownhill.BoolValue) + { + return MRES_Ignored; + } + + RunPreTickChecks(client, hParams); + + return MRES_Ignored; +} + +void RunPreTickChecks(int client, Handle hParams) +{ + // Recreate enough of CGameMovement::ProcessMovement to predict if fixes are needed. + // We only really care about a limited set of scenarios (less than waist-deep in water, MOVETYPE_WALK, air movement). + + if (!IsPlayerAlive(client)) return; + if (GetEntityMoveType(client) != MOVETYPE_WALK) return; + if (CheckWater(client)) return; + + g_iLastGroundEnt[client] = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity"); + + // If we are definitely staying on the ground this tick, don't predict it. + if (g_iLastGroundEnt[client] != -1 && !CanJump(client)) return; + + g_iLastTickPredicted[client] = g_iTick[client]; + + g_iButtons[client] = DHookGetParamObjectPtrVar(hParams, 2, 36, ObjectValueType_Int); + g_iOldButtons[client] = DHookGetParamObjectPtrVar(hParams, 2, 40, ObjectValueType_Int); + DHookGetParamObjectPtrVarVector(hParams, 2, 44, ObjectValueType_Vector, g_vVel[client]); + DHookGetParamObjectPtrVarVector(hParams, 2, 12, ObjectValueType_Vector, g_vAngles[client]); + + float velocity[3]; + DHookGetParamObjectPtrVarVector(hParams, 2, 64, ObjectValueType_Vector, velocity); + + float baseVelocity[3]; + // basevelocity is not stored in MoveData + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + + float origin[3]; + DHookGetParamObjectPtrVarVector(hParams, 2, GetEngineVersion() == Engine_CSGO ? 172 : 152, ObjectValueType_Vector, origin); + + float nextOrigin[3], mins[3], maxs[3]; + + nextOrigin = origin; + + // These roughly replicate the behavior of their equivalent CGameMovement functions. + + Duck(client, nextOrigin, mins, maxs); + + StartGravity(client, velocity); + + CheckJumpButton(client, velocity); + + CheckVelocity(velocity); + + AirAccelerate(client, velocity, hParams); + + // StartGravity dealt with Z basevelocity. + baseVelocity[2] = 0.0; + g_vLastBaseVelocity[client] = baseVelocity; + AddVectors(velocity, baseVelocity, velocity); + + // Store this for later in case we need to undo the effects of a collision. + g_vPreCollisionVelocity[client] = velocity; + + // This is basically where TryPlayerMove happens. + // We don't really care about anything after TryPlayerMove either. + + float velocity_tick[3]; + velocity_tick = velocity; + ScaleVector(velocity_tick, g_flFrameTime[client]); + + AddVectors(nextOrigin, velocity_tick, nextOrigin); + + // Check if we will hit something this tick. + TR_TraceHullFilter(origin, nextOrigin, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + float nrm[3]; + TR_GetPlaneNormal(null, nrm); + + if (g_iLastCollisionTick[client] < g_iTick[client]-1) + { + DebugMsg(client, "Collision predicted! (normal: %.3f %.3f %.3f)", nrm[0], nrm[1], nrm[2]); + } + + float collisionPoint[3]; + TR_GetEndPosition(collisionPoint); + + // Store this result for post-tick fixes. + g_iLastCollisionTick[client] = g_iTick[client]; + g_vCollisionPoint[client] = collisionPoint; + g_vCollisionNormal[client] = nrm; + + // If we are moving up too fast, we can't land anyway so these fixes aren't needed. + if (velocity[2] > NON_JUMP_VELOCITY) return; + + // Landing also requires a walkable surface. + // This will give false negatives if the surface initially collided + // is too steep but the final one isn't (rare and unlikely to matter). + if (nrm[2] < MIN_STANDABLE_ZNRM) return; + + // Check uphill incline fix first since it's more common and faster. + if (g_cvUphill.IntValue == UPHILL_NEUTRAL) + { + // Make sure it's not flat, and that we are actually going uphill (X/Y dot product < 0.0) + if (nrm[2] < 1.0 && nrm[0]*velocity[0] + nrm[1]*velocity[1] < 0.0) + { + bool shouldDoDownhillFixInstead = false; + + if (g_cvDownhill.BoolValue) + { + // We also want to make sure this isn't a case where it's actually more beneficial to do the downhill fix. + float newVelocity[3]; + ClipVelocity(velocity, nrm, newVelocity); + + if (newVelocity[0]*newVelocity[0] + newVelocity[1]*newVelocity[1] > velocity[0]*velocity[0] + velocity[1]*velocity[1]) + { + shouldDoDownhillFixInstead = true; + } + } + + if (!shouldDoDownhillFixInstead) + { + DebugMsg(client, "DO FIX: Uphill Incline"); + PreventCollision(client, hParams, origin, collisionPoint, velocity_tick); + + // This naturally prevents any edge bugs so we can skip the edge fix. + return; + } + } + } + + if (g_cvEdge.BoolValue) + { + // Do a rough estimate of where we will be at the end of the tick after colliding. + // This method assumes no more collisions will take place after the first. + // There are some very extreme circumstances where this will give false positives (unlikely to come into play). + + float tickEnd[3]; + float fraction_left = 1.0 - TR_GetFraction(); + + if (nrm[2] == 1.0) + { + // If the ground is level, all that changes is Z velocity becomes zero. + tickEnd[0] = collisionPoint[0] + velocity_tick[0]*fraction_left; + tickEnd[1] = collisionPoint[1] + velocity_tick[1]*fraction_left; + tickEnd[2] = collisionPoint[2]; + } + else + { + float velocity2[3]; + ClipVelocity(velocity, nrm, velocity2); + + if (velocity2[2] > NON_JUMP_VELOCITY) + { + // This would be an "edge bug" (slide without landing at the end of the tick) + // 100% of the time due to the Z velocity restriction. + return; + } + else + { + ScaleVector(velocity2, g_flFrameTime[client]*fraction_left); + AddVectors(collisionPoint, velocity2, tickEnd); + } + } + + // Check if there's something close enough to land on below the player at the end of this tick. + float tickEndBelow[3]; + tickEndBelow[0] = tickEnd[0]; + tickEndBelow[1] = tickEnd[1]; + tickEndBelow[2] = tickEnd[2] - LAND_HEIGHT; + + TR_TraceHullFilter(tickEnd, tickEndBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + // There's something there, can we land on it? + float nrm2[3]; + TR_GetPlaneNormal(null, nrm2); + + // Yes, it's not too steep. + if (nrm2[2] >= MIN_STANDABLE_ZNRM) return; + // Yes, the quadrant check finds ground that isn't too steep. + if (TracePlayerBBoxForGround(tickEnd, tickEndBelow, mins, maxs)) return; + } + + DebugMsg(client, "DO FIX: Edge Bug"); + DebugLaser(client, collisionPoint, tickEnd, 15.0, 0.5, g_color1); + + PreventCollision(client, hParams, origin, collisionPoint, velocity_tick); + } + } +} + +public void Hook_PlayerGroundEntChanged(int client) +{ + // We cannot get the new ground entity at this point, + // but if the previous value was -1, it must be something else now, so we landed. + if (GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") == -1) + { + g_iLastLandTick[client] = g_iTick[client]; + DebugMsg(client, "Landed"); + } +} + +bool DoTriggerjumpFix(int client, const float landingPoint[3], const float landingMins[3], const float landingMaxs[3]) +{ +/* if (!g_cvTriggerjump.BoolValue) return false; + + // It's possible to land above a trigger but also in another trigger_teleport, have the teleport move you to + // another location, and then the trigger jumping fix wouldn't fire the other trigger you technically landed above, + // but I can't imagine a mapper would ever actually stack triggers like that. + + float origin[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + + float landingMaxsBelow[3]; + landingMaxsBelow[0] = landingMaxs[0]; + landingMaxsBelow[1] = landingMaxs[1]; + landingMaxsBelow[2] = origin[2] - landingPoint[2]; + + ArrayList triggers = new ArrayList(); + + // Find triggers that are between us and the ground (using the bounding box quadrant we landed with if applicable). + TR_EnumerateEntitiesHull(landingPoint, landingPoint, landingMins, landingMaxsBelow, true, AddTrigger, triggers); + + bool didSomething = false; + + for (int i = 0; i < triggers.Length; i++) + { + int trigger = triggers.Get(i); + + // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick. + // In case that could cause side-effects, manually keep track of triggers we are actually touching + // and don't re-touch them. + if (g_bTouchingTrigger[client][trigger]) continue; + + DebugMsg(client, "DO FIX: Trigger Jumping (entity %i)", trigger); + + SDKCall(g_hMarkEntitiesAsTouching, g_IServerGameEnts, client, trigger); + didSomething = true; + } + + delete triggers; + + return didSomething;*/ + return false; +} + +bool DoStairsFix(int client) +{ +/* if (!g_cvStairs.BoolValue) return false; + if (g_iLastTickPredicted[client] != g_iTick[client]) return false; + + // This fix has undesirable side-effects on bhop. It is also very unlikely to help on bhop. + if (!g_bIsSurfMap) return false; + + // Let teleports take precedence (including teleports activated by the trigger jumping fix). + if (g_iLastMapTeleportTick[client] == g_iTick[client]) return false; + + // If moving upward, the player would never be able to slide up with any current position. + if (g_vPreCollisionVelocity[client][2] > 0.0) return false; + + // Stair step faces don't necessarily have to be completely vertical, but, if they are not, + // sliding up them at high speed -- or even just walking up -- usually doesn't work. + // Plus, it's really unlikely that there are actual stairs shaped like that. + if (g_iLastCollisionTick[client] == g_iTick[client] && g_vCollisionNormal[client][2] == 0.0) + { + // Do this first and stop if we are moving slowly (less than 1 unit per tick). + float velocity_dir[3]; + velocity_dir = g_vPreCollisionVelocity[client]; + velocity_dir[2] = 0.0; + if (NormalizeVector(velocity_dir, velocity_dir) * g_flFrameTime[client] < 1.0) return false; + + float mins[3], maxs[3]; + GetEntPropVector(client, Prop_Data, "m_vecMins", mins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", maxs); + + // We seem to have collided with a "wall", now figure out if it's a stair step. + + // Look for ground below us + float stepsize = GetEntPropFloat(client, Prop_Data, "m_flStepSize"); + + float end[3]; + end = g_vCollisionPoint[client]; + end[2] -= stepsize; + + TR_TraceHullFilter(g_vCollisionPoint[client], end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + float nrm[3]; + TR_GetPlaneNormal(null, nrm); + + // Ground below is not walkable, not stairs + if (nrm[2] < MIN_STANDABLE_ZNRM) return false; + + float start[3]; + TR_GetEndPosition(start); + + // Find triggers that we would trigger if we did touch the ground here. + ArrayList triggers = new ArrayList(); + + TR_EnumerateEntitiesHull(start, start, mins, maxs, true, AddTrigger, triggers); + + for (int i = 0; i < triggers.Length; i++) + { + int trigger = triggers.Get(i); + + if (SDKCall(g_hPassesTriggerFilters, trigger, client)) + { + // We would have triggered something on the ground here, so we cant be sure the stairs fix is safe to do. + // The most likely scenario here is this isn't stairs, but just a short ledge with a fail teleport in front. + delete triggers; + return false; + } + } + + delete triggers; + + // Now follow CGameMovement::StepMove behavior. + + // Trace up + end = start; + end[2] += stepsize; + TR_TraceHullFilter(start, end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) TR_GetEndPosition(end); + + // Trace over (only 1 unit, just to find a stair step) + start = end; + AddVectors(start, velocity_dir, end); + + TR_TraceHullFilter(start, end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + // The plane we collided with is too tall to be a stair step (i.e. it's a wall, not stairs). + // Or possibly: the ceiling is too low to get on top of it. + return false; + } + else + { + // Trace downward + start = end; + end[2] -= stepsize; + + TR_TraceHullFilter(start, end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (!TR_DidHit()) return false; // Shouldn't happen + + TR_GetPlaneNormal(null, nrm); + + // Ground atop "stair" is not walkable, not stairs + if (nrm[2] < MIN_STANDABLE_ZNRM) return false; + + // It looks like we actually collided with a stair step. + // Put the player just barely on top of the stair step we found and restore their speed + TR_GetEndPosition(end); + + DebugMsg(client, "DO FIX: Stair Sliding"); + + TeleportEntity(client, end, NULL_VECTOR, NULL_VECTOR); + SetVelocity(client, g_vPreCollisionVelocity[client]); + + return true; + } + } + } +*/ + return false; +} + +bool DoInclineCollisionFixes(int client, const float nrm[3]) +{ + if (!g_cvDownhill.BoolValue && g_cvUphill.IntValue != UPHILL_LOSS) return false; + if (g_iLastTickPredicted[client] != g_iTick[client]) return false; + + // There's no point in checking for fix if we were moving up, unless we want to do an uphill collision + if (g_vPreCollisionVelocity[client][2] > 0.0 && g_cvUphill.IntValue != UPHILL_LOSS) return false; + + // If a collision was predicted this tick (and wasn't prevented by another fix alrady), no fix is needed. + // It's possible we actually have to run the edge bug fix and an incline fix in the same tick. + // If using the old Slopefix logic, do the fix regardless of necessity just like Slopefix + // so we can be sure to trigger a double boost if applicable. + if (g_iLastCollisionTick[client] == g_iTick[client] && !g_cvUseOldSlopefixLogic.BoolValue) return false; + + // Make sure the ground is not level, otherwise a collision would do nothing important anyway. + if (nrm[2] == 1.0) return false; + + // This velocity includes changes from player input this tick as well as + // the half tick of gravity applied before collision would occur. + float velocity[3]; + velocity = g_vPreCollisionVelocity[client]; + + if (g_cvUseOldSlopefixLogic.BoolValue) + { + // The old slopefix did not consider basevelocity when calculating deflected velocity + SubtractVectors(velocity, g_vLastBaseVelocity[client], velocity); + } + + float dot = nrm[0]*velocity[0] + nrm[1]*velocity[1]; + + if (dot >= 0) + { + // If going downhill, only adjust velocity if the downhill incline fix is on. + if (!g_cvDownhill.BoolValue) return false; + } + + bool downhillFixIsBeneficial = false; + + float newVelocity[3]; + ClipVelocity(velocity, nrm, newVelocity); + + if (newVelocity[0]*newVelocity[0] + newVelocity[1]*newVelocity[1] > velocity[0]*velocity[0] + velocity[1]*velocity[1]) + { + downhillFixIsBeneficial = true; + } + + if (dot < 0) + { + // If going uphill, only adjust velocity if uphill incline fix is set to loss mode + // OR if this is actually a case where the downhill incline fix is better. + if (!((downhillFixIsBeneficial && g_cvDownhill.BoolValue) || g_cvUphill.IntValue == UPHILL_LOSS)) return false; + } + + DebugMsg(client, "DO FIX: Incline Collision (%s) (z-normal: %.3f)", downhillFixIsBeneficial ? "Downhill" : "Uphill", nrm[2]); + + // Make sure Z velocity is zero since we are on the ground. + newVelocity[2] = 0.0; + + // Since we are on the ground, we also don't need to FinishGravity(). + + if (g_cvUseOldSlopefixLogic.BoolValue) + { + // The old slopefix immediately moves basevelocity into local velocity to keep it from getting cleared. + // This results in double boosts as the player is likely still being influenced by the source of the basevelocity. + if (GetEntityFlags(client) & FL_BASEVELOCITY != 0) + { + float baseVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + AddVectors(newVelocity, baseVelocity, newVelocity); + } + + TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, newVelocity); + } + else + { + SetVelocity(client, newVelocity); + } + + return true; +} + +bool DoTelehopFix(int client) +{ + if (!g_cvTelehop.BoolValue) return false; + if (g_iLastTickPredicted[client] != g_iTick[client]) return false; + + if (g_iLastMapTeleportTick[client] != g_iTick[client]) return false; + + // If the player was teleported two ticks in a row, don't do this fix because the player likely just passed + // through a speed-stopping teleport hub, and the map really did want to stop the player this way. + if (g_bMapTeleportedSequentialTicks[client]) return false; + + // Check if we either collided this tick OR landed during this tick. + // Note that we could have landed this tick, lost Z velocity, then gotten teleported, making us no longer on the ground. + // This is why we need to remember if we landed mid-tick rather than just check ground state now. + if (!(g_iLastCollisionTick[client] == g_iTick[client] || g_iLastLandTick[client] == g_iTick[client])) return false; + + // At this point, ideally we should check if the teleport would have triggered "after" the collision (within the tick duration), + // and, if so, not restore speed, but properly doing that would involve completely duplicating TryPlayerMove but with + // multiple intermediate trigger checks which is probably a bad idea... better to just give people the benefit of the doubt sometimes. + + // Restore the velocity we would have had if we didn't collide or land. + float newVelocity[3]; + newVelocity = g_vPreCollisionVelocity[client]; + + // Don't forget to add the second half-tick of gravity ourselves. + FinishGravity(client, newVelocity); + + float origin[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + + float mins[3], maxs[3]; + GetEntPropVector(client, Prop_Data, "m_vecMins", mins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", maxs); + + TR_TraceHullFilter(origin, origin, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + // If we appear to be "stuck" after teleporting (likely because the teleport destination + // was exactly on the ground), set velocity directly to avoid side-effects of + // TeleportEntity that can cause the player to really get stuck in the ground. + // This might only be an issue in CSS, but do it on CSGO too just to be safe. + bool dontUseTeleportEntity = TR_DidHit(); + + DebugMsg(client, "DO FIX: Telehop%s", dontUseTeleportEntity ? " (no TeleportEntity)" : ""); + + SetVelocity(client, newVelocity, dontUseTeleportEntity); + + return true; +} + +// PostThink works a little better than a ProcessMovement post hook because we need to wait for ProcessImpacts (trigger activation) +public void Hook_PlayerPostThink(int client) +{ + if (!IsPlayerAlive(client)) return; + if (GetEntityMoveType(client) != MOVETYPE_WALK) return; + if (CheckWater(client)) return; + + bool landed = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") != -1 && g_iLastGroundEnt[client] == -1; + + float origin[3], landingMins[3], landingMaxs[3], nrm[3], landingPoint[3]; + + // Get info about the ground we landed on (if we need to do landing fixes). + if (landed && (g_cvTriggerjump.BoolValue || g_cvDownhill.BoolValue || g_cvUphill.IntValue == UPHILL_LOSS)) + { + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + + GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs); + + float originBelow[3]; + originBelow[0] = origin[0]; + originBelow[1] = origin[1]; + originBelow[2] = origin[2] - LAND_HEIGHT; + + TR_TraceHullFilter(origin, originBelow, landingMins, landingMaxs, MASK_PLAYERSOLID, PlayerFilter); + + if (!TR_DidHit()) + { + // This should never happen, since we know we are on the ground. + landed = false; + } + else + { + TR_GetPlaneNormal(null, nrm); + + if (nrm[2] < MIN_STANDABLE_ZNRM) + { + // This is rare, and how the incline fix should behave isn't entirely clear because maybe we should + // collide with multiple faces at once in this case, but let's just get the ground we officially + // landed on and use that for our ground normal. + + // landingMins and landingMaxs will contain the final values used to find the ground after returning. + if (TracePlayerBBoxForGround(origin, originBelow, landingMins, landingMaxs)) + { + TR_GetPlaneNormal(null, nrm); + } + else + { + // This should also never happen. + landed = false; + } + + DebugMsg(client, "Used bounding box quadrant to find ground (z-normal: %.3f)", nrm[2]); + } + + TR_GetEndPosition(landingPoint); + } + } + + if (landed && TR_GetFraction() > 0.0) + { + DoTriggerjumpFix(client, landingPoint, landingMins, landingMaxs); + + // Check if a trigger we just touched put us in the air (probably due to a teleport). + if (GetEntityFlags(client) & FL_ONGROUND == 0) landed = false; + } + + // The stair sliding fix changes the outcome of this tick more significantly, so it doesn't really make sense to do incline fixes too. + if (DoStairsFix(client)) return; + + if (landed) + { + DoInclineCollisionFixes(client, nrm); + } + + DoTelehopFix(client); +} +/* +public bool AddTrigger(int entity, ArrayList triggers) +{ + TR_ClipCurrentRayToEntity(MASK_ALL, entity); + if (TR_DidHit()) triggers.Push(entity); + + return true; +} +*/ +bool TracePlayerBBoxForGround(const float origin[3], const float originBelow[3], float mins[3], float maxs[3]) +{ + // See CGameMovement::TracePlayerBBoxForGround() + + float origMins[3], origMaxs[3]; + origMins = mins; + origMaxs = maxs; + + float nrm[3]; + + mins = origMins; + + // -x -y + maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0]; + maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + // +x +y + mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0]; + mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1]; + mins[2] = origMins[2]; + + maxs = origMaxs; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + // -x +y + mins[0] = origMins[0]; + mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1]; + mins[2] = origMins[2]; + + maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0]; + maxs[1] = origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + // +x -y + mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0]; + mins[1] = origMins[1]; + mins[2] = origMins[2]; + + maxs[0] = origMaxs[0]; + maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + return false; +}