Divinity II: Developer's Cut (2012)
| Engine | Gamebryo-derived ("Divinity Application", Larian Studios); DX9, PhysX, embedded Lua 5.1 + Osiris scripting |
| Versions tested | Developer's Cut v1.4.700.49, unofficial repack (not Steam/GOG); exe SHA-256 633B8EC683FD7CACE512F40E1943731FA9DA1342DB081E0A5657B6FEEEB9C8BB. Steam/GOG builds untested and likely need address re-finding. |
| Projects | Split-screen foundation for 2 players on one PC (this folder; no separate repo yet) |
| QA level | automated (per-instance profile isolation layer; see QA for exact scope) |
| Last updated | 2026-10-18 |
Where things stand
Nobody can play Divinity II together today. The exe has zero networking code (no
socket/lobby imports or strings) and no prior co-op attempt was found anywhere. This
project is at the foundation stage: a dinput8.dll proxy now injects into the game, and
two instances get fully separated profiles and saves, verified with two real game
instances. There is no co-op gameplay yet. Two instances can run on one PC, but
simultaneous startup is flaky (timing race in the engine's main-menu movie player, a
pre-existing bug, not ours).
Existing work
None found. Research on 2026-10-18: no entry in this repo's directory; GitHub API searches for divinity2 co-op / dv2 unpack returned 0 results; the Larian forum's Divinity 2 technical board has no co-op, multiplayer or mod-loader threads (scene essentially dead since 2012); a binary scan found no multiplayer strings or imports. NexusMods returned HTTP 403 from the research environment, so it was not verified by hand; if a mod exists there it was not seen here. Verdict: build from zero.
How the game works (for co-op)
- Engine: Gamebryo-derived "Divinity Application", 32-bit PE (WOW64 on modern Windows),
image base
0x400000, DX9 + PhysX, VS2008 runtime. - Scripting: statically linked Lua 5.1 VM (C API registration table near VA
0x014C6448) plus Osiris, Larian's behavior scripting (it debuted in this game). Quest/world logic lives in XML triggers and compiled scripts inside.dv2packs; no public mod loader. - Netcode: none at all. No
ws2_32/winsock imports. All co-op netcode must be added. - Single-instance guard: named semaphore
"Div2Lock"(string at VA0x1476B04, guard function retries ~15 s then exits). A second copy can run if the string is renamed in its exe (same length, one occurrence). - Profile/saves: built via
SHGetFolderPathAwithCSIDL_LOCAL_APPDATA(three call sites total;0x1cis the only one that matters) into the machine-wide%LOCALAPPDATA%\Divinity 2\(Profile + Savegames). All copies share it unless redirected. The0x23site builds a regenerable shader cache under ProgramData; the0x05site is an existence probe that never writes. - Injection surface: the exe imports exactly one function from
dinput8.dll(DirectInput8Create) -> adinput8proxy DLL is the cleanest in-process vector. - Debug build (
Divinity2-debug.exe) exposes an F11 console with teleport / god / lua-script-assign commands; useful for future co-op puppet tests.
What we changed
-
dinput8proxy injection (Stage 1,stage1/).- Need: in-process code execution; the game has no mod loader.
- Fix: 32-bit
dinput8.dllthat loads the real systemdinput8.dlland forwardsDirectInput8Create. Export recipe matters: the implementation is a stdcall function and a.defalias (DirectInput8Create = DefDirectInput8Create@20) produces the undecorated name the game imports, with a nativeret 20. Built with llvm-mingw (clang i686); TinyCC was tried and fails at this (stdcall decoration or a broken stack), Zig was unusable from the test network. - Test: module enumeration showed both the proxy and system
dinput8loaded in the running game; the proxy log recorded attach -> load real -> forward; the game survived past the 60 s crash window with no crash event; an isolated non-DirectInput caller got the expectedE_NOINTERFACEand lived, proving stack cleanup is correct.
-
Per-instance profile and savegame redirection (Stage 2,
stage2/).- Bug: all copies share one machine-wide profile; settings clobber each other and autosaves would overwrite each other.
- Root cause: the profile root comes from
SHGetFolderPathA(CSIDL_LOCAL_APPDATA); nothing in the engine supports per-instance paths. - Fix: the Stage 1 proxy gains a MinHook hook on
SHGetFolderPathA(hooked by name, so it survives other exe builds), enabled by theDIV2_INSTANCEenvironment variable: set to a short token, the token becomes an instance root segment and every copy gets its own...\Divinity 2\Profile\andSavegames\. Unset or invalid -> no redirection at all, so normal single-instance play is untouched. The token is validated (length and path characters) so a malformed value cannot escape the base folder. - Test:
test-hook.ps1launches both copies with tokens, hashes the real profile before and after, and scans exit-writes and crash events. Across five game-launch runs the real profile was untouched every time (0 files changed) and exit-writes landed in the per-token redirected profiles. See QA for the coexistence caveat.
-
Second-copy test harness (not a player feature).
- Need: run two copies of a single-instance game for the tests above.
- Fix: a second folder whose
Data/imagesare junctions to the shared data (no 7 GB duplicate), with theDiv2Lockstring renamed in its exe. - Test: both instances reached stable main menus together (owner confirmed two windows); the second copy alone also runs and exits cleanly, proving the copy is healthy.
-
Known game bug documented, not fixed: the main-menu movie player passes a NULL movie object into a setter (fault RVA
0xB1024A, caller is the main-menu movie player). Under dual-instance contention (two DX9 apps + two WMV decoders on one GPU) it either crashes with an unhandled access violation or exits early with code 0. This predates our work and is the reason dual-instance startup is flaky.
QA
- How to run:
stage1/build.ps1andstage2/build.ps1build the proxies (PE32 / export /ret 20checks, SHA-256 printed);stage2/test-hook.ps1runs the dual instance test and asserts: hook target is the realshell32!SHGetFolderPathA, redirection fires for the relevant CSIDL values, per-token profile trees are created, the real profile has zero file changes vs. a pre-run SHA-256 snapshot, and no new crash events appear. - Last suite result (2026-10-18, five game-launch runs): isolation assertions passed in every run (real profile untouched; exit-writes in per-token profiles). Dual-instance coexistence is flaky: 1 simultaneous run hit the known movie crash (WER 1000); 2 staggered runs had the second instance exit early with code 0; 1 staggered run had both instances alive 40 s+; 1 second-copy-alone control run was healthy (exit 0 after 75 s). So: isolation layer = deterministic pass; two healthy simultaneous instances = timing-dependent, not yet reliable.
How to play
Not yet: there is no co-op release. The DLLs and scripts here are test-harness
artifacts, not a player mod. Anyone who wants to reproduce the isolation layer can
build it from source with the build.ps1 scripts (llvm-mingw, i686 target) and the
bundled MinHook source.
Known issues
- Running two instances at once is unreliable: the second instance sometimes exits immediately or hits the main-menu movie crash (see above). Each copy is healthy on its own; the isolation layer itself is deterministic.
- Everything here is tied to one exe build (the addresses above are for SHA-256
633B8EC6...EB9C8BB). A Steam/GOG copy will need the addresses re-found by string, though theSHGetFolderPathAhook itself is build-independent by design. - The tested copy is an unofficial repack, not Steam/GOG. Friends with a store copy may have a different build; version match must be verified before any play-together plan.
Next steps
- Stage 6b: a launch-retry loop and/or a main-menu movie disable/skip to make dual-instance startup reliable.
- Use the Lua C API registration table (near VA
0x014C6448) as a control channel for driving a second character from the injected DLL. - Then the actual co-op sync core, per this repo's
playbook/how-coop-mods-work.md, and window/layout/input routing perplaybook/split-screen.md.