diff options
Diffstat (limited to 'sourcemod/scripting/gokz-core/triggerfix.sp')
| -rw-r--r-- | sourcemod/scripting/gokz-core/triggerfix.sp | 622 |
1 files changed, 622 insertions, 0 deletions
diff --git a/sourcemod/scripting/gokz-core/triggerfix.sp b/sourcemod/scripting/gokz-core/triggerfix.sp new file mode 100644 index 0000000..424928d --- /dev/null +++ b/sourcemod/scripting/gokz-core/triggerfix.sp @@ -0,0 +1,622 @@ + + +// Credits: +// RNGFix made by rio 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 MIN_STANDABLE_ZNRM 0.7 // Minimum surface normal Z component of a walkable surface + +static int processMovementTicks[MAXPLAYERS+1]; +static float playerFrameTime[MAXPLAYERS+1]; + +static bool touchingTrigger[MAXPLAYERS+1][2048]; +static int triggerTouchFired[MAXPLAYERS+1][2048]; +static int lastGroundEnt[MAXPLAYERS + 1]; +static bool duckedLastTick[MAXPLAYERS + 1]; +static bool mapTeleportedSequentialTicks[MAXPLAYERS+1]; +static bool jumpBugged[MAXPLAYERS + 1]; +static float jumpBugOrigin[MAXPLAYERS + 1][3]; + +static ConVar cvGravity; + +static Handle acceptInputHookPre; +static Handle processMovementHookPre; +static Address serverGameEnts; +static Handle markEntitiesAsTouching; +static Handle passesTriggerFilters; + +public void OnPluginStart_Triggerfix() +{ + HookEvent("player_jump", Event_PlayerJump); + + cvGravity = FindConVar("sv_gravity"); + if (cvGravity == null) + { + SetFailState("Could not find sv_gravity"); + } + + GameData gamedataConf = LoadGameConfigFile("gokz-core.games"); + if (gamedataConf == null) + { + SetFailState("Failed to load gokz-core 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); + passesTriggerFilters = EndPrepSDKCall(); + + if (passesTriggerFilters == 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"); + } + + processMovementHookPre = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPre); + DHookAddParam(processMovementHookPre, HookParamType_CBaseEntity); + DHookAddParam(processMovementHookPre, HookParamType_ObjectPtr); + DHookRaw(processMovementHookPre, false, IGameMovement); + + // MarkEntitiesAsTouching + if (!GameConfGetKeyValue(gamedataConf, "IServerGameEnts", interfaceName, sizeof(interfaceName))) + { + SetFailState("Failed to get IServerGameEnts interface name"); + } + serverGameEnts = SDKCall(CreateInterface, interfaceName, 0); + if (!serverGameEnts) + { + 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); + markEntitiesAsTouching = EndPrepSDKCall(); + + if (markEntitiesAsTouching == null) + { + SetFailState("Unable to prepare SDKCall for IServerGameEnts::MarkEntitiesAsTouching"); + } + + gamedataConf = LoadGameConfigFile("sdktools.games/engine.csgo"); + offset = gamedataConf.GetOffset("AcceptInput"); + if (offset == -1) + { + SetFailState("Failed to get AcceptInput offset"); + } + + acceptInputHookPre = DHookCreate(offset, HookType_Entity, ReturnType_Bool, ThisPointer_CBaseEntity, DHooks_AcceptInput); + DHookAddParam(acceptInputHookPre, HookParamType_CharPtr); + DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity); + DHookAddParam(acceptInputHookPre, HookParamType_CBaseEntity); + //varaint_t is a union of 12 (float[3]) plus two int type params 12 + 8 = 20 + DHookAddParam(acceptInputHookPre, HookParamType_Object, 20, DHookPass_ByVal|DHookPass_ODTOR|DHookPass_OCTOR|DHookPass_OASSIGNOP); + DHookAddParam(acceptInputHookPre, HookParamType_Int); + + delete CreateInterface; + delete gamedataConf; + + if (gB_LateLoad) + { + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) OnClientPutInServer(client); + } + + char classname[64]; + for (int entity = MaxClients+1; entity < sizeof(touchingTrigger[]); entity++) + { + if (!IsValidEntity(entity)) continue; + GetEntPropString(entity, Prop_Data, "m_iClassname", classname, sizeof(classname)); + HookTrigger(entity, classname); + } + } +} + +public void OnEntityCreated_Triggerfix(int entity, const char[] classname) +{ + if (entity >= sizeof(touchingTrigger[])) + { + return; + } + HookTrigger(entity, classname); +} + +public void OnClientConnected_Triggerfix(int client) +{ + processMovementTicks[client] = 0; + for (int i = 0; i < sizeof(touchingTrigger[]); i++) + { + touchingTrigger[client][i] = false; + triggerTouchFired[client][i] = 0; + } +} + +public void OnClientPutInServer_Triggerfix(int client) +{ + SDKHook(client, SDKHook_PostThink, Hook_PlayerPostThink); + DHookEntity(acceptInputHookPre, false, client); +} + +public void OnGameFrame_Triggerfix() +{ + // Loop through all the players and make sure that triggers that are supposed to be fired but weren't now + // get fired properly. + // This must be run OUTSIDE of usercmd, because sometimes usercmd gets delayed heavily. + for (int client = 1; client <= MaxClients; client++) + { + if (IsValidClient(client) && IsPlayerAlive(client) && !CheckWater(client) && + (GetEntityMoveType(client) == MOVETYPE_WALK || GetEntityMoveType(client) == MOVETYPE_LADDER)) + { + DoTriggerFix(client); + + // Reset the Touch tracking. + // We save a bit of performance by putting this inside the loop + // Even if triggerTouchFired is not correct, touchingTrigger still is. + // That should prevent DoTriggerFix from activating the wrong triggers. + // Plus, players respawn where they previously are as well with a timer on, + // so this should not be a big problem. + for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++) + { + triggerTouchFired[client][trigger] = 0; + } + } + } +} + +void OnPlayerRunCmd_Triggerfix(int client) +{ + // Reset the Touch tracking. + // While this is mostly unnecessary, it can also happen that the server runs multiple ticks of player movement at once, + // therefore the triggers need to be checked again. + for (int trigger = 0; trigger < sizeof(triggerTouchFired[]); trigger++) + { + triggerTouchFired[client][trigger] = 0; + } +} + +static void Event_PlayerJump(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + + jumpBugged[client] = !!lastGroundEnt[client]; + if (jumpBugged[client]) + { + GetClientAbsOrigin(client, jumpBugOrigin[client]); + // if player's origin is still in the ducking position then adjust for that. + if (duckedLastTick[client] && !Movement_GetDucking(client)) + { + jumpBugOrigin[client][2] -= 9.0; + } + } +} + +static Action Hook_TriggerStartTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + touchingTrigger[other][entity] = true; + } + + return Plugin_Continue; +} + +static Action Hook_TriggerEndTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + touchingTrigger[other][entity] = false; + } + return Plugin_Continue; +} + +static Action Hook_TriggerTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + triggerTouchFired[other][entity]++; + } + return Plugin_Continue; +} + +static MRESReturn DHook_ProcessMovementPre(Handle hParams) +{ + int client = DHookGetParam(hParams, 1); + + processMovementTicks[client]++; + playerFrameTime[client] = GetTickInterval() * GetEntPropFloat(client, Prop_Data, "m_flLaggedMovementValue"); + mapTeleportedSequentialTicks[client] = false; + + if (IsPlayerAlive(client)) + { + if (GetEntityMoveType(client) == MOVETYPE_WALK + && !CheckWater(client)) + { + lastGroundEnt[client] = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity"); + } + duckedLastTick[client] = Movement_GetDucking(client); + } + + return MRES_Ignored; +} + +static MRESReturn DHooks_AcceptInput(int client, DHookReturn hReturn, DHookParam hParams) +{ + if (!IsValidClient(client) || !IsPlayerAlive(client) || CheckWater(client) || + (GetEntityMoveType(client) != MOVETYPE_WALK && GetEntityMoveType(client) != MOVETYPE_LADDER)) + { + return MRES_Ignored; + } + + // Get args + static char param[64]; + static char command[64]; + DHookGetParamString(hParams, 1, command, sizeof(command)); + if (StrEqual(command, "AddOutput")) + { + DHookGetParamObjectPtrString(hParams, 4, 0, ObjectValueType_String, param, sizeof(param)); + char kv[16]; + SplitString(param, " ", kv, sizeof(kv)); + // KVs are case insensitive. + // Any of these inputs can change the filter behavior. + if (StrEqual(kv[0], "targetname", false) || StrEqual(kv[0], "teamnumber", false) || StrEqual(kv[0], "classname", false) || StrEqual(command, "ResponseContext", false)) + { + DoTriggerFix(client, true); + } + } + else if (StrEqual(command, "AddContext") || StrEqual(command, "RemoveContext") || StrEqual(command, "ClearContext")) + { + DoTriggerFix(client, true); + } + return MRES_Ignored; +} + +static bool DoTriggerFix(int client, bool filterFix = false) +{ + // Adapted from DoTriggerjumpFix right below. + float landingMins[3], landingMaxs[3]; + float origin[3]; + + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs); + + ArrayList triggers = new ArrayList(); + // Get a list of triggers that we are touching now. + + TR_EnumerateEntitiesHull(origin, origin, landingMins, landingMaxs, true, AddTrigger, triggers); + + bool didSomething = false; + + for (int i = 0; i < triggers.Length; i++) + { + int trigger = triggers.Get(i); + if (!touchingTrigger[client][trigger]) + { + // Normally this wouldn't happen, because the trigger should be colliding with the player's hull if it gets here. + continue; + } + char className[64]; + GetEntityClassname(trigger, className, sizeof(className)); + if (StrEqual(className, "trigger_push")) + { + // Completely ignore push triggers. + continue; + } + if (filterFix && SDKCall(passesTriggerFilters, trigger, client) && triggerTouchFired[client][trigger] < GOKZ_MAX_RETOUCH_TRIGGER_COUNT) + { + // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick. + SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger); + + // Player properties might be changed right after this so it will need to be triggered again. + // Triggers changing this filter will loop onto itself infintely so we need to avoid that. + triggerTouchFired[client][trigger]++; + didSomething = true; + } + else if (!triggerTouchFired[client][trigger]) + { + // If the player is still touching the trigger on this tick, and Touch was not called for whatever reason + // in the last tick, we make sure that it is called now. + SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger); + triggerTouchFired[client][trigger]++; + didSomething = true; + } + } + + delete triggers; + + return didSomething; +} + +static bool DoTriggerjumpFix(int client, const float landingPoint[3], const float landingMins[3], const float landingMaxs[3]) +{ + // 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). + // This will fail on triggers thinner than 0.03125 unit thick, but it's highly unlikely that a mapper would put a trigger that thin. + 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 (touchingTrigger[client][trigger]) + { + continue; + } + + SDKCall(markEntitiesAsTouching, serverGameEnts, client, trigger); + didSomething = true; + } + + delete triggers; + + return didSomething; +} + +// PostThink works a little better than a ProcessMovement post hook because we need to wait for ProcessImpacts (trigger activation) +static void Hook_PlayerPostThink(int client) +{ + if (!IsPlayerAlive(client) + || GetEntityMoveType(client) != MOVETYPE_WALK + || CheckWater(client)) + { + return; + } + + bool landed = (GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") != -1 + && lastGroundEnt[client] == -1) + || jumpBugged[client]; + + float landingMins[3], landingMaxs[3], landingPoint[3]; + + // Get info about the ground we landed on (if we need to do landing fixes). + if (landed) + { + float origin[3], nrm[3], velocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + GetEntPropVector(client, Prop_Data, "m_vecVelocity", velocity); + + if (jumpBugged[client]) + { + origin = jumpBugOrigin[client]; + } + + 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; + } + } + + TR_GetEndPosition(landingPoint); + } + } + + // reset it here because we don't need it again + jumpBugged[client] = false; + + // Must use TR_DidHit because if the unduck origin is closer than 0.03125 units from the ground, + // the trace fraction would return 0.0. + if (landed && TR_DidHit()) + { + 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; + } + } +} + +static bool PlayerFilter(int entity, int mask) +{ + return !(1 <= entity <= MaxClients); +} + +static void HookTrigger(int entity, const char[] classname) +{ + if (StrContains(classname, "trigger_") != -1) + { + SDKHook(entity, SDKHook_StartTouchPost, Hook_TriggerStartTouch); + SDKHook(entity, SDKHook_EndTouchPost, Hook_TriggerEndTouch); + SDKHook(entity, SDKHook_TouchPost, Hook_TriggerTouch); + } +} + +static 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; +} + +public bool AddTrigger(int entity, ArrayList triggers) +{ + TR_ClipCurrentRayToEntity(MASK_ALL, entity); + if (TR_DidHit()) + { + triggers.Push(entity); + } + + return true; +} + +static 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; +} |
