Skip to content

venzonite/DirectMusicVDF

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DirectMusicVDF

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.

Why this exists

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.

How it works

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:

  1. Locates the Gothic 2 install via GetModuleFileName and scans <install>\Data\ for *.vdf.
  2. Parses each VDF directly (PSVDSC_V2.00 format) and stages every _WORK\DATA\MUSIC\<sub>\*.{sgt,sty,dls} into %TEMP%\DirectMusicVDF\Music\<sub>\, preserving the original layout. We parse the VDF ourselves because zFILE_VDFS::FindFirst / Vdfs32::vdf_findopen are stubbed in NotR and pop a "Function not used!" dialog when called.
  3. Builds a lowercase basename → absolute staging path index 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.
  4. Patches the IAT slot for CreateFileW in a short list of CRT + DirectMusic modules (ucrtbase.dll, legacy msvcrt.dll / msvcr1*.dll, dmloader.dll, dmusic.dll, and the rest of the DM stack). Modules that aren't loaded or don't import CreateFileW are silently skipped — on a modern Win10/11 install this typically lands on 3–5 modules, dominated by ucrtbase.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.

Why IAT and not inline patching of kernelbase

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.

Why we redirect at CreateFileW and not in the engine

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 mutating zoptions->GetDirString(DIR_MUSIC) from inside the hook doesn't affect the path DirectMusic actually opens (the header declares zSTRING& 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.

Build

Prerequisites

  • Visual Studio 2022 (Desktop C++ workload, x86 toolset)
  • CMake ≥ 3.21
  • Git (for submodules)

Steps

Assuming you are inside repo dir of module

git submodule update --init --recursive

cmake --preset x86-release
cmake --build --preset x86-release

Output: build/x86-release/Release/DirectMusicVDF.dll.

Dependencies (pulled via submodules)

  • 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).

Install

  1. Build (or grab a release) and drop DirectMusicVDF.dll into your server's client-module folder. Register it in config.xml:

    <module src="DirectMusicVDF.dll" type="client" />
  2. Pack your music into a .vdf using 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.

  3. Make sure Gothic's Daedalus scripts define a MUSICTHEME_* instance for every theme you want to play, with file = "<segment>.sgt" pointing at one of the files in your VDF. The instance's file field is the basename DirectMusic will open.

  4. Connect to the server. On first run you should see the staging tree fill in under %TEMP%\DirectMusicVDF\Music\ and music start playing.

Squirrel API (diagnostics)

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\Music

Diagnostics

The 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.

Known caveats

  • 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 .vdf it finds in <install>\Data\. If your VDF lives elsewhere or is gated by a mod manager, the parser won't see it.

License

GPL-2.0.

About

Client-side lib for Gothic 2 Online which allows to put DirectMusic files into VDF archive.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors