Skip to content

Fix EOSError 1400 at shutdown so we can drop the EntryPatchMode workaround #1

@erwan-joly

Description

@erwan-joly

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):

  • DefaultToEntwellgf 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:

  1. Also patch the shutdown-side teardown so the unmatched allocation is released, or
  2. 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

  1. Run the Client Creator with DefaultToEntwell on a fresh NostaleClientX.exe.
  2. Double-click the generated exe → client boots into Entwell mode, runs fine.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions