Skip to main content

Dunia.dll — Overview & Symbol Table

Verified via reverse engineering

Static analysis in Ghidra (project reverse/fc2.gpr), driven interactively via the GhidraMCP bridge. This file tracks binary identification and the address table referenced throughout the rest of the engine-internals notes.

Toolchain

Live analysis is done through GhidraMCP (LaurieWired/GhidraMCP): decompiled pseudocode, disassembly, xrefs, imports/exports/strings can be read and renames/comments/types written back directly into the Ghidra DB. Requires Ghidra open with the target program loaded and analyzed; functions must exist as Function objects before they're readable via MCP (raw unanalyzed bytes need a manual Disassemble + Create Function pass in the GUI first — the MCP tool surface has no "create function" primitive).

Two binaries in the same Ghidra project

Dunia.dll — the PC client engine

  • Build: Far Cry 2\bin\Dunia.dll, 20,183,176 bytes — matches Far Cry 2 Steam v1.03 exactly. DVD/GOG/Uplay/1.00–1.02 builds are differently sized and not guaranteed to share these offsets.
  • Ubisoft's Dunia Engine, publicly documented as derived from CryEngine (heavily modified). Also powers Avatar: The Game (2009) — confirmed at the asset level (shared skeleton/rig tooling works across both titles, see .xbm/.xbg), not yet confirmed at the binary level.
  • MSVC 2008-era toolchain (the launcher exe links MSVCR80.dll). A real C++ engine core — RTTI, vtables, and class hierarchies throughout, unlike the launcher's flat C-style code. Keep Ghidra's RTTI Analyzer and Demangler on.
  • Confirmed exports (recovered from the launcher's import table — see launcher exe): RunGame(HINSTANCE*, const char*), RegisterGameFunctionProvider(void*), AddFunctionCB(void* fn, const char* name). Known-good entry points for navigating this DLL, rather than starting from DllMain.
  • Embeds a real Lua interpreter, not just native C++. Statically compiled in (no lua51.dll import) — confirmed via strings: "Lua 4.1 (alpha)", "CLuaResource"/"LuaGlobals"/"LuaState", "StopSoundMixingFromLua"/"StartSoundMixingFromLua"/"PlayMusicFromLua", "SCRIPTS\MissionTools.lua", and the interpreter's own error strings ("value for 'lua_getinfo' is not a function"). "Lua 4.1 (alpha)" is a rare, semi-official PUC-Rio branch that briefly existed between the released Lua 4.0 and 5.0 — a known fingerprint of CryEngine's historical bundled Lua fork, and the first binary-level confirmation of the CryEngine-derivation claim. See the Lua API surface for the full exposed API map.
  • Also links licensed Havok middleware for physics/animation, confirmed via string: "Havok Physics evaluation key has expired or is invalid...Please contact Havok.com..." (and an equivalent Havok Animation string) — not a from-scratch physics/animation system.
    Community-reported

    A specific version surfaced independently (Discord, 2022-12-10): Havok 5.5.0 r1. Not cross-checked against the binary by disassembly, but consistent with the evaluation-key string and a useful starting point for .hkx/physics RE.

  • Architecture picture: native C++ core for performance-critical systems (weapons, AI, entities) + licensed Havok for physics/animation + a genuinely embedded Lua layer scoped to a narrower band of designer-tunable behavior (mission sequencing, reinforcement/respawn timers, some sound/music triggers) + external .fcb/XML data files for stat tuning. Not "everything hardcoded," but nowhere near a fully-scripted authoring model either. Three separate extension mechanisms coexist in this one binary: the Lua layer above; the pure-native, CRC32-keyed function-callback registry (unrelated to Lua); and a large flat C export surface (~338 FCE_*-prefixed functions) the stock map editor drives directly via P/Invoke, not scripting at all — see the editor-facing API surface.

FarCry2_server — the Linux dedicated-server build

Discovered while researching the savegame format — the Ghidra project also contains a third program, named FarCry2_server in its project metadata. It's the Linux dedicated-server binary: an ELF (.dynamic/.got.plt, load base ~0x08048000), POSIX/glibc imports (pthread_create, mkdir, gethostbyname, ...), GCC/Itanium-mangled C++ symbols (_ZN14CPersistenceDB...), and — unlike Dunia.dll — largely unstripped, with a real .symtab/.strtab giving genuine class/method names for shared engine code (persistence, save/load, screenshot/thumbnail, and game-file-list systems are all present and linked in, even though a headless server never itself writes a player .sav).

Any address in this project starting 0x08/0x09/0x0a belongs to FarCry2_server, not Dunia.dll — every other page in this note set uses Dunia.dll's 0x10xxxxxx PC load addresses unless it says otherwise. Its better symbol coverage is worth cross-referencing against Dunia.dll going forward: it can name a PC-side function whose Windows binary only has a bare FUN_/DAT_ address.

Not a stripped-down server SKU — the full game with only the renderer compiled out. SceneRendererFacade::HasRenderer() is hardcoded to return 0 (a compile-time constant, not a runtime -dedicated check) — the rendering backend itself isn't linked in. Everything else looks fully present and self-registered: CGameModeManager's constructor builds a name-keyed factory table of CGameModeDesc*, and CFCXGameModeSingle (single-player) sits in it as a real linked class right alongside every multiplayer mode, CFCXGameModeEditor, and CFCXGameModeBenchmark — the self-registration pattern (see RegisterCreatorFuncPreMain/GetPreMainClassInfoVector) means the linker can't dead-strip an unused mode even on a dedicated-server build. Mission/campaign classes (CBaseMission, CObjective) and debug exports (SelectStoryMission, SelectLibraryMission, LoadGame/SaveGame) are linked in too. Practical effect: physics, entities, AI, animation, Domino mission scripting, sound-object model, and save/load are genuinely shared source with the PC single-player build and safe to trace here — only the actual rendering pipeline (shaders, D3D state, the real SceneRendererFacade implementation) has to fall back to Dunia.dll instead, since it was never compiled into this binary at all. See Engine Architecture for the traced main loop.

Community-reported

The Linux dedicated server was reportedly shipped as an accidental debug build (Discord, 2022-07-13) — consistent with the unstripped .symtab/.strtab confirmed above. Separately, a community member ("bajuh") reported independently reverse-engineering Dunia.dll with Ghidra, cross-referencing this same Linux server binary, specifically to build an FCB-editing tool (Discord, 2026-07-17) — a possible prior-art/collaboration lead if that tool or writeup surfaces publicly.

Named symbols (Dunia.dll, Steam v1.03)

AddressNameRole
0x10006510RunGameEntry point called from the launcher's WinMain; command-line dispatch + main loop
0x10001cc0RegisterGameFunctionProviderStashes the launcher's callback pointer into g_pGameFunctionProvider
0x10001cd0AddFunctionCBExport wrapper; real logic is FunctionRegistry_Insert
0x10004900InitDuniaEngineMain engine init, called from RunGame; likely where g_pFunctionRegistry gets constructed (unconfirmed)
0x10fd42c8g_pGameFunctionProviderGlobal: holds the launcher's RegisterDebugCommands pointer between registration and invocation in RunGame
0x10fd4280g_hGameWindowGlobal: main window handle, passed to DestroyWindow each RunGame loop iteration
0x1160629cg_pFunctionRegistryGlobal: the one engine-wide named-function registry singleton — confirmed this for both FunctionRegistry_Insert and FunctionRegistry_Invoke
0x10299430FunctionRegistry_Insert__thiscall, single caller (AddFunctionCB). Find-or-insert (name, fn)
0x102993b0FunctionRegistry_Invoke__thiscall, ~17 callers engine-wide. Finds a name and calls the stored fn ptr with 2 args, silent no-op if not found
0x10229400CRC32_HashTextbook CRC-32: reflected algorithm, 0xffffffff seed, 256-entry lookup table (DAT_10f95388), final complement. Generic, 90+ callers engine-wide
0x10228380GetNameHashWrapper: writes CRC32(name) into an output slot, 0xffffffff sentinel for null/empty
0x102487d0ArchiveEntry_OpenAtOffsetOpens a sub-stream at an entry's recorded offset; hands off to ArchiveEntry_Decompress if compressed
0x102486d0ArchiveEntry_Decompress3-way dispatch on the entry's compression scheme — see archives
0x10258d60ArchiveEntry_DecompressLzo1xScheme-1 handler; wraps Lzo1x_Decompress
0x1025a620Lzo1x_DecompressThe LZO1X token decoder — matches JackAll's Lzo1x.cs structurally
0x10258d00ArchiveEntry_DecompressZlibScheme-2 handler; wraps Zlib_DecompressChunked
0x1025d1c0Zlib_DecompressChunkedCustom blocked container wrapping raw-DEFLATE per block — not a plain deflate/zlib stream
0x1025d110Zlib_InflateRawBlockInflates one block via zlib_inflateInit2_(-15)/zlib_inflate(Z_FINISH)/zlib_inflateEnd — genuine zlib 1.2.3, raw-DEFLATE mode
0x10258e30zlib_inflateInit2_windowBits=-15, version "1.2.3", stream_size=0x38
0x10259030zlib_inflateCalled with flush=4 (Z_FINISH)
0x10d75340zlib_inflateEndPaired with the two above
0x10235080Fcb_ReadHeaderValidates an .fcb buffer's magic/version/flags, calls Fcb_AllocateTree — see .fcb
0x10234fc0Fcb_AllocateTreeAllocates the output object-tree pool, kicks off Fcb_ParseObject
0x10234d60Fcb_ParseObjectThe recursive .fcb object-tree parser
0x10234260Fcb_ReadTypeHashReads an object's TypeHash — a plain u32, or (flags bit 0) a hashed string
0x10246200Fcb_MagicConstantReturns 0x4643626e ("FCbn")
0x10246210Fcb_SupportedVersionConstantReturns 2, the only accepted .fcb version
0x10624230Spk_GetFileNameFromSoundIdBuilds a .spk filename from a sound id — see .spk
0x106242f0Spk_BuildSoundFileNameStringWraps the filename into a CryString-like object
0x1062c180Spk_GetSoundResourceFromIdResolves a sound id: builds the filename, opens via VFS_ResolvePath, reads, dispatches to Spk_ParseContainer
0x106243d0Spk_SoundResourceCtorSets the sound-resource vtable pointer before the dispatch above
0x10624b80Spk_ParseContainerThe .spk container parser — magic/count/id-table/variable-record walk
0x10a425b0Spk_CreateSoundObjectFromRecordGeneric resource-manager wrapper invoked per .spk record
0x10a3f490Spk_InitRecordDescriptorStores {id, dataPtr, size, extra} — the payload is registered opaquely at load time
0x10a3fb30Spk_GetOrLoadSoundObjectConsumer of the descriptor: inline data if present, else falls back to a standalone file
0x10a3fb00Spk_ResolveSoundObjectDataDispatcher: inline vs. standalone-file-load
0x10a3f9f0Spk_LoadStandaloneSoundFileLoads a sound object from its own standalone .sbao/.bao file (the "streamed" path)
0x10a3f4b0Spk_BuildSbaoOrBaoFileNamesprintf("%08x.sbao"/"%08x.bao", id) — confirms the shared id-namespace between .spk records and standalone files
0x10a3f960Spk_ValidateAndDispatchSoundObjectValidates the 40-byte descriptor's minimum size, dispatches by type
0x10a3f820Spk_DispatchSoundObjectByTypeSwitches on the descriptor's type tag (offset +0x20); 0x50000000 ("streamed") rejected outright for inline loading
0x10a3f280Spk_LoadSimpleFixed68ObjectType 0x10000000: fixed 68-byte sub-header, plain copy
0x10a3f310Spk_LoadTransformedFixed128ObjectType 0x20000000: fixed 128-byte sub-header, then Spk_TransformFixed128Payload
0x10a3f3c0Spk_LoadFlatCopyObjectType 0x30000000: no sub-header, whole remainder copied verbatim
0x10a3f690Spk_LoadLargeFixed256ObjectType 0x40000000: fixed 256-byte sub-header, plain copy
0x10a3f410Spk_LoadCountPrefixedListObjectType 0x60000000: a count-prefixed list of references, not a single sound
0x10a3f610Spk_LoadSelfReferentialObjectType 0x70000000: copies then fixes up an internal offset into an absolute pointer
0x10a51690Spk_ProcessCountPrefixedListSingle caller (Spk_LoadCountPrefixedListObject)
0x10a51750Spk_TransformFixed128PayloadSingle caller (Spk_LoadTransformedFixed128Object)

Deliberately left un-renamed — understood algorithmically, but each is a generic, heavily-shared engine utility with 90+ unrelated call sites, not something specific to any one subsystem:

  • FUN_1057a030 — generic map/tree find, reused by dozens of unrelated maps.
  • FUN_10769180 — generic map insert, same story.
  • FUN_1066b660 — the diamond-pickup/reward handler that calls FunctionRegistry_Invoke("AddDiamond", ...). Its own class isn't pinned down (heavy vtable/ property-reflection use) — only its role in the function-registry chain is confirmed.