Skip to content

LHScript: Implement LHScriptX<c>/<w> text script engine#247

Merged
bwrsandman merged 1 commit into
mainfrom
lhscript
Jul 12, 2026
Merged

LHScript: Implement LHScriptX<c>/<w> text script engine#247
bwrsandman merged 1 commit into
mainfrom
lhscript

Conversation

@handsomematt

Copy link
Copy Markdown
Member

Implements the LHLib text script engine (BW1W120 0x7E72600x7EA0B0) as the genuine template it was: LHScriptX<T>, LHScriptCommandX<T>, LHScriptPramX<T> and LHScriptVariable<T>, instantiated for char and wchar_t through the original's own bodge_to_force_compilation_of_versions() (which survives at 0x7E6B70 with its Mac symbol name). Static data — the Commands/Conditions/NamesToCondition tables, both Pram buffers and LastLineParsed — is reconstructed verbatim from the binary.

Results: 29 of 44 target functions at 100.0% instruction match; all 98 data objects (tables, both Pram statics, ~90 string literals) matching. ScanLine, ProcessCommand and GetConditionValue remain nonmatching with TODO notes describing what's known; the switch-heavy functions (GetConditionState, GetCommandAsText) are byte-identical in code but capped by jump-table representation in the diff tooling.

Build-model findings (all evidence-driven, details in source comments):

  • The TU was compiled /Ob0 (no inlining) + /GX — that's why trivial header-inline members (LHScreen::Depth/MaxDepth, the LHSpriteList ctor, all the LHStringOps wrappers) exist as called comdats inside this TU's range. Depth/MaxDepth move from LHScreen.cpp into the header accordingly.
  • The memory-tracking operator new has a matching placement operator delete(void*, const char*, uint32_t) (now declared in LHWin.h). Its existence is what gives /GX functions using new(__FILE__, __LINE__) on ctor types their EH frames — the mystery funclets at 0x8A7B20+ are ProcessCommand's unwinders calling it.
  • #line pins reproduce the original __FILE__/__LINE__ values (C:\Dev\Libs\lionhead\lhlib\VER5.0\LHScript.cpp, lines 312/333/355/737/1032/1035), so the tracking-new immediates and the path string match bytes.
  • LH_RETURN's real enumerator names (LH_OK/LH_FAIL/LH_ERROR/LH_NO_MEMORY) fall out of the script condition tables; the enum moves from LHTransportInfo.h to a shared LHReturn.h.
  • New LHString.h holds the LHStringOps<char>/<wchar_t> CRT wrappers (header name fabricated, marked TODO).

Depends on decomp-toolkit v0.0.20 (dtk_tag bumped here): the wide string literals need openblack/decomp-toolkit#2, without which dtk invents abs32 relocations inside UTF-16 text whose characters happen to look like in-image pointers. The new dtk also validates a pre-existing trnsctrl.obj split/symbol overlap, fixed here by giving ?_GetRangeOfTrysToCheck its true size:0x7B.

🤖 Generated with Claude Code

@decomp-dev

decomp-dev Bot commented Jul 12, 2026

Copy link
Copy Markdown

Report for BW1W110 (b2442de - 2151214)

No changes

Report for BW1W100 (b2442de - 2151214)

No changes

Report for BW1W120 (b2442de - 2151214)

Total code: 4710972 bytes (-195 bytes)
Total functions: 26701 (+26)
📈 Matched code: 1.21% (+0.05%, +2554 bytes)

✅ 29 new matches
Unit Item Bytes Before After
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::CommandIf(char *, char *, char *) +241 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::CommandIf(unsigned short *, unsigned short *, unsigned short *) +241 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::LoadFile(char *, long *) +199 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::LoadFile(char *, long *) +195 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::CreateVariable(long, struct LHScriptPramX<unsigned short> *) +181 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::CreateVariable(long, struct LHScriptPramX<char> *) +178 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: bool __thiscall LHScriptX<unsigned short>::DontProcessNumLines(class LHOSFile &, unsigned short *, unsigned long) +111 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: bool __thiscall LHScriptX<char>::DontProcessNumLines(class LHOSFile &, char *, unsigned long) +108 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::Load(char *, struct LHScriptCommandX<char> *, enum LH_RETURN (__cdecl *)(long, struct LHScriptPramX<char> *), char **, enum LH_RETURN (__cdecl *)(long, long *), long *) +93 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::Load(char *, struct LHScriptCommandX<unsigned short> *, enum LH_RETURN (__cdecl *)(long, struct LHScriptPramX<unsigned short> *), unsigned short **, enum LH_RETURN (__cdecl *)(long, long *), long *) +93 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::Load(char *, char **, enum LH_RETURN (__cdecl *)(long, long *), long *) +86 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::Load(char *, struct LHScriptCommandX<char> *, enum LH_RETURN (__cdecl *)(long, struct LHScriptPramX<char> *), long *) +86 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::Load(char *, unsigned short **, enum LH_RETURN (__cdecl *)(long, long *), long *) +86 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::Load(char *, struct LHScriptCommandX<unsigned short> *, enum LH_RETURN (__cdecl *)(long, struct LHScriptPramX<unsigned short> *), long *) +86 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: void __thiscall LHScriptX<char>::DestroyVariables(void) +84 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: void __thiscall LHScriptX<unsigned short>::DestroyVariables(void) +84 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::Load(char *, long *) +73 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::Load(char *, long *) +73 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: struct LHScriptVariable<unsigned short> * __thiscall LHScriptX<unsigned short>::FindVariable(unsigned short *) +57 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: struct LHScriptVariable<char> * __thiscall LHScriptX<char>::FindVariable(char *) +56 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::SetVariables(struct LHScriptCommandX<char> *, enum LH_RETURN (__cdecl *)(long, struct LHScriptPramX<char> *), long *) +40 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::SetVariables(struct LHScriptCommandX<unsigned short> *, enum LH_RETURN (__cdecl *)(long, struct LHScriptPramX<unsigned short> *), long *) +40 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: void __thiscall LHSpriteList::Set(struct LHSprite *, long) +23 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: __thiscall LHSpriteList::LHSpriteList(void) +16 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: void __thiscall LHScriptX<char>::InitIfLevel(void) +7 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: void __thiscall LHScriptX<unsigned short>::InitIfLevel(void) +7 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: unsigned char __thiscall LHScreen::Depth(void) +4 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: unsigned char __thiscall LHScreen::MaxDepth(void) +3 0.00% 100.00%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: void __thiscall LHScriptX<char>::PostFileOpenStuff(class LHOSFile &) +3 0.00% 100.00%
📈 18 improvements in unmatched items
Unit Item Bytes Before After
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript .data +3071 0.00% 98.45%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::ProcessCommand(long, struct LHScriptPramX<char> *) +1427 0.00% 72.55%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::ProcessCommand(long, struct LHScriptPramX<unsigned short> *) +1405 0.00% 71.42%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: long __thiscall LHScriptX<unsigned short>::GetNextWord(unsigned short **, unsigned short *, unsigned char *) +649 0.00% 97.73%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: long __thiscall LHScriptX<char>::GetNextWord(char **, char *, unsigned char *) +529 0.00% 97.36%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::ScanLine(unsigned short *) +462 0.00% 48.39%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::ScanLine(char *) +447 0.00% 47.05%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: bool __thiscall LHScriptX<unsigned short>::LoadOneLine(class LHOSFile &, unsigned short *) +195 0.00% 99.97%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: bool __thiscall LHScriptX<char>::LoadOneLine(class LHOSFile &, char *) +189 0.00% 99.97%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: unsigned short * __thiscall LHScriptCommandX<unsigned short>::GetCommandAsText(unsigned short *) +175 0.00% 80.51%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: char * __thiscall LHScriptCommandX<char>::GetCommandAsText(char *) +173 0.00% 81.04%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<char>::GetConditionValue(char *, long *) +170 0.00% 51.46%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: enum LH_RETURN __thiscall LHScriptX<unsigned short>::GetConditionValue(unsigned short *, long *) +169 0.00% 51.10%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: char __thiscall LHScriptX<unsigned short>::GetConditionState(long, unsigned char, long) +145 0.00% 85.80%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHScript public: char __thiscall LHScriptX<char>::GetConditionState(long, unsigned char, long) +129 0.00% 77.36%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHHeap2 public: static struct UniqueKeyHeap * __cdecl UniqueKeyHeap::Create(unsigned int, unsigned int) +34 75.02% 99.95%
runblack-decrypted/Lionhead/LHLib/ver5.0/LHHeap2 public: static struct Heap * __cdecl Heap::Create(unsigned int, unsigned int) +32 74.91% 99.95%
runblack-decrypted/Lionhead/LHFile/ver3.0/LHOSFile enum LH_FILE_RESULT __cdecl LHFileLength(char const *, unsigned int *) +16 66.23% 73.66%

Implement the LHLib text script engine (BW1W120 0x7E7260-0x7EA0B0) as the
genuine template it was: LHScriptX<T>, LHScriptCommandX<T>, LHScriptPramX<T>
and LHScriptVariable<T>, instantiated for char and wchar_t through the
original's own bodge_to_force_compilation_of_versions(). Static data
(Commands/Conditions/NamesToCondition tables, the Pram buffers and
LastLineParsed) reconstructed verbatim from the binary.

Key build-model findings, all evidence-driven:
- The TU was compiled with inlining disabled (/Ob0) plus /GX: every tiny
  header-inline member (LHScreen::Depth/MaxDepth, LHSpriteList ctor,
  LHStringOps wrappers, makechar/fromchar) exists as a called comdat inside
  the TU's range. Depth/MaxDepth move from LHScreen.cpp into the header.
- The memory-tracking operator new has a matching placement delete
  (declared in LHWin.h); its presence is what gives /GX functions using
  new(__FILE__, __LINE__) on ctor types their EH frames, and its calls are
  the unwind funclets parked at 0x8A7B20+.
- #line pins reproduce the original __FILE__/__LINE__ values
  (C:\Dev\Libs\lionhead\lhlib\VER5.0\LHScript.cpp, lines 312/333/355/737/
  1032/1035), matching both the pushed line immediates and the path string.
- LH_RETURN's real enumerator names (LH_OK/LH_FAIL/LH_ERROR/LH_NO_MEMORY)
  fall out of the script condition tables; the enum moves to LHReturn.h.
- LHStringOps<char>/<wchar_t> CRT wrappers land in LHString.h (name
  fabricated); LHText gains LoadFont and the LHSpriteList ctor/Set.

Result: 29 of 44 target functions at 100.0% instruction match and all 98
data objects (tables, both Pram statics, ~90 string literals) matching.
ScanLine, ProcessCommand and GetConditionValue remain nonmatching with
TODO notes; switch jump-table symbols are capped by diff-tool
representation, not bytes.

The wide string literals required decomp-toolkit v0.0.20 (dtk_tag bumped):
its abs32 reconstruction previously ignored noreloc symbols and invented
relocations inside UTF-16 text whose characters look like in-image
pointers (openblack/decomp-toolkit#2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bwrsandman bwrsandman merged commit 131c777 into main Jul 12, 2026
4 checks passed
@bwrsandman bwrsandman deleted the lhscript branch July 12, 2026 16:30
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