Skip to content

Replace some gamedata lookups with VScript function bindings - #459

Merged
Vauff merged 5 commits into
Source2ZE:devfrom
komashchenko:script_function
Aug 16, 2026
Merged

Replace some gamedata lookups with VScript function bindings#459
Vauff merged 5 commits into
Source2ZE:devfrom
komashchenko:script_function

Conversation

@komashchenko

Copy link
Copy Markdown
Contributor

Although VScript itself is disabled, its function bindings are still registered and can be accessed through CGameEntitySystem.

This PR replaces several gamedata signatures and offsets with runtime lookups for the following function bindings:

  • SetGravity
  • SetEntityName
  • EmitSoundParams
  • SetTeam (ChangeTeam)
  • IsPlayerPawn
  • IsPlayerController

SetModel could also be resolved this way, but it is currently hooked, so it is intentionally left unchanged. Teleport could potentially be obtained through SetOrigin.

Since the function bindings cannot be resolved before StartupServer is called, the appropriate failure behavior still needs to be determined. For now, lookup failures are reported through Panic, although it may be better to simply use Plat_FatalError instead.

@Vauff

Vauff commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

I would've had concerns about VScript (or even just the CS2 bindings) being cleaned up from the game in the past. But given we're 3 years in to CS2 now, and it's all still there. I don't see any reason why we can't abuse them if they're sticking around, this would've been more stable than relying on signatures/offsets, anyway. Worst case we just revert back to signatures/offsets if Valve does ever go ahead with VScript cleanup.

I would suggest sticking with "VScript" naming to avoid any confusion with cs_script though. I'll do a more thorough review once this is no longer marked as draft.

@komashchenko

Copy link
Copy Markdown
Contributor Author

The bindings no longer require waiting for CGameEntitySystem. ScriptClassDesc_t can be obtained directly through GetScriptDesc in entity, allowing the bindings to be resolved during plugin initialization. This should make the approach even more stable.
Also added resolution of the Teleport index from the SetOrigin binding using ExtractTeleportIndexFromSetOrigin. The instruction decoder was written with LLM assistance. I tested it against Windows and Linux binaries dating back to 2025, and it consistently resolved the correct index in all tested versions.
This covers everything I intended to implement, so I’m marking the PR as ready for review.

@komashchenko
komashchenko marked this pull request as ready for review August 13, 2026 17:00
Comment thread src/utils/script_function.cpp Outdated
Comment on lines +52 to +156
int ExtractTeleportIndexFromSetOrigin(void* pSetOrigin)
{
if (!pSetOrigin)
return -1;

constexpr int MaxScanSize = 0x40;

uint8* pCode = static_cast<uint8*>(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<int32*>(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<int32*>(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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this is a lot to have replacing one simple vtable index offset, maintenance could be challenging. Think we'd just prefer keeping Teleport as a gamedata offset in this case.

@Vauff
Vauff changed the base branch from main to dev August 16, 2026 04:22
@Vauff
Vauff merged commit e1e3c2f into Source2ZE:dev Aug 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants