Skip to content

Fix startup crash and silent plugin loss on game version 1.5 (#15) - #16

Open
WizGery wants to merge 1 commit into
xiaoxiao921:masterfrom
WizGery:fix/release-1-5-startup-crash
Open

Fix startup crash and silent plugin loss on game version 1.5 (#15)#16
WizGery wants to merge 1 commit into
xiaoxiao921:masterfrom
WizGery:fix/release-1-5-startup-crash

Conversation

@WizGery

@WizGery WizGery commented Jul 29, 2026

Copy link
Copy Markdown

Fixes #15. Verified working in-game against release_1_5_1164953_841.

Symptom

With d3d12.dll present the game crashes on startup, and LogOutput.log is created but stays empty.

Cause

Four separate bugs, each hidden behind the previous one. Only two are version-specific — the other two are latent and will resurface on any future patch that breaks a signature.

1. kcd2_address::rip() dereferenced without a null check

scan() returns {0} when a pattern isn't found, but rip() did *(int32_t*)(m_value + offset) unconditionally. A missing signature was therefore read as memory at address 1, killing the process during init before the logger flushed — which is exactly why the reported log is 0 bytes.

offset(), rip() and get_call() now propagate zero, so a broken signature leaves a detectable null instead of a dead process.

2. A missing signature aborted the whole of kcd2_init

The 35 hook-registration blocks handle a failed scan with:

if (!ptr) { LOG(ERROR) << "Failed to find X"; return; }

That return exits kcd2_init entirely, not just the block. On 1.5 the CPhysicalEntity_ctor signature no longer matches, so init aborted there and never reached the ExecuteBuffer hook ~500 lines below — the hook that creates the Lua manager.

The result was that no plugin loaded at all, with nothing in the log connecting a 3D-engine signature to the entire plugin system disappearing.

The script-system hook block is moved above the blocks that can abort.

I first tried making all 35 blocks independent by wrapping them in immediately-invoked lambdas. That does not work — it left the game hanging, because it installs hooks that had never run on this version and which index vtables that are null when their signatures break. Moving one block keeps the rest of the loader in the state it was tested in. Leaving this here so nobody repeats it.

3. hook_CryScriptSystem_Init dereferenced a possibly-null unique_ptr

It assumed g_lua_manager_instance already held the early-main VM manager. When that VM is never created the pointer is null, and the dereference crashes during CryScriptSystem::Init with the trace pointing into d3d12.dll and RAX=0. Only reachable once init got far enough to register the hook.

4. Four Lua signatures no longer match on 1.5

These live in the unguarded scan block, so before fix 1 they were fatal:

function RVA on 1.5
luaV_execute 0x714cb8
lua_gettable 0x718a6c
lua_custom_alloc 0x717620
lua_concat 0x1323cd0

luaV_execute shows why these break:

1.3 expected:  48 8B C4  48 89 58 ??  89 50 ??  55 56 57 41 54 ...
1.5 actual:    48 8B C4              89 50 10  53 55 56 57 41 54 ...

The compiler replaced a mov [rax+8],rbx with a later push rbx. One instruction, and the signature stops matching.

The new patterns mask their rel32 displacements so they survive the linker moving code.

Also added

A post-scan report listing by name any Lua pointer that failed to resolve:

[INFO] Lua signature scan: 19/19 resolved
[ERROR] Failed to find CPhysicalEntity_ctor
[INFO] Hooking initialized.

So the next breakage says which signature needs redoing instead of failing silently.

Note for redoing signatures on a future version

WHGame.dll is built with PGO, so MSVC splits functions into hot/cold fragments with separate .pdata entries and no prologue. Scanning backwards for int3 padding lands mid-instruction. Follow the UNW_FLAG_CHAININFO chain in UNWIND_INFO to reach the real function start — that is what turned luaV_execute from "pattern not found" into a concrete address.

lua_concat was identified from its disassembly (movsxd rbx,edx / cmp ebx,2 / test edx,edx, the exact shape of lua_concat(lua_State*, int)), and luaV_execute via the string 'for' initial value, which in Lua 5.1 appears only inside that function.

Testing

Built with MSVC 2022 / CMake, installed as d3d12.dll next to KingdomCome.exe. Game reaches the main menu and loads saves normally; 19/19 Lua signatures resolve; plugins load and run; rom.memory.scan_pattern, rom.pointer reads, rom.game access and rom.memory.dynamic_hook all verified working from a test plugin.

The only remaining error is Failed to find CPhysicalEntity_ctor, which is one of the 3D-engine signatures and now degrades gracefully.

Closes the failure reported in issue xiaoxiao921#15: with d3d12.dll present, the game
crashes on startup on release_1_5 and LogOutput.log is created but stays
empty. Verified working in-game against release_1_5_1164953_841.

There are three separate bugs behind that symptom, each hidden by the one
before it. Only the third is version-specific; the first two are latent and
would resurface on any future patch that breaks a signature.

1. kcd2_address::rip() dereferenced without a null check

scan() returns {0} when a pattern is not found, but rip() did
*(int32_t*)(m_value + offset) unconditionally, so a missing signature was
read as memory at address 1. That killed the process during init, before
the logger flushed -- which is why the reported log is 0 bytes.

offset(), rip() and get_call() now propagate zero, so a broken signature
leaves a detectable null instead of a dead process.

2. A missing signature aborted the whole of kcd2_init

The 35 hook-registration blocks handle a failed scan with
    if (!ptr) { LOG(ERROR) << "Failed to find X"; return; }
and that return exits kcd2_init entirely, not just the block. On 1.5 the
CPhysicalEntity_ctor signature no longer matches, so init aborted there and
never reached the ExecuteBuffer hook ~500 lines below -- the hook that
creates the Lua manager. The result was that no plugin loaded at all, with
nothing in the log connecting the two.

The script-system hook block is moved above the blocks that can abort.

Making all 35 blocks independent (wrapping them in immediately-invoked
lambdas) was tried first and rejected: it left the game hanging, because it
installed hooks that had never run on this version and which index vtables
that are null when their signatures are broken. Moving one block keeps the
rest of the loader in the state it was tested in.

3. hook_CryScriptSystem_Init dereferenced a possibly-null unique_ptr

It assumed g_lua_manager_instance already held the early-main VM manager.
When that VM is never created the pointer is null, and the dereference
crashes during CryScriptSystem::Init with the trace pointing into d3d12.dll
and RAX=0. This only became reachable once init got far enough to register
the hook.

4. Four Lua signatures no longer match on 1.5

These live in the unguarded scan block, so before fix 1 they were fatal:

    luaV_execute      RVA 0x714cb8
    lua_gettable      RVA 0x718a6c
    lua_custom_alloc  RVA 0x717620
    lua_concat        RVA 0x1323cd0

luaV_execute is representative of why these break. 1.3 expected
    48 8B C4  48 89 58 ??  89 50 ??  55 56 57 41 54 ...
and 1.5 has
    48 8B C4              89 50 10  53 55 56 57 41 54 ...
The compiler replaced a mov [rax+8],rbx with a later push rbx. One
instruction, and the signature stops matching.

New patterns mask their rel32 displacements so they survive the linker
moving code around.

luaV_execute was located via the string "'for' initial value", which in
Lua 5.1 appears only inside that function; lua_concat by its disassembly
(movsxd rbx,edx / cmp ebx,2 / test edx,edx, the exact shape of
lua_concat(lua_State*, int)).

Note for anyone redoing these on a future version: WHGame.dll is built with
PGO, so MSVC splits functions into hot/cold fragments with separate .pdata
entries and no prologue. Scanning backwards for int3 padding lands
mid-instruction. Follow the UNW_FLAG_CHAININFO chain in UNWIND_INFO to get
the real function start.

Also adds a post-scan report listing by name any Lua pointer that failed to
resolve, so the next breakage says which signature to redo instead of
failing silently.
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.

Game version 1.5 crash on startup with d3d12.dll

1 participant