diff --git a/AMBuilder b/AMBuilder
index 033d025c..21856d09 100644
--- a/AMBuilder
+++ b/AMBuilder
@@ -50,6 +50,7 @@ for sdk_target in MMSPlugin.sdk_targets:
'src/utils/weapon.cpp',
'src/utils/hud_manager.cpp',
'src/utils/utils.cpp',
+ 'src/utils/script_function.cpp',
'src/cs2_sdk/entity/services.cpp',
'src/cs2_sdk/schema.cpp',
'src/ctimer.cpp',
diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj
index 61b6b5a9..d3ed45a3 100644
--- a/CS2Fixes.vcxproj
+++ b/CS2Fixes.vcxproj
@@ -221,6 +221,7 @@
+
@@ -294,6 +295,7 @@
+
diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters
index 68bb9f35..8ccb3357 100644
--- a/CS2Fixes.vcxproj.filters
+++ b/CS2Fixes.vcxproj.filters
@@ -188,6 +188,9 @@
Source Files\utils
+
+ Source Files\utils
+
Source Files\cs2_sdk\entity
@@ -217,6 +220,9 @@
Header Files\utils
+
+ Header Files\utils
+
Header Files\cs2_sdk
diff --git a/gamedata/cs2fixes.jsonc b/gamedata/cs2fixes.jsonc
index f18794d8..5f19eb74 100644
--- a/gamedata/cs2fixes.jsonc
+++ b/gamedata/cs2fixes.jsonc
@@ -108,13 +108,6 @@
"windows": "40 55 53 56 57 41 54 48 8D 6C 24 ? 48 81 EC ? ? ? ? 4D 8B E0",
"linux": "55 66 0F EF C0 48 89 E5 41 57 41 56 41 55 49 89 FD 31 FF"
},
- // Should be xref'd right above "flGravity", takes a float arg
- "CBaseEntity::SetGravityScale":
- {
- "library": "server",
- "windows": "48 89 5C 24 ? 57 48 83 EC ? F3 0F 10 81 ? ? ? ? 48 8B F9 0F 29 74 24 ? 0F 28 F1 0F 2E C6 7A ? 74 ? BA ? ? ? ? 41 B8 ? ? ? ? 48 81 C1 ? ? ? ? E8 ? ? ? ? 48 8B CF F3 0F 11 B7 ? ? ? ? E8 ? ? ? ? 48 8B 5C 24 ? 0F 28 74 24 ? 48 83 C4 ? 5F C3 CC CC CC CC CC 48 89 5C 24",
- "linux": "55 48 89 E5 41 57 41 56 41 55 41 54 53 48 89 FB 48 81 EC ? ? ? ? 0F 2E 87 ? ? ? ? 7A ? 75 ? 48 81 C4 ? ? ? ? 5B 41 5C 41 5D 41 5E 41 5F 5D C3 0F 1F 40 ? 31 C9 BE ? ? ? ? 66 0F EF C9 F3 0F 11 85 ? ? ? ? 48 8D BD ? ? ? ? 48 C7 85 ? ? ? ? ? ? ? ? 0F 29 8D ? ? ? ? 4C 8D A5 ? ? ? ? 48 C7 85 ? ? ? ? ? ? ? ? 48 C7 85 ? ? ? ? ? ? ? ? 48 C7 85 ? ? ? ? ? ? ? ? 48 C7 85 ? ? ? ? ? ? ? ? C7 85 ? ? ? ? ? ? ? ? 66 89 8D ? ? ? ? E8 ? ? ? ? 48 8B 85 ? ? ? ? 48 8D 15 ? ? ? ? 83 85 ? ? ? ? ? F3 0F 10 85 ? ? ? ? C7 00 ? ? ? ? 48 8B 03 48 8B 80 ? ? ? ? 48 39 D0 0F 85 ? ? ? ? 8B 95 ? ? ? ? 4C 8D 7B ? 85 D2 0F 85 ? ? ? ? 80 BB ? ? ? ? ? 75"
- },
// "Game System %s is defined twice!\n"
// Note that this signature points to the instruction with sm_pFirst which is the first qword referenced in the function.
"IGameSystem_InitAllSystems_pFirst":
@@ -199,15 +192,6 @@
"windows": "48 89 5C 24 10 57 48 83 EC 30 48 8B DA 48 8B F9 48 85 C9",
"linux": "48 85 FF 74 ? 55 48 89 E5 41 55 41 54 49 89 FC"
},
- // Look for "SetEntityName", that will be the vscript binding definition
- // Scroll a bit down and you'll find something like this (note the offset): *(_QWORD *)(v453 + 64) = sub_1807B0350;
- // that function is just a jump to the one we want
- "CEntityIdentity_SetEntityName":
- {
- "library": "server",
- "windows": "48 89 5C 24 10 57 48 83 EC 20 48 8B D9 4C 8B C2",
- "linux": "55 48 89 F2 48 89 E5 41 55 41 54 53"
- },
// "Error - cannot add bots after game is over."
"BotNavIgnore":
{
@@ -215,14 +199,6 @@
"windows": "0F 84 ? ? ? ? 80 B8 ? ? ? ? 00 0F 84 ? ? ? ? 80 3D ? ? ? ? 00 74 15",
"linux": "0F 84 ? ? ? ? 44 0F B6 B8 ? ? ? ? 45 84 FF 0F 84"
},
- // next to "soundname", in windows it's the last referenced sub while in linux it's right after
- // this is a vscript binding though so it may be removed in the future?
- "CBaseEntity_EmitSoundParams":
- {
- "library": "server",
- "windows": "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 55 48 8B EC 48 81 EC ? ? ? ? 33 C0",
- "linux": "48 B8 ? ? ? ? ? ? ? ? 55 0F 28 D0"
- },
// "ParticleEffect", found in a function with 9 arguments
"DispatchParticleEffect":
{
@@ -401,12 +377,6 @@
"windows": 53,
"linux": 52
},
- // String: "%s<%i><%s><%s>" ChangeTeam() CTMDBG..."
- "CCSPlayerController_ChangeTeam":
- {
- "windows": 103,
- "linux": 102
- },
"CBaseEntity::Use":
{
"windows": 145,
@@ -427,23 +397,6 @@
"windows": 150,
"linux": 149
},
- "Teleport":
- {
- "windows": 163,
- "linux": 162
- },
- // For these two, look for the names, you'll find vscript bindings
- // Scroll down to where the var + 64 gets set to a function, that calls the offset we want
- "IsPlayerPawn":
- {
- "windows": 169,
- "linux": 168
- },
- "IsPlayerController":
- {
- "windows": 170,
- "linux": 169
- },
"CollisionRulesChanged":
{
"windows": 186,
diff --git a/src/addresses.cpp b/src/addresses.cpp
index 6a0fe6d7..83493cfc 100644
--- a/src/addresses.cpp
+++ b/src/addresses.cpp
@@ -21,6 +21,9 @@
#include "gameconfig.h"
#include "utils/module.h"
+#include "entityinstance.h"
+#include "tier1/strtools.h"
+#include "vscript/ivscript.h"
#include "tier0/memdbgon.h"
#define RESOLVE_SIG(gameConfig, name, variable) \
@@ -29,6 +32,14 @@
return false; \
Message("Found %s at 0x%p\n", name, variable);
+#define RESOLVE_SF(scriptDesc, funcName, variable) \
+ if (!variable.Initialize(GetScriptFunction(scriptDesc, funcName))) \
+ return false; \
+ if (variable.IsVirtual()) \
+ Message("Found %s::%s at vtable index %i\n", scriptDesc->m_pszClassname, funcName, variable.GetOffset()); \
+ else \
+ Message("Found %s::%s at 0x%p\n", scriptDesc->m_pszClassname, funcName, variable.GetPtr());
+
bool addresses::Initialize(CGameConfig* g_GameConfig)
{
modules::engine = new CModule(ROOTBIN, "engine2");
@@ -51,7 +62,6 @@ bool addresses::Initialize(CGameConfig* g_GameConfig)
#endif
RESOLVE_SIG(g_GameConfig, "SetGroundEntity", addresses::SetGroundEntity);
- RESOLVE_SIG(g_GameConfig, "CBaseEntity::SetGravityScale", addresses::SetGravityScale);
RESOLVE_SIG(g_GameConfig, "CCSPlayerController_SwitchTeam", addresses::CCSPlayerController_SwitchTeam);
RESOLVE_SIG(g_GameConfig, "CBasePlayerController_SetPawn", addresses::CBasePlayerController_SetPawn);
RESOLVE_SIG(g_GameConfig, "CBaseModelEntity_SetModel", addresses::CBaseModelEntity_SetModel);
@@ -62,8 +72,6 @@ bool addresses::Initialize(CGameConfig* g_GameConfig)
RESOLVE_SIG(g_GameConfig, "CGameRules_TerminateRound", addresses::CGameRules_TerminateRound);
RESOLVE_SIG(g_GameConfig, "CreateEntityByName", addresses::CreateEntityByName);
RESOLVE_SIG(g_GameConfig, "DispatchSpawn", addresses::DispatchSpawn);
- RESOLVE_SIG(g_GameConfig, "CEntityIdentity_SetEntityName", addresses::CEntityIdentity_SetEntityName);
- RESOLVE_SIG(g_GameConfig, "CBaseEntity_EmitSoundParams", addresses::CBaseEntity_EmitSoundParams);
RESOLVE_SIG(g_GameConfig, "DispatchParticleEffect", addresses::DispatchParticleEffect);
RESOLVE_SIG(g_GameConfig, "CBaseEntity_EmitSoundFilter", addresses::CBaseEntity_EmitSoundFilter);
RESOLVE_SIG(g_GameConfig, "CBaseEntity_SetMoveType", addresses::CBaseEntity_SetMoveType);
@@ -101,3 +109,29 @@ bool addresses::InitializeBanMap(CGameConfig* g_GameConfig)
Message("Found %s at 0x%p\n", "CCSGameRules__sm_mapGcBanInformation", addresses::sm_mapGcBanInformation);
return true;
}
+
+bool addresses::InitializeVScriptFunctions()
+{
+ void* pCBaseEntityVTable = modules::server->FindVirtualTable("CBaseEntity");
+ if (!pCBaseEntityVTable)
+ {
+ Message("Failed to find CBaseEntity vtable\n");
+ return false;
+ }
+
+ // GetScriptDesc ignores this, so the vtable pointer is sufficient here.
+ ScriptClassDesc_t* pCBaseEntityScriptDesc = reinterpret_cast(reinterpret_cast(&pCBaseEntityVTable)->GetScriptDesc());
+
+ RESOLVE_SF(pCBaseEntityScriptDesc, "SetGravity", SetGravityScale);
+ RESOLVE_SF(pCBaseEntityScriptDesc, "SetEntityName", ScriptSetEntityName);
+ RESOLVE_SF(pCBaseEntityScriptDesc, "EmitSoundParams", ScriptEmitSoundParams);
+ RESOLVE_SF(pCBaseEntityScriptDesc, "SetTeam", ChangeTeam);
+ RESOLVE_SF(pCBaseEntityScriptDesc, "IsPlayerPawn", IsPlayerPawn);
+ RESOLVE_SF(pCBaseEntityScriptDesc, "IsPlayerController", IsPlayerController);
+
+ if (!Teleport.Initialize(GetScriptFunction(pCBaseEntityScriptDesc, "SetOrigin")))
+ return false;
+ Message("Found %s::Teleport through SetOrigin at vtable index %i\n", pCBaseEntityScriptDesc->m_pszClassname, Teleport.GetOffset());
+
+ return true;
+}
diff --git a/src/addresses.h b/src/addresses.h
index 18a43c00..ea1e9cdd 100644
--- a/src/addresses.h
+++ b/src/addresses.h
@@ -21,6 +21,7 @@
#include "platform.h"
#include "stdint.h"
#include "utils/module.h"
+#include "utils/script_function.h"
#include "utlstring.h"
#include "variant.h"
@@ -74,11 +75,11 @@ namespace addresses
{
bool Initialize(CGameConfig* g_GameConfig);
bool InitializeBanMap(CGameConfig* g_GameConfig);
+ bool InitializeVScriptFunctions();
inline CUtlMap* sm_mapGcBanInformation;
inline void(FASTCALL* SetGroundEntity)(CBaseEntity* ent, CBaseEntity* ground, CBaseEntity* unk3);
- inline void(FASTCALL* SetGravityScale)(CBaseEntity*, float);
inline void(FASTCALL* CCSPlayerController_SwitchTeam)(CCSPlayerController* pController, uint32 team);
inline void(FASTCALL* CBasePlayerController_SetPawn)(CBasePlayerController* pController, CCSPlayerPawn* pPawn, bool a3, bool a4, bool a5, bool a6);
inline void(FASTCALL* CBaseModelEntity_SetModel)(CBaseModelEntity* pModel, const char* pszModel);
@@ -97,8 +98,6 @@ namespace addresses
inline void(FASTCALL* CGameRules_TerminateRound)(CGameRules* pGameRules, float delay, unsigned int reason, int64 a4);
inline CBaseEntity*(FASTCALL* CreateEntityByName)(const char* className, int iForceEdictIndex);
inline void(FASTCALL* DispatchSpawn)(CBaseEntity* pEntity, CEntityKeyValues* pEntityKeyValues);
- inline void(FASTCALL* CEntityIdentity_SetEntityName)(CEntityIdentity* pEntity, const char* pName);
- inline void(FASTCALL* CBaseEntity_EmitSoundParams)(CBaseEntity* pEntity, const char* pszSound, int nPitch, float flVolume, float flDelay);
inline int(FASTCALL* DispatchParticleEffect)(const char* pszParticleName, int iAttachType, CBaseEntity* pEntity,
char iAttachmentPoint, CUtlSymbolLarge iAttachmentName, bool bResetAllParticlesOnEntity, int nSplitScreenPlayerSlot, IRecipientFilter* a7, byte* a8);
inline StartSoundEventInfo(FASTCALL* CBaseEntity_EmitSoundFilter)(IRecipientFilter& filter, CEntityIndex ent, const EmitSound_t& params);
@@ -108,4 +107,12 @@ namespace addresses
inline void(FASTCALL* CCSPlayer_WeaponServices_EquipWeapon)(CCSPlayer_WeaponServices* pWeaponServices, CBasePlayerWeapon* pPlayerWeapon);
inline void(FASTCALL* GetSpawnGroups)(CSpawnGroupMgrGameSystem* pSpawnGroupMgr, CUtlVector* pList);
inline void(FASTCALL* CBasePlayerPawn_SnapViewAngles)(CBasePlayerPawn* pPawn, QAngle* pAngles);
-} // namespace addresses
\ No newline at end of file
+
+ inline CVScriptFunction SetGravityScale;
+ inline CVScriptFunction ScriptSetEntityName;
+ inline CVScriptFunction ScriptEmitSoundParams;
+ inline CVScriptFunction ChangeTeam;
+ inline CVScriptFunction IsPlayerPawn;
+ inline CVScriptFunction IsPlayerController;
+ inline CVScriptTeleportFunction Teleport;
+} // namespace addresses
diff --git a/src/cs2_sdk/entity/cbaseentity.h b/src/cs2_sdk/entity/cbaseentity.h
index bcf1955f..bcf7a63b 100644
--- a/src/cs2_sdk/entity/cbaseentity.h
+++ b/src/cs2_sdk/entity/cbaseentity.h
@@ -165,7 +165,7 @@ class CBaseEntity : public CEntityInstance
void SetName(const char* pName)
{
- addresses::CEntityIdentity_SetEntityName(m_pEntity, pName);
+ addresses::ScriptSetEntityName(this, pName);
}
void TakeDamage(CTakeDamageInfo& info)
@@ -175,8 +175,7 @@ class CBaseEntity : public CEntityInstance
void Teleport(const Vector* pPosition, const QAngle* pAngles, const Vector* pVelocity)
{
- static int offset = g_GameConfig->GetOffset("Teleport");
- CALL_VIRTUAL(void, offset, this, pPosition, pAngles, pVelocity);
+ addresses::Teleport(this, pPosition, pAngles, pVelocity);
}
void SetCollisionGroup(StandardCollisionGroups_t nCollisionGroup)
@@ -196,14 +195,12 @@ class CBaseEntity : public CEntityInstance
bool IsPawn()
{
- static int offset = g_GameConfig->GetOffset("IsPlayerPawn");
- return CALL_VIRTUAL(bool, offset, this);
+ return addresses::IsPlayerPawn(this);
}
bool IsController()
{
- static int offset = g_GameConfig->GetOffset("IsPlayerController");
- return CALL_VIRTUAL(bool, offset, this);
+ return addresses::IsPlayerController(this);
}
void AcceptInput(const char* pInputName, variant_t value = variant_t(""), CEntityInstance* pActivator = nullptr, CEntityInstance* pCaller = nullptr)
@@ -226,7 +223,7 @@ class CBaseEntity : public CEntityInstance
// Emit a sound event
void EmitSound(const char* pszSound, int nPitch = 100, float flVolume = 1.0, float flDelay = 0.0)
{
- addresses::CBaseEntity_EmitSoundParams(this, pszSound, nPitch, flVolume, flDelay);
+ addresses::ScriptEmitSoundParams(this, pszSound, nPitch, flVolume, flDelay);
}
StartSoundEventInfo EmitSoundFilter(IRecipientFilter& filter, const char* pszSound, float flVolume = 1.0, float flPitch = 1.0)
diff --git a/src/cs2_sdk/entity/ccsplayercontroller.h b/src/cs2_sdk/entity/ccsplayercontroller.h
index cc994b9d..5376c666 100644
--- a/src/cs2_sdk/entity/ccsplayercontroller.h
+++ b/src/cs2_sdk/entity/ccsplayercontroller.h
@@ -84,8 +84,7 @@ class CCSPlayerController : public CBasePlayerController
void ChangeTeam(int iTeam)
{
- static int offset = g_GameConfig->GetOffset("CCSPlayerController_ChangeTeam");
- CALL_VIRTUAL(void, offset, this, iTeam);
+ addresses::ChangeTeam(this, iTeam);
}
void SwitchTeam(int iTeam)
diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp
index 6dee2ab3..bc963fea 100644
--- a/src/cs2fixes.cpp
+++ b/src/cs2fixes.cpp
@@ -204,6 +204,9 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool
if (!addresses::Initialize(g_GameConfig))
bRequiredInitLoaded = false;
+ if (!addresses::InitializeVScriptFunctions())
+ bRequiredInitLoaded = false;
+
if (!InitPatches(g_GameConfig))
bRequiredInitLoaded = false;
@@ -279,7 +282,7 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool
SH_MANUALHOOK_RECONFIGURE(OnTakeDamage_Alive, offset, 0, 0);
g_iOnTakeDamageAliveId = SH_ADD_MANUALDVPHOOK(OnTakeDamage_Alive, pCCSPlayerPawnVTable, SH_MEMBER(this, &CS2Fixes::Hook_OnTakeDamage_Alive), false);
- offset = g_GameConfig->GetOffset("Teleport");
+ offset = addresses::Teleport.GetOffset();
if (offset == -1)
{
snprintf(error, maxlen, "Failed to find Teleport\n");
diff --git a/src/utils/script_function.cpp b/src/utils/script_function.cpp
new file mode 100644
index 00000000..208603cb
--- /dev/null
+++ b/src/utils/script_function.cpp
@@ -0,0 +1,165 @@
+/**
+ * =============================================================================
+ * CS2Fixes
+ * Copyright (C) 2023-2026 Source2ZE
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#include "script_function.h"
+#include "entitysystem.h"
+#include "entityclass.h"
+
+void* GetScriptFunction(ScriptClassDesc_t* pScriptDesc, const char* pszFuncName)
+{
+ // https://github.com/Wend4r/sourcesdk/blob/758e43823f02fcd40498d33a42cd93243258fe1e/public/vscript/ivscript.h#L280
+ struct ScriptFunctionBindingCurrent_t
+ {
+ ScriptFuncDescriptor_t m_desc;
+ ScriptClassDesc_t* m_pClassDesc;
+ ScriptBindingFunc_t m_pfnBinding;
+ void* m_pFunction;
+ ScriptFuncBindingFlags_t m_flags;
+ };
+
+ const auto& functionBindings = *reinterpret_cast*>(&pScriptDesc->m_FunctionBindings);
+ FOR_EACH_VEC(functionBindings, i)
+ {
+ auto& functionBinding = functionBindings.Element(i);
+ if (V_strcmp(functionBinding.m_desc.m_pszScriptName, pszFuncName) != 0)
+ continue;
+
+ return functionBinding.m_pFunction;
+ }
+
+ return nullptr;
+}
+
+int ExtractTeleportIndexFromSetOrigin(void* pSetOrigin)
+{
+ if (!pSetOrigin)
+ return -1;
+
+ constexpr int MaxScanSize = 0x40;
+
+ uint8* pCode = static_cast(pSetOrigin);
+
+ for (int i = 0; i + 9 <= MaxScanSize; i++)
+ {
+ int p = i;
+ uint8 rex = 0;
+
+ // Optional x86-64 REX prefix.
+ if ((pCode[p] & 0xF0) == 0x40)
+ rex = pCode[p++];
+
+ // Pattern #1:
+ //
+ // mov rax, [this]
+ // ...
+ // call [rax + offset]
+ //
+ // or:
+ //
+ // jmp [rax + offset]
+ //
+ // FF /2 = CALL
+ // FF /4 = JMP
+ if (pCode[p] == 0xFF)
+ {
+ uint8 modrm = pCode[p + 1];
+
+ int mod = modrm >> 6;
+ int op = (modrm >> 3) & 7;
+
+ // mod == 2 means [register + disp32].
+ // rm == 4 would require a SIB byte.
+ if (mod == 2 && (op == 2 || op == 4) && (modrm & 7) != 4)
+ {
+ int32 disp = *reinterpret_cast(pCode + p + 2);
+
+ if (disp >= 0 && (disp & 7) == 0)
+ return disp >> 3;
+ }
+ }
+
+ // Pattern #2:
+ //
+ // mov rax, [rax + offset]
+ // jmp rax
+ //
+ // or:
+ //
+ // mov rax, [rax + offset]
+ // call rax
+ //
+ // Require REX.W because we are loading a 64-bit function pointer.
+ if ((rex & 8) && pCode[p] == 0x8B)
+ {
+ uint8 modrm = pCode[p + 1];
+
+ int mod = modrm >> 6;
+ int dst = ((modrm >> 3) & 7) | ((rex & 4) ? 8 : 0);
+ int base = (modrm & 7) | ((rex & 1) ? 8 : 0);
+
+ // mov reg, [same_reg + disp32]
+ if (mod == 2 && dst == base && (modrm & 7) != 4)
+ {
+ int32 disp = *reinterpret_cast(pCode + p + 2);
+
+ if (disp < 0 || (disp & 7) != 0)
+ continue;
+
+ // The MOV instruction is 7 bytes:
+ // REX + 8B + ModRM + disp32
+ int next = i + 7;
+
+ uint8 nextRex = 0;
+
+ if ((pCode[next] & 0xF0) == 0x40)
+ nextRex = pCode[next++];
+
+ if (pCode[next] != 0xFF)
+ continue;
+
+ uint8 nextModrm = pCode[next + 1];
+
+ int nextMod = nextModrm >> 6;
+ int nextOp = (nextModrm >> 3) & 7;
+ int nextReg = (nextModrm & 7) | ((nextRex & 1) ? 8 : 0);
+
+ // mod == 3 means CALL/JMP directly through a register.
+ //
+ // FF /2 = CALL reg
+ // FF /4 = JMP reg
+ if (nextMod == 3 && (nextOp == 2 || nextOp == 4) && nextReg == dst)
+ return disp >> 3;
+ }
+ }
+ }
+
+ return -1;
+}
+
+bool CVScriptTeleportFunction::Initialize(void* pSetOrigin)
+{
+ int iOffset = ExtractTeleportIndexFromSetOrigin(pSetOrigin);
+ if (iOffset == -1)
+ return false;
+
+ m_iOffset = iOffset;
+ m_bVirtual = true;
+
+ return true;
+}
diff --git a/src/utils/script_function.h b/src/utils/script_function.h
new file mode 100644
index 00000000..544a387c
--- /dev/null
+++ b/src/utils/script_function.h
@@ -0,0 +1,108 @@
+/**
+ * =============================================================================
+ * CS2Fixes
+ * Copyright (C) 2023-2026 Source2ZE
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#pragma once
+
+#include "platform.h"
+#include "virtual.h"
+
+struct ScriptClassDesc_t;
+class CBaseEntity;
+class Vector;
+class QAngle;
+
+void* GetScriptFunction(ScriptClassDesc_t* pScriptDesc, const char* pszFuncName);
+
+template
+class CVScriptFunction
+{
+public:
+ CVScriptFunction() :
+ m_pFunction(nullptr), m_bVirtual(false) { }
+
+ inline bool Initialize(void* pFunction)
+ {
+ if (!pFunction)
+ return false;
+
+#ifdef PLATFORM_LINUX
+ // Itanium ABI: a pointer may encode the virtual index
+ uintptr_t ptr = reinterpret_cast(pFunction);
+ if (ptr & 1)
+ {
+ m_bVirtual = true;
+ m_iOffset = static_cast((ptr - 1) >> 3);
+
+ return true;
+ }
+#endif
+
+ m_pFunction = reinterpret_cast(pFunction);
+
+ return true;
+ }
+
+ inline Ret operator()(Class* pThis, Args... args)
+ {
+ if (m_bVirtual)
+ {
+ auto pFunction = vmt::GetVMethod(m_iOffset, pThis);
+
+ return pFunction(pThis, args...);
+ }
+
+ return m_pFunction(pThis, args...);
+ }
+
+ inline bool IsVirtual() const
+ {
+ return m_bVirtual;
+ }
+
+ inline void* GetPtr() const
+ {
+ if (IsVirtual())
+ return nullptr;
+
+ return reinterpret_cast(m_pFunction);
+ }
+
+ inline int GetOffset() const
+ {
+ if (IsVirtual())
+ return m_iOffset;
+
+ return -1;
+ }
+
+protected:
+ union
+ {
+ Ret(FASTCALL* m_pFunction)(Class*, Args...);
+ int m_iOffset;
+ };
+
+ bool m_bVirtual;
+};
+
+class CVScriptTeleportFunction final : public CVScriptFunction
+{
+public:
+ bool Initialize(void* pSetOrigin);
+};
diff --git a/src/utils/virtual.h b/src/utils/virtual.h
index 32c696a7..8440e434 100644
--- a/src/utils/virtual.h
+++ b/src/utils/virtual.h
@@ -19,6 +19,7 @@
#pragma once
#include "platform.h"
+#include "tier0/dbg.h"
#define CALL_VIRTUAL(retType, idx, ...) \
vmt::CallVirtual(idx, __VA_ARGS__)