$npx -y skills add HLND2T/CS2_VibeSignatures --skill find-CBaseEntity_GetChangeAccessorPathInfo_1Find and identify the CBaseEntity_GetChangeAccessorPathInfo_1 virtual function in CS2 binary using IDA Pro MCP. Use this skill when reverse engineering CS2 server.dll or libserver.so to locate the second GetChangeAccessorPathInfo override by scanning CBaseEntity vtable slots near
| 1 | # Find CBaseEntity_GetChangeAccessorPathInfo_1 |
| 2 | |
| 3 | Locate `CBaseEntity_GetChangeAccessorPathInfo_1` vfunc in CS2 `server.dll` or `libserver.so` using IDA Pro MCP tools. |
| 4 | |
| 5 | ## Background |
| 6 | |
| 7 | `CBaseEntity` overrides two separate `CEntityInstance` interface methods, `GetChangeAccessorPathInfo_1` and |
| 8 | `GetChangeAccessorPathInfo_2`, with **byte-for-byte identical implementations** (both simply forward to the same |
| 9 | internal lazy-init helper at the same member offset). Because the source bodies are identical, the compiler may |
| 10 | fold both vtable slots onto the exact same function address, or it may still emit two separate but structurally |
| 11 | identical functions. `CBaseEntity_GetChangeAccessorPathInfo_2` is already resolved (it inherits its vtable slot |
| 12 | from `CEntityInstance_GetChangeAccessorPathInfo_2`); `CBaseEntity_GetChangeAccessorPathInfo_1` occupies a |
| 13 | **different** slot within +/-2 entries of it in the same `CBaseEntity` vtable. |
| 14 | |
| 15 | ## Method |
| 16 | |
| 17 | ### 1. Load CBaseEntity_GetChangeAccessorPathInfo_2 from YAML |
| 18 | |
| 19 | **ALWAYS** Use SKILL `/get-func-from-yaml` with `func_name=CBaseEntity_GetChangeAccessorPathInfo_2`. |
| 20 | |
| 21 | If the skill returns an error, **STOP** and report to user. |
| 22 | |
| 23 | Otherwise, extract: |
| 24 | - `func_va` (the reference implementation address) |
| 25 | - `vfunc_index` |
| 26 | - `vfunc_offset` |
| 27 | - `vtable_name` (should be `CBaseEntity`) |
| 28 | |
| 29 | ### 2. Load CBaseEntity VTable from YAML |
| 30 | |
| 31 | **ALWAYS** Use SKILL `/get-vtable-from-yaml` with `class_name=CBaseEntity`. |
| 32 | |
| 33 | If the skill returns an error, **STOP** and report to user. |
| 34 | |
| 35 | Otherwise, extract: |
| 36 | - `vtable_numvfunc` |
| 37 | - `vtable_entries` |
| 38 | |
| 39 | ### 3. Enumerate Candidate Slots |
| 40 | |
| 41 | Compute the scan window around the known slot: |
| 42 | |
| 43 | - `window_start = max(0, CBaseEntity_GetChangeAccessorPathInfo_2.vfunc_index - 2)` |
| 44 | - `window_end = min(vtable_numvfunc - 1, CBaseEntity_GetChangeAccessorPathInfo_2.vfunc_index + 2)` |
| 45 | |
| 46 | For every index `i` in `[window_start, window_end]` **except** `CBaseEntity_GetChangeAccessorPathInfo_2.vfunc_index` |
| 47 | itself, read the candidate function address `vtable_entries[i]`. |
| 48 | |
| 49 | ### 4. Check for Identical-Address Folding First |
| 50 | |
| 51 | Compilers (MSVC `/OPT:ICF`, GCC/Clang identical code folding) commonly merge functions with byte-identical bodies |
| 52 | into a single implementation, so multiple vtable slots end up pointing at the **same address**. |
| 53 | |
| 54 | For each candidate index `i`: |
| 55 | - If `vtable_entries[i] == CBaseEntity_GetChangeAccessorPathInfo_2.func_va`, this slot is folded onto the same |
| 56 | implementation and is an immediate match. Record `i` as `target_vfunc_index` and stop scanning further. |
| 57 | |
| 58 | ### 5. Otherwise, Decompile and Compare Candidates |
| 59 | |
| 60 | If no candidate address matched directly, decompile the reference function and every remaining candidate: |
| 61 | |
| 62 | ```text |
| 63 | mcp__ida-pro-mcp__decompile addr="<CBaseEntity_GetChangeAccessorPathInfo_2_func_va>" |
| 64 | mcp__ida-pro-mcp__decompile addr="<candidate_func_addr>" |
| 65 | ``` |
| 66 | |
| 67 | #### Windows (`server.dll`) |
| 68 | |
| 69 | The reference implementation is a one-line forwarder: |
| 70 | |
| 71 | ```c |
| 72 | __int64 __fastcall CBaseEntity_GetChangeAccessorPathInfo_2(__int64 a1) |
| 73 | { |
| 74 | return sub_180184630(a1 + 56); |
| 75 | } |
| 76 | ``` |
| 77 | |
| 78 | A matching candidate must be **structurally identical**: |
| 79 | 1. Takes a single `(__int64 a1)` argument (this only) |
| 80 | 2. Directly tail-calls the exact same helper address as the reference (e.g. `sub_180184630`) |
| 81 | 3. Passes the exact same constant offset argument (`a1 + 56`) |
| 82 | 4. Returns the helper's result unchanged |
| 83 | |
| 84 | #### Linux (`libserver.so`) |
| 85 | |
| 86 | The reference implementation is a lazy-init pattern: |
| 87 | |
| 88 | ```c |
| 89 | __int64 __fastcall CBaseEntity_GetChangeAccessorPathInfo_2(__int64 a1) |
| 90 | { |
| 91 | __int64 result; |
| 92 | ... |
| 93 | result = *(_QWORD *)(a1 + 64); |
| 94 | if ( !result ) |
| 95 | { |
| 96 | result = operator new(384); |
| 97 | ... |
| 98 | *(_QWORD *)(a1 + 64) = result; |
| 99 | } |
| 100 | return result; |
| 101 | } |
| 102 | ``` |
| 103 | |
| 104 | A matching candidate must: |
| 105 | 1. Read/write the exact same member offset as the reference (`a1 + 64`) |
| 106 | 2. Allocate the exact same size via `operator new` (`384`) |
| 107 | 3. Initialize the same set of fixed fields at the same relative offsets (`+56`, `+24`, `+40`, `+48`, `+96` .. `+352`, etc.) |
| 108 | 4. Return the same lazily-initialized pointer |
| 109 | |
| 110 | ### 6. Confirm the Match |
| 111 | |
| 112 | Among the scanned candidates (excluding the known `_2` slot), exactly one should match either by identical address |
| 113 | (Step 4) or by identical decompiled structure (Step 5). That candidate is `CBaseEntity_GetChangeAccessorPathInfo_1`. |
| 114 | |
| 115 | If zero or more than one candidate match, **STOP** and report to user. |
| 116 | |
| 117 | ### 7. Write IDA Analysis Output as YAML |
| 118 | |
| 119 | **ALWAYS** Use SKILL `/write-vfunc-as-yaml` to wr |