/* GOKZ General Include Website: https://bitbucket.org/kztimerglobalteam/gokz */ #if defined _gokz_included_ #endinput #endif #define _gokz_included_ #include #include #include // =====[ ENUMS ]===== enum ObsMode { ObsMode_None = 0, // Not in spectator mode ObsMode_DeathCam, // Special mode for death cam animation ObsMode_FreezeCam, // Zooms to a target, and freeze-frames on them ObsMode_Fixed, // View from a fixed camera position ObsMode_InEye, // Follow a player in first person view ObsMode_Chase, // Follow a player in third person view ObsMode_Roaming // Free roaming }; // =====[ CONSTANTS ]===== #define GOKZ_SOURCE_URL "https://github.com/KZGlobalTeam/gokz" #define GOKZ_UPDATER_BASE_URL "http://updater.gokz.org/v2/" #define GOKZ_COLLISION_GROUP_STANDARD 2 #define GOKZ_COLLISION_GROUP_NOTRIGGER 1 #define GOKZ_TP_FREEZE_TICKS 5 #define EPSILON 0.000001 #define PI 3.14159265359 #define SPEED_NORMAL 250.0 #define SPEED_NO_WEAPON 260.0 #define FLOAT_MAX view_as(0x7F7FFFFF) #define SF_BUTTON_USE_ACTIVATES 1024 #define IGNORE_JUMP_TIME 0.2 stock float PLAYER_MINS[3] = {-16.0, -16.0, 0.0}; stock float PLAYER_MAXS[3] = {16.0, 16.0, 72.0}; stock float PLAYER_MAXS_DUCKED[3] = {16.0, 16.0, 54.0}; // =====[ STOCKS ]===== /** * Represents a time float as a string e.g. 01:23.45. * * @param time Time in seconds. * @param precise Whether to include fractional seconds. * @return String representation of time. */ stock char[] GOKZ_FormatTime(float time, bool precise = true) { char formattedTime[12]; int roundedTime = RoundFloat(time * 100); // Time rounded to number of centiseconds int centiseconds = roundedTime % 100; roundedTime = (roundedTime - centiseconds) / 100; int seconds = roundedTime % 60; roundedTime = (roundedTime - seconds) / 60; int minutes = roundedTime % 60; roundedTime = (roundedTime - minutes) / 60; int hours = roundedTime; if (hours == 0) { if (precise) { FormatEx(formattedTime, sizeof(formattedTime), "%02d:%02d.%02d", minutes, seconds, centiseconds); } else { FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d", minutes, seconds); } } else { if (precise) { FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d.%02d", hours, minutes, seconds, centiseconds); } else { FormatEx(formattedTime, sizeof(formattedTime), "%d:%02d:%02d", hours, minutes, seconds); } } return formattedTime; } /** * Checks if the value is a valid client entity index, if they are in-game and not GOTV. * * @param client Client index. * @return Whether client is valid. */ stock bool IsValidClient(int client) { return client >= 1 && client <= MaxClients && IsClientInGame(client) && !IsClientSourceTV(client); } /** * Returns the greater of two float values. * * @param value1 First value. * @param value2 Second value. * @return Greatest value. */ stock float FloatMax(float value1, float value2) { if (value1 >= value2) { return value1; } return value2; } /** * Returns the lesser of two float values. * * @param value1 First value. * @param value2 Second value. * @return Lesser value. */ stock float FloatMin(float value1, float value2) { if (value1 <= value2) { return value1; } return value2; } /** * Clamp a float value between an upper and lower bound. * * @param value Preferred value. * @param min Minimum value. * @param max Maximum value. * @return The closest value to the preferred value. */ stock float FloatClamp(float value, float min, float max) { if (value >= max) { return max; } if (value <= min) { return min; } return value; } /** * Returns the greater of two int values. * * @param value1 First value. * @param value2 Second value. * @return Greatest value. */ stock int IntMax(int value1, int value2) { if (value1 >= value2) { return value1; } return value2; } /** * Returns the lesser of two int values. * * @param value1 First value. * @param value2 Second value. * @return Lesser value. */ stock int IntMin(int value1, int value2) { if (value1 <= value2) { return value1; } return value2; } /** * Rounds a float to the nearest specified power of 10. * * @param value Value to round. * @param power Power of 10 to round to. * @return Rounded value. */ stock float RoundToPowerOfTen(float value, int power) { float pow = Pow(10.0, float(power)); return RoundFloat(value / pow) * pow; } /** * Sets all characters in a string to lower case. * * @param input Input string. * @param output Output buffer. * @param size Maximum size of output. */ stock void String_ToLower(const char[] input, char[] output, int size) { size--; int i = 0; while (input[i] != '\0' && i < size) { output[i] = CharToLower(input[i]); i++; } output[i] = '\0'; } /** * Gets the client's observer mode. * * @param client Client index. * @return Current observer mode. */ stock ObsMode GetObserverMode(int client) { return view_as(GetEntProp(client, Prop_Send, "m_iObserverMode")); } /** * Gets the player a client is spectating. * * @param client Client index. * @return Client index of target, or -1 if not spectating anyone. */ stock int GetObserverTarget(int client) { if (!IsValidClient(client)) { return -1; } ObsMode mode = GetObserverMode(client); if (mode == ObsMode_InEye || mode == ObsMode_Chase) { return GetEntPropEnt(client, Prop_Send, "m_hObserverTarget"); } return -1; } /** * Emits a sound to other players that are spectating the client. * * @param client Client being spectated. * @param sound Sound to play. */ stock void EmitSoundToClientSpectators(int client, const char[] sound) { for (int i = 1; i <= MaxClients; i++) { if (IsValidClient(i) && GetObserverTarget(i) == client) { EmitSoundToClient(i, sound); } } } /** * Calculates the lowest angle from angle A to angle B. * Input and result angles are between -180 and 180. * * @param angleA Angle A. * @param angleB Angle B. * @return Delta angle. */ stock float CalcDeltaAngle(float angleA, float angleB) { float difference = angleB - angleA; if (difference > 180.0) { difference = difference - 360.0; } else if (difference <= -180.0) { difference = difference + 360.0; } return difference; } /** * Strips all color control characters in a string. * The Output buffer can be the same as the input buffer. * Original code by Psychonic, thanks. * Source: smlib * * @param input Input String. * @param output Output String. * @param size Max Size of the Output string */ stock void Color_StripFromChatText(const char[] input, char[] output, int size) { int x = 0; for (int i = 0; input[i] != '\0'; i++) { if (x + 1 == size) { break; } int character = input[i]; if (character > 0x08) { output[x++] = character; } } output[x] = '\0'; } /** * Returns an integer as a string. * * @param num Integer to stringify. * @return Integer as a string. */ stock char[] IntToStringEx(int num) { char string[12]; IntToString(num, string, sizeof(string)); return string; } /** * Returns a float as a string. * * @param num Float to stringify. * @return Float as a string. */ stock char[] FloatToStringEx(float num) { char string[32]; FloatToString(num, string, sizeof(string)); return string; } /** * Increment an index, looping back to 0 if the max value is reached. * * @param index Current index. * @param buffer Max value of index. * @return Current index incremented, or 0 if max value is reached. */ stock int NextIndex(int index, int max) { index++; if (index == max) { return 0; } return index; } /** * Reorders an array with current index at the front, and previous * values after, including looping back to the end after reaching * the start of the array. * * @param input Array to reorder. * @param inputSize Size of input array. * @param buffer Output buffer. * @param bufferSize Size of buffer. * @param index Index of current/most recent value of input array. */ stock void SortByRecent(const int[] input, int inputSize, int[] buffer, int bufferSize, int index) { int reorderedIndex = 0; for (int i = index; reorderedIndex < bufferSize && i >= 0; i--) { buffer[reorderedIndex] = input[i]; reorderedIndex++; } for (int i = inputSize - 1; reorderedIndex < bufferSize && i > index; i--) { buffer[reorderedIndex] = input[i]; reorderedIndex++; } } /** * Returns the Steam account ID for a given SteamID2. * Checks for invalid input are not very extensive. * * @param steamID2 SteamID2 to convert. * @return Steam account ID, or -1 if invalid. */ stock int Steam2ToSteamAccountID(const char[] steamID2) { char pieces[3][16]; if (ExplodeString(steamID2, ":", pieces, sizeof(pieces), sizeof(pieces[])) != 3) { return -1; } int IDNumberPart1 = StringToInt(pieces[1]); int IDNumberPart2 = StringToInt(pieces[2]); if (pieces[1][0] != '0' && IDNumberPart1 == 0 || IDNumberPart1 != 0 && IDNumberPart1 != 1 || IDNumberPart2 <= 0) { return -1; } return IDNumberPart1 + (IDNumberPart2 << 1); } /** * Teleports a player and removes their velocity and base velocity * immediately and also every tick for the next 5 ticks. Automatically * makes the player crouch if there is a ceiling above them. * * @param client Client index. * @param origin Origin to teleport to. * @param angles Eye angles to set. */ stock void TeleportPlayer(int client, const float origin[3], const float angles[3], bool setAngles = true, bool holdStill = true) { // Clear the player's parent before teleporting to fix being // teleported into seemingly random places if the player has a parent. AcceptEntityInput(client, "ClearParent"); Movement_SetOrigin(client, origin); Movement_SetVelocity(client, view_as( { 0.0, 0.0, 0.0 } )); Movement_SetBaseVelocity(client, view_as( { 0.0, 0.0, 0.0 } )); if (setAngles) { // NOTE: changing angles with TeleportEntity can fail due to packet loss!!! // (Movement_SetEyeAngles is a thin wrapper of TeleportEntity) Movement_SetEyeAngles(client, angles); } // Duck the player if there is something blocking them from above Handle trace = TR_TraceHullFilterEx(origin, origin, view_as( { -16.0, -16.0, 0.0 } ), // Standing players are 32 x 32 x 72 view_as( { 16.0, 16.0, 72.0 } ), MASK_PLAYERSOLID, TraceEntityFilterPlayers, client); bool ducked = TR_DidHit(trace); if (holdStill) { // Prevent noclip exploit SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); // Intelligently hold player still to prevent booster and trigger exploits StartHoldStill(client, ducked); } else if (ducked) { ForcePlayerDuck(client); } delete trace; } static void StartHoldStill(int client, bool ducked) { DataPack data = new DataPack(); data.WriteCell(GetClientUserId(client)); data.WriteCell(0); // tick counter data.WriteCell(GOKZ_TP_FREEZE_TICKS); // number of ticks to hold still data.WriteCell(ducked); ContinueHoldStill(data); } public void ContinueHoldStill(DataPack data) { data.Reset(); int client = GetClientOfUserId(data.ReadCell()); int ticks = data.ReadCell(); int tickCount = data.ReadCell(); bool ducked = data.ReadCell(); delete data; if (!IsValidClient(client)) { return; } if (ticks < tickCount) { Movement_SetVelocity(client, view_as( { 0.0, 0.0, 0.0 } )); Movement_SetBaseVelocity(client, view_as( { 0.0, 0.0, 0.0 } )); Movement_SetGravity(client, 1.0); // Don't drop the player off of ladders. // The game will automatically change the movetype back to MOVETYPE_WALK if it can't find a ladder. // Don't change the movetype if it's currently MOVETYPE_NONE, as that means the player is paused. if (Movement_GetMovetype(client) != MOVETYPE_NONE) { Movement_SetMovetype(client, MOVETYPE_LADDER); } // Prevent noclip exploit SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); // Force duck on player and make sure that the player can't trigger triggers above them. // they can still trigger triggers even when we force ducking. if (ducked) { ForcePlayerDuck(client); if (ticks < tickCount - 1) { // Don't trigger triggers SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_NOTRIGGER); } else { // Let the player trigger triggers on the last tick SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD); } } ++ticks; data = new DataPack(); data.WriteCell(GetClientUserId(client)); data.WriteCell(ticks); data.WriteCell(tickCount); data.WriteCell(ducked); RequestFrame(ContinueHoldStill, data); } } /** * Forces the player to instantly duck. * * @param client Client index. */ stock void ForcePlayerDuck(int client) { // these are both necessary, because on their own the player will sometimes still be in a state that isn't fully ducked. SetEntPropFloat(client, Prop_Send, "m_flDuckAmount", 1.0, 0); SetEntProp(client, Prop_Send, "m_bDucking", false); SetEntProp(client, Prop_Send, "m_bDucked", true); } /** * Returns whether the player is stuck e.g. in a wall after noclipping. * * @param client Client index. * @return Whether player is stuck. */ stock bool IsPlayerStuck(int client) { float vecMin[3], vecMax[3], vecOrigin[3]; GetClientMins(client, vecMin); GetClientMaxs(client, vecMax); GetClientAbsOrigin(client, vecOrigin); TR_TraceHullFilter(vecOrigin, vecOrigin, vecMin, vecMax, MASK_PLAYERSOLID, TraceEntityFilterPlayers); return TR_DidHit(); // head in wall ? } /** * Retrieves the absolute origin of an entity. * * @param entity Index of the entity. * @param result Entity's origin if successful. * @return Returns true if successful. */ stock bool GetEntityAbsOrigin(int entity, float result[3]) { if (!IsValidEntity(entity)) { return false; } if (!HasEntProp(entity, Prop_Data, "m_vecAbsOrigin")) { return false; } GetEntPropVector(entity, Prop_Data, "m_vecAbsOrigin", result); return true; } /** * Retrieves the name of an entity. * * @param entity Index of the entity. * @param buffer Buffer to store the name. * @param maxlength Maximum length of the buffer. * @return Number of non-null bytes written. */ stock int GetEntityName(int entity, char[] buffer, int maxlength) { return GetEntPropString(entity, Prop_Data, "m_iName", buffer, maxlength); } /** * Finds an entity by name or by name and classname. * Taken from smlib https://github.com/bcserv/smlib * This can take anywhere from ~0.2% to ~11% of frametime (i5-7600k) in the worst case scenario where * every entity which has a name (4096 of them) is iterated over. Your mileage may vary. * * @param name Name of the entity to find. * @param className Optional classname to match along with name. * @param ignorePlayers Ignore player entities. * @return Entity index if successful, INVALID_ENT_REFERENCE if not. */ stock int GOKZFindEntityByName(const char[] name, const char[] className = "", bool ignorePlayers = false) { int result = INVALID_ENT_REFERENCE; if (className[0] == '\0') { // HACK: Double the limit to get non-networked entities too. // https://developer.valvesoftware.com/wiki/Entity_limit int realMaxEntities = GetMaxEntities() * 2; int startEntity = 1; if (ignorePlayers) { startEntity = MaxClients + 1; } for (int entity = startEntity; entity < realMaxEntities; entity++) { if (!IsValidEntity(entity)) { continue; } char entName[65]; GetEntityName(entity, entName, sizeof(entName)); if (StrEqual(entName, name)) { result = entity; break; } } } else { int entity = INVALID_ENT_REFERENCE; while ((entity = FindEntityByClassname(entity, className)) != INVALID_ENT_REFERENCE) { char entName[65]; GetEntityName(entity, entName, sizeof(entName)); if (StrEqual(entName, name)) { result = entity; break; } } } return result; } /** * Gets the current map's display name in lower case. * * @param buffer Buffer to store the map name. * @param maxlength Maximum length of buffer. */ stock void GetCurrentMapDisplayName(char[] buffer, int maxlength) { char map[PLATFORM_MAX_PATH]; GetCurrentMap(map, sizeof(map)); GetMapDisplayName(map, map, sizeof(map)); String_ToLower(map, buffer, maxlength); } /** * Gets the current map's file size. */ stock int GetCurrentMapFileSize() { char mapBuffer[PLATFORM_MAX_PATH]; GetCurrentMap(mapBuffer, sizeof(mapBuffer)); Format(mapBuffer, sizeof(mapBuffer), "maps/%s.bsp", mapBuffer); return FileSize(mapBuffer); } /** * Copies the elements of a source vector to a destination vector. * * @param src Source vector. * @param dest Destination vector. */ stock void CopyVector(const any src[3], any dest[3]) { dest[0] = src[0]; dest[1] = src[1]; dest[2] = src[2]; } /** * Returns whether the player is spectating. * * @param client Client index. */ stock bool IsSpectating(int client) { int team = GetClientTeam(client); return team == CS_TEAM_SPECTATOR || team == CS_TEAM_NONE; } /** * Rotate a vector on an axis. * * @param vec Vector to rotate. * @param axis Axis to rotate around. * @param theta Angle in radians. * @param result Rotated vector. */ stock void RotateVectorAxis(float vec[3], float axis[3], float theta, float result[3]) { float cosTheta = Cosine(theta); float sinTheta = Sine(theta); float axisVecCross[3]; GetVectorCrossProduct(axis, vec, axisVecCross); for (int i = 0; i < 3; i++) { result[i] = (vec[i] * cosTheta) + (axisVecCross[i] * sinTheta) + (axis[i] * GetVectorDotProduct(axis, vec)) * (1.0 - cosTheta); } } /** * Rotate a vector by pitch and yaw. * * @param vec Vector to rotate. * @param pitch Pitch angle (in degrees). * @param yaw Yaw angle (in degrees). * @param result Rotated vector. */ stock void RotateVectorPitchYaw(float vec[3], float pitch, float yaw, float result[3]) { if (pitch != 0.0) { RotateVectorAxis(vec, view_as({0.0, 1.0, 0.0}), DegToRad(pitch), result); } if (yaw != 0.0) { RotateVectorAxis(result, view_as({0.0, 0.0, 1.0}), DegToRad(yaw), result); } } /** * Attempts to return a valid spawn location. * * @param origin Spawn origin if found. * @param angles Spawn angles if found. * @return Whether a valid spawn point is found. */ stock bool GetValidSpawn(float origin[3], float angles[3]) { // Return true if the spawn found is truly valid (not in the ground or out of bounds) bool foundValidSpawn; bool searchCT; float spawnOrigin[3]; float spawnAngles[3]; int spawnEntity = -1; while (!foundValidSpawn) { if (searchCT) { spawnEntity = FindEntityByClassname(spawnEntity, "info_player_counterterrorist"); } else { spawnEntity = FindEntityByClassname(spawnEntity, "info_player_terrorist"); } if (spawnEntity != -1) { GetEntPropVector(spawnEntity, Prop_Data, "m_vecOrigin", spawnOrigin); GetEntPropVector(spawnEntity, Prop_Data, "m_angRotation", spawnAngles); if (IsSpawnValid(spawnOrigin)) { origin = spawnOrigin; angles = spawnAngles; foundValidSpawn = true; } } else if (!searchCT) { searchCT = true; } else { break; } } return foundValidSpawn; } /** * Check whether a position is a valid spawn location. * A spawn location is considered valid if it is in bounds and not stuck inside the ground. * * @param origin Origin vector. * @return Whether the origin is a valid spawn location. */ stock bool IsSpawnValid(float origin[3]) { Handle trace = TR_TraceHullFilterEx(origin, origin, PLAYER_MINS, PLAYER_MAXS, MASK_PLAYERSOLID, TraceEntityFilterPlayers); if (!TR_StartSolid(trace) && !TR_AllSolid(trace) && TR_GetFraction(trace) == 1.0) { delete trace; return true; } delete trace; return false; } /** * Get an entity's origin, angles, its bounding box's center and the distance from the center to its bounding box's edges. * * @param entity Index of the entity. * @param origin Entity's origin. * @param center Center of the entity's bounding box. * @param angles Entity's angles. * @param distFromCenter The distance between the center of the entity's bounding box and its edges. */ stock void GetEntityPositions(int entity, float origin[3], float center[3], float angles[3], float distFromCenter[3]) { int ent = entity; float maxs[3], mins[3]; GetEntPropVector(ent, Prop_Send, "m_vecOrigin", origin); // Take parent entities into account. while (GetEntPropEnt(ent, Prop_Send, "moveparent") != -1) { ent = GetEntPropEnt(ent, Prop_Send, "moveparent"); float tempOrigin[3]; GetEntPropVector(ent, Prop_Send, "m_vecOrigin", tempOrigin); for (int i = 0; i < 3; i++) { origin[i] += tempOrigin[i]; } } GetEntPropVector(ent, Prop_Data, "m_angRotation", angles); GetEntPropVector(ent, Prop_Send, "m_vecMaxs", maxs); GetEntPropVector(ent, Prop_Send, "m_vecMins", mins); for (int i = 0; i < 3; i++) { center[i] = origin[i] + (maxs[i] + mins[i]) / 2; distFromCenter[i] = (maxs[i] - mins[i]) / 2; } } /** * Find a valid position around a timer. * * @param entity Index of the timer entity. * @param originDest Result origin if a valid position is found. * @param anglesDest Result angles if a valid position is found. * @return Whether a valid position is found. */ stock bool FindValidPositionAroundTimerEntity(int entity, float originDest[3], float anglesDest[3], bool isButton) { float origin[3], center[3], angles[3], distFromCenter[3]; GetEntityPositions(entity, origin, center, angles, distFromCenter); float extraOffset[3]; if (isButton) // Test several positions within button press range. { extraOffset[0] = 32.0; extraOffset[1] = 32.0; extraOffset[2] = 32.0; } else // Test positions at the inner surface of the zone. { extraOffset[0] = -(PLAYER_MAXS[0] - PLAYER_MINS[0]) - 1.03125; extraOffset[1] = -(PLAYER_MAXS[1] - PLAYER_MINS[1]) - 1.03125; extraOffset[2] = -(PLAYER_MAXS[2] - PLAYER_MINS[2]) - 1.03125; } if (FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest)) { return true; } // Test the positions right next to the timer button/zones if the tests above fail. // This can fail when the timer has a cover brush over it. extraOffset[0] = 0.03125; extraOffset[1] = 0.03125; extraOffset[2] = 0.03125; return FindValidPositionAroundCenter(center, distFromCenter, extraOffset, originDest, anglesDest); } static bool FindValidPositionAroundCenter(float center[3], float distFromCenter[3], float extraOffset[3], float originDest[3], float anglesDest[3]) { float testOrigin[3]; int x, y; for (int i = 0; i < 3; i++) { // The search starts from the center then outwards to opposite directions. x = i == 2 ? -1 : i; for (int j = 0; j < 3; j++) { y = j == 2 ? -1 : j; for (int z = -1; z <= 1; z++) { testOrigin = center; testOrigin[0] = testOrigin[0] + (distFromCenter[0] + extraOffset[0]) * x + (PLAYER_MAXS[0] - PLAYER_MINS[0]) * x * 0.5; testOrigin[1] = testOrigin[1] + (distFromCenter[1] + extraOffset[1]) * y + (PLAYER_MAXS[1] - PLAYER_MINS[1]) * y * 0.5; testOrigin[2] = testOrigin[2] + (distFromCenter[2] + extraOffset[2]) * z + (PLAYER_MAXS[2] - PLAYER_MINS[2]) * z; // Check if there's a line of sight towards the zone as well. if (IsSpawnValid(testOrigin) && CanSeeBox(testOrigin, center, distFromCenter)) { originDest = testOrigin; // Always look towards the center. float offsetVector[3]; offsetVector[0] = -(distFromCenter[0] + extraOffset[0]) * x; offsetVector[1] = -(distFromCenter[1] + extraOffset[1]) * y; offsetVector[2] = -(distFromCenter[2] + extraOffset[2]) * z; GetVectorAngles(offsetVector, anglesDest); anglesDest[2] = 0.0; // Roll should always be 0.0 return true; } } } } return false; } static bool CanSeeBox(float origin[3], float center[3], float distFromCenter[3]) { float traceOrigin[3], traceDest[3], mins[3], maxs[3]; CopyVector(origin, traceOrigin); SubtractVectors(center, distFromCenter, mins); AddVectors(center, distFromCenter, maxs); for (int i = 0; i < 3; i++) { mins[i] += 0.03125; maxs[i] -= 0.03125; traceDest[i] = FloatClamp(traceOrigin[i], mins[i], maxs[i]); } int mask = (MASK_NPCSOLID_BRUSHONLY | MASK_OPAQUE_AND_NPCS) & ~CONTENTS_OPAQUE; Handle trace = TR_TraceRayFilterEx(traceOrigin, traceDest, mask, RayType_EndPoint, TraceEntityFilterPlayers); if (TR_DidHit(trace)) { float end[3]; TR_GetEndPosition(end, trace); for (int i = 0; i < 3; i++) { if (end[i] != traceDest[i]) { delete trace; return false; } } } delete trace; return true; } /** * Gets entity index from the address to an entity. * * @param pEntity Entity address. * @return Entity index. * @error Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle. */ stock int GOKZGetEntityFromAddress(Address pEntity) { static int offs_RefEHandle; if (offs_RefEHandle) { return EntRefToEntIndex(LoadFromAddress(pEntity + view_as
(offs_RefEHandle), NumberType_Int32) | (1 << 31)); } // if we don't have it already, attempt to lookup offset based on SDK information // CWorld is derived from CBaseEntity so it should have both offsets int offs_angRotation = FindDataMapInfo(0, "m_angRotation"), offs_vecViewOffset = FindDataMapInfo(0, "m_vecViewOffset"); if (offs_angRotation == -1) { SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_angRotation"); } else if (offs_vecViewOffset == -1) { SetFailState("Could not find offset for ((CBaseEntity) CWorld)::m_vecViewOffset"); } else if ((offs_angRotation + 0x0C) != (offs_vecViewOffset - 0x04)) { char game[32]; GetGameFolderName(game, sizeof(game)); SetFailState("Could not confirm offset of CBaseEntity::m_RefEHandle (incorrect assumption for game '%s'?)", game); } // offset seems right, cache it for the next call offs_RefEHandle = offs_angRotation + 0x0C; return GOKZGetEntityFromAddress(pEntity); } /** * Gets client index from CGameMovement class. * * @param addr Address of CGameMovement class. * @param offsetCGameMovement_player Offset of CGameMovement::player. * @return Client index. * @error Couldn't find offset for m_angRotation, m_vecViewOffset, couldn't confirm offset of m_RefEHandle. */ stock int GOKZGetClientFromGameMovementAddress(Address addr, int offsetCGameMovement_player) { Address playerAddr = view_as
(LoadFromAddress(view_as
(view_as(addr) + offsetCGameMovement_player), NumberType_Int32)); return GOKZGetEntityFromAddress(playerAddr); } /** * Gets the nearest point in the oriented bounding box of an entity to a point. * * @param entity Entity index. * @param origin Point's origin. * @param result Result point. */ stock void CalcNearestPoint(int entity, float origin[3], float result[3]) { float entOrigin[3], entMins[3], entMaxs[3], trueMins[3], trueMaxs[3]; GetEntPropVector(entity, Prop_Send, "m_vecOrigin", entOrigin); GetEntPropVector(entity, Prop_Send, "m_vecMaxs", entMaxs); GetEntPropVector(entity, Prop_Send, "m_vecMins", entMins); AddVectors(entOrigin, entMins, trueMins); AddVectors(entOrigin, entMaxs, trueMaxs); for (int i = 0; i < 3; i++) { result[i] = FloatClamp(origin[i], trueMins[i], trueMaxs[i]); } } /** * Get the shortest distance from P to the (infinite) line through vLineA and vLineB. * * @param P Point's origin. * @param vLineA Origin of the first point of the line. * @param vLineB Origin of the first point of the line. * @return The shortest distance from the point to the line. */ stock float CalcDistanceToLine(float P[3], float vLineA[3], float vLineB[3]) { float vClosest[3]; float vDir[3]; float t; float delta[3]; SubtractVectors(vLineB, vLineA, vDir); float div = GetVectorDotProduct(vDir, vDir); if (div < EPSILON) { t = 0.0; } else { t = (GetVectorDotProduct(vDir, P) - GetVectorDotProduct(vDir, vLineA)) / div; } for (int i = 0; i < 3; i++) { vClosest[i] = vLineA[i] + vDir[i]*t; } SubtractVectors(P, vClosest, delta); return GetVectorLength(delta); } /** * Gets the ideal amount of time the text should be held for HUD messages. * * The message buffer is only 16 slots long, and it is shared between 6 channels maximum. * Assuming a message is sent every game frame, each channel used should be only taking around 2.5 slots on average. * This also assumes all channels are used equally (so no other plugin taking all the channel buffer for itself). * We want to use as much of the message buffer as possible to take into account latency variances. * * @param interval HUD message update interval, in tick intervals. * @return How long the text should be held for. */ stock float GetTextHoldTime(int interval) { return 3 * interval * GetTickInterval(); }