Background
ClientPatcher.PatchDefaultToEntwell makes the client default into its EntwellNostaleClient standalone body when launched with no / unknown argv. The patch works at runtime, but the client crashes on close with:
EOSError 1400 / Invalid window handle
Why
The anchor signature covers a compare block that ends with a JNZ rel32 jumping into the Entwell body:
B9 14 00 00 00 mov ecx, 0x14
BA 01 00 00 00 mov edx, 1
E8 ?? ?? ?? ?? call <AnsiString setup for "EntwellNostaleClient">
8B 45 CC mov eax, [ebp-0x34] ; arg
BA ?? ?? ?? ?? mov edx, <"EntwellNostaleClient">
E8 ?? ?? ?? ?? call <case-folded Delphi string compare>
0F 85 rel32 jnz <entwell_body>
The two calls before the JNZ have shutdown side effects:
- the first sets up a ref-counted
AnsiString and registers it with the Delphi finalisation list,
- the second does a case-folded compare whose scratch
AnsiStrings also go on that list.
On the match branch (what an unpatched client does when the real launcher passes the expected arg), the cleanup balances at exit. On our patched fall-through path, one or both allocations are never properly decref'd — the finaliser walks the list at shutdown, touches a dangling window handle, and raises EOSError 1400.
An earlier variant NOPped only the JNZ; same crash. The current implementation avoids it by overwriting 34 bytes [pattern, pattern+34) with EB 20 + NOPs, jumping the entire compare block so those calls never run — but this patch only works when the client is started with no/unknown argv. The real Gameforge launcher still needs the block to run, so we can't apply this patch unconditionally.
The workaround we ship today
EntryPatchMode enum (ClientPatcher.cs:12-16) + dropdown in the Client Creator tab (MainForm.cs:406-414):
- DefaultToEntwell —
gf mode still works, but exits with EOSError 1400
- OnlyEntwell — replace the argc
JL with an unconditional JMP to the Entwell body; clean exit, but gf no longer works
- None — no parameter patch (double-click does nothing; user must pass argv)
None is ideal. The user has to pick between a clean shutdown and gf compatibility.
Proposed fix
Find the actual source of the 1400 — i.e. what the calls register in the finalisation list that our branch doesn't balance — and either:
- Also patch the shutdown-side teardown so the unmatched allocation is released, or
- Emit a minimal fixup that does the ref-decrement / string-disposal of whatever the compare block leaked, inline into the detour, so both branches converge on the same end state.
Once shutdown is clean for the fall-through branch:
- Delete
EntryPatchMode enum + AppSettings.ClientCreator.EntryPatchMode + the dropdown in MainForm.
- Keep a single
PatchDefaultToEntwell call, unconditional, in RunPatch.
- Delete
PatchForceEntwell (it only exists because it sidesteps the 1400 crash at the cost of breaking gf).
Repro
- Run the Client Creator with DefaultToEntwell on a fresh
NostaleClientX.exe.
- Double-click the generated exe → client boots into Entwell mode, runs fine.
- Close the client window →
EOSError 1400 / Invalid window handle dialog.
Notes for whoever picks this up
- Relevant symbol hints are
Forms.TApplication.HandleException and the Delphi RTL finalisation path for AnsiString.
morsisko / Gilgames000 references don't touch this — pure Delphi shutdown-path bug, not a protection-layer thing.
- Signature is stable across recent client builds; bit-level fix, no version-gate needed.
Background
ClientPatcher.PatchDefaultToEntwellmakes the client default into itsEntwellNostaleClientstandalone body when launched with no / unknown argv. The patch works at runtime, but the client crashes on close with:Why
The anchor signature covers a compare block that ends with a
JNZ rel32jumping into the Entwell body:The two
calls before theJNZhave shutdown side effects:AnsiStringand registers it with the Delphi finalisation list,AnsiStrings also go on that list.On the match branch (what an unpatched client does when the real launcher passes the expected arg), the cleanup balances at exit. On our patched fall-through path, one or both allocations are never properly decref'd — the finaliser walks the list at shutdown, touches a dangling window handle, and raises
EOSError 1400.An earlier variant NOPped only the
JNZ; same crash. The current implementation avoids it by overwriting 34 bytes[pattern, pattern+34)withEB 20+ NOPs, jumping the entire compare block so thosecalls never run — but this patch only works when the client is started with no/unknown argv. The real Gameforge launcher still needs the block to run, so we can't apply this patch unconditionally.The workaround we ship today
EntryPatchModeenum (ClientPatcher.cs:12-16) + dropdown in the Client Creator tab (MainForm.cs:406-414):gfmode still works, but exits with EOSError 1400JLwith an unconditionalJMPto the Entwell body; clean exit, butgfno longer worksNone is ideal. The user has to pick between a clean shutdown and
gfcompatibility.Proposed fix
Find the actual source of the 1400 — i.e. what the
calls register in the finalisation list that our branch doesn't balance — and either:Once shutdown is clean for the fall-through branch:
EntryPatchModeenum +AppSettings.ClientCreator.EntryPatchMode+ the dropdown in MainForm.PatchDefaultToEntwellcall, unconditional, inRunPatch.PatchForceEntwell(it only exists because it sidesteps the 1400 crash at the cost of breakinggf).Repro
NostaleClientX.exe.EOSError 1400 / Invalid window handledialog.Notes for whoever picks this up
Forms.TApplication.HandleExceptionand the Delphi RTL finalisation path forAnsiString.morsisko/Gilgames000references don't touch this — pure Delphi shutdown-path bug, not a protection-layer thing.