Files
DarkflameServer/docs/UgcServer.md
Aaron Kimbrell 5828fe99c7 docs: describe the UGC server on the dashboard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:06 -05:00

11 KiB

UGC Server

The UGC server turns what players build into the files the game client downloads for them: an optimized mesh (.nif) and a 128x128 icon (.dds) for every brick-built model, and icons for modular builds (cars and rockets). It also serves those files, and the models' LXFML, to the client over HTTP, so the dashboard never has to answer the client's UGC traffic. It generates no physics (.hkx).

It is a separate process like auth, chat and the dashboard: the master starts it when enable_ugc_server=1 (in masterconfig.ini, default 0) and starts it again when its link to the master drops.

What the client asks for

The client (1.10.64) has two ways of fetching UGC files, picked by UGCUSE3DSERVICES in its boot.cfg. Both use UGCSERVERIP, UGCSERVERPORT and UGCSERVERDIR (default: the patch server's directory plus /UserBrickModels). Resource types are 0 LXFML, 1 NIF, 2 HKX, 3 DDS.

  • UGCUSE3DSERVICES=7:1 (what this server supports). The folder becomes UGCSERVERDIR/UGCC<datacenter>/ (the datacenter id is DATACENTERID in boot.cfg) and files are
    • <folder><type folder><datacenter><blueprint id><.lxfml|.nif|.hkx|.dds>.gz: the file, gzip compressed. The type folder is 3DOPTIMIZED/ for NIF and HKX, IMAGE128DDS/ for DDS and nothing for LXFML.
    • the same name with .checksum instead of .gz: <Checksum><MD5>32 hex digits</MD5><Filesize>n</Filesize></Checksum>, the MD5 and size of the uncompressed file. The client checks the download against it.
    • HTTP 408 makes the client try again later; other errors are logged as failed downloads.
  • Without 3D services the client asks the world server for each file's MD5 (REQUEST_UGC_MANIFEST_INFO, answered with UGC_MANIFEST_RESPONSE) and downloads BrickModels/UserMade/<id % 1000, 3 digits>/<id, 20 digits><ext>.sd0. That needs the world to answer the manifest requests, which it doesn't yet, so it is not supported.

A model's render component uses the downloaded NIF when the model's spawn data has renderUserGen=1 with its blueprintid; NotifyClientUGCModelReady (game message 909) makes the client fetch the blueprint's NIF and HKX again. Worlds don't send either yet (they still send every model's LXFML when a property loads and the client builds the models itself); see "Not done yet".

Client settings for a server at 203.0.113.5 with the default port:

UGCUSE3DSERVICES=7:1,
UGCSERVERIP=0:203.0.113.5,
UGCSERVERPORT=1:2008,
UGCSERVERDIR=0:/ugc,
DATACENTERID=1:150,

Processing

Player models (ugc rows):

  1. The LXFML is read from ugc.lxfml (an sd0 stream). Parts come from Bricks/Brick/Part (LXFML 5: row-major rotation and translation per bone) or Scene/Model/Group/Part (LXFML 4: axis angle).
  2. Brick geometry is the client's LDD primitives, res/brickprimitives/lod<n>/<design>.g, .g1, ... (sub-part i uses the part's i-th material, material 0 meaning the part's first). Colors and opacity come from Materials.xml in res/brickdb.zip.
  3. The mesh is built like LU Toolbox's "Process Model": every brick merged into opaque and transparent meshes with the material colors as sRGB vertex colors, faces nobody can see removed (the opaque mesh is rendered from 42 directions and triangles that never show are dropped; transparent bricks don't hide anything) and lighting baked into the vertex colors as ambient occlusion computed from the same renders.
  4. The meshes are written as a Gamebryo 20.3.0.9 NIF (user version 0, the client's own version): a root node with one NiTriShape per piece, named S01_Opaque_... / S01_Alpha_... like LU Toolbox names them, each with vertex colors, a material and, for transparent pieces, alpha blending. Pieces are split at 65535 vertices/triangles.
  5. The icon is rendered by a software rasterizer (no GPU, no display), 4x4 supersampled, 3/4 view from the front left, framed to fit, on a transparent background: icon.png for the dashboard and a 32-bit icon.dds for the client.

Modular builds (ugc_modular_build rows, ldf_config like 1:4713+1:4714+1:4715):

  1. Each module LOT's ModuleComponent (component type 28) gives its part code and build type; the build type's ModularBuildComponent.xml gives the topology (root part, and which part connects to which named location) and Placement/AdditionalModelRotation.
  2. Each module's mesh is its render asset (RenderComponent.render_asset, the NIF the client assembles in game). A connection places the connecting part so its node with the location's name (when it has one) sits on the parent part's node of that name, or its origin on that node; ModuleComponent.xml's connection translation is used when the parent's NIF has no such node. (Module LXFMLs in res/BrickModels exist for only some modules and are authored in different spaces, so they aren't used.)
  3. The assembled mesh gets an icon like a model's. No mesh is written: the client assembles modular builds itself.

Storage

Files live under ugc_output_dir (default ugc next to the server binaries):

ugc/models/<id % 1000>/<id>/model.nif, model.nif.gz, model.nif.checksum, model.lxfml.gz, model.lxfml.checksum,
                              icon.dds.gz, icon.dds.checksum, icon.png
ugc/modular/<id % 1000>/<id>/icon.dds.gz, icon.dds.checksum, icon.png

A model's files are written to a temporary folder and renamed into place, so a half written model is never served. ugc_max_storage_mb (default 2048, 0 for no limit) caps the folder: when it is over, the models whose files were used longest ago are deleted. Their rows stay is_optimized = 1; when something asks for their files the server sets them back to 0 and makes them again (the request answers 408 meanwhile), so deleted files are only made again when wanted.

Database state

Migrations dlu/mysql/81_ugc_processing.sql and dlu/sqlite/64_ugc_processing.sql (the MySQL one only adds columns that aren't there yet).

ugc: is_optimized (existing) is 0 until the model has been processed, 1 once its files are made, 2 when processing failed. New: processed_at (Unix seconds of the last attempt), process_attempts, process_error (the last failure's reason, empty when none). bake_ao (existing) records whether lighting was baked into the model. Changing a model's LXFML (UpdateUgcModelData) sets it back to unprocessed.

ugc_modular_build: new is_optimized, processed_at, process_attempts, process_error, meaning the same.

The database is the queue: the UGC server looks for rows with is_optimized = 0 every poll_interval_ms (default 2000), newest first, so worlds need no change to have new models processed. Rows that failed are tried again up to max_attempts (default 3) times. Reprocessing (dashboard) sets rows back to is_optimized = 0, process_attempts = 0. No master messages are needed for any of it.

Marking models for processing (for code that writes ugc rows)

  • A new row needs nothing: is_optimized defaults to 0 (InsertNewUgcModel writes 0).
  • When a model's LXFML changes, UpdateUgcModelData sets is_optimized = 0, process_attempts = 0, process_error = '' in the same statement. Anything that writes ugc.lxfml another way must do the same, or call ResetUgcModelProcessing(id, false).
  • Deleting the row is enough when a model is deleted; its files are left until the storage cap removes them.
  • The same holds for ugc_modular_build (InsertUgcBuild rows start at 0; ResetModularBuildProcessing).

Threads

The main thread owns RakNet (the master link), mongoose (HTTP) and the database. worker_threads (default: half the CPUs) workers do only pure work: parse LXFML, build and write meshes and icons, compress files. They get their input (LXFML text, module data) from the main thread and hand results back through a queue the main thread drains, which then updates the database. Brick geometry and materials are loaded once and shared read-only (the cache has its own lock).

HTTP

port (default 2008) on listen_ip (default 0.0.0.0, the client has to reach it). All GET, no authentication (the files are what every player on a property sees anyway):

  • <client_path>/UGCC<dc>/<dc><id>.lxfml.gz|.checksum, <client_path>/UGCC<dc>/3DOPTIMIZED/<dc><id>.nif.gz|.checksum, <client_path>/UGCC<dc>/IMAGE128DDS/<dc><id>.dds.gz|.checksum for the client (client_path, default /ugc, is the client's UGCSERVERDIR). HKX answers 404. A model that exists but isn't made yet (or was evicted) is moved to the front of the queue and answers 408 so the client asks again.
  • /files/model/<id>/icon.png|model.nif and /files/modular/<id>/icon.png for the dashboard's previews (with Access-Control-Allow-Origin: *).
  • /status: JSON with the queue length, what the workers are doing and totals since start.

Files are sent from disk (mongoose streams them, with its own ETag) with Cache-Control: public, max-age=3600.

UgcServer --make-model <file.lxfml or sd0> <folder> and UgcServer --make-modular "1:4713+1:4714+1:4715" <folder> make one item's files into a folder without a database, for trying settings.

Dashboard

The UGC server is a server like auth and chat: master starts it when enable_ugc_server=1, starts it again when its link drops, passes settings reloads to it and waits for it on shutdown, and tells the dashboard about it in the server list (enabled, connected, process ID). On the dashboard it shows on the home page (Server Status, and a UGC Server card for health_view: up time, waiting/made/failed, busy workers and storage), on Server Health (uptime history and the Servers table with its process memory and CPU), on Diagnostics (its packets and HTTP requests, from the traffic report it sends every 5 seconds with its workers, totals and storage), in the server webhook alerts when it goes down or comes back, in the System Log (UgcServer_*.log) and crash dumps (crash_Ugc_<pid>.log in dump_folder), and in Prometheus (darkflame_ugc_up, darkflame_ugc_items, darkflame_server_ugc_*{server="ugc"}). See docs/Dashboard.md.

The UGC Server page (/ugc, Server Admin menu; properties_view to look, the new ugc_manage permission to make things again) reads the database: counts per state for models and for cars and rockets, a paged list with owner, state, attempts, last attempt and failure reason, and buttons to make one item, the failed ones or everything again (these only reset the columns; the UGC server picks the rows up). Icons, the live status (/status) and the mesh download come from the UGC server at ugc_public_url (dashboardconfig.ini; empty: the dashboard's host name on port 2008). "View" shows the model's LXFML in the dashboard's 3D viewer (/api/ugc/<id>/lxfml).

Not done yet

  • Worlds still send every model's LXFML to the client on property load and don't set renderUserGen, so the client keeps building its own meshes; switching them to the served NIFs (and sending NotifyClientUGCModelReady when a model is made) is the next step, and needs checking in game.
  • HKX (physics) is not generated.
  • The non-3D-services download path (world manifest packets) is not answered.