A Gothic 2 Online client module that lets Gothic 2's built-in DirectMusic
engine play .sgt / .sty / .dls segments straight from a .vdf archive,
with no admin rights, no writes to the game install, and no script changes.
Gothic 1 / 2 use Microsoft's DirectMusic runtime for adaptive music
(bar-aligned transitions, mood swaps _Std/_Thr/_Fgt, day/night swaps,
generative variation, chord continuity). Out of the box, DirectMusic doesn't
know anything about Gothic's .vdf packaging — it uses raw CreateFileW
through IDirectMusicLoader8::GetObject, which only sees the real filesystem.
That's why retail Gothic ships music loose on disk under
<install>\_Work\Data\Music\<sub>\…, and why dropping music files inside a
.vdf simply doesn't play.
DirectMusicVDF sidesteps the engine entirely: it stages the contents of
your VDF to a per-user temp directory and patches the IAT slot for
CreateFileW in the small set of modules that actually open music files on
DirectMusic's behalf (primarily ucrtbase.dll — Microsoft's DirectMusic uses
the C runtime's _wfopen rather than calling CreateFileW directly). Any
music open is silently redirected to the staging copy when the basename
matches an indexed VDF entry. The vanilla DirectMusic engine handles
everything above that line, so transitions, mood swaps, and variation keep
working exactly as in singleplayer.
zCZoneMusic / Squirrel: PlayThemeByScript("OR_DAY_STD")
│
zCMusicSys_DirectMusic::LoadThemeByScript (Gothic2.exe + 0xE8C80)
│
│ path.SearchFile(file, DIR_MUSIC) → SetSearchDirectory(...)
▼
IDirectMusicLoader8::GetObject({wszFileName = "OR_Day_Std.sgt"}, ...)
│
▼
dmloader.dll → _wfopen("...\\OR_Day_Std.sgt", ...) (Microsoft DM)
│
▼
ucrtbase.dll → _wsopen_dispatch / _openfile_internal / ...
│
▼
ucrtbase.dll IAT slot → kernel32!CreateFileW ← hook lives here
│
│ our IAT hook:
│ 1. extension is .sgt/.sty/.dls? if no → original CreateFileW
│ 2. basename in filename index? if no → original CreateFileW
│ 3. otherwise call the original CreateFileW with the staged path
▼
Microsoft loader reads the segment + its style + DLS from the staging tree,
DirectMusic plays the theme — transitions / mood / variation handled normally.
Gothic2.exe never appears in the patched-IAT list — it's an ANSI-only 2002
binary that imports CreateFileA but not CreateFileW. The W-form opens for
music come from Microsoft's DirectMusic loader (dmloader.dll) routing
through the C runtime, and that's why ucrtbase.dll is the single most
important module to patch.
On sqmodule_load the module:
- Locates the Gothic 2 install via
GetModuleFileNameand scans<install>\Data\for*.vdf. - Parses each VDF directly (
PSVDSC_V2.00format) and stages every_WORK\DATA\MUSIC\<sub>\*.{sgt,sty,dls}into%TEMP%\DirectMusicVDF\Music\<sub>\, preserving the original layout. We parse the VDF ourselves becausezFILE_VDFS::FindFirst/Vdfs32::vdf_findopenare stubbed in NotR and pop a "Function not used!" dialog when called. - Builds a
lowercase basename → absolute staging pathindex from the staged files. Subdirectory names are taken verbatim from the VDF — there is no hard-coded list of expected subdirs, so any custom subdir name in your VDF works. - Patches the IAT slot for
CreateFileWin a short list of CRT + DirectMusic modules (ucrtbase.dll, legacymsvcrt.dll/msvcr1*.dll,dmloader.dll,dmusic.dll, and the rest of the DM stack). Modules that aren't loaded or don't importCreateFileWare silently skipped — on a modern Win10/11 install this typically lands on 3–5 modules, dominated byucrtbase.dll. If none of the targets match (exotic DirectX layout), the module falls back to scanning every loaded module's IAT.
The redirect is O(1): a one-time extension test plus a hash lookup on the
basename. Files that aren't .sgt/.sty/.dls are not looked up at all.
We patch IAT slots in the modules that actually call CreateFileW for
music, rather than inline-patching kernel32!CreateFileW / kernelbase!CreateFileW
itself:
- We don't modify the prologue of any system DLL, so anti-cheat or anti-virus heuristics that scan for syscall-prolog modifications stay quiet.
- Each hook is a single 4-byte slot swap behind
VirtualProtect. No trampoline, no JMP, no co-existence problems with other hooks that take the same prolog. - We touch a tightly scoped set of importers (the CRT and DirectMusic stack) instead of every caller in the process. The hook fires only on calls that originate from those modules — exactly the ones that handle music.
Picking the right modules is empirically narrow: a one-off
CaptureStackBackTrace inside the hook showed every music open bottoming
out in ucrtbase!CreateFileW, so that's the one slot that must be patched.
The rest of the list (legacy CRTs + DM DLLs) is a belt-and-braces safety net
for other DirectX redistributables.
Higher-level hooks were tried and rejected:
zCMusicSys_DirectMusic::LoadTheme@0x004E8C70— pure stub, never invoked. Hooking it does nothing.zCMusicSys_DirectMusic::LoadThemeByScript@0x004E8C80— fires, but mutatingzoptions->GetDirString(DIR_MUSIC)from inside the hook doesn't affect the path DirectMusic actually opens (the header declareszSTRING&but the binary's behaviour doesn't propagate the change to the loader).zPATH::SearchFile@0x00588270— a vtable trampoline (mov ecx,[ecx+4]; mov eax,[ecx]; jmp [eax+0x50]). The 2-argument overload the engine actually uses for music is at a different address than the 3-argument variant exposed in headers; hooking 0x00588270 catches textures but never music.
The IAT slot for CreateFileW is the single point that always sees the music
open, so that's where the redirect lives.
- Visual Studio 2022 (Desktop C++ workload, x86 toolset)
- CMake ≥ 3.21
- Git (for submodules)
Assuming you are inside repo dir of module
git submodule update --init --recursive
cmake --preset x86-release
cmake --build --preset x86-releaseOutput: build/x86-release/Release/DirectMusicVDF.dll.
sqapi— Squirrel module API
That's it. No MinHook, no Union runtime, no gothic-api headers, no extra
DLLs. Everything the hook needs is Win32 + the IAT walker we ship in
src/IATHook.cpp (~80 LoC).
-
Build (or grab a release) and drop
DirectMusicVDF.dllinto your server's client-module folder. Register it inconfig.xml:<module src="DirectMusicVDF.dll" type="client" />
-
Pack your music into a
.vdfusing Gothic's standard layout. Drop the VDF into<Gothic2 install>\Data\— the game will auto-load it.YourMusic.vdf └── _WORK/ └── DATA/ └── Music/ ├── Orchestra/ │ ├── BAN_DAY_STD.SGT │ ├── _BAN.STY │ ├── __Orchestra.dls │ └── … ├── Dungeon/ │ └── … └── MyCustomMusic/ ← any name works └── …Subdirectory names are not hard-coded; the module reads them from the VDF catalog and reproduces the layout in staging.
-
Make sure Gothic's Daedalus scripts define a
MUSICTHEME_*instance for every theme you want to play, withfile = "<segment>.sgt"pointing at one of the files in your VDF. The instance'sfilefield is the basename DirectMusic will open. -
Connect to the server. On first run you should see the staging tree fill in under
%TEMP%\DirectMusicVDF\Music\and music start playing.
print("active: " + DirectMusicVDF_isInstalled()); // bool
print("status: " + DirectMusicVDF_status()); // human-readable
print("redirects: " + DirectMusicVDF_redirectCount()); // CreateFile redirects served
print("extracted: " + DirectMusicVDF_extractedCount()); // files staged this session
print("staging: " + DirectMusicVDF_stagingDir()); // %TEMP%\DirectMusicVDF\MusicThe module exposes counters through its Squirrel API (see above). If a theme
won't play, check DirectMusicVDF_status() for the install state and
DirectMusicVDF_redirectCount() for whether any CreateFileW redirect has
fired. Zero redirects with music expected means the Daedalus MUSICTHEME_*
instance is pointing at a basename that isn't in any of your indexed VDFs.
- Staging persists. Files extracted into
%TEMP%\DirectMusicVDF\stick around until Windows cleans%TEMP%. If you change the music inside your VDF, delete the temp folder once so the new bytes get re-staged (files are reused when the size matches the VDF entry). - Filename collisions. The index is keyed by basename, so two music files with the same name in different VDF subdirectories will resolve to the last-indexed copy. Rename the asset or keep names unique per VDF.
- VDF must be auto-loaded. Gothic auto-loads every
.vdfit finds in<install>\Data\. If your VDF lives elsewhere or is gated by a mod manager, the parser won't see it.
GPL-2.0.