Builds on Visual C++ 2008 build environment on Linux using Wine — read that first for the base Wine prefix, the toolchain archive, and the raw
cl.exe/CMake+Ninja traps. This page covers the additional setup needed to build the real ryzomcore repo's 3ds Max plugin (nel/tools/3d/plugin_maxand friends), not just standalone probes.
Verified 2026-07-07 (plugin-only config): 1008/1008 objects, all plugin binaries built. Re-verified and extended 2026-08-05 against a fresh WinXP-sourced toolchain: three full configurations build green under this setup — asset build tools + MFC tools + Max 2010 plugins (D3D/DSound/XAudio2/OpenAL drivers all ON,
mapext198m3.dlmincluded), snowballs client + server + NeL samples, and the ryzom client — with SxS manifests embedded through the traditional mt flow. The MFC GUI tools (world_editor,georges,branch_patcher,tile_edit, ...) link for the first time under Wine (unicode MFC apps need an explicit/ENTRY:wWinMainCRTStartup;NL_DEFAULT_PROPSadds it via theNL_MFC_UNICODE_APPtarget property).
Most of this page's manual setup is now automated:
tool/wine_vs2008/setup.shin the ryzomcore repo creates thedosdeviceslinks, theVC_DIRshadow directory, the lowercase-alias symlink farms, and the Max SDK header patch in one idempotent pass, andtool/wine_vs2008/toolchain.cmakeis the maintained toolchain file. The sections below document what those scripts do and why. SetNL_WINE_VS2008_PREFIXto your prefix path before running any of it.
Beyond the base VS2008 toolchain archive from the companion page, extracted into the same prefix's drive_c:
Program Files/Autodesk/3ds Max 2010 SDK/ (top-level archive folder name matches). Find3dsMaxSDK.cmake looks for maxsdk/include/maxversion.h under $ENV{PROGRAMFILES}/Autodesk/3ds Max <year> SDK/maxsdk.2019q4_external_v90_x86, matching nel_plugin_max_9_x86.bat/_2010_x86.bat/_2012_x86.bat's redist paths) → C:\2019q4_external_v90_x86 directly (not under Program Files) — one subfolder per library (zlib/include, zlib/lib, boost/include, ...), not a single unified tree.Program Files/Microsoft DirectX SDK (June 2010)/ — required unconditionally for any MSVC configure of this repo (see "Genuine upstream bugs" below), regardless of whether a Direct3D driver is actually being built.With everything extracted, one script prepares the prefix (idempotent — safe to rerun after adding archives):
export NL_WINE_VS2008_PREFIX=~/.local/share/wineprefixes/vs2008
/path/to/ryzomcore/tool/wine_vs2008/setup.sh
(NL_WINE_VS2008_EXTERNAL overrides the externals location if they live outside the prefix's drive_c.)
Vendor SDK headers #include each other with inconsistent casing (e.g. one Max SDK header does #include "assetmanagement/AssetUser.h", another does #include "assetmanagement/assetUser.h") — harmless on a case-insensitive Windows filesystem, but on a case-sensitive Linux one this defeats #pragma once's file-identity tracking: the two casings resolve to two different paths, so the header gets parsed twice, and the second pass hits error C2011: class type redefinition. Same root cause as an ordinary missing-file case mismatch (e.g. ComCtl32.lib vs. requested ComCtl32.lib — actually stored as ComCtl32.Lib; BIPEXP.H vs. requested bipexp.h), just surfacing as a redefinition instead of a not-found.
ciopfs (a case-insensitive FUSE overlay) looked like the systematic fix, but turned out broken on a stock Ubuntu 24.04 box — an isolated test showed it doesn't list any subdirectory at all, even space-free ones:
mkdir -p /tmp/t/{src/sub,mnt} && ciopfs /tmp/t/src /tmp/t/mnt && ls /tmp/t/mnt # empty - real bug, not a config issue
Instead, generate lowercase-alias symlinks across every relevant tree in one pass — the script is tool/wine_vs2008/lowercase_alias.py in the repo (reproduced here for reference), and setup.sh runs it over all the trees listed below:
#!/usr/bin/env python3
# For every file/dir under ROOT whose name isn't already all-lowercase,
# add a lowercase-named symlink alongside it (skip if a same-named entry
# already exists - real file or prior symlink).
import os, sys
root = sys.argv[1]
created = 0
skipped_collision = 0
for dirpath, dirnames, filenames in os.walk(root):
for name in list(dirnames) + filenames:
lower = name.lower()
if name == lower:
continue
full = os.path.join(dirpath, name)
lower_full = os.path.join(dirpath, lower)
if os.path.lexists(lower_full):
if not os.path.islink(lower_full):
skipped_collision += 1
continue
os.symlink(name, lower_full)
created += 1
print(f"created={created} skipped_collision={skipped_collision}")
Run against every include/lib tree that vendor code touches (roughly 2000 symlinks total on a fresh prefix, zero collisions observed):
PFX=~/.local/share/wineprefixes/vs2008/drive_c
for d in \
"$PFX/Program Files/Autodesk/3ds Max 2010 SDK/maxsdk/include" \
"$PFX/Program Files/Autodesk/3ds Max 2010 SDK/maxsdk/lib" \
"$PFX/Program Files/Microsoft Visual Studio 9.0/VC/include" \
"$PFX/Program Files/Microsoft Visual Studio 9.0/VC/lib" \
"$PFX/Program Files/Microsoft Visual Studio 9.0/VC/atlmfc" \
"$PFX/Program Files/Microsoft SDKs/Windows/v6.0A/Include" \
"$PFX/Program Files/Microsoft SDKs/Windows/v6.0A/Lib" \
"$PFX/Program Files/Microsoft DirectX SDK (June 2010)/Include" \
"$PFX/Program Files/Microsoft DirectX SDK (June 2010)/Lib" \
"$PFX/2019q4_external_v90_x86"
do
python3 lowercase_alias.py "$d"
done
This is a superset of (and replaces) individually-discovered symlinks like ComCtl32.lib/bipexp.h — run it once up front on a fresh prefix rather than reactively chasing each mismatch as it surfaces during a build.
VC_DIR shadow directoryCMakeModules/FindMSVC.cmake derives VC_DIR by regexing /bin/.+ off CMAKE_C_COMPILER's path, assuming it points directly at a real .../VC/bin/cl.exe — breaks since our compiler is a wrapper script, not literally inside a VC/bin/ tree. setup.sh builds a fake shadow directory (default <prefix>-msvc-shadow, override with NL_WINE_VS2008_SHADOW) ending in the right shape, with the repo wrapper scripts standing in for the real binaries and everything else symlinked through to the real toolchain install; for reference:
SHADOW=~/.local/share/wineprefixes/vs2008-msvc-shadow/VC
PFX="$HOME/.local/share/wineprefixes/vs2008/drive_c/Program Files/Microsoft Visual Studio 9.0/VC"
mkdir -p "$SHADOW/bin"
ln -sfn "$PFX/include" "$SHADOW/include"
ln -sfn "$PFX/lib" "$SHADOW/lib"
ln -sfn "$PFX/atlmfc" "$SHADOW/atlmfc"
ln -sfn "$PFX/redist" "$SHADOW/redist"
cp tool/wine_vs2008/winecl-env "$SHADOW/bin/winecl-env" # winecl-{cc,link,lib} source this by dirname($0), needs a local copy
cp tool/wine_vs2008/winecl-cc "$SHADOW/bin/cl.exe"
cp tool/wine_vs2008/winecl-link "$SHADOW/bin/link.exe"
cp tool/wine_vs2008/winecl-lib "$SHADOW/bin/lib.exe"
chmod +x "$SHADOW/bin/cl.exe" "$SHADOW/bin/link.exe" "$SHADOW/bin/lib.exe"
CMAKE_MT/CMAKE_RC_COMPILER stay pointed at the plain tool/wine_vs2008/winecl-{mt,rc} wrappers — only CMAKE_C_COMPILER/CMAKE_CXX_COMPILER/CMAKE_LINKER/CMAKE_AR need to live under the shadow, since only FindMSVC.cmake's regex cares.
Any TU that reaches both maxscript's maxsdk/include/maxscrpt/value.h and VC's ivec.h dies with C2365: 'empty' : redefinition — the SDK declares a global extern ScripterExport Empty empty; that collides with the intrinsics header's global empty(). The historically-working SDK copy had that extern commented out (an invisible local vendor patch, rediscovered the hard way against a pristine SDK). setup.sh applies the same one-line comment-out idempotently; plugin code never references the global.
Set for every cmake configure invocation (not needed at build time — these feed CMake's own find_path/find_library calls on the Linux host, not the compiler):
export NL_WINE_VS2008_PREFIX=~/.local/share/wineprefixes/vs2008 # or wherever the prefix lives
export PROGRAMFILES="$NL_WINE_VS2008_PREFIX/drive_c/Program Files"
export WINSDK_DIR="$PROGRAMFILES/Microsoft SDKs/Windows/v6.0A"
export DXSDK_DIR="$PROGRAMFILES/Microsoft DirectX SDK (June 2010)"
NL_WINE_VS2008_PREFIX — read by the repo wrapper scripts and toolchain file (default $HOME/toolchain_v90_prefix). NL_WINE_VS2008_SHADOW and NL_WINE_VS2008_EXTERNAL similarly override the shadow-dir and externals locations.PROGRAMFILES — Find3dsMaxSDK.cmake's hint path.WINSDK_DIR — read by FindWindowsSDK.cmake's USE_CURRENT_WINSDK() macro (see bugs below). Must be a Linux path (not C:\...) since these find_path calls run natively on the Linux host, not through Wine. Needed again on every reconfigure — the cached value doesn't survive cmake --regenerate-during-build.EXTERNAL_PATH is no longer needed: the legacy unified-externals probe (FindExternal.cmake) is no longer hard-required when WITH_EXTERNAL=OFF (it used to be, unconditionally on WIN32), and per-library resolution comes from CMAKE_PREFIX_PATH in the toolchain file. Beware what that probe does when it does run against an empty prefix path: it matches any directory containing include/zlib.h — including the Linux host's own /usr — and prepends it to the global include/library paths, poisoning every later find with host headers that will never have a matching Windows .lib (the toolchain file fails fast when the externals glob is empty for exactly this reason).tool/wine_vs2008/toolchain.cmake in the repo — extends the base setup from the companion page with everything this build needs. What it sets and why (read the file itself for the full commentary):
TARGET_CPU x86 (forced): CMakeModules/nel.cmake uses its own TARGET_CPU convention, not CMAKE_SYSTEM_PROCESSOR — unset, it falls back to the build machine's x86_64 and points every Find module (MAXSDK, DXSDK, MFC) at 64-bit library directories.VC_DIR shadow; CMAKE_MT/CMAKE_RC_COMPILER at the plain wrappers.CMAKE_FIND_ROOT_PATH_MODE_* = NEVER (the ONLY-with-empty-root trap — see the companion page)./Z7 + /MD (the /Zi-breaks-under-Wine and silent-/MDd-failure traps).CMAKE_PREFIX_PATH from a glob over the per-library externals tree (<prefix>/drive_c/2019q4_external_v90_x86/* by default), with a fail-fast error when the glob is empty.NL_EMBED_SXS_MANIFEST_MT ON — the traditional manifest flow (see the companion page's Manifest embedding section; on CMake 3.30+ CMake's own vs_link runs the mt pass, on older CMake a POST_BUILD step does).CMAKE_DISABLE_FIND_PACKAGE_MySQL — the externals carry no mariadb client for VC9, and with find-root NEVER the stock FindMySQL happily finds the Linux host's own mariadb, putting a nonexistent mariadb.lib on MSVC link lines.Matching tool/quick_start/configure_targets.py's real PluginMax spec branch (NeLSpecPluginMax), for an MSVC/x86/non-Hunter target — read the script itself (GenerateCMakeOptions) rather than trust a summary; it's the single source of truth for what a real build actually passes, and doesn't match what the option names suggest at a glance (e.g. WITH_TOOLS, the root-level "Build Tools" option, is not part of this spec at all — WITH_NEL_MAXPLUGIN=ON alone already satisfies nel/CMakeLists.txt's IF(WITH_NEL_TOOLS OR WITH_NEL_MAXPLUGIN) gate):
cmake -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=/path/to/ryzomcore/tool/wine_vs2008/toolchain.cmake \
-DWITH_SSE2=OFF -DWITH_SSE3=OFF \
-DWITH_EXTERNAL=OFF -DWITH_STLPORT=OFF -DWITH_NEL_TESTS=OFF -DWITH_TESTING=OFF -DWITH_STATIC=ON \
-DWITH_FFMPEG=OFF -DWITH_MSQUIC=OFF -DWITH_QT=OFF -DWITH_QT5=OFF -DWITH_QT6=OFF \
-DWITH_STATIC_LIBXML2=OFF -DWITH_STATIC_CURL=OFF -DCURL_NO_CURL_CMAKE=ON \
-DFINAL_VERSION=ON \
-DWITH_LUA51=OFF -DWITH_LUA53=ON \
-DWITH_MFC=ON \
-DWITH_DRIVER_DIRECT3D=OFF -DWITH_DRIVER_DSOUND=OFF -DWITH_DRIVER_XAUDIO2=OFF \
-DWITH_DRIVER_OPENGL=ON -DWITH_DRIVER_OPENGL3=ON -DWITH_DRIVER_OPENAL=OFF \
-DWITH_RYZOM_CLIENT=OFF -DWITH_RYZOM_SERVER=OFF \
-DWITH_NEL_TOOLS=OFF -DWITH_RYZOM_TOOLS=OFF -DWITH_ASSIMP=OFF \
-DWITH_NEL_SAMPLES=OFF -DWITH_SNOWBALLS=OFF -DWITH_NELNS=OFF \
-DWITH_RYZOM=OFF -DWITH_NEL_MAXPLUGIN=ON \
-DMAXSDK_DIR="$HOME/.local/share/wineprefixes/vs2008/drive_c/Program Files/Autodesk/3ds Max 2010 SDK/maxsdk" \
/path/to/ryzomcore
Notes:
WITH_DRIVER_DIRECT3D/WITH_DRIVER_DSOUND/WITH_DRIVER_XAUDIO2/WITH_DRIVER_OPENAL can all be ON (matching the real spec; the OpenAL target_link_libraries signature bug that used to force it off is fixed). The DirectX SDK June 2010 in the prefix covers D3D/DSound/XAudio2.-DWITH_RYZOM=ON -DWITH_RYZOM_TOOLS=ON -DWITH_NEL_TOOLS=ON -DWITH_MFC=ON for the full asset-tools + MFC-tools + plugins build, or -DWITH_NEL_SAMPLES=ON -DWITH_SNOWBALLS=ON / -DWITH_RYZOM=ON -DWITH_RYZOM_CLIENT=ON -DWITH_RYZOM_PATCH=ON -DWITH_LOGIC=OFF for the snowballs and ryzom-client configurations. WITH_NELNS stays OFF — nelns genuinely requires C++14 (cxx_return_type_deduction) and is out of VC9 scope. zone_painter and the GUI sample's atlas step self-exclude on pre-VS2010 toolchains.WITH_STATIC_CURL isn't an actual option anywhere in this codebase's CMake — CMake warns it's unused. Harmless; kept for fidelity with the real script.tool/wine_vs2008/package_max_plugin.sh <build-bin-dir> <externals-root> <out-dir> assembles the plugin set in the historical install arrangement (root support DLLs, renamed plugins/, scripts/ + scripts/startup/, macroscripts/) — the script mirror of tool/quick_start/win32/install/redist/nel_plugin_max_2010_x86.bat, minus code signing.Two real, pre-existing defects in CMakeModules/FindWindowsSDK.cmake's USE_CURRENT_WINSDK() macro, surfaced only because this is (as far as anyone can tell) the first time this exact code path has run without a Windows registry to short-circuit past it — on real Windows, VS2008's SDK registers in the registry, so DETECT_WINSDK_VERSION_HELPER succeeds and this macro's fallback logic never runs. Both fixed and committed to the repo:
Bug 1 — SET(WINSDK_DIR "") defeats the subsequent find_path(). A plain (non-cache) set() creates a normal variable that shadows the cache entry of the same name; find_path() then treats the mere existence of that normal variable (even empty) as "already resolved" and silently skips its own search — confirmed in complete isolation, independent of any cross-compiling or root-path setting:
# Reproduces with or without a pre-existing valid CACHE value for MYVAR:
set(MYVAR "")
find_path(MYVAR Windows.h HINTS /real/path/with/Include)
# MYVAR stays empty
Fix: UNSET(WINSDK_DIR CACHE) instead of SET(WINSDK_DIR "").
Bug 2 — the find_path target was wrong, once bug 1 stopped masking it. FIND_PATH(WINSDK_DIR Windows.h HINTS ${WINSDKENV_DIR}/Include/um ${WINSDKENV_DIR}/Include) searches for a bare filename with the hints already pointing inside Include/ — find_path returns the directory containing the found file, so WINSDK_DIR ends up as .../Include itself, not the SDK root the rest of the macro (and everything downstream) expects (which then appends /Include again, doubling the path). Fix: search for the nested path Include/Windows.h from the candidate root instead — the same pattern Find3dsMaxSDK.cmake (include/maxversion.h) and FindDirectXSDK.cmake (Include/dxsdkver.h) already use correctly:
FIND_PATH(WINSDK_DIR Include/Windows.h HINTS ${WINSDKENV_DIR})
IF(NOT WINSDK_DIR)
FIND_PATH(WINSDK_DIR Include/um/Windows.h HINTS ${WINSDKENV_DIR}) # newer Windows Kits layout
ENDIF()
A third apparent blocker — error C2719: formal parameter with __declspec(align('16')) won't be aligned on std::vector<CBone> (VS2008's STL takes resize()'s fill value by value, which 32-bit MSVC can't do for an SSE-aligned type; confirmed even a bare default-constructed std::vector<AlignedType> member triggers it, no specific call site needed) — turned out not to be a bug at all: tool/quick_start/configure_targets.py already forces -DWITH_SSE2=OFF -DWITH_SSE3=OFF for any x86 target, which makes NL_ALIGN_SSE2 a no-op and sidesteps the whole class of issue. The fix was matching that existing convention, not patching NeL's alignment macros or the vendor STL.
The same toolchain (all of the setup above: archives, symlink farm, VC_DIR shadow, environment variables, toolchain file) also builds the standalone pipeline_max export tools — the .max-processing reimplementation that runs the Max export pipeline without 3ds Max. This became possible once those tools stopped depending on libgsf (a Linux-only OLE reader) and got a native cross-platform OLE backend. Building them under VS2008 gives an x87 / no-SSE2 reference matching the Max 2010 that produced the corpus's reference exports, which is the instrument for chasing the remaining float-precision residual (see the design doc's §2b VS2008 x87 reference build for the porting details, the two latent bugs it surfaced — an empty-std::list dereference caught by MSVC's checked iterators, and a SuperClassId static-initialization-order fiasco — and the measured precision improvement).
Use a separate build dir next to the plugin one (both are full NeL builds under Wine — keep them; a clean rebuild is ~500 objects). The configure differs from the plugin spec by turning the plugin off and the tools + 3D + native OLE on; no Max SDK is needed:
cmake -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=/path/to/ryzomcore/tool/wine_vs2008/toolchain.cmake \
-DWITH_SSE2=OFF -DWITH_SSE3=OFF \
-DWITH_EXTERNAL=OFF -DWITH_STLPORT=OFF -DWITH_NEL_TESTS=OFF -DWITH_TESTING=OFF -DWITH_STATIC=ON \
-DWITH_FFMPEG=OFF -DWITH_MSQUIC=OFF -DWITH_QT=OFF -DWITH_QT5=OFF -DWITH_QT6=OFF \
-DWITH_STATIC_LIBXML2=OFF -DWITH_STATIC_CURL=OFF -DCURL_NO_CURL_CMAKE=ON \
-DFINAL_VERSION=ON -DWITH_LUA51=OFF -DWITH_LUA53=ON -DWITH_MFC=ON \
-DWITH_DRIVER_DIRECT3D=OFF -DWITH_DRIVER_DSOUND=OFF -DWITH_DRIVER_XAUDIO2=OFF \
-DWITH_DRIVER_OPENGL=ON -DWITH_DRIVER_OPENGL3=ON -DWITH_DRIVER_OPENAL=OFF \
-DWITH_RYZOM_CLIENT=OFF -DWITH_RYZOM_SERVER=OFF -DWITH_RYZOM_TOOLS=OFF -DWITH_ASSIMP=OFF \
-DWITH_NEL_SAMPLES=OFF -DWITH_SNOWBALLS=OFF -DWITH_NELNS=OFF -DWITH_RYZOM=OFF \
-DWITH_NEL_MAXPLUGIN=OFF -DWITH_NEL_TOOLS=ON -DWITH_3D=ON \
-DWITH_PIPELINE_NATIVE_OLE=ON -DWITH_LIBGSF=OFF \
/path/to/ryzomcore
Targets are .exe (ninja pipeline_max_export_zone.exe, …_export_shape.exe, pipeline_max_corpus_test.exe, …). Running an exe needs the release external DLLs (libxml2/jpeg62/libpng16/zlib/freetype/…) from 2019q4_external_v90_x86/*/bin/ on its search path (copy them next to the exe) and Windows-form path arguments (winepath -w); a thin per-tool wrapper that does both lets the x64 corpus drivers (zone_corpus.py, etc.) drive the VS2008 binaries via --bin.